6f0b35437ed8e5148badece1f5799f65e66b381c
[lhc/web/wiklou.git] / resources / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.1.0-pre (ddcf828854)
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: Fri Feb 21 2014 19:44:50 GMT-0800 (PST)
10 */
11 ( function () {
12
13 'use strict';
14 /**
15 * Namespace for all classes, static methods and static properties.
16 *
17 * @class
18 * @singleton
19 */
20 OO.ui = {};
21
22 OO.ui.bind = $.proxy;
23
24 /**
25 * Get the user's language and any fallback languages.
26 *
27 * These language codes are used to localize user interface elements in the user's language.
28 *
29 * In environments that provide a localization system, this function should be overridden to
30 * return the user's language(s). The default implementation returns English (en) only.
31 *
32 * @returns {string[]} Language codes, in descending order of priority
33 */
34 OO.ui.getUserLanguages = function () {
35 return [ 'en' ];
36 };
37
38 /**
39 * Get a value in an object keyed by language code.
40 *
41 * @param {Object.<string,Mixed>} obj Object keyed by language code
42 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
43 * @param {string} [fallback] Fallback code, used if no matching language can be found
44 * @returns {Mixed} Local value
45 */
46 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
47 var i, len, langs;
48
49 // Requested language
50 if ( obj[lang] ) {
51 return obj[lang];
52 }
53 // Known user language
54 langs = OO.ui.getUserLanguages();
55 for ( i = 0, len = langs.length; i < len; i++ ) {
56 lang = langs[i];
57 if ( obj[lang] ) {
58 return obj[lang];
59 }
60 }
61 // Fallback language
62 if ( obj[fallback] ) {
63 return obj[fallback];
64 }
65 // First existing language
66 for ( lang in obj ) {
67 return obj[lang];
68 }
69
70 return undefined;
71 };
72
73 ( function () {
74
75 /**
76 * Message store for the default implementation of OO.ui.msg
77 *
78 * Environments that provide a localization system should not use this, but should override
79 * OO.ui.msg altogether.
80 *
81 * @private
82 */
83 var messages = {
84 // Label text for button to exit from dialog
85 'ooui-dialog-action-close': 'Close',
86 // Tool tip for a button that moves items in a list down one place
87 'ooui-outline-control-move-down': 'Move item down',
88 // Tool tip for a button that moves items in a list up one place
89 'ooui-outline-control-move-up': 'Move item up',
90 // Label for the toolbar group that contains a list of all other available tools
91 'ooui-toolbar-more': 'More'
92 };
93
94 /**
95 * Get a localized message.
96 *
97 * In environments that provide a localization system, this function should be overridden to
98 * return the message translated in the user's language. The default implementation always returns
99 * English messages.
100 *
101 * After the message key, message parameters may optionally be passed. In the default implementation,
102 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
103 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
104 * they support unnamed, ordered message parameters.
105 *
106 * @abstract
107 * @param {string} key Message key
108 * @param {Mixed...} [params] Message parameters
109 * @returns {string} Translated message with parameters substituted
110 */
111 OO.ui.msg = function ( key ) {
112 var message = messages[key], params = Array.prototype.slice.call( arguments, 1 );
113 if ( typeof message === 'string' ) {
114 // Perform $1 substitution
115 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
116 var i = parseInt( n, 10 );
117 return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
118 } );
119 } else {
120 // Return placeholder if message not found
121 message = '[' + key + ']';
122 }
123 return message;
124 };
125
126 OO.ui.deferMsg = function ( key ) {
127 return function () {
128 return OO.ui.msg( key );
129 };
130 };
131
132 OO.ui.resolveMsg = function ( msg ) {
133 if ( $.isFunction( msg ) ) {
134 return msg();
135 }
136 return msg;
137 };
138
139 } )();
140
141 // Add more as you need
142 OO.ui.Keys = {
143 'UNDEFINED': 0,
144 'BACKSPACE': 8,
145 'DELETE': 46,
146 'LEFT': 37,
147 'RIGHT': 39,
148 'UP': 38,
149 'DOWN': 40,
150 'ENTER': 13,
151 'END': 35,
152 'HOME': 36,
153 'TAB': 9,
154 'PAGEUP': 33,
155 'PAGEDOWN': 34,
156 'ESCAPE': 27,
157 'SHIFT': 16,
158 'SPACE': 32
159 };
160 /**
161 * DOM element abstraction.
162 *
163 * @class
164 * @abstract
165 *
166 * @constructor
167 * @param {Object} [config] Configuration options
168 * @cfg {Function} [$] jQuery for the frame the widget is in
169 * @cfg {string[]} [classes] CSS class names
170 * @cfg {jQuery} [$content] Content elements to append
171 */
172 OO.ui.Element = function OoUiElement( config ) {
173 // Configuration initialization
174 config = config || {};
175
176 // Properties
177 this.$ = config.$ || OO.ui.Element.getJQuery( document );
178 this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
179 this.elementGroup = null;
180
181 // Initialization
182 if ( $.isArray( config.classes ) ) {
183 this.$element.addClass( config.classes.join( ' ' ) );
184 }
185 if ( config.$content ) {
186 this.$element.append( config.$content );
187 }
188 };
189
190 /* Static Properties */
191
192 /**
193 * @static
194 * @property
195 * @inheritable
196 */
197 OO.ui.Element.static = {};
198
199 /**
200 * HTML tag name.
201 *
202 * This may be ignored if getTagName is overridden.
203 *
204 * @static
205 * @property {string}
206 * @inheritable
207 */
208 OO.ui.Element.static.tagName = 'div';
209
210 /* Static Methods */
211
212 /**
213 * Gets a jQuery function within a specific document.
214 *
215 * @static
216 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
217 * @param {OO.ui.Frame} [frame] Frame of the document context
218 * @returns {Function} Bound jQuery function
219 */
220 OO.ui.Element.getJQuery = function ( context, frame ) {
221 function wrapper( selector ) {
222 return $( selector, wrapper.context );
223 }
224
225 wrapper.context = this.getDocument( context );
226
227 if ( frame ) {
228 wrapper.frame = frame;
229 }
230
231 return wrapper;
232 };
233
234 /**
235 * Get the document of an element.
236 *
237 * @static
238 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
239 * @returns {HTMLDocument} Document object
240 * @throws {Error} If context is invalid
241 */
242 OO.ui.Element.getDocument = function ( obj ) {
243 var doc =
244 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
245 ( obj[0] && obj[0].ownerDocument ) ||
246 // Empty jQuery selections might have a context
247 obj.context ||
248 // HTMLElement
249 obj.ownerDocument ||
250 // Window
251 obj.document ||
252 // HTMLDocument
253 ( obj.nodeType === 9 && obj );
254
255 if ( doc ) {
256 return doc;
257 }
258
259 throw new Error( 'Invalid context' );
260 };
261
262 /**
263 * Get the window of an element or document.
264 *
265 * @static
266 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
267 * @returns {Window} Window object
268 */
269 OO.ui.Element.getWindow = function ( obj ) {
270 var doc = this.getDocument( obj );
271 return doc.parentWindow || doc.defaultView;
272 };
273
274 /**
275 * Get the direction of an element or document.
276 *
277 * @static
278 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
279 * @returns {string} Text direction, either `ltr` or `rtl`
280 */
281 OO.ui.Element.getDir = function ( obj ) {
282 var isDoc, isWin;
283
284 if ( obj instanceof jQuery ) {
285 obj = obj[0];
286 }
287 isDoc = obj.nodeType === 9;
288 isWin = obj.document !== undefined;
289 if ( isDoc || isWin ) {
290 if ( isWin ) {
291 obj = obj.document;
292 }
293 obj = obj.body;
294 }
295 return $( obj ).css( 'direction' );
296 };
297
298 /**
299 * Get the offset between two frames.
300 *
301 * TODO: Make this function not use recursion.
302 *
303 * @static
304 * @param {Window} from Window of the child frame
305 * @param {Window} [to=window] Window of the parent frame
306 * @param {Object} [offset] Offset to start with, used internally
307 * @returns {Object} Offset object, containing left and top properties
308 */
309 OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
310 var i, len, frames, frame, rect;
311
312 if ( !to ) {
313 to = window;
314 }
315 if ( !offset ) {
316 offset = { 'top': 0, 'left': 0 };
317 }
318 if ( from.parent === from ) {
319 return offset;
320 }
321
322 // Get iframe element
323 frames = from.parent.document.getElementsByTagName( 'iframe' );
324 for ( i = 0, len = frames.length; i < len; i++ ) {
325 if ( frames[i].contentWindow === from ) {
326 frame = frames[i];
327 break;
328 }
329 }
330
331 // Recursively accumulate offset values
332 if ( frame ) {
333 rect = frame.getBoundingClientRect();
334 offset.left += rect.left;
335 offset.top += rect.top;
336 if ( from !== to ) {
337 this.getFrameOffset( from.parent, offset );
338 }
339 }
340 return offset;
341 };
342
343 /**
344 * Get the offset between two elements.
345 *
346 * @static
347 * @param {jQuery} $from
348 * @param {jQuery} $to
349 * @returns {Object} Translated position coordinates, containing top and left properties
350 */
351 OO.ui.Element.getRelativePosition = function ( $from, $to ) {
352 var from = $from.offset(),
353 to = $to.offset();
354 return { 'top': Math.round( from.top - to.top ), 'left': Math.round( from.left - to.left ) };
355 };
356
357 /**
358 * Get element border sizes.
359 *
360 * @static
361 * @param {HTMLElement} el Element to measure
362 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
363 */
364 OO.ui.Element.getBorders = function ( el ) {
365 var doc = el.ownerDocument,
366 win = doc.parentWindow || doc.defaultView,
367 style = win && win.getComputedStyle ?
368 win.getComputedStyle( el, null ) :
369 el.currentStyle,
370 $el = $( el ),
371 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
372 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
373 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
374 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
375
376 return {
377 'top': Math.round( top ),
378 'left': Math.round( left ),
379 'bottom': Math.round( bottom ),
380 'right': Math.round( right )
381 };
382 };
383
384 /**
385 * Get dimensions of an element or window.
386 *
387 * @static
388 * @param {HTMLElement|Window} el Element to measure
389 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
390 */
391 OO.ui.Element.getDimensions = function ( el ) {
392 var $el, $win,
393 doc = el.ownerDocument || el.document,
394 win = doc.parentWindow || doc.defaultView;
395
396 if ( win === el || el === doc.documentElement ) {
397 $win = $( win );
398 return {
399 'borders': { 'top': 0, 'left': 0, 'bottom': 0, 'right': 0 },
400 'scroll': {
401 'top': $win.scrollTop(),
402 'left': $win.scrollLeft()
403 },
404 'scrollbar': { 'right': 0, 'bottom': 0 },
405 'rect': {
406 'top': 0,
407 'left': 0,
408 'bottom': $win.innerHeight(),
409 'right': $win.innerWidth()
410 }
411 };
412 } else {
413 $el = $( el );
414 return {
415 'borders': this.getBorders( el ),
416 'scroll': {
417 'top': $el.scrollTop(),
418 'left': $el.scrollLeft()
419 },
420 'scrollbar': {
421 'right': $el.innerWidth() - el.clientWidth,
422 'bottom': $el.innerHeight() - el.clientHeight
423 },
424 'rect': el.getBoundingClientRect()
425 };
426 }
427 };
428
429 /**
430 * Get closest scrollable container.
431 *
432 * Traverses up until either a scrollable element or the root is reached, in which case the window
433 * will be returned.
434 *
435 * @static
436 * @param {HTMLElement} el Element to find scrollable container for
437 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
438 * @return {HTMLElement|Window} Closest scrollable container
439 */
440 OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
441 var i, val,
442 props = [ 'overflow' ],
443 $parent = $( el ).parent();
444
445 if ( dimension === 'x' || dimension === 'y' ) {
446 props.push( 'overflow-' + dimension );
447 }
448
449 while ( $parent.length ) {
450 if ( $parent[0] === el.ownerDocument.body ) {
451 return $parent[0];
452 }
453 i = props.length;
454 while ( i-- ) {
455 val = $parent.css( props[i] );
456 if ( val === 'auto' || val === 'scroll' ) {
457 return $parent[0];
458 }
459 }
460 $parent = $parent.parent();
461 }
462 return this.getDocument( el ).body;
463 };
464
465 /**
466 * Scroll element into view
467 *
468 * @static
469 * @param {HTMLElement} el Element to scroll into view
470 * @param {Object} [config={}] Configuration config
471 * @param {string} [config.duration] jQuery animation duration value
472 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
473 * to scroll in both directions
474 * @param {Function} [config.complete] Function to call when scrolling completes
475 */
476 OO.ui.Element.scrollIntoView = function ( el, config ) {
477 // Configuration initialization
478 config = config || {};
479
480 var anim = {},
481 callback = typeof config.complete === 'function' && config.complete,
482 sc = this.getClosestScrollableContainer( el, config.direction ),
483 $sc = $( sc ),
484 eld = this.getDimensions( el ),
485 scd = this.getDimensions( sc ),
486 rel = {
487 'top': eld.rect.top - ( scd.rect.top + scd.borders.top ),
488 'bottom': scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
489 'left': eld.rect.left - ( scd.rect.left + scd.borders.left ),
490 'right': scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
491 };
492
493 if ( !config.direction || config.direction === 'y' ) {
494 if ( rel.top < 0 ) {
495 anim.scrollTop = scd.scroll.top + rel.top;
496 } else if ( rel.top > 0 && rel.bottom < 0 ) {
497 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
498 }
499 }
500 if ( !config.direction || config.direction === 'x' ) {
501 if ( rel.left < 0 ) {
502 anim.scrollLeft = scd.scroll.left + rel.left;
503 } else if ( rel.left > 0 && rel.right < 0 ) {
504 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
505 }
506 }
507 if ( !$.isEmptyObject( anim ) ) {
508 $sc.stop( true ).animate( anim, config.duration || 'fast' );
509 if ( callback ) {
510 $sc.queue( function ( next ) {
511 callback();
512 next();
513 } );
514 }
515 } else {
516 if ( callback ) {
517 callback();
518 }
519 }
520 };
521
522 /* Methods */
523
524 /**
525 * Get the HTML tag name.
526 *
527 * Override this method to base the result on instance information.
528 *
529 * @returns {string} HTML tag name
530 */
531 OO.ui.Element.prototype.getTagName = function () {
532 return this.constructor.static.tagName;
533 };
534
535 /**
536 * Get the DOM document.
537 *
538 * @returns {HTMLDocument} Document object
539 */
540 OO.ui.Element.prototype.getElementDocument = function () {
541 return OO.ui.Element.getDocument( this.$element );
542 };
543
544 /**
545 * Get the DOM window.
546 *
547 * @returns {Window} Window object
548 */
549 OO.ui.Element.prototype.getElementWindow = function () {
550 return OO.ui.Element.getWindow( this.$element );
551 };
552
553 /**
554 * Get closest scrollable container.
555 *
556 * @method
557 * @see #static-method-getClosestScrollableContainer
558 */
559 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
560 return OO.ui.Element.getClosestScrollableContainer( this.$element[0] );
561 };
562
563 /**
564 * Get group element is in.
565 *
566 * @returns {OO.ui.GroupElement|null} Group element, null if none
567 */
568 OO.ui.Element.prototype.getElementGroup = function () {
569 return this.elementGroup;
570 };
571
572 /**
573 * Set group element is in.
574 *
575 * @param {OO.ui.GroupElement|null} group Group element, null if none
576 * @chainable
577 */
578 OO.ui.Element.prototype.setElementGroup = function ( group ) {
579 this.elementGroup = group;
580 return this;
581 };
582
583 /**
584 * Scroll element into view
585 *
586 * @method
587 * @see #static-method-scrollIntoView
588 * @param {Object} [config={}]
589 */
590 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
591 return OO.ui.Element.scrollIntoView( this.$element[0], config );
592 };
593
594 ( function () {
595 // Static
596 var specialFocusin;
597
598 function handler( e ) {
599 jQuery.event.simulate( 'focusin', e.target, jQuery.event.fix( e ), /* bubble = */ true );
600 }
601
602 specialFocusin = {
603 setup: function () {
604 var doc = this.ownerDocument || this,
605 attaches = $.data( doc, 'ooui-focusin-attaches' );
606 if ( !attaches ) {
607 doc.addEventListener( 'focus', handler, true );
608 }
609 $.data( doc, 'ooui-focusin-attaches', ( attaches || 0 ) + 1 );
610 },
611 teardown: function () {
612 var doc = this.ownerDocument || this,
613 attaches = $.data( doc, 'ooui-focusin-attaches' ) - 1;
614 if ( !attaches ) {
615 doc.removeEventListener( 'focus', handler, true );
616 $.removeData( doc, 'ooui-focusin-attaches' );
617 } else {
618 $.data( doc, 'ooui-focusin-attaches', attaches );
619 }
620 }
621 };
622
623 /**
624 * Bind a handler for an event on the DOM element.
625 *
626 * Uses jQuery internally for everything except for events which are
627 * known to have issues in the browser or in jQuery. This method
628 * should become obsolete eventually.
629 *
630 * @param {string} event
631 * @param {Function} callback
632 */
633 OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) {
634 var orig;
635
636 if ( event === 'focusin' ) {
637 // jQuery 1.8.3 has a bug with handling focusin events inside iframes.
638 // Firefox doesn't support focusin at all, so we listen for 'focus' on the
639 // document, and simulate a 'focusin' event on the target element and make
640 // it bubble from there.
641 //
642 // - http://jsfiddle.net/sw3hr/
643 // - http://bugs.jquery.com/ticket/14180
644 // - https://github.com/jquery/jquery/commit/1cecf64e5aa4153
645
646 // Replace jQuery's override with our own
647 orig = $.event.special.focusin;
648 $.event.special.focusin = specialFocusin;
649
650 this.$element.on( event, callback );
651
652 // Restore
653 $.event.special.focusin = orig;
654
655 } else {
656 this.$element.on( event, callback );
657 }
658 };
659
660 /**
661 * @param {string} event
662 * @param {Function} callback
663 */
664 OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
665 var orig;
666 if ( event === 'focusin' ) {
667 orig = $.event.special.focusin;
668 $.event.special.focusin = specialFocusin;
669 this.$element.off( event, callback );
670 $.event.special.focusin = orig;
671 } else {
672 this.$element.off( event, callback );
673 }
674 };
675 }() );
676 /**
677 * Embedded iframe with the same styles as its parent.
678 *
679 * @class
680 * @extends OO.ui.Element
681 * @mixins OO.EventEmitter
682 *
683 * @constructor
684 * @param {Object} [config] Configuration options
685 */
686 OO.ui.Frame = function OoUiFrame( config ) {
687 // Parent constructor
688 OO.ui.Element.call( this, config );
689
690 // Mixin constructors
691 OO.EventEmitter.call( this );
692
693 // Properties
694 this.loading = false;
695 this.loaded = false;
696 this.config = config;
697
698 // Initialize
699 this.$element
700 .addClass( 'oo-ui-frame' )
701 .attr( { 'frameborder': 0, 'scrolling': 'no' } );
702
703 };
704
705 /* Inheritance */
706
707 OO.inheritClass( OO.ui.Frame, OO.ui.Element );
708
709 OO.mixinClass( OO.ui.Frame, OO.EventEmitter );
710
711 /* Static Properties */
712
713 OO.ui.Frame.static.tagName = 'iframe';
714
715 /* Events */
716
717 /**
718 * @event load
719 */
720
721 /* Static Methods */
722
723 /**
724 * Transplant the CSS styles from as parent document to a frame's document.
725 *
726 * This loops over the style sheets in the parent document, and copies their nodes to the
727 * frame's document. It then polls the document to see when all styles have loaded, and once they
728 * have, invokes the callback.
729 *
730 * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
731 * and invoke the callback anyway. This protects against cases like a display: none; iframe in
732 * Firefox, where the styles won't load until the iframe becomes visible.
733 *
734 * For details of how we arrived at the strategy used in this function, see #load.
735 *
736 * @static
737 * @inheritable
738 * @param {HTMLDocument} parentDoc Document to transplant styles from
739 * @param {HTMLDocument} frameDoc Document to transplant styles to
740 * @param {Function} [callback] Callback to execute once styles have loaded
741 * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
742 */
743 OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, callback, timeout ) {
744 var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes,
745 $pollNodes = $( [] ),
746 // Fake font-family value
747 fontFamily = 'oo-ui-frame-transplantStyles-loaded';
748
749 for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
750 styleNode = parentDoc.styleSheets[i].ownerNode;
751 if ( callback && styleNode.nodeName.toLowerCase() === 'link' ) {
752 // External stylesheet
753 // Create a node with a unique ID that we're going to monitor to see when the CSS
754 // has loaded
755 pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i;
756 $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
757 .attr( 'id', pollNodeId )
758 .appendTo( frameDoc.body )
759 );
760
761 // Add <style>@import url(...); #pollNodeId { font-family: ... }</style>
762 // The font-family rule will only take effect once the @import finishes
763 newNode = frameDoc.createElement( 'style' );
764 newNode.textContent = '@import url(' + styleNode.href + ');\n' +
765 '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
766 } else {
767 // Not an external stylesheet, or no polling required; just copy the node over
768 newNode = frameDoc.importNode( styleNode, true );
769 }
770 frameDoc.head.appendChild( newNode );
771 }
772
773 if ( callback ) {
774 // Poll every 100ms until all external stylesheets have loaded
775 $pendingPollNodes = $pollNodes;
776 timeoutID = setTimeout( function pollExternalStylesheets() {
777 while (
778 $pendingPollNodes.length > 0 &&
779 $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
780 ) {
781 $pendingPollNodes = $pendingPollNodes.slice( 1 );
782 }
783
784 if ( $pendingPollNodes.length === 0 ) {
785 // We're done!
786 if ( timeoutID !== null ) {
787 timeoutID = null;
788 $pollNodes.remove();
789 callback();
790 }
791 } else {
792 timeoutID = setTimeout( pollExternalStylesheets, 100 );
793 }
794 }, 100 );
795 // ...but give up after a while
796 if ( timeout !== 0 ) {
797 setTimeout( function () {
798 if ( timeoutID ) {
799 clearTimeout( timeoutID );
800 timeoutID = null;
801 $pollNodes.remove();
802 callback();
803 }
804 }, timeout || 5000 );
805 }
806 }
807 };
808
809 /* Methods */
810
811 /**
812 * Load the frame contents.
813 *
814 * Once the iframe's stylesheets are loaded, the `initialize` event will be emitted.
815 *
816 * Sounds simple right? Read on...
817 *
818 * When you create a dynamic iframe using open/write/close, the window.load event for the
819 * iframe is triggered when you call close, and there's no further load event to indicate that
820 * everything is actually loaded.
821 *
822 * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
823 * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
824 * are added to document.styleSheets immediately, and the only way you can determine whether they've
825 * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
826 * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
827 *
828 * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` tags.
829 * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until
830 * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import`
831 * has finished. And because the contents of the `<style>` tag are from the same origin, accessing
832 * .cssRules is allowed.
833 *
834 * However, now that we control the styles we're injecting, we might as well do away with
835 * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
836 * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
837 * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family
838 * rule is not applied until after the `@import` finishes.
839 *
840 * All this stylesheet injection and polling magic is in #transplantStyles.
841 *
842 * @private
843 * @fires load
844 */
845 OO.ui.Frame.prototype.load = function () {
846 var win = this.$element.prop( 'contentWindow' ),
847 doc = win.document,
848 frame = this;
849
850 this.loading = true;
851
852 // Figure out directionality:
853 this.dir = this.$element.closest( '[dir]' ).prop( 'dir' ) || 'ltr';
854
855 // Initialize contents
856 doc.open();
857 doc.write(
858 '<!doctype html>' +
859 '<html>' +
860 '<body class="oo-ui-frame-body oo-ui-' + this.dir + '" style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
861 '<div class="oo-ui-frame-content"></div>' +
862 '</body>' +
863 '</html>'
864 );
865 doc.close();
866
867 // Properties
868 this.$ = OO.ui.Element.getJQuery( doc, this );
869 this.$content = this.$( '.oo-ui-frame-content' );
870 this.$document = this.$( doc );
871
872 this.constructor.static.transplantStyles(
873 this.getElementDocument(),
874 this.$document[0],
875 function () {
876 frame.loading = false;
877 frame.loaded = true;
878 frame.emit( 'load' );
879 }
880 );
881 };
882
883 /**
884 * Run a callback as soon as the frame has been loaded.
885 *
886 *
887 * This will start loading if it hasn't already, and runs
888 * immediately if the frame is already loaded.
889 *
890 * Don't call this until the element is attached.
891 *
892 * @param {Function} callback
893 */
894 OO.ui.Frame.prototype.run = function ( callback ) {
895 if ( this.loaded ) {
896 callback();
897 } else {
898 if ( !this.loading ) {
899 this.load();
900 }
901 this.once( 'load', callback );
902 }
903 };
904
905 /**
906 * Sets the size of the frame.
907 *
908 * @param {number} width Frame width in pixels
909 * @param {number} height Frame height in pixels
910 * @chainable
911 */
912 OO.ui.Frame.prototype.setSize = function ( width, height ) {
913 this.$element.css( { 'width': width, 'height': height } );
914 return this;
915 };
916 /**
917 * Container for elements in a child frame.
918 *
919 * There are two ways to specify a title: set the static `title` property or provide a `title`
920 * property in the configuration options. The latter will override the former.
921 *
922 * @class
923 * @abstract
924 * @extends OO.ui.Element
925 * @mixins OO.EventEmitter
926 *
927 * @constructor
928 * @param {Object} [config] Configuration options
929 * @cfg {string|Function} [title] Title string or function that returns a string
930 * @cfg {string} [icon] Symbolic name of icon
931 * @fires initialize
932 */
933 OO.ui.Window = function OoUiWindow( config ) {
934 // Parent constructor
935 OO.ui.Element.call( this, config );
936
937 // Mixin constructors
938 OO.EventEmitter.call( this );
939
940 // Properties
941 this.visible = false;
942 this.opening = false;
943 this.closing = false;
944 this.title = OO.ui.resolveMsg( config.title || this.constructor.static.title );
945 this.icon = config.icon || this.constructor.static.icon;
946 this.frame = new OO.ui.Frame( { '$': this.$ } );
947 this.$frame = this.$( '<div>' );
948 this.$ = function () {
949 throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
950 };
951
952 // Initialization
953 this.$element
954 .addClass( 'oo-ui-window' )
955 // Hide the window using visibility: hidden; while the iframe is still loading
956 // Can't use display: none; because that prevents the iframe from loading in Firefox
957 .css( 'visibility', 'hidden' )
958 .append( this.$frame );
959 this.$frame
960 .addClass( 'oo-ui-window-frame' )
961 .append( this.frame.$element );
962
963 // Events
964 this.frame.connect( this, { 'load': 'initialize' } );
965 };
966
967 /* Inheritance */
968
969 OO.inheritClass( OO.ui.Window, OO.ui.Element );
970
971 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
972
973 /* Events */
974
975 /**
976 * Initialize contents.
977 *
978 * Fired asynchronously after construction when iframe is ready.
979 *
980 * @event initialize
981 */
982
983 /**
984 * Open window.
985 *
986 * Fired after window has been opened.
987 *
988 * @event open
989 * @param {Object} data Window opening data
990 */
991
992 /**
993 * Close window.
994 *
995 * Fired after window has been closed.
996 *
997 * @event close
998 * @param {Object} data Window closing data
999 */
1000
1001 /* Static Properties */
1002
1003 /**
1004 * Symbolic name of icon.
1005 *
1006 * @static
1007 * @inheritable
1008 * @property {string}
1009 */
1010 OO.ui.Window.static.icon = 'window';
1011
1012 /**
1013 * Window title.
1014 *
1015 * Subclasses must implement this property before instantiating the window.
1016 * Alternatively, override #getTitle with an alternative implementation.
1017 *
1018 * @static
1019 * @abstract
1020 * @inheritable
1021 * @property {string|Function} Title string or function that returns a string
1022 */
1023 OO.ui.Window.static.title = null;
1024
1025 /* Methods */
1026
1027 /**
1028 * Check if window is visible.
1029 *
1030 * @method
1031 * @returns {boolean} Window is visible
1032 */
1033 OO.ui.Window.prototype.isVisible = function () {
1034 return this.visible;
1035 };
1036
1037 /**
1038 * Check if window is opening.
1039 *
1040 * @method
1041 * @returns {boolean} Window is opening
1042 */
1043 OO.ui.Window.prototype.isOpening = function () {
1044 return this.opening;
1045 };
1046
1047 /**
1048 * Check if window is closing.
1049 *
1050 * @method
1051 * @returns {boolean} Window is closing
1052 */
1053 OO.ui.Window.prototype.isClosing = function () {
1054 return this.closing;
1055 };
1056
1057 /**
1058 * Get the window frame.
1059 *
1060 * @method
1061 * @returns {OO.ui.Frame} Frame of window
1062 */
1063 OO.ui.Window.prototype.getFrame = function () {
1064 return this.frame;
1065 };
1066
1067 /**
1068 * Get the title of the window.
1069 *
1070 * @returns {string} Title text
1071 */
1072 OO.ui.Window.prototype.getTitle = function () {
1073 return this.title;
1074 };
1075
1076 /**
1077 * Get the window icon.
1078 *
1079 * @returns {string} Symbolic name of icon
1080 */
1081 OO.ui.Window.prototype.getIcon = function () {
1082 return this.icon;
1083 };
1084
1085 /**
1086 * Set the size of window frame.
1087 *
1088 * @param {number} [width=auto] Custom width
1089 * @param {number} [height=auto] Custom height
1090 * @chainable
1091 */
1092 OO.ui.Window.prototype.setSize = function ( width, height ) {
1093 if ( !this.frame.$content ) {
1094 return;
1095 }
1096
1097 this.frame.$element.css( {
1098 'width': width === undefined ? 'auto' : width,
1099 'height': height === undefined ? 'auto' : height
1100 } );
1101
1102 return this;
1103 };
1104
1105 /**
1106 * Set the title of the window.
1107 *
1108 * @param {string|Function} title Title text or a function that returns text
1109 * @chainable
1110 */
1111 OO.ui.Window.prototype.setTitle = function ( title ) {
1112 this.title = OO.ui.resolveMsg( title );
1113 if ( this.$title ) {
1114 this.$title.text( title );
1115 }
1116 return this;
1117 };
1118
1119 /**
1120 * Set the icon of the window.
1121 *
1122 * @param {string} icon Symbolic name of icon
1123 * @chainable
1124 */
1125 OO.ui.Window.prototype.setIcon = function ( icon ) {
1126 if ( this.$icon ) {
1127 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
1128 }
1129 this.icon = icon;
1130 if ( this.$icon ) {
1131 this.$icon.addClass( 'oo-ui-icon-' + this.icon );
1132 }
1133
1134 return this;
1135 };
1136
1137 /**
1138 * Set the position of window to fit with contents..
1139 *
1140 * @param {string} left Left offset
1141 * @param {string} top Top offset
1142 * @chainable
1143 */
1144 OO.ui.Window.prototype.setPosition = function ( left, top ) {
1145 this.$element.css( { 'left': left, 'top': top } );
1146 return this;
1147 };
1148
1149 /**
1150 * Set the height of window to fit with contents.
1151 *
1152 * @param {number} [min=0] Min height
1153 * @param {number} [max] Max height (defaults to content's outer height)
1154 * @chainable
1155 */
1156 OO.ui.Window.prototype.fitHeightToContents = function ( min, max ) {
1157 var height = this.frame.$content.outerHeight();
1158
1159 this.frame.$element.css(
1160 'height', Math.max( min || 0, max === undefined ? height : Math.min( max, height ) )
1161 );
1162
1163 return this;
1164 };
1165
1166 /**
1167 * Set the width of window to fit with contents.
1168 *
1169 * @param {number} [min=0] Min height
1170 * @param {number} [max] Max height (defaults to content's outer width)
1171 * @chainable
1172 */
1173 OO.ui.Window.prototype.fitWidthToContents = function ( min, max ) {
1174 var width = this.frame.$content.outerWidth();
1175
1176 this.frame.$element.css(
1177 'width', Math.max( min || 0, max === undefined ? width : Math.min( max, width ) )
1178 );
1179
1180 return this;
1181 };
1182
1183 /**
1184 * Initialize window contents.
1185 *
1186 * The first time the window is opened, #initialize is called when it's safe to begin populating
1187 * its contents. See #setup for a way to make changes each time the window opens.
1188 *
1189 * Once this method is called, this.$$ can be used to create elements within the frame.
1190 *
1191 * @method
1192 * @fires initialize
1193 * @chainable
1194 */
1195 OO.ui.Window.prototype.initialize = function () {
1196 // Properties
1197 this.$ = this.frame.$;
1198 this.$title = this.$( '<div class="oo-ui-window-title"></div>' )
1199 .text( this.title );
1200 this.$icon = this.$( '<div class="oo-ui-window-icon"></div>' )
1201 .addClass( 'oo-ui-icon-' + this.icon );
1202 this.$head = this.$( '<div class="oo-ui-window-head"></div>' );
1203 this.$body = this.$( '<div class="oo-ui-window-body"></div>' );
1204 this.$foot = this.$( '<div class="oo-ui-window-foot"></div>' );
1205 this.$overlay = this.$( '<div class="oo-ui-window-overlay"></div>' );
1206
1207 // Initialization
1208 this.frame.$content.append(
1209 this.$head.append( this.$icon, this.$title ),
1210 this.$body,
1211 this.$foot,
1212 this.$overlay
1213 );
1214
1215 // Undo the visibility: hidden; hack from the constructor and apply display: none;
1216 // We can do this safely now that the iframe has initialized
1217 this.$element.hide().css( 'visibility', '' );
1218
1219 this.emit( 'initialize' );
1220
1221 return this;
1222 };
1223
1224 /**
1225 * Setup window for use.
1226 *
1227 * Each time the window is opened, once it's ready to be interacted with, this will set it up for
1228 * use in a particular context, based on the `data` argument.
1229 *
1230 * When you override this method, you must call the parent method at the very beginning.
1231 *
1232 * @method
1233 * @abstract
1234 * @param {Object} [data] Window opening data
1235 */
1236 OO.ui.Window.prototype.setup = function () {
1237 // Override to do something
1238 };
1239
1240 /**
1241 * Tear down window after use.
1242 *
1243 * Each time the window is closed, and it's done being interacted with, this will tear it down and
1244 * do something with the user's interactions within the window, based on the `data` argument.
1245 *
1246 * When you override this method, you must call the parent method at the very end.
1247 *
1248 * @method
1249 * @abstract
1250 * @param {Object} [data] Window closing data
1251 */
1252 OO.ui.Window.prototype.teardown = function () {
1253 // Override to do something
1254 };
1255
1256 /**
1257 * Open window.
1258 *
1259 * Do not override this method. See #setup for a way to make changes each time the window opens.
1260 *
1261 * @method
1262 * @param {Object} [data] Window opening data
1263 * @fires open
1264 * @chainable
1265 */
1266 OO.ui.Window.prototype.open = function ( data ) {
1267 if ( !this.opening && !this.closing && !this.visible ) {
1268 this.opening = true;
1269 this.frame.run( OO.ui.bind( function () {
1270 this.$element.show();
1271 this.visible = true;
1272 this.frame.$element.focus();
1273 this.emit( 'opening', data );
1274 this.setup( data );
1275 this.emit( 'open', data );
1276 this.opening = false;
1277 }, this ) );
1278 }
1279
1280 return this;
1281 };
1282
1283 /**
1284 * Close window.
1285 *
1286 * See #teardown for a way to do something each time the window closes.
1287 *
1288 * @method
1289 * @param {Object} [data] Window closing data
1290 * @fires close
1291 * @chainable
1292 */
1293 OO.ui.Window.prototype.close = function ( data ) {
1294 if ( !this.opening && !this.closing && this.visible ) {
1295 this.frame.$content.find( ':focus' ).blur();
1296 this.closing = true;
1297 this.$element.hide();
1298 this.visible = false;
1299 this.emit( 'closing', data );
1300 this.teardown( data );
1301 this.emit( 'close', data );
1302 this.closing = false;
1303 }
1304
1305 return this;
1306 };
1307 /**
1308 * Set of mutually exclusive windows.
1309 *
1310 * @class
1311 * @extends OO.ui.Element
1312 * @mixins OO.EventEmitter
1313 *
1314 * @constructor
1315 * @param {OO.Factory} factory Window factory
1316 * @param {Object} [config] Configuration options
1317 */
1318 OO.ui.WindowSet = function OoUiWindowSet( factory, config ) {
1319 // Parent constructor
1320 OO.ui.Element.call( this, config );
1321
1322 // Mixin constructors
1323 OO.EventEmitter.call( this );
1324
1325 // Properties
1326 this.factory = factory;
1327
1328 /**
1329 * List of all windows associated with this window set
1330 * @property {OO.ui.Window[]}
1331 */
1332 this.windowList = [];
1333
1334 /**
1335 * Mapping of OO.ui.Window objects created by name from the #factory.
1336 * @property {Object}
1337 */
1338 this.windows = {};
1339 this.currentWindow = null;
1340
1341 // Initialization
1342 this.$element.addClass( 'oo-ui-windowSet' );
1343 };
1344
1345 /* Inheritance */
1346
1347 OO.inheritClass( OO.ui.WindowSet, OO.ui.Element );
1348
1349 OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter );
1350
1351 /* Events */
1352
1353 /**
1354 * @event opening
1355 * @param {OO.ui.Window} win Window that's being opened
1356 * @param {Object} config Window opening information
1357 */
1358
1359 /**
1360 * @event open
1361 * @param {OO.ui.Window} win Window that's been opened
1362 * @param {Object} config Window opening information
1363 */
1364
1365 /**
1366 * @event closing
1367 * @param {OO.ui.Window} win Window that's being closed
1368 * @param {Object} config Window closing information
1369 */
1370
1371 /**
1372 * @event close
1373 * @param {OO.ui.Window} win Window that's been closed
1374 * @param {Object} config Window closing information
1375 */
1376
1377 /* Methods */
1378
1379 /**
1380 * Handle a window that's being opened.
1381 *
1382 * @method
1383 * @param {OO.ui.Window} win Window that's being opened
1384 * @param {Object} [config] Window opening information
1385 * @fires opening
1386 */
1387 OO.ui.WindowSet.prototype.onWindowOpening = function ( win, config ) {
1388 if ( this.currentWindow && this.currentWindow !== win ) {
1389 this.currentWindow.close();
1390 }
1391 this.currentWindow = win;
1392 this.emit( 'opening', win, config );
1393 };
1394
1395 /**
1396 * Handle a window that's been opened.
1397 *
1398 * @method
1399 * @param {OO.ui.Window} win Window that's been opened
1400 * @param {Object} [config] Window opening information
1401 * @fires open
1402 */
1403 OO.ui.WindowSet.prototype.onWindowOpen = function ( win, config ) {
1404 this.emit( 'open', win, config );
1405 };
1406
1407 /**
1408 * Handle a window that's being closed.
1409 *
1410 * @method
1411 * @param {OO.ui.Window} win Window that's being closed
1412 * @param {Object} [config] Window closing information
1413 * @fires closing
1414 */
1415 OO.ui.WindowSet.prototype.onWindowClosing = function ( win, config ) {
1416 this.currentWindow = null;
1417 this.emit( 'closing', win, config );
1418 };
1419
1420 /**
1421 * Handle a window that's been closed.
1422 *
1423 * @method
1424 * @param {OO.ui.Window} win Window that's been closed
1425 * @param {Object} [config] Window closing information
1426 * @fires close
1427 */
1428 OO.ui.WindowSet.prototype.onWindowClose = function ( win, config ) {
1429 this.emit( 'close', win, config );
1430 };
1431
1432 /**
1433 * Get the current window.
1434 *
1435 * @method
1436 * @returns {OO.ui.Window} Current window
1437 */
1438 OO.ui.WindowSet.prototype.getCurrentWindow = function () {
1439 return this.currentWindow;
1440 };
1441
1442 /**
1443 * Return a given window.
1444 *
1445 * @param {string} name Symbolic name of window
1446 * @return {OO.ui.Window} Window with specified name
1447 */
1448 OO.ui.WindowSet.prototype.getWindow = function ( name ) {
1449 var win;
1450
1451 if ( !this.factory.lookup( name ) ) {
1452 throw new Error( 'Unknown window: ' + name );
1453 }
1454 if ( !( name in this.windows ) ) {
1455 win = this.windows[name] = this.factory.create( name, this, { '$': this.$ } );
1456 this.addWindow( win );
1457 }
1458 return this.windows[name];
1459 };
1460
1461 /**
1462 * Add a given window to this window set.
1463 *
1464 * Connects event handlers and attaches it to the DOM. Calling
1465 * OO.ui.Window#open will not work until the window is added to the set.
1466 *
1467 * @param {OO.ui.Window} win
1468 */
1469 OO.ui.WindowSet.prototype.addWindow = function ( win ) {
1470 if ( this.windowList.indexOf( win ) !== -1 ) {
1471 // Already set up
1472 return;
1473 }
1474 this.windowList.push( win );
1475
1476 win.connect( this, {
1477 'opening': [ 'onWindowOpening', win ],
1478 'open': [ 'onWindowOpen', win ],
1479 'closing': [ 'onWindowClosing', win ],
1480 'close': [ 'onWindowClose', win ]
1481 } );
1482 this.$element.append( win.$element );
1483 };
1484 /**
1485 * Modal dialog box.
1486 *
1487 * @class
1488 * @abstract
1489 * @extends OO.ui.Window
1490 *
1491 * @constructor
1492 * @param {Object} [config] Configuration options
1493 * @cfg {boolean} [footless] Hide foot
1494 * @cfg {boolean} [small] Make the dialog small
1495 */
1496 OO.ui.Dialog = function OoUiDialog( config ) {
1497 // Configuration initialization
1498 config = config || {};
1499
1500 // Parent constructor
1501 OO.ui.Window.call( this, config );
1502
1503 // Properties
1504 this.visible = false;
1505 this.footless = !!config.footless;
1506 this.small = !!config.small;
1507 this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
1508 this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
1509
1510 // Events
1511 this.$element.on( 'mousedown', false );
1512 this.connect( this, { 'opening': 'onOpening' } );
1513
1514 // Initialization
1515 this.$element.addClass( 'oo-ui-dialog' );
1516 };
1517
1518 /* Inheritance */
1519
1520 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
1521
1522 /* Static Properties */
1523
1524 /**
1525 * Symbolic name of dialog.
1526 *
1527 * @abstract
1528 * @static
1529 * @property {string}
1530 * @inheritable
1531 */
1532 OO.ui.Dialog.static.name = '';
1533
1534 /* Methods */
1535
1536 /**
1537 * Handle close button click events.
1538 *
1539 * @method
1540 */
1541 OO.ui.Dialog.prototype.onCloseButtonClick = function () {
1542 this.close( { 'action': 'cancel' } );
1543 };
1544
1545 /**
1546 * Handle window mouse wheel events.
1547 *
1548 * @method
1549 * @param {jQuery.Event} e Mouse wheel event
1550 */
1551 OO.ui.Dialog.prototype.onWindowMouseWheel = function () {
1552 return false;
1553 };
1554
1555 /**
1556 * Handle document key down events.
1557 *
1558 * @method
1559 * @param {jQuery.Event} e Key down event
1560 */
1561 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
1562 switch ( e.which ) {
1563 case OO.ui.Keys.PAGEUP:
1564 case OO.ui.Keys.PAGEDOWN:
1565 case OO.ui.Keys.END:
1566 case OO.ui.Keys.HOME:
1567 case OO.ui.Keys.LEFT:
1568 case OO.ui.Keys.UP:
1569 case OO.ui.Keys.RIGHT:
1570 case OO.ui.Keys.DOWN:
1571 // Prevent any key events that might cause scrolling
1572 return false;
1573 }
1574 };
1575
1576 /**
1577 * Handle frame document key down events.
1578 *
1579 * @method
1580 * @param {jQuery.Event} e Key down event
1581 */
1582 OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) {
1583 if ( e.which === OO.ui.Keys.ESCAPE ) {
1584 this.close( { 'action': 'cancel' } );
1585 return false;
1586 }
1587 };
1588
1589 /** */
1590 OO.ui.Dialog.prototype.onOpening = function () {
1591 this.$element.addClass( 'oo-ui-dialog-open' );
1592 };
1593
1594 /**
1595 * @inheritdoc
1596 */
1597 OO.ui.Dialog.prototype.initialize = function () {
1598 // Parent method
1599 OO.ui.Window.prototype.initialize.call( this );
1600
1601 // Properties
1602 this.closeButton = new OO.ui.ButtonWidget( {
1603 '$': this.$,
1604 'frameless': true,
1605 'icon': 'close',
1606 'title': OO.ui.msg( 'ooui-dialog-action-close' )
1607 } );
1608
1609 // Events
1610 this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
1611 this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
1612
1613 // Initialization
1614 this.frame.$content.addClass( 'oo-ui-dialog-content' );
1615 if ( this.footless ) {
1616 this.frame.$content.addClass( 'oo-ui-dialog-content-footless' );
1617 }
1618 if ( this.small ) {
1619 this.$frame.addClass( 'oo-ui-window-frame-small' );
1620 }
1621 this.closeButton.$element.addClass( 'oo-ui-window-closeButton' );
1622 this.$head.append( this.closeButton.$element );
1623 };
1624
1625 /**
1626 * @inheritdoc
1627 */
1628 OO.ui.Dialog.prototype.setup = function ( data ) {
1629 // Parent method
1630 OO.ui.Window.prototype.setup.call( this, data );
1631
1632 // Prevent scrolling in top-level window
1633 this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler );
1634 this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler );
1635 };
1636
1637 /**
1638 * @inheritdoc
1639 */
1640 OO.ui.Dialog.prototype.teardown = function ( data ) {
1641 // Parent method
1642 OO.ui.Window.prototype.teardown.call( this, data );
1643
1644 // Allow scrolling in top-level window
1645 this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler );
1646 this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler );
1647 };
1648
1649 /**
1650 * @inheritdoc
1651 */
1652 OO.ui.Dialog.prototype.close = function ( data ) {
1653 var dialog = this;
1654 if ( !dialog.opening && !dialog.closing && dialog.visible ) {
1655 // Trigger transition
1656 dialog.$element.removeClass( 'oo-ui-dialog-open' );
1657 // Allow transition to complete before actually closing
1658 setTimeout( function () {
1659 // Parent method
1660 OO.ui.Window.prototype.close.call( dialog, data );
1661 }, 250 );
1662 }
1663 };
1664 /**
1665 * Container for elements.
1666 *
1667 * @class
1668 * @abstract
1669 * @extends OO.ui.Element
1670 * @mixins OO.EventEmitter
1671 *
1672 * @constructor
1673 * @param {Object} [config] Configuration options
1674 */
1675 OO.ui.Layout = function OoUiLayout( config ) {
1676 // Initialize config
1677 config = config || {};
1678
1679 // Parent constructor
1680 OO.ui.Element.call( this, config );
1681
1682 // Mixin constructors
1683 OO.EventEmitter.call( this );
1684
1685 // Initialization
1686 this.$element.addClass( 'oo-ui-layout' );
1687 };
1688
1689 /* Inheritance */
1690
1691 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1692
1693 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1694 /**
1695 * User interface control.
1696 *
1697 * @class
1698 * @abstract
1699 * @extends OO.ui.Element
1700 * @mixins OO.EventEmitter
1701 *
1702 * @constructor
1703 * @param {Object} [config] Configuration options
1704 * @cfg {boolean} [disabled=false] Disable
1705 */
1706 OO.ui.Widget = function OoUiWidget( config ) {
1707 // Initialize config
1708 config = $.extend( { 'disabled': false }, config );
1709
1710 // Parent constructor
1711 OO.ui.Element.call( this, config );
1712
1713 // Mixin constructors
1714 OO.EventEmitter.call( this );
1715
1716 // Properties
1717 this.disabled = null;
1718 this.wasDisabled = null;
1719
1720 // Initialization
1721 this.$element.addClass( 'oo-ui-widget' );
1722 this.setDisabled( !!config.disabled );
1723 };
1724
1725 /* Inheritance */
1726
1727 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1728
1729 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1730
1731 /* Events */
1732
1733 /**
1734 * @event disable
1735 * @param {boolean} disabled Widget is disabled
1736 */
1737
1738 /* Methods */
1739
1740 /**
1741 * Check if the widget is disabled.
1742 *
1743 * @method
1744 * @param {boolean} Button is disabled
1745 */
1746 OO.ui.Widget.prototype.isDisabled = function () {
1747 return this.disabled;
1748 };
1749
1750 /**
1751 * Update the disabled state, in case of changes in parent widget.
1752 *
1753 * @method
1754 * @chainable
1755 */
1756 OO.ui.Widget.prototype.updateDisabled = function () {
1757 this.setDisabled( this.disabled );
1758 return this;
1759 };
1760
1761 /**
1762 * Set the disabled state of the widget.
1763 *
1764 * This should probably change the widgets's appearance and prevent it from being used.
1765 *
1766 * @method
1767 * @param {boolean} disabled Disable widget
1768 * @chainable
1769 */
1770 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1771 var isDisabled;
1772
1773 this.disabled = !!disabled;
1774 isDisabled = this.isDisabled();
1775 if ( isDisabled !== this.wasDisabled ) {
1776 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1777 this.emit( 'disable', isDisabled );
1778 }
1779 this.wasDisabled = isDisabled;
1780 return this;
1781 };
1782 /**
1783 * Element with a button.
1784 *
1785 * @class
1786 * @abstract
1787 *
1788 * @constructor
1789 * @param {jQuery} $button Button node, assigned to #$button
1790 * @param {Object} [config] Configuration options
1791 * @cfg {boolean} [frameless] Render button without a frame
1792 * @cfg {number} [tabIndex] Button's tab index
1793 */
1794 OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
1795 // Configuration initialization
1796 config = config || {};
1797
1798 // Properties
1799 this.$button = $button;
1800 this.tabIndex = null;
1801 this.active = false;
1802 this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
1803
1804 // Events
1805 this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
1806
1807 // Initialization
1808 this.$element.addClass( 'oo-ui-buttonedElement' );
1809 this.$button
1810 .addClass( 'oo-ui-buttonedElement-button' )
1811 .attr( 'role', 'button' )
1812 .prop( 'tabIndex', config.tabIndex || 0 );
1813 if ( config.frameless ) {
1814 this.$element.addClass( 'oo-ui-buttonedElement-frameless' );
1815 } else {
1816 this.$element.addClass( 'oo-ui-buttonedElement-framed' );
1817 }
1818 };
1819
1820 /* Methods */
1821
1822 /**
1823 * Handles mouse down events.
1824 *
1825 * @method
1826 * @param {jQuery.Event} e Mouse down event
1827 */
1828 OO.ui.ButtonedElement.prototype.onMouseDown = function () {
1829 this.tabIndex = this.$button.attr( 'tabIndex' );
1830 // Remove the tab-index while the button is down to prevent the button from stealing focus
1831 this.$button.removeAttr( 'tabIndex' );
1832 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
1833 };
1834
1835 /**
1836 * Handles mouse up events.
1837 *
1838 * @method
1839 * @param {jQuery.Event} e Mouse up event
1840 */
1841 OO.ui.ButtonedElement.prototype.onMouseUp = function () {
1842 // Restore the tab-index after the button is up to restore the button's accesssibility
1843 this.$button.attr( 'tabIndex', this.tabIndex );
1844 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
1845 };
1846
1847 /**
1848 * Set active state.
1849 *
1850 * @method
1851 * @param {boolean} [value] Make button active
1852 * @chainable
1853 */
1854 OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
1855 this.$element.toggleClass( 'oo-ui-buttonedElement-active', !!value );
1856 return this;
1857 };
1858 /**
1859 * Element that can be automatically clipped to visible boundaies.
1860 *
1861 * @class
1862 * @abstract
1863 *
1864 * @constructor
1865 * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
1866 * @param {Object} [config] Configuration options
1867 */
1868 OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
1869 // Configuration initialization
1870 config = config || {};
1871
1872 // Properties
1873 this.$clippable = $clippable;
1874 this.clipping = false;
1875 this.clipped = false;
1876 this.$clippableContainer = null;
1877 this.$clippableScroller = null;
1878 this.$clippableWindow = null;
1879 this.idealWidth = null;
1880 this.idealHeight = null;
1881 this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
1882 this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
1883
1884 // Initialization
1885 this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
1886 };
1887
1888 /* Methods */
1889
1890 /**
1891 * Set clipping.
1892 *
1893 * @method
1894 * @param {boolean} value Enable clipping
1895 * @chainable
1896 */
1897 OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
1898 value = !!value;
1899
1900 if ( this.clipping !== value ) {
1901 this.clipping = value;
1902 if ( this.clipping ) {
1903 this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
1904 // If the clippable container is the body, we have to listen to scroll events and check
1905 // jQuery.scrollTop on the window because of browser inconsistencies
1906 this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
1907 this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
1908 this.$clippableContainer;
1909 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
1910 this.$clippableWindow = this.$( this.getElementWindow() )
1911 .on( 'resize', this.onClippableWindowResizeHandler );
1912 // Initial clip after visible
1913 setTimeout( OO.ui.bind( this.clip, this ) );
1914 } else {
1915 this.$clippableContainer = null;
1916 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
1917 this.$clippableScroller = null;
1918 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
1919 this.$clippableWindow = null;
1920 }
1921 }
1922
1923 return this;
1924 };
1925
1926 /**
1927 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
1928 *
1929 * @method
1930 * @return {boolean} Element will be clipped to the visible area
1931 */
1932 OO.ui.ClippableElement.prototype.isClipping = function () {
1933 return this.clipping;
1934 };
1935
1936 /**
1937 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
1938 *
1939 * @method
1940 * @return {boolean} Part of the element is being clipped
1941 */
1942 OO.ui.ClippableElement.prototype.isClipped = function () {
1943 return this.clipped;
1944 };
1945
1946 /**
1947 * Set the ideal size.
1948 *
1949 * @method
1950 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
1951 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
1952 */
1953 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
1954 this.idealWidth = width;
1955 this.idealHeight = height;
1956 };
1957
1958 /**
1959 * Clip element to visible boundaries and allow scrolling when needed.
1960 *
1961 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
1962 * overlapped by, the visible area of the nearest scrollable container.
1963 *
1964 * @method
1965 * @chainable
1966 */
1967 OO.ui.ClippableElement.prototype.clip = function () {
1968 if ( !this.clipping ) {
1969 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
1970 return this;
1971 }
1972
1973 var buffer = 10,
1974 cOffset = this.$clippable.offset(),
1975 ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 },
1976 ccHeight = this.$clippableContainer.innerHeight() - buffer,
1977 ccWidth = this.$clippableContainer.innerWidth() - buffer,
1978 scrollTop = this.$clippableScroller.scrollTop(),
1979 scrollLeft = this.$clippableScroller.scrollLeft(),
1980 desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
1981 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
1982 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
1983 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
1984 clipWidth = desiredWidth < naturalWidth,
1985 clipHeight = desiredHeight < naturalHeight;
1986
1987 if ( clipWidth ) {
1988 this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } );
1989 } else {
1990 this.$clippable.css( { 'overflow-x': '', 'width': this.idealWidth || '' } );
1991 }
1992 if ( clipHeight ) {
1993 this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
1994 } else {
1995 this.$clippable.css( { 'overflow-y': '', 'height': this.idealHeight || '' } );
1996 }
1997
1998 this.clipped = clipWidth || clipHeight;
1999
2000 return this;
2001 };
2002 /**
2003 * Element with named flags, used for styling, that can be added, removed and listed and checked.
2004 *
2005 * @class
2006 * @abstract
2007 *
2008 * @constructor
2009 * @param {Object} [config] Configuration options
2010 * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
2011 */
2012 OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
2013 // Config initialization
2014 config = config || {};
2015
2016 // Properties
2017 this.flags = {};
2018
2019 // Initialization
2020 this.setFlags( config.flags );
2021 };
2022
2023 /* Methods */
2024
2025 /**
2026 * Check if a flag is set.
2027 *
2028 * @method
2029 * @param {string} flag Flag name to check
2030 * @returns {boolean} Has flag
2031 */
2032 OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
2033 return flag in this.flags;
2034 };
2035
2036 /**
2037 * Get the names of all flags.
2038 *
2039 * @method
2040 * @returns {string[]} flags Flag names
2041 */
2042 OO.ui.FlaggableElement.prototype.getFlags = function () {
2043 return Object.keys( this.flags );
2044 };
2045
2046 /**
2047 * Add one or more flags.
2048 *
2049 * @method
2050 * @param {string[]|Object.<string, boolean>} flags List of flags to add, or list of set/remove
2051 * values, keyed by flag name
2052 * @chainable
2053 */
2054 OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
2055 var i, len, flag,
2056 classPrefix = 'oo-ui-flaggableElement-';
2057
2058 if ( $.isArray( flags ) ) {
2059 for ( i = 0, len = flags.length; i < len; i++ ) {
2060 flag = flags[i];
2061 // Set
2062 this.flags[flag] = true;
2063 this.$element.addClass( classPrefix + flag );
2064 }
2065 } else if ( OO.isPlainObject( flags ) ) {
2066 for ( flag in flags ) {
2067 if ( flags[flag] ) {
2068 // Set
2069 this.flags[flag] = true;
2070 this.$element.addClass( classPrefix + flag );
2071 } else {
2072 // Remove
2073 delete this.flags[flag];
2074 this.$element.removeClass( classPrefix + flag );
2075 }
2076 }
2077 }
2078 return this;
2079 };
2080 /**
2081 * Element containing a sequence of child elements.
2082 *
2083 * @class
2084 * @abstract
2085 *
2086 * @constructor
2087 * @param {jQuery} $group Container node, assigned to #$group
2088 * @param {Object} [config] Configuration options
2089 * @cfg {Object.<string,string>} [aggregations] Events to aggregate, keyed by item event name
2090 */
2091 OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
2092 // Configuration
2093 config = config || {};
2094
2095 // Properties
2096 this.$group = $group;
2097 this.items = [];
2098 this.$items = this.$( [] );
2099 this.aggregate = !$.isEmptyObject( config.aggregations );
2100 this.aggregations = config.aggregations || {};
2101 };
2102
2103 /* Methods */
2104
2105 /**
2106 * Get items.
2107 *
2108 * @method
2109 * @returns {OO.ui.Element[]} Items
2110 */
2111 OO.ui.GroupElement.prototype.getItems = function () {
2112 return this.items.slice( 0 );
2113 };
2114
2115 /**
2116 * Add items.
2117 *
2118 * @method
2119 * @param {OO.ui.Element[]} items Item
2120 * @param {number} [index] Index to insert items at
2121 * @chainable
2122 */
2123 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
2124 var i, len, item, event, events, currentIndex,
2125 $items = this.$( [] );
2126
2127 for ( i = 0, len = items.length; i < len; i++ ) {
2128 item = items[i];
2129
2130 // Check if item exists then remove it first, effectively "moving" it
2131 currentIndex = $.inArray( item, this.items );
2132 if ( currentIndex >= 0 ) {
2133 this.removeItems( [ item ] );
2134 // Adjust index to compensate for removal
2135 if ( currentIndex < index ) {
2136 index--;
2137 }
2138 }
2139 // Add the item
2140 if ( this.aggregate ) {
2141 events = {};
2142 for ( event in this.aggregations ) {
2143 events[event] = [ 'emit', this.aggregations[event], item ];
2144 }
2145 item.connect( this, events );
2146 }
2147 item.setElementGroup( this );
2148 $items = $items.add( item.$element );
2149 }
2150
2151 if ( index === undefined || index < 0 || index >= this.items.length ) {
2152 this.$group.append( $items );
2153 this.items.push.apply( this.items, items );
2154 } else if ( index === 0 ) {
2155 this.$group.prepend( $items );
2156 this.items.unshift.apply( this.items, items );
2157 } else {
2158 this.$items.eq( index ).before( $items );
2159 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
2160 }
2161
2162 this.$items = this.$items.add( $items );
2163
2164 return this;
2165 };
2166
2167 /**
2168 * Remove items.
2169 *
2170 * Items will be detached, not removed, so they can be used later.
2171 *
2172 * @method
2173 * @param {OO.ui.Element[]} items Items to remove
2174 * @chainable
2175 */
2176 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
2177 var i, len, item, index;
2178
2179 // Remove specific items
2180 for ( i = 0, len = items.length; i < len; i++ ) {
2181 item = items[i];
2182 index = $.inArray( item, this.items );
2183 if ( index !== -1 ) {
2184 if ( this.aggregate ) {
2185 item.disconnect( this );
2186 }
2187 item.setElementGroup( null );
2188 this.items.splice( index, 1 );
2189 item.$element.detach();
2190 this.$items = this.$items.not( item.$element );
2191 }
2192 }
2193
2194 return this;
2195 };
2196
2197 /**
2198 * Clear all items.
2199 *
2200 * Items will be detached, not removed, so they can be used later.
2201 *
2202 * @method
2203 * @chainable
2204 */
2205 OO.ui.GroupElement.prototype.clearItems = function () {
2206 var i, len, item;
2207
2208 // Remove all items
2209 for ( i = 0, len = this.items.length; i < len; i++ ) {
2210 item = this.items[i];
2211 if ( this.aggregate ) {
2212 item.disconnect( this );
2213 }
2214 item.setElementGroup( null );
2215 }
2216 this.items = [];
2217 this.$items.detach();
2218 this.$items = this.$( [] );
2219 };
2220 /**
2221 * Element containing an icon.
2222 *
2223 * @class
2224 * @abstract
2225 *
2226 * @constructor
2227 * @param {jQuery} $icon Icon node, assigned to #$icon
2228 * @param {Object} [config] Configuration options
2229 * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
2230 * use the 'default' key to specify the icon to be used when there is no icon in the user's
2231 * language
2232 */
2233 OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) {
2234 // Config intialization
2235 config = config || {};
2236
2237 // Properties
2238 this.$icon = $icon;
2239 this.icon = null;
2240
2241 // Initialization
2242 this.$icon.addClass( 'oo-ui-iconedElement-icon' );
2243 this.setIcon( config.icon || this.constructor.static.icon );
2244 };
2245
2246 /* Static Properties */
2247
2248 OO.ui.IconedElement.static = {};
2249
2250 /**
2251 * Icon.
2252 *
2253 * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
2254 *
2255 * For i18n purposes, this property can be an object containing a `default` icon name property and
2256 * additional icon names keyed by language code.
2257 *
2258 * Example of i18n icon definition:
2259 * { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' }
2260 *
2261 * @static
2262 * @inheritable
2263 * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
2264 * use the 'default' key to specify the icon to be used when there is no icon in the user's
2265 * language
2266 */
2267 OO.ui.IconedElement.static.icon = null;
2268
2269 /* Methods */
2270
2271 /**
2272 * Set icon.
2273 *
2274 * @method
2275 * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID;
2276 * use the 'default' key to specify the icon to be used when there is no icon in the user's
2277 * language
2278 * @chainable
2279 */
2280 OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
2281 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
2282
2283 if ( this.icon ) {
2284 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
2285 }
2286 if ( typeof icon === 'string' ) {
2287 icon = icon.trim();
2288 if ( icon.length ) {
2289 this.$icon.addClass( 'oo-ui-icon-' + icon );
2290 this.icon = icon;
2291 }
2292 }
2293 this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
2294
2295 return this;
2296 };
2297
2298 /**
2299 * Get icon.
2300 *
2301 * @method
2302 * @returns {string} Icon
2303 */
2304 OO.ui.IconedElement.prototype.getIcon = function () {
2305 return this.icon;
2306 };
2307 /**
2308 * Element containing an indicator.
2309 *
2310 * @class
2311 * @abstract
2312 *
2313 * @constructor
2314 * @param {jQuery} $indicator Indicator node, assigned to #$indicator
2315 * @param {Object} [config] Configuration options
2316 * @cfg {string} [indicator] Symbolic indicator name
2317 * @cfg {string} [indicatorTitle] Indicator title text or a function that return text
2318 */
2319 OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
2320 // Config intialization
2321 config = config || {};
2322
2323 // Properties
2324 this.$indicator = $indicator;
2325 this.indicator = null;
2326 this.indicatorLabel = null;
2327
2328 // Initialization
2329 this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
2330 this.setIndicator( config.indicator || this.constructor.static.indicator );
2331 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
2332 };
2333
2334 /* Static Properties */
2335
2336 OO.ui.IndicatedElement.static = {};
2337
2338 /**
2339 * indicator.
2340 *
2341 * @static
2342 * @inheritable
2343 * @property {string|null} Symbolic indicator name or null for no indicator
2344 */
2345 OO.ui.IndicatedElement.static.indicator = null;
2346
2347 /**
2348 * Indicator title.
2349 *
2350 * @static
2351 * @inheritable
2352 * @property {string|Function|null} Indicator title text, a function that return text or null for no
2353 * indicator title
2354 */
2355 OO.ui.IndicatedElement.static.indicatorTitle = null;
2356
2357 /* Methods */
2358
2359 /**
2360 * Set indicator.
2361 *
2362 * @method
2363 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
2364 * @chainable
2365 */
2366 OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) {
2367 if ( this.indicator ) {
2368 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
2369 this.indicator = null;
2370 }
2371 if ( typeof indicator === 'string' ) {
2372 indicator = indicator.trim();
2373 if ( indicator.length ) {
2374 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
2375 this.indicator = indicator;
2376 }
2377 }
2378 this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator );
2379
2380 return this;
2381 };
2382
2383 /**
2384 * Set indicator label.
2385 *
2386 * @method
2387 * @param {string|Function|null} indicator Indicator title text, a function that return text or null
2388 * for no indicator title
2389 * @chainable
2390 */
2391 OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
2392 this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
2393
2394 if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
2395 this.$indicator.attr( 'title', indicatorTitle );
2396 } else {
2397 this.$indicator.removeAttr( 'title' );
2398 }
2399
2400 return this;
2401 };
2402
2403 /**
2404 * Get indicator.
2405 *
2406 * @method
2407 * @returns {string} title Symbolic name of indicator
2408 */
2409 OO.ui.IndicatedElement.prototype.getIndicator = function () {
2410 return this.indicator;
2411 };
2412
2413 /**
2414 * Get indicator title.
2415 *
2416 * @method
2417 * @returns {string} Indicator title text
2418 */
2419 OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
2420 return this.indicatorTitle;
2421 };
2422 /**
2423 * Element containing a label.
2424 *
2425 * @class
2426 * @abstract
2427 *
2428 * @constructor
2429 * @param {jQuery} $label Label node, assigned to #$label
2430 * @param {Object} [config] Configuration options
2431 * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
2432 */
2433 OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
2434 // Config intialization
2435 config = config || {};
2436
2437 // Properties
2438 this.$label = $label;
2439 this.label = null;
2440
2441 // Initialization
2442 this.$label.addClass( 'oo-ui-labeledElement-label' );
2443 this.setLabel( config.label || this.constructor.static.label );
2444 };
2445
2446 /* Static Properties */
2447
2448 OO.ui.LabeledElement.static = {};
2449
2450 /**
2451 * Label.
2452 *
2453 * @static
2454 * @inheritable
2455 * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
2456 * no label
2457 */
2458 OO.ui.LabeledElement.static.label = null;
2459
2460 /* Methods */
2461
2462 /**
2463 * Set the label.
2464 *
2465 * An empty string will result in the label being hidden. A string containing only whitespace will
2466 * be converted to a single &nbsp;
2467 *
2468 * @method
2469 * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
2470 * text; or null for no label
2471 * @chainable
2472 */
2473 OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
2474 var empty = false;
2475
2476 this.label = label = OO.ui.resolveMsg( label ) || null;
2477 if ( typeof label === 'string' && label.length ) {
2478 if ( label.match( /^\s*$/ ) ) {
2479 // Convert whitespace only string to a single non-breaking space
2480 this.$label.html( '&nbsp;' );
2481 } else {
2482 this.$label.text( label );
2483 }
2484 } else if ( label instanceof jQuery ) {
2485 this.$label.empty().append( label );
2486 } else {
2487 this.$label.empty();
2488 empty = true;
2489 }
2490 this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
2491 this.$label.css( 'display', empty ? 'none' : '' );
2492
2493 return this;
2494 };
2495
2496 /**
2497 * Get the label.
2498 *
2499 * @method
2500 * @returns {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2501 * text; or null for no label
2502 */
2503 OO.ui.LabeledElement.prototype.getLabel = function () {
2504 return this.label;
2505 };
2506
2507 /**
2508 * Fit the label.
2509 *
2510 * @method
2511 * @chainable
2512 */
2513 OO.ui.LabeledElement.prototype.fitLabel = function () {
2514 if ( this.$label.autoEllipsis ) {
2515 this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
2516 }
2517 return this;
2518 };
2519 /**
2520 * Popuppable element.
2521 *
2522 * @class
2523 * @abstract
2524 *
2525 * @constructor
2526 * @param {Object} [config] Configuration options
2527 * @cfg {number} [popupWidth=320] Width of popup
2528 * @cfg {number} [popupHeight] Height of popup
2529 * @cfg {Object} [popup] Configuration to pass to popup
2530 */
2531 OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
2532 // Configuration initialization
2533 config = $.extend( { 'popupWidth': 320 }, config );
2534
2535 // Properties
2536 this.popup = new OO.ui.PopupWidget( $.extend(
2537 { 'align': 'center', 'autoClose': true },
2538 config.popup,
2539 { '$': this.$, '$autoCloseIgnore': this.$element }
2540 ) );
2541 this.popupWidth = config.popupWidth;
2542 this.popupHeight = config.popupHeight;
2543 };
2544
2545 /* Methods */
2546
2547 /**
2548 * Get popup.
2549 *
2550 * @method
2551 * @returns {OO.ui.PopupWidget} Popup widget
2552 */
2553 OO.ui.PopuppableElement.prototype.getPopup = function () {
2554 return this.popup;
2555 };
2556
2557 /**
2558 * Show popup.
2559 *
2560 * @method
2561 */
2562 OO.ui.PopuppableElement.prototype.showPopup = function () {
2563 this.popup.show().display( this.popupWidth, this.popupHeight );
2564 };
2565
2566 /**
2567 * Hide popup.
2568 *
2569 * @method
2570 */
2571 OO.ui.PopuppableElement.prototype.hidePopup = function () {
2572 this.popup.hide();
2573 };
2574 /**
2575 * Element with a title.
2576 *
2577 * @class
2578 * @abstract
2579 *
2580 * @constructor
2581 * @param {jQuery} $label Titled node, assigned to #$titled
2582 * @param {Object} [config] Configuration options
2583 * @cfg {string|Function} [title] Title text or a function that returns text
2584 */
2585 OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
2586 // Config intialization
2587 config = config || {};
2588
2589 // Properties
2590 this.$titled = $titled;
2591 this.title = null;
2592
2593 // Initialization
2594 this.setTitle( config.title || this.constructor.static.title );
2595 };
2596
2597 /* Static Properties */
2598
2599 OO.ui.TitledElement.static = {};
2600
2601 /**
2602 * Title.
2603 *
2604 * @static
2605 * @inheritable
2606 * @property {string|Function} Title text or a function that returns text
2607 */
2608 OO.ui.TitledElement.static.title = null;
2609
2610 /* Methods */
2611
2612 /**
2613 * Set title.
2614 *
2615 * @method
2616 * @param {string|Function|null} title Title text, a function that returns text or null for no title
2617 * @chainable
2618 */
2619 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
2620 this.title = title = OO.ui.resolveMsg( title ) || null;
2621
2622 if ( typeof title === 'string' && title.length ) {
2623 this.$titled.attr( 'title', title );
2624 } else {
2625 this.$titled.removeAttr( 'title' );
2626 }
2627
2628 return this;
2629 };
2630
2631 /**
2632 * Get title.
2633 *
2634 * @method
2635 * @returns {string} Title string
2636 */
2637 OO.ui.TitledElement.prototype.getTitle = function () {
2638 return this.title;
2639 };
2640 /**
2641 * Generic toolbar tool.
2642 *
2643 * @class
2644 * @abstract
2645 * @extends OO.ui.Widget
2646 * @mixins OO.ui.IconedElement
2647 *
2648 * @constructor
2649 * @param {OO.ui.ToolGroup} toolGroup
2650 * @param {Object} [config] Configuration options
2651 * @cfg {string|Function} [title] Title text or a function that returns text
2652 */
2653 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
2654 // Config intialization
2655 config = config || {};
2656
2657 // Parent constructor
2658 OO.ui.Widget.call( this, config );
2659
2660 // Mixin constructors
2661 OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
2662
2663 // Properties
2664 this.toolGroup = toolGroup;
2665 this.toolbar = this.toolGroup.getToolbar();
2666 this.active = false;
2667 this.$title = this.$( '<span>' );
2668 this.$link = this.$( '<a>' );
2669 this.title = null;
2670
2671 // Events
2672 this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
2673
2674 // Initialization
2675 this.$title.addClass( 'oo-ui-tool-title' );
2676 this.$link
2677 .addClass( 'oo-ui-tool-link' )
2678 .append( this.$icon, this.$title );
2679 this.$element
2680 .data( 'oo-ui-tool', this )
2681 .addClass(
2682 'oo-ui-tool ' + 'oo-ui-tool-name-' +
2683 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
2684 )
2685 .append( this.$link );
2686 this.setTitle( config.title || this.constructor.static.title );
2687 };
2688
2689 /* Inheritance */
2690
2691 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
2692
2693 OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
2694
2695 /* Events */
2696
2697 /**
2698 * @event select
2699 */
2700
2701 /* Static Properties */
2702
2703 OO.ui.Tool.static.tagName = 'span';
2704
2705 /**
2706 * Symbolic name of tool.
2707 *
2708 * @abstract
2709 * @static
2710 * @property {string}
2711 * @inheritable
2712 */
2713 OO.ui.Tool.static.name = '';
2714
2715 /**
2716 * Tool group.
2717 *
2718 * @abstract
2719 * @static
2720 * @property {string}
2721 * @inheritable
2722 */
2723 OO.ui.Tool.static.group = '';
2724
2725 /**
2726 * Tool title.
2727 *
2728 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
2729 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
2730 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
2731 * appended to the title if the tool is part of a bar tool group.
2732 *
2733 * @abstract
2734 * @static
2735 * @property {string|Function} Title text or a function that returns text
2736 * @inheritable
2737 */
2738 OO.ui.Tool.static.title = '';
2739
2740 /**
2741 * Tool can be automatically added to tool groups.
2742 *
2743 * @static
2744 * @property {boolean}
2745 * @inheritable
2746 */
2747 OO.ui.Tool.static.autoAdd = true;
2748
2749 /**
2750 * Check if this tool is compatible with given data.
2751 *
2752 * @method
2753 * @static
2754 * @inheritable
2755 * @param {Mixed} data Data to check
2756 * @returns {boolean} Tool can be used with data
2757 */
2758 OO.ui.Tool.static.isCompatibleWith = function () {
2759 return false;
2760 };
2761
2762 /* Methods */
2763
2764 /**
2765 * Handle the toolbar state being updated.
2766 *
2767 * This is an abstract method that must be overridden in a concrete subclass.
2768 *
2769 * @abstract
2770 * @method
2771 */
2772 OO.ui.Tool.prototype.onUpdateState = function () {
2773 throw new Error(
2774 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
2775 );
2776 };
2777
2778 /**
2779 * Handle the tool being selected.
2780 *
2781 * This is an abstract method that must be overridden in a concrete subclass.
2782 *
2783 * @abstract
2784 * @method
2785 */
2786 OO.ui.Tool.prototype.onSelect = function () {
2787 throw new Error(
2788 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
2789 );
2790 };
2791
2792 /**
2793 * Check if the button is active.
2794 *
2795 * @method
2796 * @param {boolean} Button is active
2797 */
2798 OO.ui.Tool.prototype.isActive = function () {
2799 return this.active;
2800 };
2801
2802 /**
2803 * Make the button appear active or inactive.
2804 *
2805 * @method
2806 * @param {boolean} state Make button appear active
2807 */
2808 OO.ui.Tool.prototype.setActive = function ( state ) {
2809 this.active = !!state;
2810 if ( this.active ) {
2811 this.$element.addClass( 'oo-ui-tool-active' );
2812 } else {
2813 this.$element.removeClass( 'oo-ui-tool-active' );
2814 }
2815 };
2816
2817 /**
2818 * Get the tool title.
2819 *
2820 * @method
2821 * @param {string|Function} title Title text or a function that returns text
2822 * @chainable
2823 */
2824 OO.ui.Tool.prototype.setTitle = function ( title ) {
2825 this.title = OO.ui.resolveMsg( title );
2826 this.updateTitle();
2827 return this;
2828 };
2829
2830 /**
2831 * Get the tool title.
2832 *
2833 * @method
2834 * @returns {string} Title text
2835 */
2836 OO.ui.Tool.prototype.getTitle = function () {
2837 return this.title;
2838 };
2839
2840 /**
2841 * Get the tool's symbolic name.
2842 *
2843 * @method
2844 * @returns {string} Symbolic name of tool
2845 */
2846 OO.ui.Tool.prototype.getName = function () {
2847 return this.constructor.static.name;
2848 };
2849
2850 /**
2851 * Update the title.
2852 *
2853 * @method
2854 */
2855 OO.ui.Tool.prototype.updateTitle = function () {
2856 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
2857 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
2858 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
2859 tooltipParts = [];
2860
2861 this.$title.empty()
2862 .text( this.title )
2863 .append(
2864 this.$( '<span>' )
2865 .addClass( 'oo-ui-tool-accel' )
2866 .text( accel )
2867 );
2868
2869 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
2870 tooltipParts.push( this.title );
2871 }
2872 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
2873 tooltipParts.push( accel );
2874 }
2875 if ( tooltipParts.length ) {
2876 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
2877 } else {
2878 this.$link.removeAttr( 'title' );
2879 }
2880 };
2881
2882 /**
2883 * Destroy tool.
2884 *
2885 * @method
2886 */
2887 OO.ui.Tool.prototype.destroy = function () {
2888 this.toolbar.disconnect( this );
2889 this.$element.remove();
2890 };
2891 /**
2892 * Collection of tool groups.
2893 *
2894 * @class
2895 * @extends OO.ui.Element
2896 * @mixins OO.EventEmitter
2897 * @mixins OO.ui.GroupElement
2898 *
2899 * @constructor
2900 * @param {OO.Factory} toolFactory Factory for creating tools
2901 * @param {Object} [config] Configuration options
2902 * @cfg {boolean} [actions] Add an actions section opposite to the tools
2903 * @cfg {boolean} [shadow] Add a shadow below the toolbar
2904 */
2905 OO.ui.Toolbar = function OoUiToolbar( toolFactory, config ) {
2906 // Configuration initialization
2907 config = config || {};
2908
2909 // Parent constructor
2910 OO.ui.Element.call( this, config );
2911
2912 // Mixin constructors
2913 OO.EventEmitter.call( this );
2914 OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
2915
2916 // Properties
2917 this.toolFactory = toolFactory;
2918 this.groups = [];
2919 this.tools = {};
2920 this.$bar = this.$( '<div>' );
2921 this.$actions = this.$( '<div>' );
2922 this.initialized = false;
2923
2924 // Events
2925 this.$element
2926 .add( this.$bar ).add( this.$group ).add( this.$actions )
2927 .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
2928
2929 // Initialization
2930 this.$group.addClass( 'oo-ui-toolbar-tools' );
2931 this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
2932 if ( config.actions ) {
2933 this.$actions.addClass( 'oo-ui-toolbar-actions' );
2934 this.$bar.append( this.$actions );
2935 }
2936 this.$bar.append( '<div style="clear:both"></div>' );
2937 if ( config.shadow ) {
2938 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
2939 }
2940 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
2941 };
2942
2943 /* Inheritance */
2944
2945 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
2946
2947 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
2948 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
2949
2950 /* Methods */
2951
2952 /**
2953 * Get the tool factory.
2954 *
2955 * @method
2956 * @returns {OO.Factory} Tool factory
2957 */
2958 OO.ui.Toolbar.prototype.getToolFactory = function () {
2959 return this.toolFactory;
2960 };
2961
2962 /**
2963 * Handles mouse down events.
2964 *
2965 * @method
2966 * @param {jQuery.Event} e Mouse down event
2967 */
2968 OO.ui.Toolbar.prototype.onMouseDown = function ( e ) {
2969 var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
2970 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
2971 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
2972 return false;
2973 }
2974 };
2975
2976 /**
2977 * Sets up handles and preloads required information for the toolbar to work.
2978 * This must be called immediately after it is attached to a visible document.
2979 */
2980 OO.ui.Toolbar.prototype.initialize = function () {
2981 this.initialized = true;
2982 };
2983
2984 /**
2985 * Setup toolbar.
2986 *
2987 * Tools can be specified in the following ways:
2988 * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
2989 * - All tools in a group: `{ 'group': 'group-name' }`
2990 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
2991 *
2992 * @method
2993 * @param {Object.<string,Array>} groups List of tool group configurations
2994 * @param {Array|string} [groups.include] Tools to include
2995 * @param {Array|string} [groups.exclude] Tools to exclude
2996 * @param {Array|string} [groups.promote] Tools to promote to the beginning
2997 * @param {Array|string} [groups.demote] Tools to demote to the end
2998 */
2999 OO.ui.Toolbar.prototype.setup = function ( groups ) {
3000 var i, len, type, group,
3001 items = [],
3002 // TODO: Use a registry instead
3003 defaultType = 'bar',
3004 constructors = {
3005 'bar': OO.ui.BarToolGroup,
3006 'list': OO.ui.ListToolGroup,
3007 'menu': OO.ui.MenuToolGroup
3008 };
3009
3010 // Cleanup previous groups
3011 this.reset();
3012
3013 // Build out new groups
3014 for ( i = 0, len = groups.length; i < len; i++ ) {
3015 group = groups[i];
3016 if ( group.include === '*' ) {
3017 // Apply defaults to catch-all groups
3018 if ( group.type === undefined ) {
3019 group.type = 'list';
3020 }
3021 if ( group.label === undefined ) {
3022 group.label = 'ooui-toolbar-more';
3023 }
3024 }
3025 type = constructors[group.type] ? group.type : defaultType;
3026 items.push(
3027 new constructors[type]( this, $.extend( { '$': this.$ }, group ) )
3028 );
3029 }
3030 this.addItems( items );
3031 };
3032
3033 /**
3034 * Remove all tools and groups from the toolbar.
3035 */
3036 OO.ui.Toolbar.prototype.reset = function () {
3037 var i, len;
3038
3039 this.groups = [];
3040 this.tools = {};
3041 for ( i = 0, len = this.items.length; i < len; i++ ) {
3042 this.items[i].destroy();
3043 }
3044 this.clearItems();
3045 };
3046
3047 /**
3048 * Destroys toolbar, removing event handlers and DOM elements.
3049 *
3050 * Call this whenever you are done using a toolbar.
3051 */
3052 OO.ui.Toolbar.prototype.destroy = function () {
3053 this.reset();
3054 this.$element.remove();
3055 };
3056
3057 /**
3058 * Check if tool has not been used yet.
3059 *
3060 * @param {string} name Symbolic name of tool
3061 * @return {boolean} Tool is available
3062 */
3063 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
3064 return !this.tools[name];
3065 };
3066
3067 /**
3068 * Prevent tool from being used again.
3069 *
3070 * @param {OO.ui.Tool} tool Tool to reserve
3071 */
3072 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
3073 this.tools[tool.getName()] = tool;
3074 };
3075
3076 /**
3077 * Allow tool to be used again.
3078 *
3079 * @param {OO.ui.Tool} tool Tool to release
3080 */
3081 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
3082 delete this.tools[tool.getName()];
3083 };
3084
3085 /**
3086 * Get accelerator label for tool.
3087 *
3088 * This is a stub that should be overridden to provide access to accelerator information.
3089 *
3090 * @param {string} name Symbolic name of tool
3091 * @returns {string|undefined} Tool accelerator label if available
3092 */
3093 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
3094 return undefined;
3095 };
3096 /**
3097 * Factory for tools.
3098 *
3099 * @class
3100 * @extends OO.Factory
3101 * @constructor
3102 */
3103 OO.ui.ToolFactory = function OoUiToolFactory() {
3104 // Parent constructor
3105 OO.Factory.call( this );
3106 };
3107
3108 /* Inheritance */
3109
3110 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3111
3112 /* Methods */
3113
3114 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3115 var i, len, included, promoted, demoted,
3116 auto = [],
3117 used = {};
3118
3119 // Collect included and not excluded tools
3120 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3121
3122 // Promotion
3123 promoted = this.extract( promote, used );
3124 demoted = this.extract( demote, used );
3125
3126 // Auto
3127 for ( i = 0, len = included.length; i < len; i++ ) {
3128 if ( !used[included[i]] ) {
3129 auto.push( included[i] );
3130 }
3131 }
3132
3133 return promoted.concat( auto ).concat( demoted );
3134 };
3135
3136 /**
3137 * Get a flat list of names from a list of names or groups.
3138 *
3139 * Tools can be specified in the following ways:
3140 * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
3141 * - All tools in a group: `{ 'group': 'group-name' }`
3142 * - All tools: `'*'`
3143 *
3144 * @private
3145 * @param {Array|string} collection List of tools
3146 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3147 * names will be added as properties
3148 * @return {string[]} List of extracted names
3149 */
3150 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3151 var i, len, item, name, tool,
3152 names = [];
3153
3154 if ( collection === '*' ) {
3155 for ( name in this.registry ) {
3156 tool = this.registry[name];
3157 if (
3158 // Only add tools by group name when auto-add is enabled
3159 tool.static.autoAdd &&
3160 // Exclude already used tools
3161 ( !used || !used[name] )
3162 ) {
3163 names.push( name );
3164 if ( used ) {
3165 used[name] = true;
3166 }
3167 }
3168 }
3169 } else if ( $.isArray( collection ) ) {
3170 for ( i = 0, len = collection.length; i < len; i++ ) {
3171 item = collection[i];
3172 // Allow plain strings as shorthand for named tools
3173 if ( typeof item === 'string' ) {
3174 item = { 'name': item };
3175 }
3176 if ( OO.isPlainObject( item ) ) {
3177 if ( item.group ) {
3178 for ( name in this.registry ) {
3179 tool = this.registry[name];
3180 if (
3181 // Include tools with matching group
3182 tool.static.group === item.group &&
3183 // Only add tools by group name when auto-add is enabled
3184 tool.static.autoAdd &&
3185 // Exclude already used tools
3186 ( !used || !used[name] )
3187 ) {
3188 names.push( name );
3189 if ( used ) {
3190 used[name] = true;
3191 }
3192 }
3193 }
3194 }
3195 // Include tools with matching name and exclude already used tools
3196 else if ( item.name && ( !used || !used[item.name] ) ) {
3197 names.push( item.name );
3198 if ( used ) {
3199 used[item.name] = true;
3200 }
3201 }
3202 }
3203 }
3204 }
3205 return names;
3206 };
3207 /**
3208 * Collection of tools.
3209 *
3210 * @class
3211 * @abstract
3212 * @extends OO.ui.Widget
3213 * @mixins OO.ui.GroupElement
3214 *
3215 * Tools can be specified in the following ways:
3216 * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
3217 * - All tools in a group: `{ 'group': 'group-name' }`
3218 * - All tools: `'*'`
3219 *
3220 * @constructor
3221 * @param {OO.ui.Toolbar} toolbar
3222 * @param {Object} [config] Configuration options
3223 * @cfg {Array|string} [include=[]] List of tools to include
3224 * @cfg {Array|string} [exclude=[]] List of tools to exclude
3225 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
3226 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
3227 */
3228 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
3229 // Configuration initialization
3230 config = config || {};
3231
3232 // Parent constructor
3233 OO.ui.Widget.call( this, config );
3234
3235 // Mixin constructors
3236 OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
3237
3238 // Properties
3239 this.toolbar = toolbar;
3240 this.tools = {};
3241 this.pressed = null;
3242 this.include = config.include || [];
3243 this.exclude = config.exclude || [];
3244 this.promote = config.promote || [];
3245 this.demote = config.demote || [];
3246 this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
3247
3248 // Events
3249 this.$element.on( {
3250 'mousedown': OO.ui.bind( this.onMouseDown, this ),
3251 'mouseup': OO.ui.bind( this.onMouseUp, this ),
3252 'mouseover': OO.ui.bind( this.onMouseOver, this ),
3253 'mouseout': OO.ui.bind( this.onMouseOut, this )
3254 } );
3255 this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } );
3256
3257 // Initialization
3258 this.$group.addClass( 'oo-ui-toolGroup-tools' );
3259 this.$element
3260 .addClass( 'oo-ui-toolGroup' )
3261 .append( this.$group );
3262 this.populate();
3263 };
3264
3265 /* Inheritance */
3266
3267 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
3268
3269 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
3270
3271 /* Events */
3272
3273 /**
3274 * @event update
3275 */
3276
3277 /* Static Properties */
3278
3279 /**
3280 * Show labels in tooltips.
3281 *
3282 * @static
3283 * @property {boolean}
3284 * @inheritable
3285 */
3286 OO.ui.ToolGroup.static.titleTooltips = false;
3287
3288 /**
3289 * Show acceleration labels in tooltips.
3290 *
3291 * @static
3292 * @property {boolean}
3293 * @inheritable
3294 */
3295 OO.ui.ToolGroup.static.accelTooltips = false;
3296
3297 /* Methods */
3298
3299 /**
3300 * Handle mouse down events.
3301 *
3302 * @method
3303 * @param {jQuery.Event} e Mouse down event
3304 */
3305 OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
3306 if ( !this.disabled && e.which === 1 ) {
3307 this.pressed = this.getTargetTool( e );
3308 if ( this.pressed ) {
3309 this.pressed.setActive( true );
3310 this.getElementDocument().addEventListener(
3311 'mouseup', this.onCapturedMouseUpHandler, true
3312 );
3313 return false;
3314 }
3315 }
3316 };
3317
3318 /**
3319 * Handle captured mouse up events.
3320 *
3321 * @method
3322 * @param {Event} e Mouse up event
3323 */
3324 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
3325 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
3326 // onMouseUp may be called a second time, depending on where the mouse is when the button is
3327 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
3328 this.onMouseUp( e );
3329 };
3330
3331 /**
3332 * Handle mouse up events.
3333 *
3334 * @method
3335 * @param {jQuery.Event} e Mouse up event
3336 */
3337 OO.ui.ToolGroup.prototype.onMouseUp = function ( e ) {
3338 var tool = this.getTargetTool( e );
3339
3340 if ( !this.disabled && e.which === 1 && this.pressed && this.pressed === tool ) {
3341 this.pressed.onSelect();
3342 }
3343
3344 this.pressed = null;
3345 return false;
3346 };
3347
3348 /**
3349 * Handle mouse over events.
3350 *
3351 * @method
3352 * @param {jQuery.Event} e Mouse over event
3353 */
3354 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
3355 var tool = this.getTargetTool( e );
3356
3357 if ( this.pressed && this.pressed === tool ) {
3358 this.pressed.setActive( true );
3359 }
3360 };
3361
3362 /**
3363 * Handle mouse out events.
3364 *
3365 * @method
3366 * @param {jQuery.Event} e Mouse out event
3367 */
3368 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
3369 var tool = this.getTargetTool( e );
3370
3371 if ( this.pressed && this.pressed === tool ) {
3372 this.pressed.setActive( false );
3373 }
3374 };
3375
3376 /**
3377 * Get the closest tool to a jQuery.Event.
3378 *
3379 * Only tool links are considered, which prevents other elements in the tool such as popups from
3380 * triggering tool group interactions.
3381 *
3382 * @method
3383 * @private
3384 * @param {jQuery.Event} e
3385 * @returns {OO.ui.Tool|null} Tool, `null` if none was found
3386 */
3387 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
3388 var tool,
3389 $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
3390
3391 if ( $item.length ) {
3392 tool = $item.parent().data( 'oo-ui-tool' );
3393 }
3394
3395 return tool && !tool.isDisabled() ? tool : null;
3396 };
3397
3398 /**
3399 * Handle tool registry register events.
3400 *
3401 * If a tool is registered after the group is created, we must repopulate the list to account for:
3402 * - a tool being added that may be included
3403 * - a tool already included being overridden
3404 *
3405 * @param {string} name Symbolic name of tool
3406 */
3407 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
3408 this.populate();
3409 };
3410
3411 /**
3412 * Get the toolbar this group is in.
3413 *
3414 * @return {OO.ui.Toolbar} Toolbar of group
3415 */
3416 OO.ui.ToolGroup.prototype.getToolbar = function () {
3417 return this.toolbar;
3418 };
3419
3420 /**
3421 * Add and remove tools based on configuration.
3422 *
3423 * @method
3424 */
3425 OO.ui.ToolGroup.prototype.populate = function () {
3426 var i, len, name, tool,
3427 toolFactory = this.toolbar.getToolFactory(),
3428 names = {},
3429 add = [],
3430 remove = [],
3431 list = this.toolbar.getToolFactory().getTools(
3432 this.include, this.exclude, this.promote, this.demote
3433 );
3434
3435 // Build a list of needed tools
3436 for ( i = 0, len = list.length; i < len; i++ ) {
3437 name = list[i];
3438 if (
3439 // Tool exists
3440 toolFactory.lookup( name ) &&
3441 // Tool is available or is already in this group
3442 ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
3443 ) {
3444 tool = this.tools[name];
3445 if ( !tool ) {
3446 // Auto-initialize tools on first use
3447 this.tools[name] = tool = toolFactory.create( name, this );
3448 tool.updateTitle();
3449 }
3450 this.toolbar.reserveTool( tool );
3451 add.push( tool );
3452 names[name] = true;
3453 }
3454 }
3455 // Remove tools that are no longer needed
3456 for ( name in this.tools ) {
3457 if ( !names[name] ) {
3458 this.tools[name].destroy();
3459 this.toolbar.releaseTool( this.tools[name] );
3460 remove.push( this.tools[name] );
3461 delete this.tools[name];
3462 }
3463 }
3464 if ( remove.length ) {
3465 this.removeItems( remove );
3466 }
3467 // Update emptiness state
3468 if ( add.length ) {
3469 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
3470 } else {
3471 this.$element.addClass( 'oo-ui-toolGroup-empty' );
3472 }
3473 // Re-add tools (moving existing ones to new locations)
3474 this.addItems( add );
3475 };
3476
3477 /**
3478 * Destroy tool group.
3479 *
3480 * @method
3481 */
3482 OO.ui.ToolGroup.prototype.destroy = function () {
3483 var name;
3484
3485 this.clearItems();
3486 this.toolbar.getToolFactory().disconnect( this );
3487 for ( name in this.tools ) {
3488 this.toolbar.releaseTool( this.tools[name] );
3489 this.tools[name].disconnect( this ).destroy();
3490 delete this.tools[name];
3491 }
3492 this.$element.remove();
3493 };
3494 /**
3495 * Layout made of a fieldset and optional legend.
3496 *
3497 * Just add OO.ui.FieldLayout items.
3498 *
3499 * @class
3500 * @extends OO.ui.Layout
3501 * @mixins OO.ui.LabeledElement
3502 * @mixins OO.ui.IconedElement
3503 * @mixins OO.ui.GroupElement
3504 *
3505 * @constructor
3506 * @param {Object} [config] Configuration options
3507 * @cfg {string} [icon] Symbolic icon name
3508 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
3509 */
3510 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
3511 // Config initialization
3512 config = config || {};
3513
3514 // Parent constructor
3515 OO.ui.Layout.call( this, config );
3516
3517 // Mixin constructors
3518 OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
3519 OO.ui.LabeledElement.call( this, this.$( '<legend>' ), config );
3520 OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
3521
3522 // Initialization
3523 this.$element
3524 .addClass( 'oo-ui-fieldsetLayout' )
3525 .append( this.$icon, this.$label, this.$group );
3526 if ( $.isArray( config.items ) ) {
3527 this.addItems( config.items );
3528 }
3529 };
3530
3531 /* Inheritance */
3532
3533 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
3534
3535 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
3536 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
3537 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
3538
3539 /* Static Properties */
3540
3541 OO.ui.FieldsetLayout.static.tagName = 'fieldset';
3542 /**
3543 * Layout made of a field and optional label.
3544 *
3545 * @class
3546 * @extends OO.ui.Layout
3547 * @mixins OO.ui.LabeledElement
3548 *
3549 * Available label alignment modes include:
3550 * - 'left': Label is before the field and aligned away from it, best for when the user will be
3551 * scanning for a specific label in a form with many fields
3552 * - 'right': Label is before the field and aligned toward it, best for forms the user is very
3553 * familiar with and will tab through field checking quickly to verify which field they are in
3554 * - 'top': Label is before the field and above it, best for when the use will need to fill out all
3555 * fields from top to bottom in a form with few fields
3556 * - 'inline': Label is after the field and aligned toward it, best for small boolean fields like
3557 * checkboxes or radio buttons
3558 *
3559 * @constructor
3560 * @param {OO.ui.Widget} field Field widget
3561 * @param {Object} [config] Configuration options
3562 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
3563 */
3564 OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
3565 // Config initialization
3566 config = $.extend( { 'align': 'left' }, config );
3567
3568 // Parent constructor
3569 OO.ui.Layout.call( this, config );
3570
3571 // Mixin constructors
3572 OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
3573
3574 // Properties
3575 this.$field = this.$( '<div>' );
3576 this.field = field;
3577 this.align = null;
3578
3579 // Events
3580 if ( this.field instanceof OO.ui.InputWidget ) {
3581 this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
3582 }
3583
3584 // Initialization
3585 this.$element.addClass( 'oo-ui-fieldLayout' );
3586 this.$field
3587 .addClass( 'oo-ui-fieldLayout-field' )
3588 .append( this.field.$element );
3589 this.setAlignment( config.align );
3590 };
3591
3592 /* Inheritance */
3593
3594 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
3595
3596 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
3597
3598 /* Methods */
3599
3600 /**
3601 * Handles label mouse click events.
3602 *
3603 * @method
3604 * @param {jQuery.Event} e Mouse click event
3605 */
3606 OO.ui.FieldLayout.prototype.onLabelClick = function () {
3607 this.field.simulateLabelClick();
3608 return false;
3609 };
3610
3611 /**
3612 * Get the field.
3613 *
3614 * @returns {OO.ui.Widget} Field widget
3615 */
3616 OO.ui.FieldLayout.prototype.getField = function () {
3617 return this.field;
3618 };
3619
3620 /**
3621 * Set the field alignment mode.
3622 *
3623 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
3624 * @chainable
3625 */
3626 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
3627 if ( value !== this.align ) {
3628 // Default to 'left'
3629 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
3630 value = 'left';
3631 }
3632 // Reorder elements
3633 if ( value === 'inline' ) {
3634 this.$element.append( this.$field, this.$label );
3635 } else {
3636 this.$element.append( this.$label, this.$field );
3637 }
3638 // Set classes
3639 if ( this.align ) {
3640 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
3641 }
3642 this.align = value;
3643 this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
3644 }
3645
3646 return this;
3647 };
3648 /**
3649 * Layout made of proportionally sized columns and rows.
3650 *
3651 * @class
3652 * @extends OO.ui.Layout
3653 *
3654 * @constructor
3655 * @param {OO.ui.PanelLayout[]} panels Panels in the grid
3656 * @param {Object} [config] Configuration options
3657 * @cfg {number[]} [widths] Widths of columns as ratios
3658 * @cfg {number[]} [heights] Heights of columns as ratios
3659 */
3660 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
3661 var i, len, widths;
3662
3663 // Config initialization
3664 config = config || {};
3665
3666 // Parent constructor
3667 OO.ui.Layout.call( this, config );
3668
3669 // Properties
3670 this.panels = [];
3671 this.widths = [];
3672 this.heights = [];
3673
3674 // Initialization
3675 this.$element.addClass( 'oo-ui-gridLayout' );
3676 for ( i = 0, len = panels.length; i < len; i++ ) {
3677 this.panels.push( panels[i] );
3678 this.$element.append( panels[i].$element );
3679 }
3680 if ( config.widths || config.heights ) {
3681 this.layout( config.widths || [1], config.heights || [1] );
3682 } else {
3683 // Arrange in columns by default
3684 widths = [];
3685 for ( i = 0, len = this.panels.length; i < len; i++ ) {
3686 widths[i] = 1;
3687 }
3688 this.layout( widths, [1] );
3689 }
3690 };
3691
3692 /* Inheritance */
3693
3694 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
3695
3696 /* Events */
3697
3698 /**
3699 * @event layout
3700 */
3701
3702 /**
3703 * @event update
3704 */
3705
3706 /* Static Properties */
3707
3708 OO.ui.GridLayout.static.tagName = 'div';
3709
3710 /* Methods */
3711
3712 /**
3713 * Set grid dimensions.
3714 *
3715 * @method
3716 * @param {number[]} widths Widths of columns as ratios
3717 * @param {number[]} heights Heights of rows as ratios
3718 * @fires layout
3719 * @throws {Error} If grid is not large enough to fit all panels
3720 */
3721 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
3722 var x, y,
3723 xd = 0,
3724 yd = 0,
3725 cols = widths.length,
3726 rows = heights.length;
3727
3728 // Verify grid is big enough to fit panels
3729 if ( cols * rows < this.panels.length ) {
3730 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
3731 }
3732
3733 // Sum up denominators
3734 for ( x = 0; x < cols; x++ ) {
3735 xd += widths[x];
3736 }
3737 for ( y = 0; y < rows; y++ ) {
3738 yd += heights[y];
3739 }
3740 // Store factors
3741 this.widths = [];
3742 this.heights = [];
3743 for ( x = 0; x < cols; x++ ) {
3744 this.widths[x] = widths[x] / xd;
3745 }
3746 for ( y = 0; y < rows; y++ ) {
3747 this.heights[y] = heights[y] / yd;
3748 }
3749 // Synchronize view
3750 this.update();
3751 this.emit( 'layout' );
3752 };
3753
3754 /**
3755 * Update panel positions and sizes.
3756 *
3757 * @method
3758 * @fires update
3759 */
3760 OO.ui.GridLayout.prototype.update = function () {
3761 var x, y, panel,
3762 i = 0,
3763 left = 0,
3764 top = 0,
3765 dimensions,
3766 width = 0,
3767 height = 0,
3768 cols = this.widths.length,
3769 rows = this.heights.length;
3770
3771 for ( y = 0; y < rows; y++ ) {
3772 for ( x = 0; x < cols; x++ ) {
3773 panel = this.panels[i];
3774 width = this.widths[x];
3775 height = this.heights[y];
3776 dimensions = {
3777 'width': Math.round( width * 100 ) + '%',
3778 'height': Math.round( height * 100 ) + '%',
3779 'top': Math.round( top * 100 ) + '%'
3780 };
3781 // If RTL, reverse:
3782 if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
3783 dimensions.right = Math.round( left * 100 ) + '%';
3784 } else {
3785 dimensions.left = Math.round( left * 100 ) + '%';
3786 }
3787 panel.$element.css( dimensions );
3788 i++;
3789 left += width;
3790 }
3791 top += height;
3792 left = 0;
3793 }
3794
3795 this.emit( 'update' );
3796 };
3797
3798 /**
3799 * Get a panel at a given position.
3800 *
3801 * The x and y position is affected by the current grid layout.
3802 *
3803 * @method
3804 * @param {number} x Horizontal position
3805 * @param {number} y Vertical position
3806 * @returns {OO.ui.PanelLayout} The panel at the given postion
3807 */
3808 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
3809 return this.panels[( x * this.widths.length ) + y];
3810 };
3811 /**
3812 * Layout containing a series of pages.
3813 *
3814 * @class
3815 * @extends OO.ui.Layout
3816 *
3817 * @constructor
3818 * @param {Object} [config] Configuration options
3819 * @cfg {boolean} [continuous=false] Show all pages, one after another
3820 * @cfg {boolean} [autoFocus=false] Focus on the first focusable element when changing to a page
3821 * @cfg {boolean} [outlined=false] Show an outline
3822 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
3823 * @cfg {Object[]} [adders] List of adders for controls, each with name, icon and title properties
3824 */
3825 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
3826 // Initialize configuration
3827 config = config || {};
3828
3829 // Parent constructor
3830 OO.ui.Layout.call( this, config );
3831
3832 // Properties
3833 this.currentPageName = null;
3834 this.pages = {};
3835 this.ignoreFocus = false;
3836 this.stackLayout = new OO.ui.StackLayout( { '$': this.$, 'continuous': !!config.continuous } );
3837 this.autoFocus = !!config.autoFocus;
3838 this.outlined = !!config.outlined;
3839 if ( this.outlined ) {
3840 this.editable = !!config.editable;
3841 this.adders = config.adders || null;
3842 this.outlineControlsWidget = null;
3843 this.outlineWidget = new OO.ui.OutlineWidget( { '$': this.$ } );
3844 this.outlinePanel = new OO.ui.PanelLayout( { '$': this.$, 'scrollable': true } );
3845 this.gridLayout = new OO.ui.GridLayout(
3846 [this.outlinePanel, this.stackLayout], { '$': this.$, 'widths': [1, 2] }
3847 );
3848 if ( this.editable ) {
3849 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
3850 this.outlineWidget,
3851 { '$': this.$, 'adders': this.adders }
3852 );
3853 }
3854 }
3855
3856 // Events
3857 this.stackLayout.connect( this, { 'set': 'onStackLayoutSet' } );
3858 if ( this.outlined ) {
3859 this.outlineWidget.connect( this, { 'select': 'onOutlineWidgetSelect' } );
3860 // Event 'focus' does not bubble, but 'focusin' does
3861 this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) );
3862 }
3863
3864 // Initialization
3865 this.$element.addClass( 'oo-ui-bookletLayout' );
3866 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
3867 if ( this.outlined ) {
3868 this.outlinePanel.$element
3869 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
3870 .append( this.outlineWidget.$element );
3871 if ( this.editable ) {
3872 this.outlinePanel.$element
3873 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
3874 .append( this.outlineControlsWidget.$element );
3875 }
3876 this.$element.append( this.gridLayout.$element );
3877 } else {
3878 this.$element.append( this.stackLayout.$element );
3879 }
3880 };
3881
3882 /* Inheritance */
3883
3884 OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout );
3885
3886 /* Events */
3887
3888 /**
3889 * @event set
3890 * @param {OO.ui.PageLayout} page Current page
3891 */
3892
3893 /**
3894 * @event add
3895 * @param {OO.ui.PageLayout[]} page Added pages
3896 * @param {number} index Index pages were added at
3897 */
3898
3899 /**
3900 * @event remove
3901 * @param {OO.ui.PageLayout[]} pages Removed pages
3902 */
3903
3904 /* Methods */
3905
3906 /**
3907 * Handle stack layout focus.
3908 *
3909 * @method
3910 * @param {jQuery.Event} e Focusin event
3911 */
3912 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
3913 var name, $target;
3914
3915 if ( this.ignoreFocus ) {
3916 // Avoid recursion from programmatic focus trigger in #onStackLayoutSet
3917 return;
3918 }
3919
3920 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
3921 for ( name in this.pages ) {
3922 if ( this.pages[ name ].$element[0] === $target[0] ) {
3923 this.setPage( name );
3924 break;
3925 }
3926 }
3927 };
3928
3929 /**
3930 * Handle stack layout set events.
3931 *
3932 * @method
3933 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
3934 */
3935 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
3936 if ( page ) {
3937 page.scrollElementIntoView( { 'complete': OO.ui.bind( function () {
3938 this.ignoreFocus = true;
3939 if ( this.autoFocus ) {
3940 page.$element.find( ':input:first' ).focus();
3941 }
3942 this.ignoreFocus = false;
3943 }, this ) } );
3944 }
3945 };
3946
3947 /**
3948 * Handle outline widget select events.
3949 *
3950 * @method
3951 * @param {OO.ui.OptionWidget|null} item Selected item
3952 */
3953 OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
3954 if ( item ) {
3955 this.setPage( item.getData() );
3956 }
3957 };
3958
3959 /**
3960 * Check if booklet has an outline.
3961 *
3962 * @method
3963 * @returns {boolean} Booklet is outlined
3964 */
3965 OO.ui.BookletLayout.prototype.isOutlined = function () {
3966 return this.outlined;
3967 };
3968
3969 /**
3970 * Check if booklet has editing controls.
3971 *
3972 * @method
3973 * @returns {boolean} Booklet is outlined
3974 */
3975 OO.ui.BookletLayout.prototype.isEditable = function () {
3976 return this.editable;
3977 };
3978
3979 /**
3980 * Get the outline widget.
3981 *
3982 * @method
3983 * @returns {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
3984 */
3985 OO.ui.BookletLayout.prototype.getOutline = function () {
3986 return this.outlineWidget;
3987 };
3988
3989 /**
3990 * Get the outline controls widget. If the outline is not editable, null is returned.
3991 *
3992 * @method
3993 * @returns {OO.ui.OutlineControlsWidget|null} The outline controls widget.
3994 */
3995 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
3996 return this.outlineControlsWidget;
3997 };
3998
3999 /**
4000 * Get a page by name.
4001 *
4002 * @method
4003 * @param {string} name Symbolic name of page
4004 * @returns {OO.ui.PageLayout|undefined} Page, if found
4005 */
4006 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
4007 return this.pages[name];
4008 };
4009
4010 /**
4011 * Get the current page name.
4012 *
4013 * @method
4014 * @returns {string|null} Current page name
4015 */
4016 OO.ui.BookletLayout.prototype.getPageName = function () {
4017 return this.currentPageName;
4018 };
4019
4020 /**
4021 * Add a page to the layout.
4022 *
4023 * When pages are added with the same names as existing pages, the existing pages will be
4024 * automatically removed before the new pages are added.
4025 *
4026 * @method
4027 * @param {OO.ui.PageLayout[]} pages Pages to add
4028 * @param {number} index Index to insert pages after
4029 * @fires add
4030 * @chainable
4031 */
4032 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
4033 var i, len, name, page, item,
4034 items = [],
4035 remove = [];
4036
4037 for ( i = 0, len = pages.length; i < len; i++ ) {
4038 page = pages[i];
4039 name = page.getName();
4040 if ( name in this.pages ) {
4041 // Remove page with same name
4042 remove.push( this.pages[name] );
4043 }
4044 this.pages[page.getName()] = page;
4045 if ( this.outlined ) {
4046 item = new OO.ui.OutlineItemWidget( name, page, { '$': this.$ } );
4047 page.setOutlineItem( item );
4048 items.push( item );
4049 }
4050 }
4051 if ( remove.length ) {
4052 this.removePages( remove );
4053 }
4054
4055 if ( this.outlined && items.length ) {
4056 this.outlineWidget.addItems( items, index );
4057 this.updateOutlineWidget();
4058 }
4059 this.stackLayout.addItems( pages, index );
4060 this.emit( 'add', pages, index );
4061
4062 return this;
4063 };
4064
4065 /**
4066 * Remove a page from the layout.
4067 *
4068 * @method
4069 * @fires remove
4070 * @chainable
4071 */
4072 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
4073 var i, len, name, page,
4074 items = [];
4075
4076 for ( i = 0, len = pages.length; i < len; i++ ) {
4077 page = pages[i];
4078 name = page.getName();
4079 delete this.pages[name];
4080 if ( this.outlined ) {
4081 items.push( this.outlineWidget.getItemFromData( name ) );
4082 page.setOutlineItem( null );
4083 }
4084 }
4085 if ( this.outlined && items.length ) {
4086 this.outlineWidget.removeItems( items );
4087 this.updateOutlineWidget();
4088 }
4089 this.stackLayout.removeItems( pages );
4090 this.emit( 'remove', pages );
4091
4092 return this;
4093 };
4094
4095 /**
4096 * Clear all pages from the layout.
4097 *
4098 * @method
4099 * @fires remove
4100 * @chainable
4101 */
4102 OO.ui.BookletLayout.prototype.clearPages = function () {
4103 var i, len,
4104 pages = this.stackLayout.getItems();
4105
4106 this.pages = {};
4107 this.currentPageName = null;
4108 if ( this.outlined ) {
4109 this.outlineWidget.clearItems();
4110 for ( i = 0, len = pages.length; i < len; i++ ) {
4111 pages[i].setOutlineItem( null );
4112 }
4113 }
4114 this.stackLayout.clearItems();
4115
4116 this.emit( 'remove', pages );
4117
4118 return this;
4119 };
4120
4121 /**
4122 * Set the current page by name.
4123 *
4124 * @method
4125 * @fires set
4126 * @param {string} name Symbolic name of page
4127 */
4128 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
4129 var selectedItem,
4130 page = this.pages[name];
4131
4132 if ( this.outlined ) {
4133 selectedItem = this.outlineWidget.getSelectedItem();
4134 if ( selectedItem && selectedItem.getData() !== name ) {
4135 this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
4136 }
4137 }
4138
4139 if ( page ) {
4140 this.currentPageName = name;
4141 this.stackLayout.setItem( page );
4142 this.emit( 'set', page );
4143 }
4144 };
4145
4146 /**
4147 * Call this after adding or removing items from the OutlineWidget.
4148 *
4149 * @method
4150 * @chainable
4151 */
4152 OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
4153 // Auto-select first item when nothing is selected anymore
4154 if ( !this.outlineWidget.getSelectedItem() ) {
4155 this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
4156 }
4157
4158 return this;
4159 };
4160 /**
4161 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
4162 *
4163 * @class
4164 * @extends OO.ui.Layout
4165 *
4166 * @constructor
4167 * @param {Object} [config] Configuration options
4168 * @cfg {boolean} [scrollable] Allow vertical scrolling
4169 * @cfg {boolean} [padded] Pad the content from the edges
4170 */
4171 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
4172 // Config initialization
4173 config = config || {};
4174
4175 // Parent constructor
4176 OO.ui.Layout.call( this, config );
4177
4178 // Initialization
4179 this.$element.addClass( 'oo-ui-panelLayout' );
4180 if ( config.scrollable ) {
4181 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
4182 }
4183
4184 if ( config.padded ) {
4185 this.$element.addClass( 'oo-ui-panelLayout-padded' );
4186 }
4187
4188 // Add directionality class:
4189 this.$element.addClass( 'oo-ui-' + OO.ui.Element.getDir( this.$.context ) );
4190 };
4191
4192 /* Inheritance */
4193
4194 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
4195 /**
4196 * Page within an OO.ui.BookletLayout.
4197 *
4198 * @class
4199 * @extends OO.ui.PanelLayout
4200 *
4201 * @constructor
4202 * @param {string} name Unique symbolic name of page
4203 * @param {Object} [config] Configuration options
4204 * @param {string} [outlineItem] Outline item widget
4205 */
4206 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
4207 // Configuration initialization
4208 config = $.extend( { 'scrollable': true }, config );
4209
4210 // Parent constructor
4211 OO.ui.PanelLayout.call( this, config );
4212
4213 // Properties
4214 this.name = name;
4215 this.outlineItem = config.outlineItem || null;
4216
4217 // Initialization
4218 this.$element.addClass( 'oo-ui-pageLayout' );
4219 };
4220
4221 /* Inheritance */
4222
4223 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
4224
4225 /* Methods */
4226
4227 /**
4228 * Get page name.
4229 *
4230 * @returns {string} Symbolic name of page
4231 */
4232 OO.ui.PageLayout.prototype.getName = function () {
4233 return this.name;
4234 };
4235
4236 /**
4237 * Get outline item.
4238 *
4239 * @returns {OO.ui.OutlineItemWidget|null} Outline item widget
4240 */
4241 OO.ui.PageLayout.prototype.getOutlineItem = function () {
4242 return this.outlineItem;
4243 };
4244
4245 /**
4246 * Get outline item.
4247 *
4248 * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
4249 * @chainable
4250 */
4251 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
4252 this.outlineItem = outlineItem;
4253 return this;
4254 };
4255 /**
4256 * Layout containing a series of mutually exclusive pages.
4257 *
4258 * @class
4259 * @extends OO.ui.PanelLayout
4260 * @mixins OO.ui.GroupElement
4261 *
4262 * @constructor
4263 * @param {Object} [config] Configuration options
4264 * @cfg {boolean} [continuous=false] Show all pages, one after another
4265 * @cfg {string} [icon=''] Symbolic icon name
4266 * @cfg {OO.ui.Layout[]} [items] Layouts to add
4267 */
4268 OO.ui.StackLayout = function OoUiStackLayout( config ) {
4269 // Config initialization
4270 config = $.extend( { 'scrollable': true }, config );
4271
4272 // Parent constructor
4273 OO.ui.PanelLayout.call( this, config );
4274
4275 // Mixin constructors
4276 OO.ui.GroupElement.call( this, this.$element, config );
4277
4278 // Properties
4279 this.currentItem = null;
4280 this.continuous = !!config.continuous;
4281
4282 // Initialization
4283 this.$element.addClass( 'oo-ui-stackLayout' );
4284 if ( this.continuous ) {
4285 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
4286 }
4287 if ( $.isArray( config.items ) ) {
4288 this.addItems( config.items );
4289 }
4290 };
4291
4292 /* Inheritance */
4293
4294 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
4295
4296 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
4297
4298 /* Events */
4299
4300 /**
4301 * @event set
4302 * @param {OO.ui.PanelLayout|null} [item] Current item
4303 */
4304
4305 /* Methods */
4306
4307 /**
4308 * Add items.
4309 *
4310 * Adding an existing item (by value) will move it.
4311 *
4312 * @method
4313 * @param {OO.ui.PanelLayout[]} items Items to add
4314 * @param {number} [index] Index to insert items after
4315 * @chainable
4316 */
4317 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
4318 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
4319
4320 if ( !this.currentItem && items.length ) {
4321 this.setItem( items[0] );
4322 }
4323
4324 return this;
4325 };
4326
4327 /**
4328 * Remove items.
4329 *
4330 * Items will be detached, not removed, so they can be used later.
4331 *
4332 * @method
4333 * @param {OO.ui.PanelLayout[]} items Items to remove
4334 * @chainable
4335 */
4336 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
4337 OO.ui.GroupElement.prototype.removeItems.call( this, items );
4338 if ( $.inArray( this.currentItem, items ) !== -1 ) {
4339 this.currentItem = null;
4340 if ( !this.currentItem && this.items.length ) {
4341 this.setItem( this.items[0] );
4342 }
4343 }
4344
4345 return this;
4346 };
4347
4348 /**
4349 * Clear all items.
4350 *
4351 * Items will be detached, not removed, so they can be used later.
4352 *
4353 * @method
4354 * @chainable
4355 */
4356 OO.ui.StackLayout.prototype.clearItems = function () {
4357 this.currentItem = null;
4358 OO.ui.GroupElement.prototype.clearItems.call( this );
4359
4360 return this;
4361 };
4362
4363 /**
4364 * Show item.
4365 *
4366 * Any currently shown item will be hidden.
4367 *
4368 * @method
4369 * @param {OO.ui.PanelLayout} item Item to show
4370 * @chainable
4371 */
4372 OO.ui.StackLayout.prototype.setItem = function ( item ) {
4373 if ( !this.continuous ) {
4374 this.$items.css( 'display', '' );
4375 }
4376 if ( $.inArray( item, this.items ) !== -1 ) {
4377 if ( !this.continuous ) {
4378 item.$element.css( 'display', 'block' );
4379 }
4380 } else {
4381 item = null;
4382 }
4383 this.currentItem = item;
4384 this.emit( 'set', item );
4385
4386 return this;
4387 };
4388 /**
4389 * Horizontal bar layout of tools as icon buttons.
4390 *
4391 * @class
4392 * @abstract
4393 * @extends OO.ui.ToolGroup
4394 *
4395 * @constructor
4396 * @param {OO.ui.Toolbar} toolbar
4397 * @param {Object} [config] Configuration options
4398 */
4399 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
4400 // Parent constructor
4401 OO.ui.ToolGroup.call( this, toolbar, config );
4402
4403 // Initialization
4404 this.$element.addClass( 'oo-ui-barToolGroup' );
4405 };
4406
4407 /* Inheritance */
4408
4409 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
4410
4411 /* Static Properties */
4412
4413 OO.ui.BarToolGroup.static.titleTooltips = true;
4414
4415 OO.ui.BarToolGroup.static.accelTooltips = true;
4416 /**
4417 * Popup list of tools with an icon and optional label.
4418 *
4419 * @class
4420 * @abstract
4421 * @extends OO.ui.ToolGroup
4422 * @mixins OO.ui.IconedElement
4423 * @mixins OO.ui.IndicatedElement
4424 * @mixins OO.ui.LabeledElement
4425 * @mixins OO.ui.TitledElement
4426 * @mixins OO.ui.ClippableElement
4427 *
4428 * @constructor
4429 * @param {OO.ui.Toolbar} toolbar
4430 * @param {Object} [config] Configuration options
4431 */
4432 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
4433 // Configuration initialization
4434 config = config || {};
4435
4436 // Parent constructor
4437 OO.ui.ToolGroup.call( this, toolbar, config );
4438
4439 // Mixin constructors
4440 OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
4441 OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
4442 OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
4443 OO.ui.TitledElement.call( this, this.$element, config );
4444 OO.ui.ClippableElement.call( this, this.$group, config );
4445
4446 // Properties
4447 this.active = false;
4448 this.dragging = false;
4449 this.onBlurHandler = OO.ui.bind( this.onBlur, this );
4450 this.$handle = this.$( '<span>' );
4451
4452 // Events
4453 this.$handle.on( {
4454 'mousedown': OO.ui.bind( this.onHandleMouseDown, this ),
4455 'mouseup': OO.ui.bind( this.onHandleMouseUp, this )
4456 } );
4457
4458 // Initialization
4459 this.$handle
4460 .addClass( 'oo-ui-popupToolGroup-handle' )
4461 .append( this.$icon, this.$label, this.$indicator );
4462 this.$element
4463 .addClass( 'oo-ui-popupToolGroup' )
4464 .prepend( this.$handle );
4465 };
4466
4467 /* Inheritance */
4468
4469 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
4470
4471 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconedElement );
4472 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatedElement );
4473 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabeledElement );
4474 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
4475 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
4476
4477 /* Static Properties */
4478
4479 /* Methods */
4480
4481 /**
4482 * Handle focus being lost.
4483 *
4484 * The event is actually generated from a mouseup, so it is not a normal blur event object.
4485 *
4486 * @method
4487 * @param {jQuery.Event} e Mouse up event
4488 */
4489 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
4490 // Only deactivate when clicking outside the dropdown element
4491 if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
4492 this.setActive( false );
4493 }
4494 };
4495
4496 /**
4497 * @inheritdoc
4498 */
4499 OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) {
4500 this.setActive( false );
4501 return OO.ui.ToolGroup.prototype.onMouseUp.call( this, e );
4502 };
4503
4504 /**
4505 * @inheritdoc
4506 */
4507 OO.ui.PopupToolGroup.prototype.onMouseDown = function ( e ) {
4508 return OO.ui.ToolGroup.prototype.onMouseDown.call( this, e );
4509 };
4510
4511 /**
4512 * Handle mouse up events.
4513 *
4514 * @method
4515 * @param {jQuery.Event} e Mouse up event
4516 */
4517 OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
4518 return false;
4519 };
4520
4521 /**
4522 * Handle mouse down events.
4523 *
4524 * @method
4525 * @param {jQuery.Event} e Mouse down event
4526 */
4527 OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
4528 if ( !this.disabled && e.which === 1 ) {
4529 this.setActive( !this.active );
4530 }
4531 return false;
4532 };
4533
4534 /**
4535 * Switch into active mode.
4536 *
4537 * When active, mouseup events anywhere in the document will trigger deactivation.
4538 *
4539 * @method
4540 */
4541 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
4542 value = !!value;
4543 if ( this.active !== value ) {
4544 this.active = value;
4545 if ( value ) {
4546 this.setClipping( true );
4547 this.$element.addClass( 'oo-ui-popupToolGroup-active' );
4548 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
4549 } else {
4550 this.setClipping( false );
4551 this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
4552 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
4553 }
4554 }
4555 };
4556 /**
4557 * Drop down list layout of tools as labeled icon buttons.
4558 *
4559 * @class
4560 * @abstract
4561 * @extends OO.ui.PopupToolGroup
4562 *
4563 * @constructor
4564 * @param {OO.ui.Toolbar} toolbar
4565 * @param {Object} [config] Configuration options
4566 */
4567 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
4568 // Parent constructor
4569 OO.ui.PopupToolGroup.call( this, toolbar, config );
4570
4571 // Initialization
4572 this.$element.addClass( 'oo-ui-listToolGroup' );
4573 };
4574
4575 /* Inheritance */
4576
4577 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
4578
4579 /* Static Properties */
4580
4581 OO.ui.ListToolGroup.static.accelTooltips = true;
4582 /**
4583 * Drop down menu layout of tools as selectable menu items.
4584 *
4585 * @class
4586 * @abstract
4587 * @extends OO.ui.PopupToolGroup
4588 *
4589 * @constructor
4590 * @param {OO.ui.Toolbar} toolbar
4591 * @param {Object} [config] Configuration options
4592 */
4593 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
4594 // Configuration initialization
4595 config = config || {};
4596
4597 // Parent constructor
4598 OO.ui.PopupToolGroup.call( this, toolbar, config );
4599
4600 // Events
4601 this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
4602
4603 // Initialization
4604 this.$element.addClass( 'oo-ui-menuToolGroup' );
4605 };
4606
4607 /* Inheritance */
4608
4609 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
4610
4611 /* Static Properties */
4612
4613 OO.ui.MenuToolGroup.static.accelTooltips = true;
4614
4615 /* Methods */
4616
4617 /**
4618 * Handle the toolbar state being updated.
4619 *
4620 * When the state changes, the title of each active item in the menu will be joined together and
4621 * used as a label for the group. The label will be empty if none of the items are active.
4622 *
4623 * @method
4624 */
4625 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
4626 var name,
4627 labelTexts = [];
4628
4629 for ( name in this.tools ) {
4630 if ( this.tools[name].isActive() ) {
4631 labelTexts.push( this.tools[name].getTitle() );
4632 }
4633 }
4634
4635 this.setLabel( labelTexts.join( ', ' ) );
4636 };
4637 /**
4638 * UserInterface popup tool.
4639 *
4640 * @abstract
4641 * @class
4642 * @extends OO.ui.Tool
4643 * @mixins OO.ui.PopuppableElement
4644 *
4645 * @constructor
4646 * @param {OO.ui.Toolbar} toolbar
4647 * @param {Object} [config] Configuration options
4648 */
4649 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
4650 // Parent constructor
4651 OO.ui.Tool.call( this, toolbar, config );
4652
4653 // Mixin constructors
4654 OO.ui.PopuppableElement.call( this, config );
4655
4656 // Initialization
4657 this.$element
4658 .addClass( 'oo-ui-popupTool' )
4659 .append( this.popup.$element );
4660 };
4661
4662 /* Inheritance */
4663
4664 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
4665
4666 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
4667
4668 /* Methods */
4669
4670 /**
4671 * Handle the tool being selected.
4672 *
4673 * @inheritdoc
4674 */
4675 OO.ui.PopupTool.prototype.onSelect = function () {
4676 if ( !this.disabled ) {
4677 if ( this.popup.isVisible() ) {
4678 this.hidePopup();
4679 } else {
4680 this.showPopup();
4681 }
4682 }
4683 this.setActive( false );
4684 return false;
4685 };
4686
4687 /**
4688 * Handle the toolbar state being updated.
4689 *
4690 * @inheritdoc
4691 */
4692 OO.ui.PopupTool.prototype.onUpdateState = function () {
4693 this.setActive( false );
4694 };
4695 /**
4696 * Group widget.
4697 *
4698 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
4699 *
4700 * @class
4701 * @abstract
4702 * @extends OO.ui.GroupElement
4703 *
4704 * @constructor
4705 * @param {jQuery} $group Container node, assigned to #$group
4706 * @param {Object} [config] Configuration options
4707 */
4708 OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
4709 // Parent constructor
4710 OO.ui.GroupElement.call( this, $element, config );
4711 };
4712
4713 /* Inheritance */
4714
4715 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
4716
4717 /* Methods */
4718
4719 /**
4720 * Set the disabled state of the widget.
4721 *
4722 * This will also update the disabled state of child widgets.
4723 *
4724 * @method
4725 * @param {boolean} disabled Disable widget
4726 * @chainable
4727 */
4728 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
4729 var i, len;
4730
4731 // Parent method
4732 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
4733
4734 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
4735 if ( this.items ) {
4736 for ( i = 0, len = this.items.length; i < len; i++ ) {
4737 this.items[i].updateDisabled();
4738 }
4739 }
4740
4741 return this;
4742 };
4743 /**
4744 * Item widget.
4745 *
4746 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
4747 *
4748 * @class
4749 * @abstract
4750 *
4751 * @constructor
4752 */
4753 OO.ui.ItemWidget = function OoUiItemWidget() {
4754 //
4755 };
4756
4757 /* Methods */
4758
4759 /**
4760 * Check if widget is disabled.
4761 *
4762 * Checks parent if present, making disabled state inheritable.
4763 *
4764 * @returns {boolean} Widget is disabled
4765 */
4766 OO.ui.ItemWidget.prototype.isDisabled = function () {
4767 return this.disabled ||
4768 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
4769 };
4770
4771 /**
4772 * Set group element is in.
4773 *
4774 * @param {OO.ui.GroupElement|null} group Group element, null if none
4775 * @chainable
4776 */
4777 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
4778 // Parent method
4779 OO.ui.Element.prototype.setElementGroup.call( this, group );
4780
4781 // Initialize item disabled states
4782 this.updateDisabled();
4783
4784 return this;
4785 };
4786 /**
4787 * Container for multiple related buttons.
4788 *
4789 * @class
4790 * @extends OO.ui.Widget
4791 * @mixins OO.ui.GroupElement
4792 *
4793 * @constructor
4794 * @param {Object} [config] Configuration options
4795 * @cfg {OO.ui.ButtonWidget} [items] Buttons to add
4796 */
4797 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
4798 // Parent constructor
4799 OO.ui.Widget.call( this, config );
4800
4801 // Mixin constructors
4802 OO.ui.GroupElement.call( this, this.$element, config );
4803
4804 // Initialization
4805 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
4806 if ( $.isArray( config.items ) ) {
4807 this.addItems( config.items );
4808 }
4809 };
4810
4811 /* Inheritance */
4812
4813 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
4814
4815 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
4816 /**
4817 * Creates an OO.ui.ButtonWidget object.
4818 *
4819 * @class
4820 * @abstract
4821 * @extends OO.ui.Widget
4822 * @mixins OO.ui.ButtonedElement
4823 * @mixins OO.ui.IconedElement
4824 * @mixins OO.ui.IndicatedElement
4825 * @mixins OO.ui.LabeledElement
4826 * @mixins OO.ui.TitledElement
4827 * @mixins OO.ui.FlaggableElement
4828 *
4829 * @constructor
4830 * @param {Object} [config] Configuration options
4831 * @cfg {string} [title=''] Title text
4832 * @cfg {string} [href] Hyperlink to visit when clicked
4833 * @cfg {string} [target] Target to open hyperlink in
4834 */
4835 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
4836 // Configuration initialization
4837 config = $.extend( { 'target': '_blank' }, config );
4838
4839 // Parent constructor
4840 OO.ui.Widget.call( this, config );
4841
4842 // Mixin constructors
4843 OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
4844 OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
4845 OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
4846 OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
4847 OO.ui.TitledElement.call( this, this.$button, config );
4848 OO.ui.FlaggableElement.call( this, config );
4849
4850 // Properties
4851 this.isHyperlink = typeof config.href === 'string';
4852
4853 // Events
4854 this.$button.on( {
4855 'click': OO.ui.bind( this.onClick, this ),
4856 'keypress': OO.ui.bind( this.onKeyPress, this )
4857 } );
4858
4859 // Initialization
4860 this.$button
4861 .append( this.$icon, this.$label, this.$indicator )
4862 .attr( { 'href': config.href, 'target': config.target } );
4863 this.$element
4864 .addClass( 'oo-ui-buttonWidget' )
4865 .append( this.$button );
4866 };
4867
4868 /* Inheritance */
4869
4870 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
4871
4872 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonedElement );
4873 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconedElement );
4874 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatedElement );
4875 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabeledElement );
4876 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
4877 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
4878
4879 /* Events */
4880
4881 /**
4882 * @event click
4883 */
4884
4885 /* Methods */
4886
4887 /**
4888 * Handles mouse click events.
4889 *
4890 * @method
4891 * @param {jQuery.Event} e Mouse click event
4892 * @fires click
4893 */
4894 OO.ui.ButtonWidget.prototype.onClick = function () {
4895 if ( !this.disabled ) {
4896 this.emit( 'click' );
4897 if ( this.isHyperlink ) {
4898 return true;
4899 }
4900 }
4901 return false;
4902 };
4903
4904 /**
4905 * Handles keypress events.
4906 *
4907 * @method
4908 * @param {jQuery.Event} e Keypress event
4909 * @fires click
4910 */
4911 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
4912 if ( !this.disabled && e.which === OO.ui.Keys.SPACE ) {
4913 if ( this.isHyperlink ) {
4914 this.onClick();
4915 return true;
4916 }
4917 }
4918 return false;
4919 };
4920 /**
4921 * Creates an OO.ui.InputWidget object.
4922 *
4923 * @class
4924 * @abstract
4925 * @extends OO.ui.Widget
4926 *
4927 * @constructor
4928 * @param {Object} [config] Configuration options
4929 * @cfg {string} [name=''] HTML input name
4930 * @cfg {string} [value=''] Input value
4931 * @cfg {boolean} [readOnly=false] Prevent changes
4932 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
4933 */
4934 OO.ui.InputWidget = function OoUiInputWidget( config ) {
4935 // Config intialization
4936 config = $.extend( { 'readOnly': false }, config );
4937
4938 // Parent constructor
4939 OO.ui.Widget.call( this, config );
4940
4941 // Properties
4942 this.$input = this.getInputElement( config );
4943 this.value = '';
4944 this.readOnly = false;
4945 this.inputFilter = config.inputFilter;
4946
4947 // Events
4948 this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) );
4949
4950 // Initialization
4951 this.$input
4952 .attr( 'name', config.name )
4953 .prop( 'disabled', this.disabled );
4954 this.setReadOnly( config.readOnly );
4955 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
4956 this.setValue( config.value );
4957 };
4958
4959 /* Inheritance */
4960
4961 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
4962
4963 /* Events */
4964
4965 /**
4966 * @event change
4967 * @param value
4968 */
4969
4970 /* Methods */
4971
4972 /**
4973 * Get input element.
4974 *
4975 * @method
4976 * @param {Object} [config] Configuration options
4977 * @returns {jQuery} Input element
4978 */
4979 OO.ui.InputWidget.prototype.getInputElement = function () {
4980 return this.$( '<input>' );
4981 };
4982
4983 /**
4984 * Handle potentially value-changing events.
4985 *
4986 * @method
4987 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
4988 */
4989 OO.ui.InputWidget.prototype.onEdit = function () {
4990 if ( !this.disabled ) {
4991 // Allow the stack to clear so the value will be updated
4992 setTimeout( OO.ui.bind( function () {
4993 this.setValue( this.$input.val() );
4994 }, this ) );
4995 }
4996 };
4997
4998 /**
4999 * Get the value of the input.
5000 *
5001 * @method
5002 * @returns {string} Input value
5003 */
5004 OO.ui.InputWidget.prototype.getValue = function () {
5005 return this.value;
5006 };
5007
5008 /**
5009 * Sets the direction of the current input, either RTL or LTR
5010 *
5011 * @method
5012 * @param {boolean} isRTL
5013 */
5014 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
5015 if ( isRTL ) {
5016 this.$input.removeClass( 'oo-ui-ltr' );
5017 this.$input.addClass( 'oo-ui-rtl' );
5018 } else {
5019 this.$input.removeClass( 'oo-ui-rtl' );
5020 this.$input.addClass( 'oo-ui-ltr' );
5021 }
5022 };
5023
5024 /**
5025 * Set the value of the input.
5026 *
5027 * @method
5028 * @param {string} value New value
5029 * @fires change
5030 * @chainable
5031 */
5032 OO.ui.InputWidget.prototype.setValue = function ( value ) {
5033 value = this.sanitizeValue( value );
5034 if ( this.value !== value ) {
5035 this.value = value;
5036 this.emit( 'change', this.value );
5037 }
5038 // Update the DOM if it has changed. Note that with sanitizeValue, it
5039 // is possible for the DOM value to change without this.value changing.
5040 if ( this.$input.val() !== this.value ) {
5041 this.$input.val( this.value );
5042 }
5043 return this;
5044 };
5045
5046 /**
5047 * Sanitize incoming value.
5048 *
5049 * Ensures value is a string, and converts undefined and null to empty strings.
5050 *
5051 * @method
5052 * @param {string} value Original value
5053 * @returns {string} Sanitized value
5054 */
5055 OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
5056 if ( value === undefined || value === null ) {
5057 return '';
5058 } else if ( this.inputFilter ) {
5059 return this.inputFilter( String( value ) );
5060 } else {
5061 return String( value );
5062 }
5063 };
5064
5065 /**
5066 * Simulate the behavior of clicking on a label bound to this input.
5067 *
5068 * @method
5069 */
5070 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
5071 if ( !this.isDisabled() ) {
5072 if ( this.$input.is( ':checkbox,:radio' ) ) {
5073 this.$input.click();
5074 } else if ( this.$input.is( ':input' ) ) {
5075 this.$input.focus();
5076 }
5077 }
5078 };
5079
5080 /**
5081 * Check if the widget is read-only.
5082 *
5083 * @method
5084 * @param {boolean} Input is read-only
5085 */
5086 OO.ui.InputWidget.prototype.isReadOnly = function () {
5087 return this.readOnly;
5088 };
5089
5090 /**
5091 * Set the read-only state of the widget.
5092 *
5093 * This should probably change the widgets's appearance and prevent it from being used.
5094 *
5095 * @method
5096 * @param {boolean} state Make input read-only
5097 * @chainable
5098 */
5099 OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
5100 this.readOnly = !!state;
5101 this.$input.prop( 'readonly', this.readOnly );
5102 return this;
5103 };
5104
5105 /**
5106 * @inheritdoc
5107 */
5108 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
5109 OO.ui.Widget.prototype.setDisabled.call( this, state );
5110 if ( this.$input ) {
5111 this.$input.prop( 'disabled', this.disabled );
5112 }
5113 return this;
5114 };
5115 /**
5116 * Creates an OO.ui.CheckboxInputWidget object.
5117 *
5118 * @class
5119 * @extends OO.ui.InputWidget
5120 *
5121 * @constructor
5122 * @param {Object} [config] Configuration options
5123 */
5124 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
5125 // Parent constructor
5126 OO.ui.InputWidget.call( this, config );
5127
5128 // Initialization
5129 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
5130 };
5131
5132 /* Inheritance */
5133
5134 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
5135
5136 /* Events */
5137
5138 /* Methods */
5139
5140 /**
5141 * Get input element.
5142 *
5143 * @returns {jQuery} Input element
5144 */
5145 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
5146 return this.$( '<input type="checkbox" />' );
5147 };
5148
5149 /**
5150 * Get checked state of the checkbox
5151 *
5152 * @returns {boolean} If the checkbox is checked
5153 */
5154 OO.ui.CheckboxInputWidget.prototype.getValue = function () {
5155 return this.value;
5156 };
5157
5158 /**
5159 * Set value
5160 */
5161 OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
5162 value = !!value;
5163 if ( this.value !== value ) {
5164 this.value = value;
5165 this.$input.prop( 'checked', this.value );
5166 this.emit( 'change', this.value );
5167 }
5168 };
5169
5170 /**
5171 * @inheritdoc
5172 */
5173 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
5174 if ( !this.disabled ) {
5175 // Allow the stack to clear so the value will be updated
5176 setTimeout( OO.ui.bind( function () {
5177 this.setValue( this.$input.prop( 'checked' ) );
5178 }, this ) );
5179 }
5180 };
5181 /**
5182 * Creates an OO.ui.LabelWidget object.
5183 *
5184 * @class
5185 * @extends OO.ui.Widget
5186 * @mixins OO.ui.LabeledElement
5187 *
5188 * @constructor
5189 * @param {Object} [config] Configuration options
5190 */
5191 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
5192 // Config intialization
5193 config = config || {};
5194
5195 // Parent constructor
5196 OO.ui.Widget.call( this, config );
5197
5198 // Mixin constructors
5199 OO.ui.LabeledElement.call( this, this.$element, config );
5200
5201 // Properties
5202 this.input = config.input;
5203
5204 // Events
5205 if ( this.input instanceof OO.ui.InputWidget ) {
5206 this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
5207 }
5208
5209 // Initialization
5210 this.$element.addClass( 'oo-ui-labelWidget' );
5211 };
5212
5213 /* Inheritance */
5214
5215 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
5216
5217 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
5218
5219 /* Static Properties */
5220
5221 OO.ui.LabelWidget.static.tagName = 'label';
5222
5223 /* Methods */
5224
5225 /**
5226 * Handles label mouse click events.
5227 *
5228 * @method
5229 * @param {jQuery.Event} e Mouse click event
5230 */
5231 OO.ui.LabelWidget.prototype.onClick = function () {
5232 this.input.simulateLabelClick();
5233 return false;
5234 };
5235 /**
5236 * Lookup input widget.
5237 *
5238 * Mixin that adds a menu showing suggested values to a text input. Subclasses must handle `select`
5239 * events on #lookupMenu to make use of selections.
5240 *
5241 * @class
5242 * @abstract
5243 *
5244 * @constructor
5245 * @param {OO.ui.TextInputWidget} input Input widget
5246 * @param {Object} [config] Configuration options
5247 * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
5248 */
5249 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
5250 // Config intialization
5251 config = config || {};
5252
5253 // Properties
5254 this.lookupInput = input;
5255 this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
5256 this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
5257 '$': OO.ui.Element.getJQuery( this.$overlay ),
5258 'input': this.lookupInput,
5259 '$container': config.$container
5260 } );
5261 this.lookupCache = {};
5262 this.lookupQuery = null;
5263 this.lookupRequest = null;
5264 this.populating = false;
5265
5266 // Events
5267 this.$overlay.append( this.lookupMenu.$element );
5268
5269 this.lookupInput.$input.on( {
5270 'focus': OO.ui.bind( this.onLookupInputFocus, this ),
5271 'blur': OO.ui.bind( this.onLookupInputBlur, this ),
5272 'mousedown': OO.ui.bind( this.onLookupInputMouseDown, this )
5273 } );
5274 this.lookupInput.connect( this, { 'change': 'onLookupInputChange' } );
5275
5276 // Initialization
5277 this.$element.addClass( 'oo-ui-lookupWidget' );
5278 this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
5279 };
5280
5281 /* Methods */
5282
5283 /**
5284 * Handle input focus event.
5285 *
5286 * @method
5287 * @param {jQuery.Event} e Input focus event
5288 */
5289 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
5290 this.openLookupMenu();
5291 };
5292
5293 /**
5294 * Handle input blur event.
5295 *
5296 * @method
5297 * @param {jQuery.Event} e Input blur event
5298 */
5299 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
5300 this.lookupMenu.hide();
5301 };
5302
5303 /**
5304 * Handle input mouse down event.
5305 *
5306 * @method
5307 * @param {jQuery.Event} e Input mouse down event
5308 */
5309 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
5310 this.openLookupMenu();
5311 };
5312
5313 /**
5314 * Handle input change event.
5315 *
5316 * @method
5317 * @param {string} value New input value
5318 */
5319 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
5320 this.openLookupMenu();
5321 };
5322
5323 /**
5324 * Open the menu.
5325 *
5326 * @method
5327 * @chainable
5328 */
5329 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
5330 var value = this.lookupInput.getValue();
5331
5332 if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
5333 this.populateLookupMenu();
5334 if ( !this.lookupMenu.isVisible() ) {
5335 this.lookupMenu.show();
5336 }
5337 } else {
5338 this.lookupMenu.clearItems();
5339 this.lookupMenu.hide();
5340 }
5341
5342 return this;
5343 };
5344
5345 /**
5346 * Populate lookup menu with current information.
5347 *
5348 * @method
5349 * @chainable
5350 */
5351 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
5352 if ( !this.populating ) {
5353 this.populating = true;
5354 this.getLookupMenuItems()
5355 .done( OO.ui.bind( function ( items ) {
5356 this.lookupMenu.clearItems();
5357 if ( items.length ) {
5358 this.lookupMenu.show();
5359 this.lookupMenu.addItems( items );
5360 this.initializeLookupMenuSelection();
5361 this.openLookupMenu();
5362 } else {
5363 this.lookupMenu.hide();
5364 }
5365 this.populating = false;
5366 }, this ) )
5367 .fail( OO.ui.bind( function () {
5368 this.lookupMenu.clearItems();
5369 this.populating = false;
5370 }, this ) );
5371 }
5372
5373 return this;
5374 };
5375
5376 /**
5377 * Set selection in the lookup menu with current information.
5378 *
5379 * @method
5380 * @chainable
5381 */
5382 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
5383 if ( !this.lookupMenu.getSelectedItem() ) {
5384 this.lookupMenu.intializeSelection( this.lookupMenu.getFirstSelectableItem() );
5385 }
5386 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5387 };
5388
5389 /**
5390 * Get lookup menu items for the current query.
5391 *
5392 * @method
5393 * @returns {jQuery.Promise} Promise object which will be passed menu items as the first argument
5394 * of the done event
5395 */
5396 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
5397 var value = this.lookupInput.getValue(),
5398 deferred = $.Deferred();
5399
5400 if ( value && value !== this.lookupQuery ) {
5401 // Abort current request if query has changed
5402 if ( this.lookupRequest ) {
5403 this.lookupRequest.abort();
5404 this.lookupQuery = null;
5405 this.lookupRequest = null;
5406 }
5407 if ( value in this.lookupCache ) {
5408 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
5409 } else {
5410 this.lookupQuery = value;
5411 this.lookupRequest = this.getLookupRequest()
5412 .always( OO.ui.bind( function () {
5413 this.lookupQuery = null;
5414 this.lookupRequest = null;
5415 }, this ) )
5416 .done( OO.ui.bind( function ( data ) {
5417 this.lookupCache[value] = this.getLookupCacheItemFromData( data );
5418 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
5419 }, this ) )
5420 .fail( function () {
5421 deferred.reject();
5422 } );
5423 this.pushPending();
5424 this.lookupRequest.always( OO.ui.bind( function () {
5425 this.popPending();
5426 }, this ) );
5427 }
5428 }
5429 return deferred.promise();
5430 };
5431
5432 /**
5433 * Get a new request object of the current lookup query value.
5434 *
5435 * @method
5436 * @abstract
5437 * @returns {jqXHR} jQuery AJAX object, or promise object with an .abort() method
5438 */
5439 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
5440 // Stub, implemented in subclass
5441 return null;
5442 };
5443
5444 /**
5445 * Handle successful lookup request.
5446 *
5447 * Overriding methods should call #populateLookupMenu when results are available and cache results
5448 * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
5449 *
5450 * @method
5451 * @abstract
5452 * @param {Mixed} data Response from server
5453 */
5454 OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
5455 // Stub, implemented in subclass
5456 };
5457
5458 /**
5459 * Get a list of menu item widgets from the data stored by the lookup request's done handler.
5460 *
5461 * @method
5462 * @abstract
5463 * @param {Mixed} data Cached result data, usually an array
5464 * @returns {OO.ui.MenuItemWidget[]} Menu items
5465 */
5466 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
5467 // Stub, implemented in subclass
5468 return [];
5469 };
5470 /**
5471 * Creates an OO.ui.OptionWidget object.
5472 *
5473 * @class
5474 * @abstract
5475 * @extends OO.ui.Widget
5476 * @mixins OO.ui.IconedElement
5477 * @mixins OO.ui.LabeledElement
5478 * @mixins OO.ui.IndicatedElement
5479 * @mixins OO.ui.FlaggableElement
5480 *
5481 * @constructor
5482 * @param {Mixed} data Option data
5483 * @param {Object} [config] Configuration options
5484 * @cfg {boolean} [selected=false] Select option
5485 * @cfg {boolean} [highlighted=false] Highlight option
5486 * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
5487 */
5488 OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
5489 // Config intialization
5490 config = config || {};
5491
5492 // Parent constructor
5493 OO.ui.Widget.call( this, config );
5494
5495 // Mixin constructors
5496 OO.ui.ItemWidget.call( this );
5497 OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
5498 OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
5499 OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
5500 OO.ui.FlaggableElement.call( this, config );
5501
5502 // Properties
5503 this.data = data;
5504 this.selected = false;
5505 this.highlighted = false;
5506
5507 // Initialization
5508 this.$element
5509 .data( 'oo-ui-optionWidget', this )
5510 .attr( 'rel', config.rel )
5511 .addClass( 'oo-ui-optionWidget' )
5512 .append( this.$label );
5513 this.setSelected( config.selected );
5514 this.setHighlighted( config.highlighted );
5515
5516 // Options
5517 this.$element
5518 .prepend( this.$icon )
5519 .append( this.$indicator );
5520 };
5521
5522 /* Inheritance */
5523
5524 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
5525
5526 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
5527 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
5528 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
5529 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
5530 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
5531
5532 /* Static Properties */
5533
5534 OO.ui.OptionWidget.static.tagName = 'li';
5535
5536 OO.ui.OptionWidget.static.selectable = true;
5537
5538 OO.ui.OptionWidget.static.highlightable = true;
5539
5540 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
5541
5542 /* Methods */
5543
5544 /**
5545 * Check if option can be selected.
5546 *
5547 * @method
5548 * @returns {boolean} Item is selectable
5549 */
5550 OO.ui.OptionWidget.prototype.isSelectable = function () {
5551 return this.constructor.static.selectable && !this.disabled;
5552 };
5553
5554 /**
5555 * Check if option can be highlighted.
5556 *
5557 * @method
5558 * @returns {boolean} Item is highlightable
5559 */
5560 OO.ui.OptionWidget.prototype.isHighlightable = function () {
5561 return this.constructor.static.highlightable && !this.disabled;
5562 };
5563
5564 /**
5565 * Check if option is selected.
5566 *
5567 * @method
5568 * @returns {boolean} Item is selected
5569 */
5570 OO.ui.OptionWidget.prototype.isSelected = function () {
5571 return this.selected;
5572 };
5573
5574 /**
5575 * Check if option is highlighted.
5576 *
5577 * @method
5578 * @returns {boolean} Item is highlighted
5579 */
5580 OO.ui.OptionWidget.prototype.isHighlighted = function () {
5581 return this.highlighted;
5582 };
5583
5584 /**
5585 * Set selected state.
5586 *
5587 * @method
5588 * @param {boolean} [state=false] Select option
5589 * @chainable
5590 */
5591 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
5592 if ( !this.disabled && this.constructor.static.selectable ) {
5593 this.selected = !!state;
5594 if ( this.selected ) {
5595 this.$element.addClass( 'oo-ui-optionWidget-selected' );
5596 if ( this.constructor.static.scrollIntoViewOnSelect ) {
5597 this.scrollElementIntoView();
5598 }
5599 } else {
5600 this.$element.removeClass( 'oo-ui-optionWidget-selected' );
5601 }
5602 }
5603 return this;
5604 };
5605
5606 /**
5607 * Set highlighted state.
5608 *
5609 * @method
5610 * @param {boolean} [state=false] Highlight option
5611 * @chainable
5612 */
5613 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
5614 if ( !this.disabled && this.constructor.static.highlightable ) {
5615 this.highlighted = !!state;
5616 if ( this.highlighted ) {
5617 this.$element.addClass( 'oo-ui-optionWidget-highlighted' );
5618 } else {
5619 this.$element.removeClass( 'oo-ui-optionWidget-highlighted' );
5620 }
5621 }
5622 return this;
5623 };
5624
5625 /**
5626 * Make the option's highlight flash.
5627 *
5628 * @method
5629 * @param {Function} [done] Callback to execute when flash effect is complete.
5630 */
5631 OO.ui.OptionWidget.prototype.flash = function ( done ) {
5632 var $this = this.$element;
5633
5634 if ( !this.disabled && this.constructor.static.highlightable ) {
5635 $this.removeClass( 'oo-ui-optionWidget-highlighted' );
5636 setTimeout( OO.ui.bind( function () {
5637 $this.addClass( 'oo-ui-optionWidget-highlighted' );
5638 if ( done ) {
5639 setTimeout( done, 100 );
5640 }
5641 }, this ), 100 );
5642 }
5643 };
5644
5645 /**
5646 * Get option data.
5647 *
5648 * @method
5649 * @returns {Mixed} Option data
5650 */
5651 OO.ui.OptionWidget.prototype.getData = function () {
5652 return this.data;
5653 };
5654 /**
5655 * Create an OO.ui.SelectWidget object.
5656 *
5657 * @class
5658 * @abstract
5659 * @extends OO.ui.Widget
5660 * @mixins OO.ui.GroupElement
5661 *
5662 * @constructor
5663 * @param {Object} [config] Configuration options
5664 * @cfg {OO.ui.OptionWidget[]} [items] Options to add
5665 */
5666 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
5667 // Config intialization
5668 config = config || {};
5669
5670 // Parent constructor
5671 OO.ui.Widget.call( this, config );
5672
5673 // Mixin constructors
5674 OO.ui.GroupWidget.call( this, this.$element, config );
5675
5676 // Properties
5677 this.pressed = false;
5678 this.selecting = null;
5679 this.hashes = {};
5680
5681 // Events
5682 this.$element.on( {
5683 'mousedown': OO.ui.bind( this.onMouseDown, this ),
5684 'mouseup': OO.ui.bind( this.onMouseUp, this ),
5685 'mousemove': OO.ui.bind( this.onMouseMove, this ),
5686 'mouseover': OO.ui.bind( this.onMouseOver, this ),
5687 'mouseleave': OO.ui.bind( this.onMouseLeave, this )
5688 } );
5689
5690 // Initialization
5691 this.$element.addClass( 'oo-ui-selectWidget' );
5692 if ( $.isArray( config.items ) ) {
5693 this.addItems( config.items );
5694 }
5695 };
5696
5697 /* Inheritance */
5698
5699 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
5700
5701 // Need to mixin base class as well
5702 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
5703
5704 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
5705
5706 /* Events */
5707
5708 /**
5709 * @event highlight
5710 * @param {OO.ui.OptionWidget|null} item Highlighted item
5711 */
5712
5713 /**
5714 * @event select
5715 * @param {OO.ui.OptionWidget|null} item Selected item
5716 */
5717
5718 /**
5719 * @event add
5720 * @param {OO.ui.OptionWidget[]} items Added items
5721 * @param {number} index Index items were added at
5722 */
5723
5724 /**
5725 * @event remove
5726 * @param {OO.ui.OptionWidget[]} items Removed items
5727 */
5728
5729 /* Static Properties */
5730
5731 OO.ui.SelectWidget.static.tagName = 'ul';
5732
5733 /* Methods */
5734
5735 /**
5736 * Handle mouse down events.
5737 *
5738 * @method
5739 * @private
5740 * @param {jQuery.Event} e Mouse down event
5741 */
5742 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
5743 var item;
5744
5745 if ( !this.disabled && e.which === 1 ) {
5746 this.pressed = true;
5747 item = this.getTargetItem( e );
5748 if ( item && item.isSelectable() ) {
5749 this.intializeSelection( item );
5750 this.selecting = item;
5751 this.$( this.$.context ).one( 'mouseup', OO.ui.bind( this.onMouseUp, this ) );
5752 }
5753 }
5754 return false;
5755 };
5756
5757 /**
5758 * Handle mouse up events.
5759 *
5760 * @method
5761 * @private
5762 * @param {jQuery.Event} e Mouse up event
5763 */
5764 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
5765 var item;
5766 this.pressed = false;
5767 if ( !this.selecting ) {
5768 item = this.getTargetItem( e );
5769 if ( item && item.isSelectable() ) {
5770 this.selecting = item;
5771 }
5772 }
5773 if ( !this.disabled && e.which === 1 && this.selecting ) {
5774 this.selectItem( this.selecting );
5775 this.selecting = null;
5776 }
5777 return false;
5778 };
5779
5780 /**
5781 * Handle mouse move events.
5782 *
5783 * @method
5784 * @private
5785 * @param {jQuery.Event} e Mouse move event
5786 */
5787 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
5788 var item;
5789
5790 if ( !this.disabled && this.pressed ) {
5791 item = this.getTargetItem( e );
5792 if ( item && item !== this.selecting && item.isSelectable() ) {
5793 this.intializeSelection( item );
5794 this.selecting = item;
5795 }
5796 }
5797 return false;
5798 };
5799
5800 /**
5801 * Handle mouse over events.
5802 *
5803 * @method
5804 * @private
5805 * @param {jQuery.Event} e Mouse over event
5806 */
5807 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
5808 var item;
5809
5810 if ( !this.disabled ) {
5811 item = this.getTargetItem( e );
5812 if ( item && item.isHighlightable() ) {
5813 this.highlightItem( item );
5814 }
5815 }
5816 return false;
5817 };
5818
5819 /**
5820 * Handle mouse leave events.
5821 *
5822 * @method
5823 * @private
5824 * @param {jQuery.Event} e Mouse over event
5825 */
5826 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
5827 if ( !this.disabled ) {
5828 this.highlightItem();
5829 }
5830 return false;
5831 };
5832
5833 /**
5834 * Get the closest item to a jQuery.Event.
5835 *
5836 * @method
5837 * @private
5838 * @param {jQuery.Event} e
5839 * @returns {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5840 */
5841 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
5842 var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
5843 if ( $item.length ) {
5844 return $item.data( 'oo-ui-optionWidget' );
5845 }
5846 return null;
5847 };
5848
5849 /**
5850 * Get selected item.
5851 *
5852 * @method
5853 * @returns {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5854 */
5855 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
5856 var i, len;
5857
5858 for ( i = 0, len = this.items.length; i < len; i++ ) {
5859 if ( this.items[i].isSelected() ) {
5860 return this.items[i];
5861 }
5862 }
5863 return null;
5864 };
5865
5866 /**
5867 * Get highlighted item.
5868 *
5869 * @method
5870 * @returns {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5871 */
5872 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
5873 var i, len;
5874
5875 for ( i = 0, len = this.items.length; i < len; i++ ) {
5876 if ( this.items[i].isHighlighted() ) {
5877 return this.items[i];
5878 }
5879 }
5880 return null;
5881 };
5882
5883 /**
5884 * Get an existing item with equivilant data.
5885 *
5886 * @method
5887 * @param {Object} data Item data to search for
5888 * @returns {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
5889 */
5890 OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
5891 var hash = OO.getHash( data );
5892
5893 if ( hash in this.hashes ) {
5894 return this.hashes[hash];
5895 }
5896
5897 return null;
5898 };
5899
5900 /**
5901 * Highlight an item.
5902 *
5903 * Highlighting is mutually exclusive.
5904 *
5905 * @method
5906 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
5907 * @fires highlight
5908 * @chainable
5909 */
5910 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
5911 var i, len;
5912
5913 for ( i = 0, len = this.items.length; i < len; i++ ) {
5914 this.items[i].setHighlighted( this.items[i] === item );
5915 }
5916 this.emit( 'highlight', item );
5917
5918 return this;
5919 };
5920
5921 /**
5922 * Select an item.
5923 *
5924 * @method
5925 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5926 * @fires select
5927 * @chainable
5928 */
5929 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
5930 var i, len;
5931
5932 for ( i = 0, len = this.items.length; i < len; i++ ) {
5933 this.items[i].setSelected( this.items[i] === item );
5934 }
5935 this.emit( 'select', item );
5936
5937 return this;
5938 };
5939
5940 /**
5941 * Setup selection and highlighting.
5942 *
5943 * This should be used to synchronize the UI with the model without emitting events that would in
5944 * turn update the model.
5945 *
5946 * @param {OO.ui.OptionWidget} [item] Item to select
5947 * @chainable
5948 */
5949 OO.ui.SelectWidget.prototype.intializeSelection = function( item ) {
5950 var i, len, selected;
5951
5952 for ( i = 0, len = this.items.length; i < len; i++ ) {
5953 selected = this.items[i] === item;
5954 this.items[i].setSelected( selected );
5955 this.items[i].setHighlighted( selected );
5956 }
5957
5958 return this;
5959 };
5960
5961 /**
5962 * Get an item relative to another one.
5963 *
5964 * @method
5965 * @param {OO.ui.OptionWidget} item Item to start at
5966 * @param {number} direction Direction to move in
5967 * @returns {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
5968 */
5969 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
5970 var inc = direction > 0 ? 1 : -1,
5971 len = this.items.length,
5972 index = item instanceof OO.ui.OptionWidget ?
5973 $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
5974 stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
5975 i = inc > 0 ?
5976 // Default to 0 instead of -1, if nothing is selected let's start at the beginning
5977 Math.max( index, -1 ) :
5978 // Default to n-1 instead of -1, if nothing is selected let's start at the end
5979 Math.min( index, len );
5980
5981 while ( true ) {
5982 i = ( i + inc + len ) % len;
5983 item = this.items[i];
5984 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
5985 return item;
5986 }
5987 // Stop iterating when we've looped all the way around
5988 if ( i === stopAt ) {
5989 break;
5990 }
5991 }
5992 return null;
5993 };
5994
5995 /**
5996 * Get the next selectable item.
5997 *
5998 * @method
5999 * @returns {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
6000 */
6001 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
6002 var i, len, item;
6003
6004 for ( i = 0, len = this.items.length; i < len; i++ ) {
6005 item = this.items[i];
6006 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
6007 return item;
6008 }
6009 }
6010
6011 return null;
6012 };
6013
6014 /**
6015 * Add items.
6016 *
6017 * When items are added with the same values as existing items, the existing items will be
6018 * automatically removed before the new items are added.
6019 *
6020 * @method
6021 * @param {OO.ui.OptionWidget[]} items Items to add
6022 * @param {number} [index] Index to insert items after
6023 * @fires add
6024 * @chainable
6025 */
6026 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
6027 var i, len, item, hash,
6028 remove = [];
6029
6030 for ( i = 0, len = items.length; i < len; i++ ) {
6031 item = items[i];
6032 hash = OO.getHash( item.getData() );
6033 if ( hash in this.hashes ) {
6034 // Remove item with same value
6035 remove.push( this.hashes[hash] );
6036 }
6037 this.hashes[hash] = item;
6038 }
6039 if ( remove.length ) {
6040 this.removeItems( remove );
6041 }
6042
6043 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
6044
6045 // Always provide an index, even if it was omitted
6046 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
6047
6048 return this;
6049 };
6050
6051 /**
6052 * Remove items.
6053 *
6054 * Items will be detached, not removed, so they can be used later.
6055 *
6056 * @method
6057 * @param {OO.ui.OptionWidget[]} items Items to remove
6058 * @fires remove
6059 * @chainable
6060 */
6061 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
6062 var i, len, item, hash;
6063
6064 for ( i = 0, len = items.length; i < len; i++ ) {
6065 item = items[i];
6066 hash = OO.getHash( item.getData() );
6067 if ( hash in this.hashes ) {
6068 // Remove existing item
6069 delete this.hashes[hash];
6070 }
6071 if ( item.isSelected() ) {
6072 this.selectItem( null );
6073 }
6074 }
6075 OO.ui.GroupElement.prototype.removeItems.call( this, items );
6076
6077 this.emit( 'remove', items );
6078
6079 return this;
6080 };
6081
6082 /**
6083 * Clear all items.
6084 *
6085 * Items will be detached, not removed, so they can be used later.
6086 *
6087 * @method
6088 * @fires remove
6089 * @chainable
6090 */
6091 OO.ui.SelectWidget.prototype.clearItems = function () {
6092 var items = this.items.slice();
6093
6094 // Clear all items
6095 this.hashes = {};
6096 OO.ui.GroupElement.prototype.clearItems.call( this );
6097 this.selectItem( null );
6098
6099 this.emit( 'remove', items );
6100
6101 return this;
6102 };
6103 /**
6104 * Creates an OO.ui.MenuItemWidget object.
6105 *
6106 * @class
6107 * @extends OO.ui.OptionWidget
6108 *
6109 * @constructor
6110 * @param {Mixed} data Item data
6111 * @param {Object} [config] Configuration options
6112 */
6113 OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
6114 // Configuration initialization
6115 config = $.extend( { 'icon': 'check' }, config );
6116
6117 // Parent constructor
6118 OO.ui.OptionWidget.call( this, data, config );
6119
6120 // Initialization
6121 this.$element.addClass( 'oo-ui-menuItemWidget' );
6122 };
6123
6124 /* Inheritance */
6125
6126 OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.OptionWidget );
6127 /**
6128 * Create an OO.ui.MenuWidget object.
6129 *
6130 * @class
6131 * @extends OO.ui.SelectWidget
6132 * @mixins OO.ui.ClippableElement
6133 *
6134 * @constructor
6135 * @param {Object} [config] Configuration options
6136 * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
6137 */
6138 OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
6139 // Config intialization
6140 config = config || {};
6141
6142 // Parent constructor
6143 OO.ui.SelectWidget.call( this, config );
6144
6145 // Mixin constructors
6146 OO.ui.ClippableElement.call( this, this.$group, config );
6147
6148 // Properties
6149 this.newItems = null;
6150 this.$input = config.input ? config.input.$input : null;
6151 this.$previousFocus = null;
6152 this.isolated = !config.input;
6153 this.visible = false;
6154 this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
6155
6156 // Initialization
6157 this.$element.hide().addClass( 'oo-ui-menuWidget' );
6158 };
6159
6160 /* Inheritance */
6161
6162 OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
6163
6164 OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
6165
6166 /* Methods */
6167
6168 /**
6169 * Handles key down events.
6170 *
6171 * @method
6172 * @param {jQuery.Event} e Key down event
6173 */
6174 OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
6175 var nextItem,
6176 handled = false,
6177 highlightItem = this.getHighlightedItem();
6178
6179 if ( !this.disabled && this.visible ) {
6180 if ( !highlightItem ) {
6181 highlightItem = this.getSelectedItem();
6182 }
6183 switch ( e.keyCode ) {
6184 case OO.ui.Keys.ENTER:
6185 this.selectItem( highlightItem );
6186 handled = true;
6187 break;
6188 case OO.ui.Keys.UP:
6189 nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
6190 handled = true;
6191 break;
6192 case OO.ui.Keys.DOWN:
6193 nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
6194 handled = true;
6195 break;
6196 case OO.ui.Keys.ESCAPE:
6197 if ( highlightItem ) {
6198 highlightItem.setHighlighted( false );
6199 }
6200 this.hide();
6201 handled = true;
6202 break;
6203 }
6204
6205 if ( nextItem ) {
6206 this.highlightItem( nextItem );
6207 nextItem.scrollElementIntoView();
6208 }
6209
6210 if ( handled ) {
6211 e.preventDefault();
6212 e.stopPropagation();
6213 return false;
6214 }
6215 }
6216 };
6217
6218 /**
6219 * Check if the menu is visible.
6220 *
6221 * @method
6222 * @returns {boolean} Menu is visible
6223 */
6224 OO.ui.MenuWidget.prototype.isVisible = function () {
6225 return this.visible;
6226 };
6227
6228 /**
6229 * Bind key down listener
6230 *
6231 * @method
6232 */
6233 OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
6234 if ( this.$input ) {
6235 this.$input.on( 'keydown', this.onKeyDownHandler );
6236 } else {
6237 // Capture menu navigation keys
6238 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
6239 }
6240 };
6241
6242 /**
6243 * Unbind key down listener
6244 *
6245 * @method
6246 */
6247 OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
6248 if ( this.$input ) {
6249 this.$input.off( 'keydown' );
6250 } else {
6251 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
6252 }
6253 };
6254
6255 /**
6256 * Select an item.
6257 *
6258 * The menu will stay open if an item is silently selected.
6259 *
6260 * @method
6261 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6262 * @chainable
6263 */
6264 OO.ui.MenuWidget.prototype.selectItem = function ( item ) {
6265 // Parent method
6266 OO.ui.SelectWidget.prototype.selectItem.call( this, item );
6267
6268 if ( !this.disabled ) {
6269 if ( item ) {
6270 this.disabled = true;
6271 item.flash( OO.ui.bind( function () {
6272 this.hide();
6273 this.disabled = false;
6274 }, this ) );
6275 } else {
6276 this.hide();
6277 }
6278 }
6279
6280 return this;
6281 };
6282
6283 /**
6284 * Add items.
6285 *
6286 * Adding an existing item (by value) will move it.
6287 *
6288 * @method
6289 * @param {OO.ui.MenuItemWidget[]} items Items to add
6290 * @param {number} [index] Index to insert items after
6291 * @chainable
6292 */
6293 OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
6294 var i, len, item;
6295
6296 // Parent method
6297 OO.ui.SelectWidget.prototype.addItems.call( this, items, index );
6298
6299 // Auto-initialize
6300 if ( !this.newItems ) {
6301 this.newItems = [];
6302 }
6303
6304 for ( i = 0, len = items.length; i < len; i++ ) {
6305 item = items[i];
6306 if ( this.visible ) {
6307 // Defer fitting label until
6308 item.fitLabel();
6309 } else {
6310 this.newItems.push( item );
6311 }
6312 }
6313
6314 return this;
6315 };
6316
6317 /**
6318 * Show the menu.
6319 *
6320 * @method
6321 * @chainable
6322 */
6323 OO.ui.MenuWidget.prototype.show = function () {
6324 var i, len;
6325
6326 if ( this.items.length ) {
6327 this.$element.show();
6328 this.visible = true;
6329 this.bindKeyDownListener();
6330
6331 // Change focus to enable keyboard navigation
6332 if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
6333 this.$previousFocus = this.$( ':focus' );
6334 this.$input.focus();
6335 }
6336 if ( this.newItems && this.newItems.length ) {
6337 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
6338 this.newItems[i].fitLabel();
6339 }
6340 this.newItems = null;
6341 }
6342
6343 this.setClipping( true );
6344 }
6345
6346 return this;
6347 };
6348
6349 /**
6350 * Hide the menu.
6351 *
6352 * @method
6353 * @chainable
6354 */
6355 OO.ui.MenuWidget.prototype.hide = function () {
6356 this.$element.hide();
6357 this.visible = false;
6358 this.unbindKeyDownListener();
6359
6360 if ( this.isolated && this.$previousFocus ) {
6361 this.$previousFocus.focus();
6362 this.$previousFocus = null;
6363 }
6364
6365 this.setClipping( false );
6366
6367 return this;
6368 };
6369 /**
6370 * Inline menu of options.
6371 *
6372 * @class
6373 * @extends OO.ui.Widget
6374 * @mixins OO.ui.IconedElement
6375 * @mixins OO.ui.IndicatedElement
6376 * @mixins OO.ui.LabeledElement
6377 * @mixins OO.ui.TitledElement
6378 *
6379 * @constructor
6380 * @param {Object} [config] Configuration options
6381 * @cfg {Object} [menu] Configuration options to pass to menu widget
6382 */
6383 OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
6384 // Configuration initialization
6385 config = $.extend( { 'indicator': 'down' }, config );
6386
6387 // Parent constructor
6388 OO.ui.Widget.call( this, config );
6389
6390 // Mixin constructors
6391 OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
6392 OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
6393 OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
6394 OO.ui.TitledElement.call( this, this.$label, config );
6395
6396 // Properties
6397 this.menu = new OO.ui.MenuWidget( $.extend( { '$': this.$ }, config.menu ) );
6398 this.$handle = this.$( '<span>' );
6399
6400 // Events
6401 this.$element.on( { 'click': OO.ui.bind( this.onClick, this ) } );
6402 this.menu.connect( this, { 'select': 'onMenuSelect' } );
6403
6404 // Initialization
6405 this.$handle
6406 .addClass( 'oo-ui-inlineMenuWidget-handle' )
6407 .append( this.$icon, this.$label, this.$indicator );
6408 this.$element
6409 .addClass( 'oo-ui-inlineMenuWidget' )
6410 .append( this.$handle, this.menu.$element );
6411 };
6412
6413 /* Inheritance */
6414
6415 OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
6416
6417 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
6418 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
6419 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
6420 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
6421
6422 /* Methods */
6423
6424 /**
6425 * Get the menu.
6426 *
6427 * @return {OO.ui.MenuWidget} Menu of widget
6428 */
6429 OO.ui.InlineMenuWidget.prototype.getMenu = function () {
6430 return this.menu;
6431 };
6432
6433 /**
6434 * Handles menu select events.
6435 *
6436 * @method
6437 * @param {OO.ui.MenuItemWidget} item Selected menu item
6438 */
6439 OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
6440 this.setLabel( item.getLabel() );
6441 };
6442
6443 /**
6444 * Handles mouse click events.
6445 *
6446 * @method
6447 * @param {jQuery.Event} e Mouse click event
6448 */
6449 OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
6450 // Skip clicks within the menu
6451 if ( $.contains( this.menu.$element[0], e.target ) ) {
6452 return;
6453 }
6454
6455 if ( !this.disabled ) {
6456 if ( this.menu.isVisible() ) {
6457 this.menu.hide();
6458 } else {
6459 this.menu.show();
6460 }
6461 }
6462 return false;
6463 };
6464 /**
6465 * Creates an OO.ui.MenuSectionItemWidget object.
6466 *
6467 * @class
6468 * @extends OO.ui.OptionWidget
6469 *
6470 * @constructor
6471 * @param {Mixed} data Item data
6472 * @param {Object} [config] Configuration options
6473 */
6474 OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
6475 // Parent constructor
6476 OO.ui.OptionWidget.call( this, data, config );
6477
6478 // Initialization
6479 this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
6480 };
6481
6482 /* Inheritance */
6483
6484 OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.OptionWidget );
6485
6486 OO.ui.MenuSectionItemWidget.static.selectable = false;
6487
6488 OO.ui.MenuSectionItemWidget.static.highlightable = false;
6489 /**
6490 * Create an OO.ui.OutlineWidget object.
6491 *
6492 * @class
6493 * @extends OO.ui.SelectWidget
6494 *
6495 * @constructor
6496 * @param {Object} [config] Configuration options
6497 */
6498 OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
6499 // Config intialization
6500 config = config || {};
6501
6502 // Parent constructor
6503 OO.ui.SelectWidget.call( this, config );
6504
6505 // Initialization
6506 this.$element.addClass( 'oo-ui-outlineWidget' );
6507 };
6508
6509 /* Inheritance */
6510
6511 OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
6512 /**
6513 * Creates an OO.ui.OutlineControlsWidget object.
6514 *
6515 * @class
6516 *
6517 * @constructor
6518 * @param {OO.ui.OutlineWidget} outline Outline to control
6519 * @param {Object} [config] Configuration options
6520 * @cfg {Object[]} [adders] List of icons to show as addable item types, each an object with
6521 * name, title and icon properties
6522 */
6523 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
6524 // Configuration initialization
6525 config = config || {};
6526
6527 // Parent constructor
6528 OO.ui.Widget.call( this, config );
6529
6530 // Properties
6531 this.outline = outline;
6532 this.adders = {};
6533 this.$adders = this.$( '<div>' );
6534 this.$movers = this.$( '<div>' );
6535 this.addButton = new OO.ui.ButtonWidget( {
6536 '$': this.$,
6537 'frameless': true,
6538 'icon': 'add-item'
6539 } );
6540 this.upButton = new OO.ui.ButtonWidget( {
6541 '$': this.$,
6542 'frameless': true,
6543 'icon': 'collapse',
6544 'title': OO.ui.msg( 'ooui-outline-control-move-up' )
6545 } );
6546 this.downButton = new OO.ui.ButtonWidget( {
6547 '$': this.$,
6548 'frameless': true,
6549 'icon': 'expand',
6550 'title': OO.ui.msg( 'ooui-outline-control-move-down' )
6551 } );
6552
6553 // Events
6554 outline.connect( this, {
6555 'select': 'onOutlineChange',
6556 'add': 'onOutlineChange',
6557 'remove': 'onOutlineChange'
6558 } );
6559 this.upButton.connect( this, { 'click': ['emit', 'move', -1] } );
6560 this.downButton.connect( this, { 'click': ['emit', 'move', 1] } );
6561
6562 // Initialization
6563 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
6564 this.$adders.addClass( 'oo-ui-outlineControlsWidget-adders' );
6565 this.$movers
6566 .addClass( 'oo-ui-outlineControlsWidget-movers' )
6567 .append( this.upButton.$element, this.downButton.$element );
6568 this.$element.append( this.$adders, this.$movers );
6569 if ( config.adders && config.adders.length ) {
6570 this.setupAdders( config.adders );
6571 }
6572 };
6573
6574 /* Inheritance */
6575
6576 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
6577
6578 /* Events */
6579
6580 /**
6581 * @event move
6582 * @param {number} places Number of places to move
6583 */
6584
6585 /* Methods */
6586
6587 /**
6588 * Handle outline change events.
6589 *
6590 * @method
6591 */
6592 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
6593 var i, len, firstMovable, lastMovable,
6594 movable = false,
6595 items = this.outline.getItems(),
6596 selectedItem = this.outline.getSelectedItem();
6597
6598 if ( selectedItem && selectedItem.isMovable() ) {
6599 movable = true;
6600 i = -1;
6601 len = items.length;
6602 while ( ++i < len ) {
6603 if ( items[i].isMovable() ) {
6604 firstMovable = items[i];
6605 break;
6606 }
6607 }
6608 i = len;
6609 while ( i-- ) {
6610 if ( items[i].isMovable() ) {
6611 lastMovable = items[i];
6612 break;
6613 }
6614 }
6615 }
6616 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
6617 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
6618 };
6619
6620 /**
6621 * Setup adders icons.
6622 *
6623 * @method
6624 * @param {Object[]} adders List of configuations for adder buttons, each containing a name, title
6625 * and icon property
6626 */
6627 OO.ui.OutlineControlsWidget.prototype.setupAdders = function ( adders ) {
6628 var i, len, addition, button,
6629 $buttons = this.$( [] );
6630
6631 this.$adders.append( this.addButton.$element );
6632 for ( i = 0, len = adders.length; i < len; i++ ) {
6633 addition = adders[i];
6634 button = new OO.ui.ButtonWidget( {
6635 '$': this.$, 'frameless': true, 'icon': addition.icon, 'title': addition.title
6636 } );
6637 button.connect( this, { 'click': ['emit', 'add', addition.name] } );
6638 this.adders[addition.name] = button;
6639 this.$adders.append( button.$element );
6640 $buttons = $buttons.add( button.$element );
6641 }
6642 };
6643 /**
6644 * Creates an OO.ui.OutlineItemWidget object.
6645 *
6646 * @class
6647 * @extends OO.ui.OptionWidget
6648 *
6649 * @constructor
6650 * @param {Mixed} data Item data
6651 * @param {Object} [config] Configuration options
6652 * @cfg {number} [level] Indentation level
6653 * @cfg {boolean} [movable] Allow modification from outline controls
6654 */
6655 OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
6656 // Config intialization
6657 config = config || {};
6658
6659 // Parent constructor
6660 OO.ui.OptionWidget.call( this, data, config );
6661
6662 // Properties
6663 this.level = 0;
6664 this.movable = !!config.movable;
6665
6666 // Initialization
6667 this.$element.addClass( 'oo-ui-outlineItemWidget' );
6668 this.setLevel( config.level );
6669 };
6670
6671 /* Inheritance */
6672
6673 OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.OptionWidget );
6674
6675 /* Static Properties */
6676
6677 OO.ui.OutlineItemWidget.static.highlightable = false;
6678
6679 OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
6680
6681 OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
6682
6683 OO.ui.OutlineItemWidget.static.levels = 3;
6684
6685 /* Methods */
6686
6687 /**
6688 * Check if item is movable.
6689 *
6690 * Movablilty is used by outline controls.
6691 *
6692 * @returns {boolean} Item is movable
6693 */
6694 OO.ui.OutlineItemWidget.prototype.isMovable = function () {
6695 return this.movable;
6696 };
6697
6698 /**
6699 * Get indentation level.
6700 *
6701 * @returns {number} Indentation level
6702 */
6703 OO.ui.OutlineItemWidget.prototype.getLevel = function () {
6704 return this.level;
6705 };
6706
6707 /**
6708 * Set movability.
6709 *
6710 * Movablilty is used by outline controls.
6711 *
6712 * @param {boolean} movable Item is movable
6713 * @chainable
6714 */
6715 OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
6716 this.movable = !!movable;
6717 return this;
6718 };
6719
6720 /**
6721 * Set indentation level.
6722 *
6723 * @method
6724 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
6725 * @chainable
6726 */
6727 OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
6728 var levels = this.constructor.static.levels,
6729 levelClass = this.constructor.static.levelClass,
6730 i = levels;
6731
6732 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
6733 while ( i-- ) {
6734 if ( this.level === i ) {
6735 this.$element.addClass( levelClass + i );
6736 } else {
6737 this.$element.removeClass( levelClass + i );
6738 }
6739 }
6740
6741 return this;
6742 };
6743 /**
6744 * Create an OO.ui.ButtonSelect object.
6745 *
6746 * @class
6747 * @extends OO.ui.OptionWidget
6748 * @mixins OO.ui.ButtonedElement
6749 * @mixins OO.ui.FlaggableElement
6750 *
6751 * @constructor
6752 * @param {Mixed} data Option data
6753 * @param {Object} [config] Configuration options
6754 */
6755 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
6756 // Parent constructor
6757 OO.ui.OptionWidget.call( this, data, config );
6758
6759 // Mixin constructors
6760 OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
6761 OO.ui.FlaggableElement.call( this, config );
6762
6763 // Initialization
6764 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
6765 this.$button.append( this.$element.contents() );
6766 this.$element.append( this.$button );
6767 };
6768
6769 /* Inheritance */
6770
6771 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget );
6772
6773 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
6774 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.FlaggableElement );
6775
6776 /* Methods */
6777
6778 /**
6779 * @inheritdoc
6780 */
6781 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
6782 OO.ui.OptionWidget.prototype.setSelected.call( this, state );
6783
6784 this.setActive( state );
6785
6786 return this;
6787 };
6788 /**
6789 * Create an OO.ui.ButtonSelect object.
6790 *
6791 * @class
6792 * @extends OO.ui.SelectWidget
6793 *
6794 * @constructor
6795 * @param {Object} [config] Configuration options
6796 */
6797 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
6798 // Parent constructor
6799 OO.ui.SelectWidget.call( this, config );
6800
6801 // Initialization
6802 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
6803 };
6804
6805 /* Inheritance */
6806
6807 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
6808 /**
6809 * Creates an OO.ui.PopupWidget object.
6810 *
6811 * @class
6812 * @extends OO.ui.Widget
6813 * @mixins OO.ui.LabeledElement
6814 *
6815 * @constructor
6816 * @param {Object} [config] Configuration options
6817 * @cfg {boolean} [tail=true] Show tail pointing to origin of popup
6818 * @cfg {string} [align='center'] Alignment of popup to origin
6819 * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
6820 * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
6821 * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
6822 * @cfg {boolean} [head] Show label and close button at the top
6823 */
6824 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
6825 // Config intialization
6826 config = config || {};
6827
6828 // Parent constructor
6829 OO.ui.Widget.call( this, config );
6830
6831 // Mixin constructors
6832 OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
6833
6834 // Properties
6835 this.visible = false;
6836 this.$popup = this.$( '<div>' );
6837 this.$head = this.$( '<div>' );
6838 this.$body = this.$( '<div>' );
6839 this.$tail = this.$( '<div>' );
6840 this.$container = config.$container || this.$( 'body' );
6841 this.autoClose = !!config.autoClose;
6842 this.$autoCloseIgnore = config.$autoCloseIgnore;
6843 this.transitionTimeout = null;
6844 this.tail = false;
6845 this.align = config.align || 'center';
6846 this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'frameless': true, 'icon': 'close' } );
6847 this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
6848
6849 // Events
6850 this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
6851
6852 // Initialization
6853 this.useTail( config.tail !== undefined ? !!config.tail : true );
6854 this.$body.addClass( 'oo-ui-popupWidget-body' );
6855 this.$tail.addClass( 'oo-ui-popupWidget-tail' );
6856 this.$head
6857 .addClass( 'oo-ui-popupWidget-head' )
6858 .append( this.$label, this.closeButton.$element );
6859 if ( !config.head ) {
6860 this.$head.hide();
6861 }
6862 this.$popup
6863 .addClass( 'oo-ui-popupWidget-popup' )
6864 .append( this.$head, this.$body );
6865 this.$element.hide()
6866 .addClass( 'oo-ui-popupWidget' )
6867 .append( this.$popup, this.$tail );
6868 };
6869
6870 /* Inheritance */
6871
6872 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
6873
6874 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
6875
6876 /* Events */
6877
6878 /**
6879 * @event hide
6880 */
6881
6882 /**
6883 * @event show
6884 */
6885
6886 /* Methods */
6887
6888 /**
6889 * Handles mouse down events.
6890 *
6891 * @method
6892 * @param {jQuery.Event} e Mouse down event
6893 */
6894 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
6895 if (
6896 this.visible &&
6897 !$.contains( this.$element[0], e.target ) &&
6898 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
6899 ) {
6900 this.hide();
6901 }
6902 };
6903
6904 /**
6905 * Bind mouse down listener
6906 *
6907 * @method
6908 */
6909 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
6910 // Capture clicks outside popup
6911 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
6912 };
6913
6914 /**
6915 * Handles close button click events.
6916 *
6917 * @method
6918 */
6919 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
6920 if ( this.visible ) {
6921 this.hide();
6922 }
6923 };
6924
6925 /**
6926 * Unbind mouse down listener
6927 *
6928 * @method
6929 */
6930 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
6931 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
6932 };
6933
6934 /**
6935 * Check if the popup is visible.
6936 *
6937 * @method
6938 * @returns {boolean} Popup is visible
6939 */
6940 OO.ui.PopupWidget.prototype.isVisible = function () {
6941 return this.visible;
6942 };
6943
6944 /**
6945 * Set whether to show a tail.
6946 *
6947 * @method
6948 * @returns {boolean} Make tail visible
6949 */
6950 OO.ui.PopupWidget.prototype.useTail = function ( value ) {
6951 value = !!value;
6952 if ( this.tail !== value ) {
6953 this.tail = value;
6954 if ( value ) {
6955 this.$element.addClass( 'oo-ui-popupWidget-tailed' );
6956 } else {
6957 this.$element.removeClass( 'oo-ui-popupWidget-tailed' );
6958 }
6959 }
6960 };
6961
6962 /**
6963 * Check if showing a tail.
6964 *
6965 * @method
6966 * @returns {boolean} tail is visible
6967 */
6968 OO.ui.PopupWidget.prototype.hasTail = function () {
6969 return this.tail;
6970 };
6971
6972 /**
6973 * Show the context.
6974 *
6975 * @method
6976 * @fires show
6977 * @chainable
6978 */
6979 OO.ui.PopupWidget.prototype.show = function () {
6980 if ( !this.visible ) {
6981 this.$element.show();
6982 this.visible = true;
6983 this.emit( 'show' );
6984 if ( this.autoClose ) {
6985 this.bindMouseDownListener();
6986 }
6987 }
6988 return this;
6989 };
6990
6991 /**
6992 * Hide the context.
6993 *
6994 * @method
6995 * @fires hide
6996 * @chainable
6997 */
6998 OO.ui.PopupWidget.prototype.hide = function () {
6999 if ( this.visible ) {
7000 this.$element.hide();
7001 this.visible = false;
7002 this.emit( 'hide' );
7003 if ( this.autoClose ) {
7004 this.unbindMouseDownListener();
7005 }
7006 }
7007 return this;
7008 };
7009
7010 /**
7011 * Updates the position and size.
7012 *
7013 * @method
7014 * @param {number} width Width
7015 * @param {number} height Height
7016 * @param {boolean} [transition=false] Use a smooth transition
7017 * @chainable
7018 */
7019 OO.ui.PopupWidget.prototype.display = function ( width, height, transition ) {
7020 var padding = 10,
7021 originOffset = Math.round( this.$element.offset().left ),
7022 containerLeft = Math.round( this.$container.offset().left ),
7023 containerWidth = this.$container.innerWidth(),
7024 containerRight = containerLeft + containerWidth,
7025 popupOffset = width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align],
7026 popupLeft = popupOffset - padding,
7027 popupRight = popupOffset + padding + width + padding,
7028 overlapLeft = ( originOffset + popupLeft ) - containerLeft,
7029 overlapRight = containerRight - ( originOffset + popupRight );
7030
7031 // Prevent transition from being interrupted
7032 clearTimeout( this.transitionTimeout );
7033 if ( transition ) {
7034 // Enable transition
7035 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
7036 }
7037
7038 if ( overlapRight < 0 ) {
7039 popupOffset += overlapRight;
7040 } else if ( overlapLeft < 0 ) {
7041 popupOffset -= overlapLeft;
7042 }
7043
7044 // Position body relative to anchor and resize
7045 this.$popup.css( {
7046 'left': popupOffset,
7047 'width': width,
7048 'height': height === undefined ? 'auto' : height
7049 } );
7050
7051 if ( transition ) {
7052 // Prevent transitioning after transition is complete
7053 this.transitionTimeout = setTimeout( OO.ui.bind( function () {
7054 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
7055 }, this ), 200 );
7056 } else {
7057 // Prevent transitioning immediately
7058 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
7059 }
7060
7061 return this;
7062 };
7063 /**
7064 * Button that shows and hides a popup.
7065 *
7066 * @class
7067 * @extends OO.ui.ButtonWidget
7068 * @mixins OO.ui.PopuppableElement
7069 *
7070 * @constructor
7071 * @param {Object} [config] Configuration options
7072 */
7073 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
7074 // Parent constructor
7075 OO.ui.ButtonWidget.call( this, config );
7076
7077 // Mixin constructors
7078 OO.ui.PopuppableElement.call( this, config );
7079
7080 // Initialization
7081 this.$element
7082 .addClass( 'oo-ui-popupButtonWidget' )
7083 .append( this.popup.$element );
7084 };
7085
7086 /* Inheritance */
7087
7088 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
7089
7090 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
7091
7092 /* Methods */
7093
7094 /**
7095 * Handles mouse click events.
7096 *
7097 * @method
7098 * @param {jQuery.Event} e Mouse click event
7099 */
7100 OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
7101 // Skip clicks within the popup
7102 if ( $.contains( this.popup.$element[0], e.target ) ) {
7103 return;
7104 }
7105
7106 if ( !this.disabled ) {
7107 if ( this.popup.isVisible() ) {
7108 this.hidePopup();
7109 } else {
7110 this.showPopup();
7111 }
7112 OO.ui.ButtonWidget.prototype.onClick.call( this );
7113 }
7114 return false;
7115 };
7116 /**
7117 * Creates an OO.ui.SearchWidget object.
7118 *
7119 * @class
7120 * @extends OO.ui.Widget
7121 *
7122 * @constructor
7123 * @param {Object} [config] Configuration options
7124 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
7125 * @cfg {string} [value] Initial query value
7126 */
7127 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
7128 // Configuration intialization
7129 config = config || {};
7130
7131 // Parent constructor
7132 OO.ui.Widget.call( this, config );
7133
7134 // Properties
7135 this.query = new OO.ui.TextInputWidget( {
7136 '$': this.$,
7137 'icon': 'search',
7138 'placeholder': config.placeholder,
7139 'value': config.value
7140 } );
7141 this.results = new OO.ui.SelectWidget( { '$': this.$ } );
7142 this.$query = this.$( '<div>' );
7143 this.$results = this.$( '<div>' );
7144
7145 // Events
7146 this.query.connect( this, {
7147 'change': 'onQueryChange',
7148 'enter': 'onQueryEnter'
7149 } );
7150 this.results.connect( this, {
7151 'highlight': 'onResultsHighlight',
7152 'select': 'onResultsSelect'
7153 } );
7154 this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) );
7155
7156 // Initialization
7157 this.$query
7158 .addClass( 'oo-ui-searchWidget-query' )
7159 .append( this.query.$element );
7160 this.$results
7161 .addClass( 'oo-ui-searchWidget-results' )
7162 .append( this.results.$element );
7163 this.$element
7164 .addClass( 'oo-ui-searchWidget' )
7165 .append( this.$results, this.$query );
7166 };
7167
7168 /* Inheritance */
7169
7170 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
7171
7172 /* Events */
7173
7174 /**
7175 * @event highlight
7176 * @param {Object|null} item Item data or null if no item is highlighted
7177 */
7178
7179 /**
7180 * @event select
7181 * @param {Object|null} item Item data or null if no item is selected
7182 */
7183
7184 /* Methods */
7185
7186 /**
7187 * Handle query key down events.
7188 *
7189 * @method
7190 * @param {jQuery.Event} e Key down event
7191 */
7192 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
7193 var highlightedItem, nextItem,
7194 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
7195
7196 if ( dir ) {
7197 highlightedItem = this.results.getHighlightedItem();
7198 if ( !highlightedItem ) {
7199 highlightedItem = this.results.getSelectedItem();
7200 }
7201 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
7202 this.results.highlightItem( nextItem );
7203 nextItem.scrollElementIntoView();
7204 }
7205 };
7206
7207 /**
7208 * Handle select widget select events.
7209 *
7210 * Clears existing results. Subclasses should repopulate items according to new query.
7211 *
7212 * @method
7213 * @param {string} value New value
7214 */
7215 OO.ui.SearchWidget.prototype.onQueryChange = function () {
7216 // Reset
7217 this.results.clearItems();
7218 };
7219
7220 /**
7221 * Handle select widget enter key events.
7222 *
7223 * Selects highlighted item.
7224 *
7225 * @method
7226 * @param {string} value New value
7227 */
7228 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
7229 // Reset
7230 this.results.selectItem( this.results.getHighlightedItem() );
7231 };
7232
7233 /**
7234 * Handle select widget highlight events.
7235 *
7236 * @method
7237 * @param {OO.ui.OptionWidget} item Highlighted item
7238 * @fires highlight
7239 */
7240 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
7241 this.emit( 'highlight', item ? item.getData() : null );
7242 };
7243
7244 /**
7245 * Handle select widget select events.
7246 *
7247 * @method
7248 * @param {OO.ui.OptionWidget} item Selected item
7249 * @fires select
7250 */
7251 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
7252 this.emit( 'select', item ? item.getData() : null );
7253 };
7254
7255 /**
7256 * Get the query input.
7257 *
7258 * @method
7259 * @returns {OO.ui.TextInputWidget} Query input
7260 */
7261 OO.ui.SearchWidget.prototype.getQuery = function () {
7262 return this.query;
7263 };
7264
7265 /**
7266 * Get the results list.
7267 *
7268 * @method
7269 * @returns {OO.ui.SelectWidget} Select list
7270 */
7271 OO.ui.SearchWidget.prototype.getResults = function () {
7272 return this.results;
7273 };
7274 /**
7275 * Creates an OO.ui.TextInputWidget object.
7276 *
7277 * @class
7278 * @extends OO.ui.InputWidget
7279 *
7280 * @constructor
7281 * @param {Object} [config] Configuration options
7282 * @cfg {string} [placeholder] Placeholder text
7283 * @cfg {string} [icon] Symbolic name of icon
7284 * @cfg {boolean} [multiline=false] Allow multiple lines of text
7285 * @cfg {boolean} [autosize=false] Automatically resize to fit content
7286 */
7287 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
7288 config = config || {};
7289
7290 // Parent constructor
7291 OO.ui.InputWidget.call( this, config );
7292
7293 // Properties
7294 this.pending = 0;
7295 this.multiline = !!config.multiline;
7296 this.autosize = !!config.autosize;
7297
7298 // Events
7299 this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
7300
7301 // Initialization
7302 this.$element.addClass( 'oo-ui-textInputWidget' );
7303 if ( config.icon ) {
7304 this.$element.addClass( 'oo-ui-textInputWidget-decorated' );
7305 this.$element.append(
7306 this.$( '<span>' )
7307 .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon )
7308 .mousedown( OO.ui.bind( function () {
7309 this.$input.focus();
7310 return false;
7311 }, this ) )
7312 );
7313 }
7314 if ( config.placeholder ) {
7315 this.$input.attr( 'placeholder', config.placeholder );
7316 }
7317 };
7318
7319 /* Inheritance */
7320
7321 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
7322
7323 /* Events */
7324
7325 /**
7326 * User presses enter inside the text box.
7327 *
7328 * Not called if input is multiline.
7329 *
7330 * @event enter
7331 */
7332
7333 /* Methods */
7334
7335 /**
7336 * Handles key press events.
7337 *
7338 * @param {jQuery.Event} e Key press event
7339 * @fires enter If enter key is pressed and input is not multiline
7340 */
7341 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
7342 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
7343 this.emit( 'enter' );
7344 }
7345 };
7346
7347 /**
7348 * @inheritdoc
7349 */
7350 OO.ui.TextInputWidget.prototype.onEdit = function () {
7351 var $clone, scrollHeight, innerHeight, outerHeight;
7352
7353 // Automatic size adjustment
7354 if ( this.multiline && this.autosize ) {
7355 $clone = this.$input.clone()
7356 .val( this.$input.val() )
7357 .css( { 'height': 0 } )
7358 .insertAfter( this.$input );
7359 // Set inline height property to 0 to measure scroll height
7360 scrollHeight = $clone[0].scrollHeight;
7361 // Remove inline height property to measure natural heights
7362 $clone.css( 'height', '' );
7363 innerHeight = $clone.innerHeight();
7364 outerHeight = $clone.outerHeight();
7365 $clone.remove();
7366 // Only apply inline height when expansion beyond natural height is needed
7367 this.$input.css(
7368 'height',
7369 // Use the difference between the inner and outer height as a buffer
7370 scrollHeight > outerHeight ? scrollHeight + ( outerHeight - innerHeight ) : ''
7371 );
7372 }
7373
7374 // Parent method
7375 return OO.ui.InputWidget.prototype.onEdit.call( this );
7376 };
7377
7378 /**
7379 * Get input element.
7380 *
7381 * @method
7382 * @param {Object} [config] Configuration options
7383 * @returns {jQuery} Input element
7384 */
7385 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
7386 return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
7387 };
7388
7389 /* Methods */
7390
7391 /**
7392 * Checks if input supports multiple lines.
7393 *
7394 * @method
7395 * @returns {boolean} Input supports multiple lines
7396 */
7397 OO.ui.TextInputWidget.prototype.isMultiline = function () {
7398 return !!this.multiline;
7399 };
7400
7401 /**
7402 * Checks if input automatically adjusts its size.
7403 *
7404 * @method
7405 * @returns {boolean} Input automatically adjusts its size
7406 */
7407 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
7408 return !!this.autosize;
7409 };
7410
7411 /**
7412 * Checks if input is pending.
7413 *
7414 * @method
7415 * @returns {boolean} Input is pending
7416 */
7417 OO.ui.TextInputWidget.prototype.isPending = function () {
7418 return !!this.pending;
7419 };
7420
7421 /**
7422 * Increases the pending stack.
7423 *
7424 * @method
7425 * @chainable
7426 */
7427 OO.ui.TextInputWidget.prototype.pushPending = function () {
7428 this.pending++;
7429 this.$element.addClass( 'oo-ui-textInputWidget-pending' );
7430 this.$input.addClass( 'oo-ui-texture-pending' );
7431 return this;
7432 };
7433
7434 /**
7435 * Reduces the pending stack.
7436 *
7437 * Clamped at zero.
7438 *
7439 * @method
7440 * @chainable
7441 */
7442 OO.ui.TextInputWidget.prototype.popPending = function () {
7443 this.pending = Math.max( 0, this.pending - 1 );
7444 if ( !this.pending ) {
7445 this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
7446 this.$input.removeClass( 'oo-ui-texture-pending' );
7447 }
7448 return this;
7449 };
7450 /**
7451 * Creates an OO.ui.TextInputMenuWidget object.
7452 *
7453 * @class
7454 * @extends OO.ui.MenuWidget
7455 *
7456 * @constructor
7457 * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
7458 * @param {Object} [config] Configuration options
7459 * @cfg {jQuery} [$container=input.$element] Element to render menu under
7460 */
7461 OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
7462 // Parent constructor
7463 OO.ui.MenuWidget.call( this, config );
7464
7465 // Properties
7466 this.input = input;
7467 this.$container = config.$container || this.input.$element;
7468 this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
7469
7470 // Initialization
7471 this.$element.addClass( 'oo-ui-textInputMenuWidget' );
7472 };
7473
7474 /* Inheritance */
7475
7476 OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
7477
7478 /* Methods */
7479
7480 /**
7481 * Handle window resize event.
7482 *
7483 * @method
7484 * @param {jQuery.Event} e Window resize event
7485 */
7486 OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
7487 this.position();
7488 };
7489
7490 /**
7491 * Shows the menu.
7492 *
7493 * @method
7494 * @chainable
7495 */
7496 OO.ui.TextInputMenuWidget.prototype.show = function () {
7497 // Parent method
7498 OO.ui.MenuWidget.prototype.show.call( this );
7499
7500 this.position();
7501 this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
7502 return this;
7503 };
7504
7505 /**
7506 * Hides the menu.
7507 *
7508 * @method
7509 * @chainable
7510 */
7511 OO.ui.TextInputMenuWidget.prototype.hide = function () {
7512 // Parent method
7513 OO.ui.MenuWidget.prototype.hide.call( this );
7514
7515 this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
7516 return this;
7517 };
7518
7519 /**
7520 * Positions the menu.
7521 *
7522 * @method
7523 * @chainable
7524 */
7525 OO.ui.TextInputMenuWidget.prototype.position = function () {
7526 var frameOffset,
7527 $container = this.$container,
7528 dimensions = $container.offset();
7529
7530 // Position under input
7531 dimensions.top += $container.height();
7532
7533 // Compensate for frame position if in a differnt frame
7534 if ( this.input.$.frame && this.input.$.context !== this.$element[0].ownerDocument ) {
7535 frameOffset = OO.ui.Element.getRelativePosition(
7536 this.input.$.frame.$element, this.$element.offsetParent()
7537 );
7538 dimensions.left += frameOffset.left;
7539 dimensions.top += frameOffset.top;
7540 } else {
7541 // Fix for RTL (for some reason, no need to fix if the frameoffset is set)
7542 if ( this.$element.css( 'direction' ) === 'rtl' ) {
7543 dimensions.right = this.$element.parent().position().left -
7544 dimensions.width - dimensions.left;
7545 // Erase the value for 'left':
7546 delete dimensions.left;
7547 }
7548 }
7549
7550 this.$element.css( dimensions );
7551 this.setIdealSize( $container.width() );
7552 return this;
7553 };
7554 /**
7555 * Mixin for widgets with a boolean state.
7556 *
7557 * @class
7558 * @abstract
7559 *
7560 * @constructor
7561 * @param {Object} [config] Configuration options
7562 * @cfg {boolean} [value=false] Initial value
7563 */
7564 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
7565 // Configuration initialization
7566 config = config || {};
7567
7568 // Properties
7569 this.value = null;
7570
7571 // Initialization
7572 this.$element.addClass( 'oo-ui-toggleWidget' );
7573 this.setValue( !!config.value );
7574 };
7575
7576 /* Events */
7577
7578 /**
7579 * @event change
7580 * @param {boolean} value Changed value
7581 */
7582
7583 /* Methods */
7584
7585 /**
7586 * Get the value of the toggle.
7587 *
7588 * @method
7589 * @returns {boolean} Toggle value
7590 */
7591 OO.ui.ToggleWidget.prototype.getValue = function () {
7592 return this.value;
7593 };
7594
7595 /**
7596 * Set the value of the toggle.
7597 *
7598 * @method
7599 * @param {boolean} value New value
7600 * @fires change
7601 * @chainable
7602 */
7603 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
7604 value = !!value;
7605 if ( this.value !== value ) {
7606 this.value = value;
7607 this.emit( 'change', value );
7608 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
7609 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
7610 }
7611 return this;
7612 };
7613 /**
7614 * @class
7615 * @extends OO.ui.ButtonWidget
7616 * @mixins OO.ui.ToggleWidget
7617 *
7618 * @constructor
7619 * @param {Object} [config] Configuration options
7620 * @cfg {boolean} [value=false] Initial value
7621 */
7622 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
7623 // Configuration initialization
7624 config = config || {};
7625
7626 // Parent constructor
7627 OO.ui.ButtonWidget.call( this, config );
7628
7629 // Mixin constructors
7630 OO.ui.ToggleWidget.call( this, config );
7631
7632 // Initialization
7633 this.$element.addClass( 'oo-ui-toggleButtonWidget' );
7634 };
7635
7636 /* Inheritance */
7637
7638 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
7639
7640 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
7641
7642 /* Methods */
7643
7644 /**
7645 * @inheritdoc
7646 */
7647 OO.ui.ToggleButtonWidget.prototype.onClick = function () {
7648 if ( !this.disabled ) {
7649 this.setValue( !this.value );
7650 }
7651
7652 // Parent method
7653 return OO.ui.ButtonWidget.prototype.onClick.call( this );
7654 };
7655
7656 /**
7657 * @inheritdoc
7658 */
7659 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
7660 value = !!value;
7661 if ( value !== this.value ) {
7662 this.setActive( value );
7663 }
7664
7665 // Parent method
7666 OO.ui.ToggleWidget.prototype.setValue.call( this, value );
7667
7668 return this;
7669 };
7670 /**
7671 * @class
7672 * @abstract
7673 * @extends OO.ui.Widget
7674 * @mixins OO.ui.ToggleWidget
7675 *
7676 * @constructor
7677 * @param {Object} [config] Configuration options
7678 * @cfg {boolean} [value=false] Initial value
7679 */
7680 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
7681 // Parent constructor
7682 OO.ui.Widget.call( this, config );
7683
7684 // Mixin constructors
7685 OO.ui.ToggleWidget.call( this, config );
7686
7687 // Properties
7688 this.dragging = false;
7689 this.dragStart = null;
7690 this.sliding = false;
7691 this.$on = this.$( '<span>' );
7692 this.$grip = this.$( '<span>' );
7693
7694 // Events
7695 this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
7696
7697 // Initialization
7698 this.$on.addClass( 'oo-ui-toggleSwitchWidget-on' );
7699 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
7700 this.$element
7701 .addClass( 'oo-ui-toggleSwitchWidget' )
7702 .append( this.$on, this.$grip );
7703 };
7704
7705 /* Inheritance */
7706
7707 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
7708
7709 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
7710
7711 /* Methods */
7712
7713 /**
7714 * Handles mouse down events.
7715 *
7716 * @method
7717 * @param {jQuery.Event} e Mouse down event
7718 */
7719 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
7720 if ( !this.disabled && e.which === 1 ) {
7721 this.setValue( !this.value );
7722 }
7723 };
7724 }() );