PrefixSearch: Avoid notice when no subpage exists
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui.js
blobf2e3202bcc58590796c560fb1adc6585f26a4e3a
1 /*!
2  * OOjs UI v0.1.0-pre (85cfc2e735)
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2014 OOjs Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2014-07-03T02:33:09Z
10  */
11 ( function ( OO ) {
13 'use strict';
15 /**
16  * Namespace for all classes, static methods and static properties.
17  *
18  * @class
19  * @singleton
20  */
21 OO.ui = {};
23 OO.ui.bind = $.proxy;
25 /**
26  * @property {Object}
27  */
28 OO.ui.Keys = {
29         'UNDEFINED': 0,
30         'BACKSPACE': 8,
31         'DELETE': 46,
32         'LEFT': 37,
33         'RIGHT': 39,
34         'UP': 38,
35         'DOWN': 40,
36         'ENTER': 13,
37         'END': 35,
38         'HOME': 36,
39         'TAB': 9,
40         'PAGEUP': 33,
41         'PAGEDOWN': 34,
42         'ESCAPE': 27,
43         'SHIFT': 16,
44         'SPACE': 32
47 /**
48  * Get the user's language and any fallback languages.
49  *
50  * These language codes are used to localize user interface elements in the user's language.
51  *
52  * In environments that provide a localization system, this function should be overridden to
53  * return the user's language(s). The default implementation returns English (en) only.
54  *
55  * @return {string[]} Language codes, in descending order of priority
56  */
57 OO.ui.getUserLanguages = function () {
58         return [ 'en' ];
61 /**
62  * Get a value in an object keyed by language code.
63  *
64  * @param {Object.<string,Mixed>} obj Object keyed by language code
65  * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
66  * @param {string} [fallback] Fallback code, used if no matching language can be found
67  * @return {Mixed} Local value
68  */
69 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
70         var i, len, langs;
72         // Requested language
73         if ( obj[lang] ) {
74                 return obj[lang];
75         }
76         // Known user language
77         langs = OO.ui.getUserLanguages();
78         for ( i = 0, len = langs.length; i < len; i++ ) {
79                 lang = langs[i];
80                 if ( obj[lang] ) {
81                         return obj[lang];
82                 }
83         }
84         // Fallback language
85         if ( obj[fallback] ) {
86                 return obj[fallback];
87         }
88         // First existing language
89         for ( lang in obj ) {
90                 return obj[lang];
91         }
93         return undefined;
96 ( function () {
98         /**
99          * Message store for the default implementation of OO.ui.msg
100          *
101          * Environments that provide a localization system should not use this, but should override
102          * OO.ui.msg altogether.
103          *
104          * @private
105          */
106         var messages = {
107                 // Label text for button to exit from dialog
108                 'ooui-dialog-action-close': 'Close',
109                 // Tool tip for a button that moves items in a list down one place
110                 'ooui-outline-control-move-down': 'Move item down',
111                 // Tool tip for a button that moves items in a list up one place
112                 'ooui-outline-control-move-up': 'Move item up',
113                 // Tool tip for a button that removes items from a list
114                 'ooui-outline-control-remove': 'Remove item',
115                 // Label for the toolbar group that contains a list of all other available tools
116                 'ooui-toolbar-more': 'More',
118                 // Label for the generic dialog used to confirm things
119                 'ooui-dialog-confirm-title': 'Confirm',
120                 // The default prompt of a confirmation dialog
121                 'ooui-dialog-confirm-default-prompt': 'Are you sure?',
122                 // The default OK button text on a confirmation dialog
123                 'ooui-dialog-confirm-default-ok': 'OK',
124                 // The default cancel button text on a confirmation dialog
125                 'ooui-dialog-confirm-default-cancel': 'Cancel'
126         };
128         /**
129          * Get a localized message.
130          *
131          * In environments that provide a localization system, this function should be overridden to
132          * return the message translated in the user's language. The default implementation always returns
133          * English messages.
134          *
135          * After the message key, message parameters may optionally be passed. In the default implementation,
136          * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
137          * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
138          * they support unnamed, ordered message parameters.
139          *
140          * @abstract
141          * @param {string} key Message key
142          * @param {Mixed...} [params] Message parameters
143          * @return {string} Translated message with parameters substituted
144          */
145         OO.ui.msg = function ( key ) {
146                 var message = messages[key], params = Array.prototype.slice.call( arguments, 1 );
147                 if ( typeof message === 'string' ) {
148                         // Perform $1 substitution
149                         message = message.replace( /\$(\d+)/g, function ( unused, n ) {
150                                 var i = parseInt( n, 10 );
151                                 return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
152                         } );
153                 } else {
154                         // Return placeholder if message not found
155                         message = '[' + key + ']';
156                 }
157                 return message;
158         };
160         /** */
161         OO.ui.deferMsg = function ( key ) {
162                 return function () {
163                         return OO.ui.msg( key );
164                 };
165         };
167         /** */
168         OO.ui.resolveMsg = function ( msg ) {
169                 if ( $.isFunction( msg ) ) {
170                         return msg();
171                 }
172                 return msg;
173         };
175 } )();
178  * DOM element abstraction.
180  * @abstract
181  * @class
183  * @constructor
184  * @param {Object} [config] Configuration options
185  * @cfg {Function} [$] jQuery for the frame the widget is in
186  * @cfg {string[]} [classes] CSS class names
187  * @cfg {string} [text] Text to insert
188  * @cfg {jQuery} [$content] Content elements to append (after text)
189  */
190 OO.ui.Element = function OoUiElement( config ) {
191         // Configuration initialization
192         config = config || {};
194         // Properties
195         this.$ = config.$ || OO.ui.Element.getJQuery( document );
196         this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
197         this.elementGroup = null;
199         // Initialization
200         if ( $.isArray( config.classes ) ) {
201                 this.$element.addClass( config.classes.join( ' ' ) );
202         }
203         if ( config.text ) {
204                 this.$element.text( config.text );
205         }
206         if ( config.$content ) {
207                 this.$element.append( config.$content );
208         }
211 /* Setup */
213 OO.initClass( OO.ui.Element );
215 /* Static Properties */
218  * HTML tag name.
220  * This may be ignored if getTagName is overridden.
222  * @static
223  * @inheritable
224  * @property {string}
225  */
226 OO.ui.Element.static.tagName = 'div';
228 /* Static Methods */
231  * Get a jQuery function within a specific document.
233  * @static
234  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
235  * @param {OO.ui.Frame} [frame] Frame of the document context
236  * @return {Function} Bound jQuery function
237  */
238 OO.ui.Element.getJQuery = function ( context, frame ) {
239         function wrapper( selector ) {
240                 return $( selector, wrapper.context );
241         }
243         wrapper.context = this.getDocument( context );
245         if ( frame ) {
246                 wrapper.frame = frame;
247         }
249         return wrapper;
253  * Get the document of an element.
255  * @static
256  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
257  * @return {HTMLDocument|null} Document object
258  */
259 OO.ui.Element.getDocument = function ( obj ) {
260         // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
261         return ( obj[0] && obj[0].ownerDocument ) ||
262                 // Empty jQuery selections might have a context
263                 obj.context ||
264                 // HTMLElement
265                 obj.ownerDocument ||
266                 // Window
267                 obj.document ||
268                 // HTMLDocument
269                 ( obj.nodeType === 9 && obj ) ||
270                 null;
274  * Get the window of an element or document.
276  * @static
277  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
278  * @return {Window} Window object
279  */
280 OO.ui.Element.getWindow = function ( obj ) {
281         var doc = this.getDocument( obj );
282         return doc.parentWindow || doc.defaultView;
286  * Get the direction of an element or document.
288  * @static
289  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
290  * @return {string} Text direction, either `ltr` or `rtl`
291  */
292 OO.ui.Element.getDir = function ( obj ) {
293         var isDoc, isWin;
295         if ( obj instanceof jQuery ) {
296                 obj = obj[0];
297         }
298         isDoc = obj.nodeType === 9;
299         isWin = obj.document !== undefined;
300         if ( isDoc || isWin ) {
301                 if ( isWin ) {
302                         obj = obj.document;
303                 }
304                 obj = obj.body;
305         }
306         return $( obj ).css( 'direction' );
310  * Get the offset between two frames.
312  * TODO: Make this function not use recursion.
314  * @static
315  * @param {Window} from Window of the child frame
316  * @param {Window} [to=window] Window of the parent frame
317  * @param {Object} [offset] Offset to start with, used internally
318  * @return {Object} Offset object, containing left and top properties
319  */
320 OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
321         var i, len, frames, frame, rect;
323         if ( !to ) {
324                 to = window;
325         }
326         if ( !offset ) {
327                 offset = { 'top': 0, 'left': 0 };
328         }
329         if ( from.parent === from ) {
330                 return offset;
331         }
333         // Get iframe element
334         frames = from.parent.document.getElementsByTagName( 'iframe' );
335         for ( i = 0, len = frames.length; i < len; i++ ) {
336                 if ( frames[i].contentWindow === from ) {
337                         frame = frames[i];
338                         break;
339                 }
340         }
342         // Recursively accumulate offset values
343         if ( frame ) {
344                 rect = frame.getBoundingClientRect();
345                 offset.left += rect.left;
346                 offset.top += rect.top;
347                 if ( from !== to ) {
348                         this.getFrameOffset( from.parent, offset );
349                 }
350         }
351         return offset;
355  * Get the offset between two elements.
357  * @static
358  * @param {jQuery} $from
359  * @param {jQuery} $to
360  * @return {Object} Translated position coordinates, containing top and left properties
361  */
362 OO.ui.Element.getRelativePosition = function ( $from, $to ) {
363         var from = $from.offset(),
364                 to = $to.offset();
365         return { 'top': Math.round( from.top - to.top ), 'left': Math.round( from.left - to.left ) };
369  * Get element border sizes.
371  * @static
372  * @param {HTMLElement} el Element to measure
373  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
374  */
375 OO.ui.Element.getBorders = function ( el ) {
376         var doc = el.ownerDocument,
377                 win = doc.parentWindow || doc.defaultView,
378                 style = win && win.getComputedStyle ?
379                         win.getComputedStyle( el, null ) :
380                         el.currentStyle,
381                 $el = $( el ),
382                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
383                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
384                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
385                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
387         return {
388                 'top': Math.round( top ),
389                 'left': Math.round( left ),
390                 'bottom': Math.round( bottom ),
391                 'right': Math.round( right )
392         };
396  * Get dimensions of an element or window.
398  * @static
399  * @param {HTMLElement|Window} el Element to measure
400  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
401  */
402 OO.ui.Element.getDimensions = function ( el ) {
403         var $el, $win,
404                 doc = el.ownerDocument || el.document,
405                 win = doc.parentWindow || doc.defaultView;
407         if ( win === el || el === doc.documentElement ) {
408                 $win = $( win );
409                 return {
410                         'borders': { 'top': 0, 'left': 0, 'bottom': 0, 'right': 0 },
411                         'scroll': {
412                                 'top': $win.scrollTop(),
413                                 'left': $win.scrollLeft()
414                         },
415                         'scrollbar': { 'right': 0, 'bottom': 0 },
416                         'rect': {
417                                 'top': 0,
418                                 'left': 0,
419                                 'bottom': $win.innerHeight(),
420                                 'right': $win.innerWidth()
421                         }
422                 };
423         } else {
424                 $el = $( el );
425                 return {
426                         'borders': this.getBorders( el ),
427                         'scroll': {
428                                 'top': $el.scrollTop(),
429                                 'left': $el.scrollLeft()
430                         },
431                         'scrollbar': {
432                                 'right': $el.innerWidth() - el.clientWidth,
433                                 'bottom': $el.innerHeight() - el.clientHeight
434                         },
435                         'rect': el.getBoundingClientRect()
436                 };
437         }
441  * Get closest scrollable container.
443  * Traverses up until either a scrollable element or the root is reached, in which case the window
444  * will be returned.
446  * @static
447  * @param {HTMLElement} el Element to find scrollable container for
448  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
449  * @return {HTMLElement|Window} Closest scrollable container
450  */
451 OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
452         var i, val,
453                 props = [ 'overflow' ],
454                 $parent = $( el ).parent();
456         if ( dimension === 'x' || dimension === 'y' ) {
457                 props.push( 'overflow-' + dimension );
458         }
460         while ( $parent.length ) {
461                 if ( $parent[0] === el.ownerDocument.body ) {
462                         return $parent[0];
463                 }
464                 i = props.length;
465                 while ( i-- ) {
466                         val = $parent.css( props[i] );
467                         if ( val === 'auto' || val === 'scroll' ) {
468                                 return $parent[0];
469                         }
470                 }
471                 $parent = $parent.parent();
472         }
473         return this.getDocument( el ).body;
477  * Scroll element into view.
479  * @static
480  * @param {HTMLElement} el Element to scroll into view
481  * @param {Object} [config={}] Configuration config
482  * @param {string} [config.duration] jQuery animation duration value
483  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
484  *  to scroll in both directions
485  * @param {Function} [config.complete] Function to call when scrolling completes
486  */
487 OO.ui.Element.scrollIntoView = function ( el, config ) {
488         // Configuration initialization
489         config = config || {};
491         var rel, anim = {},
492                 callback = typeof config.complete === 'function' && config.complete,
493                 sc = this.getClosestScrollableContainer( el, config.direction ),
494                 $sc = $( sc ),
495                 eld = this.getDimensions( el ),
496                 scd = this.getDimensions( sc ),
497                 $win = $( this.getWindow( el ) );
499         // Compute the distances between the edges of el and the edges of the scroll viewport
500         if ( $sc.is( 'body' ) ) {
501                 // If the scrollable container is the <body> this is easy
502                 rel = {
503                         'top': eld.rect.top,
504                         'bottom': $win.innerHeight() - eld.rect.bottom,
505                         'left': eld.rect.left,
506                         'right': $win.innerWidth() - eld.rect.right
507                 };
508         } else {
509                 // Otherwise, we have to subtract el's coordinates from sc's coordinates
510                 rel = {
511                         'top': eld.rect.top - ( scd.rect.top + scd.borders.top ),
512                         'bottom': scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
513                         'left': eld.rect.left - ( scd.rect.left + scd.borders.left ),
514                         'right': scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
515                 };
516         }
518         if ( !config.direction || config.direction === 'y' ) {
519                 if ( rel.top < 0 ) {
520                         anim.scrollTop = scd.scroll.top + rel.top;
521                 } else if ( rel.top > 0 && rel.bottom < 0 ) {
522                         anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
523                 }
524         }
525         if ( !config.direction || config.direction === 'x' ) {
526                 if ( rel.left < 0 ) {
527                         anim.scrollLeft = scd.scroll.left + rel.left;
528                 } else if ( rel.left > 0 && rel.right < 0 ) {
529                         anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
530                 }
531         }
532         if ( !$.isEmptyObject( anim ) ) {
533                 $sc.stop( true ).animate( anim, config.duration || 'fast' );
534                 if ( callback ) {
535                         $sc.queue( function ( next ) {
536                                 callback();
537                                 next();
538                         } );
539                 }
540         } else {
541                 if ( callback ) {
542                         callback();
543                 }
544         }
547 /* Methods */
550  * Get the HTML tag name.
552  * Override this method to base the result on instance information.
554  * @return {string} HTML tag name
555  */
556 OO.ui.Element.prototype.getTagName = function () {
557         return this.constructor.static.tagName;
561  * Check if the element is attached to the DOM
562  * @return {boolean} The element is attached to the DOM
563  */
564 OO.ui.Element.prototype.isElementAttached = function () {
565         return $.contains( this.getElementDocument(), this.$element[0] );
569  * Get the DOM document.
571  * @return {HTMLDocument} Document object
572  */
573 OO.ui.Element.prototype.getElementDocument = function () {
574         return OO.ui.Element.getDocument( this.$element );
578  * Get the DOM window.
580  * @return {Window} Window object
581  */
582 OO.ui.Element.prototype.getElementWindow = function () {
583         return OO.ui.Element.getWindow( this.$element );
587  * Get closest scrollable container.
588  */
589 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
590         return OO.ui.Element.getClosestScrollableContainer( this.$element[0] );
594  * Get group element is in.
596  * @return {OO.ui.GroupElement|null} Group element, null if none
597  */
598 OO.ui.Element.prototype.getElementGroup = function () {
599         return this.elementGroup;
603  * Set group element is in.
605  * @param {OO.ui.GroupElement|null} group Group element, null if none
606  * @chainable
607  */
608 OO.ui.Element.prototype.setElementGroup = function ( group ) {
609         this.elementGroup = group;
610         return this;
614  * Scroll element into view.
616  * @param {Object} [config={}]
617  */
618 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
619         return OO.ui.Element.scrollIntoView( this.$element[0], config );
623  * Bind a handler for an event on this.$element
625  * @deprecated Use jQuery#on instead.
626  * @param {string} event
627  * @param {Function} callback
628  */
629 OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) {
630         OO.ui.Element.onDOMEvent( this.$element, event, callback );
634  * Unbind a handler bound with #offDOMEvent
636  * @deprecated Use jQuery#off instead.
637  * @param {string} event
638  * @param {Function} callback
639  */
640 OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
641         OO.ui.Element.offDOMEvent( this.$element, event, callback );
644 ( function () {
645         /**
646          * Bind a handler for an event on a DOM element.
647          *
648          * Used to be for working around a jQuery bug (jqbug.com/14180),
649          * but obsolete as of jQuery 1.11.0.
650          *
651          * @static
652          * @deprecated Use jQuery#on instead.
653          * @param {HTMLElement|jQuery} el DOM element
654          * @param {string} event Event to bind
655          * @param {Function} callback Callback to call when the event fires
656          */
657         OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
658                 $( el ).on( event, callback );
659         };
661         /**
662          * Unbind a handler bound with #static-method-onDOMEvent.
663          *
664          * @deprecated Use jQuery#off instead.
665          * @static
666          * @param {HTMLElement|jQuery} el DOM element
667          * @param {string} event Event to unbind
668          * @param {Function} [callback] Callback to unbind
669          */
670         OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
671                 $( el ).off( event, callback );
672         };
673 }() );
676  * Embedded iframe with the same styles as its parent.
678  * @class
679  * @extends OO.ui.Element
680  * @mixins OO.EventEmitter
682  * @constructor
683  * @param {Object} [config] Configuration options
684  */
685 OO.ui.Frame = function OoUiFrame( config ) {
686         // Parent constructor
687         OO.ui.Frame.super.call( this, config );
689         // Mixin constructors
690         OO.EventEmitter.call( this );
692         // Properties
693         this.loading = null;
694         this.config = config;
696         // Initialize
697         this.$element
698                 .addClass( 'oo-ui-frame' )
699                 .attr( { 'frameborder': 0, 'scrolling': 'no' } );
703 /* Setup */
705 OO.inheritClass( OO.ui.Frame, OO.ui.Element );
706 OO.mixinClass( OO.ui.Frame, OO.EventEmitter );
708 /* Static Properties */
711  * @static
712  * @inheritdoc
713  */
714 OO.ui.Frame.static.tagName = 'iframe';
716 /* Events */
719  * @event load
720  */
722 /* Static Methods */
725  * Transplant the CSS styles from as parent document to a frame's document.
727  * This loops over the style sheets in the parent document, and copies their nodes to the
728  * frame's document. It then polls the document to see when all styles have loaded, and once they
729  * have, resolves the promise.
731  * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
732  * and resolve the promise anyway. This protects against cases like a display: none; iframe in
733  * Firefox, where the styles won't load until the iframe becomes visible.
735  * For details of how we arrived at the strategy used in this function, see #load.
737  * @static
738  * @inheritable
739  * @param {HTMLDocument} parentDoc Document to transplant styles from
740  * @param {HTMLDocument} frameDoc Document to transplant styles to
741  * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
742  * @return {jQuery.Promise} Promise resolved when styles have loaded
743  */
744 OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
745         var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes,
746                 $pollNodes = $( [] ),
747                 // Fake font-family value
748                 fontFamily = 'oo-ui-frame-transplantStyles-loaded',
749                 deferred = $.Deferred();
751         for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
752                 styleNode = parentDoc.styleSheets[i].ownerNode;
753                 if ( styleNode.disabled ) {
754                         continue;
755                 }
756                 if ( styleNode.nodeName.toLowerCase() === 'link' ) {
757                         // External stylesheet
758                         // Create a node with a unique ID that we're going to monitor to see when the CSS
759                         // has loaded
760                         pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i;
761                         $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
762                                 .attr( 'id', pollNodeId )
763                                 .appendTo( frameDoc.body )
764                         );
766                         // Add <style>@import url(...); #pollNodeId { font-family: ... }</style>
767                         // The font-family rule will only take effect once the @import finishes
768                         newNode = frameDoc.createElement( 'style' );
769                         newNode.textContent = '@import url(' + styleNode.href + ');\n' +
770                                 '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
771                 } else {
772                         // Not an external stylesheet, or no polling required; just copy the node over
773                         newNode = frameDoc.importNode( styleNode, true );
774                 }
775                 frameDoc.head.appendChild( newNode );
776         }
778         // Poll every 100ms until all external stylesheets have loaded
779         $pendingPollNodes = $pollNodes;
780         timeoutID = setTimeout( function pollExternalStylesheets() {
781                 while (
782                         $pendingPollNodes.length > 0 &&
783                         $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
784                 ) {
785                         $pendingPollNodes = $pendingPollNodes.slice( 1 );
786                 }
788                 if ( $pendingPollNodes.length === 0 ) {
789                         // We're done!
790                         if ( timeoutID !== null ) {
791                                 timeoutID = null;
792                                 $pollNodes.remove();
793                                 deferred.resolve();
794                         }
795                 } else {
796                         timeoutID = setTimeout( pollExternalStylesheets, 100 );
797                 }
798         }, 100 );
799         // ...but give up after a while
800         if ( timeout !== 0 ) {
801                 setTimeout( function () {
802                         if ( timeoutID ) {
803                                 clearTimeout( timeoutID );
804                                 timeoutID = null;
805                                 $pollNodes.remove();
806                                 deferred.reject();
807                         }
808                 }, timeout || 5000 );
809         }
811         return deferred.promise();
814 /* Methods */
817  * Load the frame contents.
819  * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
820  * promise will be resolved. Calling while loading will return a promise but not trigger a new
821  * loading cycle. Calling after loading is complete will return a promise that's already been
822  * resolved.
824  * Sounds simple right? Read on...
826  * When you create a dynamic iframe using open/write/close, the window.load event for the
827  * iframe is triggered when you call close, and there's no further load event to indicate that
828  * everything is actually loaded.
830  * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
831  * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
832  * are added to document.styleSheets immediately, and the only way you can determine whether they've
833  * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
834  * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
836  * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` tags.
837  * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until
838  * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import`
839  * has finished. And because the contents of the `<style>` tag are from the same origin, accessing
840  * .cssRules is allowed.
842  * However, now that we control the styles we're injecting, we might as well do away with
843  * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
844  * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
845  * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family
846  * rule is not applied until after the `@import` finishes.
848  * All this stylesheet injection and polling magic is in #transplantStyles.
850  * @return {jQuery.Promise} Promise resolved when loading is complete
851  * @fires load
852  */
853 OO.ui.Frame.prototype.load = function () {
854         var win, doc;
856         // Return existing promise if already loading or loaded
857         if ( this.loading ) {
858                 return this.loading.promise();
859         }
861         // Load the frame
862         this.loading = $.Deferred();
864         win = this.$element.prop( 'contentWindow' );
865         doc = win.document;
867         // Figure out directionality:
868         this.dir = OO.ui.Element.getDir( this.$element ) || 'ltr';
870         // Initialize contents
871         doc.open();
872         doc.write(
873                 '<!doctype html>' +
874                 '<html>' +
875                         '<body class="oo-ui-frame-body oo-ui-' + this.dir + '" style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
876                                 '<div class="oo-ui-frame-content"></div>' +
877                         '</body>' +
878                 '</html>'
879         );
880         doc.close();
882         // Properties
883         this.$ = OO.ui.Element.getJQuery( doc, this );
884         this.$content = this.$( '.oo-ui-frame-content' ).attr( 'tabIndex', 0 );
885         this.$document = this.$( doc );
887         // Initialization
888         this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
889                 .always( OO.ui.bind( function () {
890                         this.emit( 'load' );
891                         this.loading.resolve();
892                 }, this ) );
894         return this.loading.promise();
898  * Set the size of the frame.
900  * @param {number} width Frame width in pixels
901  * @param {number} height Frame height in pixels
902  * @chainable
903  */
904 OO.ui.Frame.prototype.setSize = function ( width, height ) {
905         this.$element.css( { 'width': width, 'height': height } );
906         return this;
910  * Container for elements in a child frame.
912  * There are two ways to specify a title: set the static `title` property or provide a `title`
913  * property in the configuration options. The latter will override the former.
915  * @abstract
916  * @class
917  * @extends OO.ui.Element
918  * @mixins OO.EventEmitter
920  * @constructor
921  * @param {Object} [config] Configuration options
922  * @cfg {string|Function} [title] Title string or function that returns a string
923  * @cfg {string} [icon] Symbolic name of icon
924  * @fires initialize
925  */
926 OO.ui.Window = function OoUiWindow( config ) {
927         var element = this;
928         // Parent constructor
929         OO.ui.Window.super.call( this, config );
931         // Mixin constructors
932         OO.EventEmitter.call( this );
934         // Properties
935         this.visible = false;
936         this.opening = null;
937         this.closing = null;
938         this.opened = null;
939         this.title = OO.ui.resolveMsg( config.title || this.constructor.static.title );
940         this.icon = config.icon || this.constructor.static.icon;
941         this.frame = new OO.ui.Frame( { '$': this.$ } );
942         this.$frame = this.$( '<div>' );
943         this.$ = function () {
944                 throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
945         };
947         // Initialization
948         this.$element
949                 .addClass( 'oo-ui-window' )
950                 // Hide the window using visibility: hidden; while the iframe is still loading
951                 // Can't use display: none; because that prevents the iframe from loading in Firefox
952                 .css( 'visibility', 'hidden' )
953                 .append( this.$frame );
954         this.$frame
955                 .addClass( 'oo-ui-window-frame' )
956                 .append( this.frame.$element );
958         // Events
959         this.frame.on( 'load', function () {
960                 element.initialize();
961                 // Undo the visibility: hidden; hack and apply display: none;
962                 // We can do this safely now that the iframe has initialized
963                 // (don't do this from within #initialize because it has to happen
964                 // after the all subclasses have been handled as well).
965                 element.$element.hide().css( 'visibility', '' );
966         } );
969 /* Setup */
971 OO.inheritClass( OO.ui.Window, OO.ui.Element );
972 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
974 /* Events */
977  * Window is setup.
979  * Fired after the setup process has been executed.
981  * @event setup
982  * @param {Object} data Window opening data
983  */
986  * Window is ready.
988  * Fired after the ready process has been executed.
990  * @event ready
991  * @param {Object} data Window opening data
992  */
995  * Window is torn down
997  * Fired after the teardown process has been executed.
999  * @event teardown
1000  * @param {Object} data Window closing data
1001  */
1003 /* Static Properties */
1006  * Symbolic name of icon.
1008  * @static
1009  * @inheritable
1010  * @property {string}
1011  */
1012 OO.ui.Window.static.icon = 'window';
1015  * Window title.
1017  * Subclasses must implement this property before instantiating the window.
1018  * Alternatively, override #getTitle with an alternative implementation.
1020  * @static
1021  * @abstract
1022  * @inheritable
1023  * @property {string|Function} Title string or function that returns a string
1024  */
1025 OO.ui.Window.static.title = null;
1027 /* Methods */
1030  * Check if window is visible.
1032  * @return {boolean} Window is visible
1033  */
1034 OO.ui.Window.prototype.isVisible = function () {
1035         return this.visible;
1039  * Check if window is opening.
1041  * @return {boolean} Window is opening
1042  */
1043 OO.ui.Window.prototype.isOpening = function () {
1044         return !!this.opening && this.opening.state() === 'pending';
1048  * Check if window is closing.
1050  * @return {boolean} Window is closing
1051  */
1052 OO.ui.Window.prototype.isClosing = function () {
1053         return !!this.closing && this.closing.state() === 'pending';
1057  * Check if window is opened.
1059  * @return {boolean} Window is opened
1060  */
1061 OO.ui.Window.prototype.isOpened = function () {
1062         return !!this.opened && this.opened.state() === 'pending';
1066  * Get the window frame.
1068  * @return {OO.ui.Frame} Frame of window
1069  */
1070 OO.ui.Window.prototype.getFrame = function () {
1071         return this.frame;
1075  * Get the title of the window.
1077  * @return {string} Title text
1078  */
1079 OO.ui.Window.prototype.getTitle = function () {
1080         return this.title;
1084  * Get the window icon.
1086  * @return {string} Symbolic name of icon
1087  */
1088 OO.ui.Window.prototype.getIcon = function () {
1089         return this.icon;
1093  * Set the size of window frame.
1095  * @param {number} [width=auto] Custom width
1096  * @param {number} [height=auto] Custom height
1097  * @chainable
1098  */
1099 OO.ui.Window.prototype.setSize = function ( width, height ) {
1100         if ( !this.frame.$content ) {
1101                 return;
1102         }
1104         this.frame.$element.css( {
1105                 'width': width === undefined ? 'auto' : width,
1106                 'height': height === undefined ? 'auto' : height
1107         } );
1109         return this;
1113  * Set the title of the window.
1115  * @param {string|Function} title Title text or a function that returns text
1116  * @chainable
1117  */
1118 OO.ui.Window.prototype.setTitle = function ( title ) {
1119         this.title = OO.ui.resolveMsg( title );
1120         if ( this.$title ) {
1121                 this.$title.text( title );
1122         }
1123         return this;
1127  * Set the icon of the window.
1129  * @param {string} icon Symbolic name of icon
1130  * @chainable
1131  */
1132 OO.ui.Window.prototype.setIcon = function ( icon ) {
1133         if ( this.$icon ) {
1134                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
1135         }
1136         this.icon = icon;
1137         if ( this.$icon ) {
1138                 this.$icon.addClass( 'oo-ui-icon-' + this.icon );
1139         }
1141         return this;
1145  * Set the position of window to fit with contents.
1147  * @param {string} left Left offset
1148  * @param {string} top Top offset
1149  * @chainable
1150  */
1151 OO.ui.Window.prototype.setPosition = function ( left, top ) {
1152         this.$element.css( { 'left': left, 'top': top } );
1153         return this;
1157  * Set the height of window to fit with contents.
1159  * @param {number} [min=0] Min height
1160  * @param {number} [max] Max height (defaults to content's outer height)
1161  * @chainable
1162  */
1163 OO.ui.Window.prototype.fitHeightToContents = function ( min, max ) {
1164         var height = this.frame.$content.outerHeight();
1166         this.frame.$element.css(
1167                 'height', Math.max( min || 0, max === undefined ? height : Math.min( max, height ) )
1168         );
1170         return this;
1174  * Set the width of window to fit with contents.
1176  * @param {number} [min=0] Min height
1177  * @param {number} [max] Max height (defaults to content's outer width)
1178  * @chainable
1179  */
1180 OO.ui.Window.prototype.fitWidthToContents = function ( min, max ) {
1181         var width = this.frame.$content.outerWidth();
1183         this.frame.$element.css(
1184                 'width', Math.max( min || 0, max === undefined ? width : Math.min( max, width ) )
1185         );
1187         return this;
1191  * Initialize window contents.
1193  * The first time the window is opened, #initialize is called when it's safe to begin populating
1194  * its contents. See #setup for a way to make changes each time the window opens.
1196  * Once this method is called, this.$$ can be used to create elements within the frame.
1198  * @chainable
1199  */
1200 OO.ui.Window.prototype.initialize = function () {
1201         // Properties
1202         this.$ = this.frame.$;
1203         this.$title = this.$( '<div class="oo-ui-window-title"></div>' )
1204                 .text( this.title );
1205         this.$icon = this.$( '<div class="oo-ui-window-icon"></div>' )
1206                 .addClass( 'oo-ui-icon-' + this.icon );
1207         this.$head = this.$( '<div class="oo-ui-window-head"></div>' );
1208         this.$body = this.$( '<div class="oo-ui-window-body"></div>' );
1209         this.$foot = this.$( '<div class="oo-ui-window-foot"></div>' );
1210         this.$overlay = this.$( '<div class="oo-ui-window-overlay"></div>' );
1212         // Initialization
1213         this.frame.$content.append(
1214                 this.$head.append( this.$icon, this.$title ),
1215                 this.$body,
1216                 this.$foot,
1217                 this.$overlay
1218         );
1220         return this;
1224  * Get a process for setting up a window for use.
1226  * Each time the window is opened this process will set it up for use in a particular context, based
1227  * on the `data` argument.
1229  * When you override this method, you can add additional setup steps to the process the parent
1230  * method provides using the 'first' and 'next' methods.
1232  * @abstract
1233  * @param {Object} [data] Window opening data
1234  * @return {OO.ui.Process} Setup process
1235  */
1236 OO.ui.Window.prototype.getSetupProcess = function () {
1237         return new OO.ui.Process();
1241  * Get a process for readying a window for use.
1243  * Each time the window is open and setup, this process will ready it up for use in a particular
1244  * context, based on the `data` argument.
1246  * When you override this method, you can add additional setup steps to the process the parent
1247  * method provides using the 'first' and 'next' methods.
1249  * @abstract
1250  * @param {Object} [data] Window opening data
1251  * @return {OO.ui.Process} Setup process
1252  */
1253 OO.ui.Window.prototype.getReadyProcess = function () {
1254         return new OO.ui.Process();
1258  * Get a process for tearing down a window after use.
1260  * Each time the window is closed this process will tear it down and do something with the user's
1261  * interactions within the window, based on the `data` argument.
1263  * When you override this method, you can add additional teardown steps to the process the parent
1264  * method provides using the 'first' and 'next' methods.
1266  * @abstract
1267  * @param {Object} [data] Window closing data
1268  * @return {OO.ui.Process} Teardown process
1269  */
1270 OO.ui.Window.prototype.getTeardownProcess = function () {
1271         return new OO.ui.Process();
1275  * Open window.
1277  * Do not override this method. Use #getSetupProcess to do something each time the window closes.
1279  * @param {Object} [data] Window opening data
1280  * @fires initialize
1281  * @fires opening
1282  * @fires open
1283  * @fires ready
1284  * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
1285  *   first argument will be a promise which will be resolved when the window begins closing
1286  */
1287 OO.ui.Window.prototype.open = function ( data ) {
1288         // Return existing promise if already opening or open
1289         if ( this.opening ) {
1290                 return this.opening.promise();
1291         }
1293         // Open the window
1294         this.opening = $.Deferred();
1296         this.$ariaHidden = $( 'body' ).children().not( this.$element.parentsUntil( 'body' ).last() )
1297                 .attr( 'aria-hidden', '' );
1299         this.frame.load().done( OO.ui.bind( function () {
1300                 this.$element.show();
1301                 this.visible = true;
1302                 this.getSetupProcess( data ).execute().done( OO.ui.bind( function () {
1303                         this.$element.addClass( 'oo-ui-window-setup' );
1304                         this.emit( 'setup', data );
1305                         setTimeout( OO.ui.bind( function () {
1306                                 this.frame.$content.focus();
1307                                 this.getReadyProcess( data ).execute().done( OO.ui.bind( function () {
1308                                         this.$element.addClass( 'oo-ui-window-ready' );
1309                                         this.emit( 'ready', data );
1310                                         this.opened = $.Deferred();
1311                                         // Now that we are totally done opening, it's safe to allow closing
1312                                         this.closing = null;
1313                                         this.opening.resolve( this.opened.promise() );
1314                                 }, this ) );
1315                         }, this ) );
1316                 }, this ) );
1317         }, this ) );
1319         return this.opening.promise();
1323  * Close window.
1325  * Do not override this method. Use #getTeardownProcess to do something each time the window closes.
1327  * @param {Object} [data] Window closing data
1328  * @fires closing
1329  * @fires close
1330  * @return {jQuery.Promise} Promise resolved when window is closed
1331  */
1332 OO.ui.Window.prototype.close = function ( data ) {
1333         var close;
1335         // Return existing promise if already closing or closed
1336         if ( this.closing ) {
1337                 return this.closing.promise();
1338         }
1340         // Close after opening is done if opening is in progress
1341         if ( this.opening && this.opening.state() === 'pending' ) {
1342                 close = OO.ui.bind( function () {
1343                         return this.close( data );
1344                 }, this );
1345                 return this.opening.then( close, close );
1346         }
1348         // Close the window
1349         // This.closing needs to exist before we emit the closing event so that handlers can call
1350         // window.close() and trigger the safety check above
1351         this.closing = $.Deferred();
1352         this.frame.$content.find( ':focus' ).blur();
1353         this.$element.removeClass( 'oo-ui-window-ready' );
1354         this.getTeardownProcess( data ).execute().done( OO.ui.bind( function () {
1355                 this.$element.removeClass( 'oo-ui-window-setup' );
1356                 this.emit( 'teardown', data );
1357                 // To do something different with #opened, resolve/reject #opened in the teardown process
1358                 if ( this.opened && this.opened.state() === 'pending' ) {
1359                         this.opened.resolve();
1360                 }
1361                 this.$element.hide();
1362                 if ( this.$ariaHidden ) {
1363                         this.$ariaHidden.removeAttr( 'aria-hidden' );
1364                         this.$ariaHidden = undefined;
1365                 }
1366                 this.visible = false;
1367                 this.closing.resolve();
1368                 // Now that we are totally done closing, it's safe to allow opening
1369                 this.opening = null;
1370         }, this ) );
1372         return this.closing.promise();
1376  * Set of mutually exclusive windows.
1378  * @class
1379  * @extends OO.ui.Element
1380  * @mixins OO.EventEmitter
1382  * @constructor
1383  * @param {OO.Factory} factory Window factory
1384  * @param {Object} [config] Configuration options
1385  */
1386 OO.ui.WindowSet = function OoUiWindowSet( factory, config ) {
1387         // Parent constructor
1388         OO.ui.WindowSet.super.call( this, config );
1390         // Mixin constructors
1391         OO.EventEmitter.call( this );
1393         // Properties
1394         this.factory = factory;
1396         /**
1397          * List of all windows associated with this window set.
1398          *
1399          * @property {OO.ui.Window[]}
1400          */
1401         this.windowList = [];
1403         /**
1404          * Mapping of OO.ui.Window objects created by name from the #factory.
1405          *
1406          * @property {Object}
1407          */
1408         this.windows = {};
1409         this.currentWindow = null;
1411         // Initialization
1412         this.$element.addClass( 'oo-ui-windowSet' );
1415 /* Setup */
1417 OO.inheritClass( OO.ui.WindowSet, OO.ui.Element );
1418 OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter );
1420 /* Events */
1423  * @event setup
1424  * @param {OO.ui.Window} win Window that's been setup
1425  * @param {Object} config Window opening information
1426  */
1429  * @event ready
1430  * @param {OO.ui.Window} win Window that's ready
1431  * @param {Object} config Window opening information
1432  */
1435  * @event teardown
1436  * @param {OO.ui.Window} win Window that's been torn down
1437  * @param {Object} config Window closing information
1438  */
1440 /* Methods */
1443  * Handle a window setup event.
1445  * @param {OO.ui.Window} win Window that's been setup
1446  * @param {Object} [config] Window opening information
1447  * @fires setup
1448  */
1449 OO.ui.WindowSet.prototype.onWindowSetup = function ( win, config ) {
1450         if ( this.currentWindow && this.currentWindow !== win ) {
1451                 this.currentWindow.close();
1452         }
1453         this.currentWindow = win;
1454         this.emit( 'setup', win, config );
1458  * Handle a window ready event.
1460  * @param {OO.ui.Window} win Window that's ready
1461  * @param {Object} [config] Window opening information
1462  * @fires ready
1463  */
1464 OO.ui.WindowSet.prototype.onWindowReady = function ( win, config ) {
1465         this.emit( 'ready', win, config );
1469  * Handle a window teardown event.
1471  * @param {OO.ui.Window} win Window that's been torn down
1472  * @param {Object} [config] Window closing information
1473  * @fires teardown
1474  */
1475 OO.ui.WindowSet.prototype.onWindowTeardown = function ( win, config ) {
1476         this.currentWindow = null;
1477         this.emit( 'teardown', win, config );
1481  * Get the current window.
1483  * @return {OO.ui.Window|null} Current window or null if none open
1484  */
1485 OO.ui.WindowSet.prototype.getCurrentWindow = function () {
1486         return this.currentWindow;
1490  * Return a given window.
1492  * @param {string} name Symbolic name of window
1493  * @return {OO.ui.Window} Window with specified name
1494  */
1495 OO.ui.WindowSet.prototype.getWindow = function ( name ) {
1496         var win;
1498         if ( !this.factory.lookup( name ) ) {
1499                 throw new Error( 'Unknown window: ' + name );
1500         }
1501         if ( !( name in this.windows ) ) {
1502                 win = this.windows[name] = this.createWindow( name );
1503                 this.addWindow( win );
1504         }
1505         return this.windows[name];
1509  * Create a window for use in this window set.
1511  * @param {string} name Symbolic name of window
1512  * @return {OO.ui.Window} Window with specified name
1513  */
1514 OO.ui.WindowSet.prototype.createWindow = function ( name ) {
1515         return this.factory.create( name, { '$': this.$ } );
1519  * Add a given window to this window set.
1521  * Connects event handlers and attaches it to the DOM. Calling
1522  * OO.ui.Window#open will not work until the window is added to the set.
1524  * @param {OO.ui.Window} win Window to add
1525  */
1526 OO.ui.WindowSet.prototype.addWindow = function ( win ) {
1527         if ( this.windowList.indexOf( win ) !== -1 ) {
1528                 // Already set up
1529                 return;
1530         }
1531         this.windowList.push( win );
1533         win.connect( this, {
1534                 'setup': [ 'onWindowSetup', win ],
1535                 'ready': [ 'onWindowReady', win ],
1536                 'teardown': [ 'onWindowTeardown', win ]
1537         } );
1538         this.$element.append( win.$element );
1542  * Modal dialog window.
1544  * @abstract
1545  * @class
1546  * @extends OO.ui.Window
1548  * @constructor
1549  * @param {Object} [config] Configuration options
1550  * @cfg {boolean} [footless] Hide foot
1551  * @cfg {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large`
1552  */
1553 OO.ui.Dialog = function OoUiDialog( config ) {
1554         // Configuration initialization
1555         config = $.extend( { 'size': 'large' }, config );
1557         // Parent constructor
1558         OO.ui.Dialog.super.call( this, config );
1560         // Properties
1561         this.visible = false;
1562         this.footless = !!config.footless;
1563         this.size = null;
1564         this.pending = 0;
1565         this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
1566         this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
1568         // Events
1569         this.$element.on( 'mousedown', false );
1571         // Initialization
1572         this.$element.addClass( 'oo-ui-dialog' ).attr( 'role', 'dialog' );
1573         this.setSize( config.size );
1576 /* Setup */
1578 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
1580 /* Static Properties */
1583  * Symbolic name of dialog.
1585  * @abstract
1586  * @static
1587  * @inheritable
1588  * @property {string}
1589  */
1590 OO.ui.Dialog.static.name = '';
1593  * Map of symbolic size names and CSS classes.
1595  * @static
1596  * @inheritable
1597  * @property {Object}
1598  */
1599 OO.ui.Dialog.static.sizeCssClasses = {
1600         'small': 'oo-ui-dialog-small',
1601         'medium': 'oo-ui-dialog-medium',
1602         'large': 'oo-ui-dialog-large'
1605 /* Methods */
1608  * Handle close button click events.
1609  */
1610 OO.ui.Dialog.prototype.onCloseButtonClick = function () {
1611         this.close( { 'action': 'cancel' } );
1615  * Handle window mouse wheel events.
1617  * @param {jQuery.Event} e Mouse wheel event
1618  */
1619 OO.ui.Dialog.prototype.onWindowMouseWheel = function () {
1620         return false;
1624  * Handle document key down events.
1626  * @param {jQuery.Event} e Key down event
1627  */
1628 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
1629         switch ( e.which ) {
1630                 case OO.ui.Keys.PAGEUP:
1631                 case OO.ui.Keys.PAGEDOWN:
1632                 case OO.ui.Keys.END:
1633                 case OO.ui.Keys.HOME:
1634                 case OO.ui.Keys.LEFT:
1635                 case OO.ui.Keys.UP:
1636                 case OO.ui.Keys.RIGHT:
1637                 case OO.ui.Keys.DOWN:
1638                         // Prevent any key events that might cause scrolling
1639                         return false;
1640         }
1644  * Handle frame document key down events.
1646  * @param {jQuery.Event} e Key down event
1647  */
1648 OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) {
1649         if ( e.which === OO.ui.Keys.ESCAPE ) {
1650                 this.close( { 'action': 'cancel' } );
1651                 return false;
1652         }
1656  * Set dialog size.
1658  * @param {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large`
1659  */
1660 OO.ui.Dialog.prototype.setSize = function ( size ) {
1661         var name, state, cssClass,
1662                 sizeCssClasses = OO.ui.Dialog.static.sizeCssClasses;
1664         if ( !sizeCssClasses[size] ) {
1665                 size = 'large';
1666         }
1667         this.size = size;
1668         for ( name in sizeCssClasses ) {
1669                 state = name === size;
1670                 cssClass = sizeCssClasses[name];
1671                 this.$element.toggleClass( cssClass, state );
1672         }
1676  * @inheritdoc
1677  */
1678 OO.ui.Dialog.prototype.initialize = function () {
1679         // Parent method
1680         OO.ui.Dialog.super.prototype.initialize.call( this );
1682         // Properties
1683         this.closeButton = new OO.ui.ButtonWidget( {
1684                 '$': this.$,
1685                 'frameless': true,
1686                 'icon': 'close',
1687                 'title': OO.ui.msg( 'ooui-dialog-action-close' )
1688         } );
1690         // Events
1691         this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
1692         this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
1694         // Initialization
1695         this.frame.$content.addClass( 'oo-ui-dialog-content' );
1696         if ( this.footless ) {
1697                 this.frame.$content.addClass( 'oo-ui-dialog-content-footless' );
1698         }
1699         this.closeButton.$element.addClass( 'oo-ui-window-closeButton' );
1700         this.$head.append( this.closeButton.$element );
1704  * @inheritdoc
1705  */
1706 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
1707         return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
1708                 .next( function () {
1709                         // Prevent scrolling in top-level window
1710                         this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler );
1711                         this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler );
1712                 }, this );
1716  * @inheritdoc
1717  */
1718 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
1719         return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
1720                 .first( function () {
1721                         // Wait for closing transition
1722                         return OO.ui.Process.static.delay( 250 );
1723                 }, this )
1724                 .next( function () {
1725                         // Allow scrolling in top-level window
1726                         this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler );
1727                         this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler );
1728                 }, this );
1732  * Check if input is pending.
1734  * @return {boolean}
1735  */
1736 OO.ui.Dialog.prototype.isPending = function () {
1737         return !!this.pending;
1741  * Increase the pending stack.
1743  * @chainable
1744  */
1745 OO.ui.Dialog.prototype.pushPending = function () {
1746         if ( this.pending === 0 ) {
1747                 this.frame.$content.addClass( 'oo-ui-dialog-pending' );
1748                 this.$head.addClass( 'oo-ui-texture-pending' );
1749                 this.$foot.addClass( 'oo-ui-texture-pending' );
1750         }
1751         this.pending++;
1753         return this;
1757  * Reduce the pending stack.
1759  * Clamped at zero.
1761  * @chainable
1762  */
1763 OO.ui.Dialog.prototype.popPending = function () {
1764         if ( this.pending === 1 ) {
1765                 this.frame.$content.removeClass( 'oo-ui-dialog-pending' );
1766                 this.$head.removeClass( 'oo-ui-texture-pending' );
1767                 this.$foot.removeClass( 'oo-ui-texture-pending' );
1768         }
1769         this.pending = Math.max( 0, this.pending - 1 );
1771         return this;
1775  * Container for elements.
1777  * @abstract
1778  * @class
1779  * @extends OO.ui.Element
1780  * @mixins OO.EventEmitter
1782  * @constructor
1783  * @param {Object} [config] Configuration options
1784  */
1785 OO.ui.Layout = function OoUiLayout( config ) {
1786         // Initialize config
1787         config = config || {};
1789         // Parent constructor
1790         OO.ui.Layout.super.call( this, config );
1792         // Mixin constructors
1793         OO.EventEmitter.call( this );
1795         // Initialization
1796         this.$element.addClass( 'oo-ui-layout' );
1799 /* Setup */
1801 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1802 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1805  * User interface control.
1807  * @abstract
1808  * @class
1809  * @extends OO.ui.Element
1810  * @mixins OO.EventEmitter
1812  * @constructor
1813  * @param {Object} [config] Configuration options
1814  * @cfg {boolean} [disabled=false] Disable
1815  */
1816 OO.ui.Widget = function OoUiWidget( config ) {
1817         // Initialize config
1818         config = $.extend( { 'disabled': false }, config );
1820         // Parent constructor
1821         OO.ui.Widget.super.call( this, config );
1823         // Mixin constructors
1824         OO.EventEmitter.call( this );
1826         // Properties
1827         this.disabled = null;
1828         this.wasDisabled = null;
1830         // Initialization
1831         this.$element.addClass( 'oo-ui-widget' );
1832         this.setDisabled( !!config.disabled );
1835 /* Setup */
1837 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1838 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1840 /* Events */
1843  * @event disable
1844  * @param {boolean} disabled Widget is disabled
1845  */
1847 /* Methods */
1850  * Check if the widget is disabled.
1852  * @param {boolean} Button is disabled
1853  */
1854 OO.ui.Widget.prototype.isDisabled = function () {
1855         return this.disabled;
1859  * Update the disabled state, in case of changes in parent widget.
1861  * @chainable
1862  */
1863 OO.ui.Widget.prototype.updateDisabled = function () {
1864         this.setDisabled( this.disabled );
1865         return this;
1869  * Set the disabled state of the widget.
1871  * This should probably change the widgets' appearance and prevent it from being used.
1873  * @param {boolean} disabled Disable widget
1874  * @chainable
1875  */
1876 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1877         var isDisabled;
1879         this.disabled = !!disabled;
1880         isDisabled = this.isDisabled();
1881         if ( isDisabled !== this.wasDisabled ) {
1882                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1883                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1884                 this.emit( 'disable', isDisabled );
1885         }
1886         this.wasDisabled = isDisabled;
1887         return this;
1891  * A list of functions, called in sequence.
1893  * If a function added to a process returns boolean false the process will stop; if it returns an
1894  * object with a `promise` method the process will use the promise to either continue to the next
1895  * step when the promise is resolved or stop when the promise is rejected.
1897  * @class
1899  * @constructor
1900  */
1901 OO.ui.Process = function () {
1902         // Properties
1903         this.steps = [];
1906 /* Setup */
1908 OO.initClass( OO.ui.Process );
1910 /* Static Methods */
1913  * Generate a promise which is resolved after a set amount of time.
1915  * @param {number} length Number of milliseconds before resolving the promise
1916  * @return {jQuery.Promise} Promise that will be resolved after a set amount of time
1917  */
1918 OO.ui.Process.static.delay = function ( length ) {
1919         var deferred = $.Deferred();
1921         setTimeout( function () {
1922                 deferred.resolve();
1923         }, length );
1925         return deferred.promise();
1928 /* Methods */
1931  * Start the process.
1933  * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
1934  *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
1935  *   process, the remaining steps will not be taken
1936  */
1937 OO.ui.Process.prototype.execute = function () {
1938         var i, len, promise;
1940         /**
1941          * Continue execution.
1942          *
1943          * @ignore
1944          * @param {Array} step A function and the context it should be called in
1945          * @return {Function} Function that continues the process
1946          */
1947         function proceed( step ) {
1948                 return function () {
1949                         // Execute step in the correct context
1950                         var result = step[0].call( step[1] );
1952                         if ( result === false ) {
1953                                 // Use rejected promise for boolean false results
1954                                 return $.Deferred().reject().promise();
1955                         }
1956                         // Duck-type the object to see if it can produce a promise
1957                         if ( result && $.isFunction( result.promise ) ) {
1958                                 // Use a promise generated from the result
1959                                 return result.promise();
1960                         }
1961                         // Use resolved promise for other results
1962                         return $.Deferred().resolve().promise();
1963                 };
1964         }
1966         if ( this.steps.length ) {
1967                 // Generate a chain reaction of promises
1968                 promise = proceed( this.steps[0] )();
1969                 for ( i = 1, len = this.steps.length; i < len; i++ ) {
1970                         promise = promise.then( proceed( this.steps[i] ) );
1971                 }
1972         } else {
1973                 promise = $.Deferred().resolve().promise();
1974         }
1976         return promise;
1980  * Add step to the beginning of the process.
1982  * @param {Function} step Function to execute; if it returns boolean false the process will stop; if
1983  *   it returns an object with a `promise` method the process will use the promise to either
1984  *   continue to the next step when the promise is resolved or stop when the promise is rejected
1985  * @param {Object} [context=null] Context to call the step function in
1986  * @chainable
1987  */
1988 OO.ui.Process.prototype.first = function ( step, context ) {
1989         this.steps.unshift( [ step, context || null ] );
1990         return this;
1994  * Add step to the end of the process.
1996  * @param {Function} step Function to execute; if it returns boolean false the process will stop; if
1997  *   it returns an object with a `promise` method the process will use the promise to either
1998  *   continue to the next step when the promise is resolved or stop when the promise is rejected
1999  * @param {Object} [context=null] Context to call the step function in
2000  * @chainable
2001  */
2002 OO.ui.Process.prototype.next = function ( step, context ) {
2003         this.steps.push( [ step, context || null ] );
2004         return this;
2008  * Dialog for showing a confirmation/warning message.
2010  * @class
2011  * @extends OO.ui.Dialog
2013  * @constructor
2014  * @param {Object} [config] Configuration options
2015  */
2016 OO.ui.ConfirmationDialog = function OoUiConfirmationDialog( config ) {
2017         // Configuration initialization
2018         config = $.extend( { 'size': 'small' }, config );
2020         // Parent constructor
2021         OO.ui.Dialog.call( this, config );
2024 /* Inheritance */
2026 OO.inheritClass( OO.ui.ConfirmationDialog, OO.ui.Dialog );
2028 /* Static Properties */
2030 OO.ui.ConfirmationDialog.static.name = 'confirm';
2032 OO.ui.ConfirmationDialog.static.icon = 'help';
2034 OO.ui.ConfirmationDialog.static.title = OO.ui.deferMsg( 'ooui-dialog-confirm-title' );
2036 /* Methods */
2039  * @inheritdoc
2040  */
2041 OO.ui.ConfirmationDialog.prototype.initialize = function () {
2042         // Parent method
2043         OO.ui.Dialog.prototype.initialize.call( this );
2045         // Set up the layout
2046         var contentLayout = new OO.ui.PanelLayout( {
2047                 '$': this.$,
2048                 'padded': true
2049         } );
2051         this.$promptContainer = this.$( '<div>' ).addClass( 'oo-ui-dialog-confirm-promptContainer' );
2053         this.cancelButton = new OO.ui.ButtonWidget();
2054         this.cancelButton.connect( this, { 'click': [ 'close', 'cancel' ] } );
2056         this.okButton = new OO.ui.ButtonWidget();
2057         this.okButton.connect( this, { 'click': [ 'close', 'ok' ] } );
2059         // Make the buttons
2060         contentLayout.$element.append( this.$promptContainer );
2061         this.$body.append( contentLayout.$element );
2063         this.$foot.append(
2064                 this.okButton.$element,
2065                 this.cancelButton.$element
2066         );
2070  * Setup a confirmation dialog.
2072  * @param {Object} [data] Window opening data including text of the dialog and text for the buttons
2073  * @param {jQuery|string} [data.prompt] Text to display or list of nodes to use as content of the dialog.
2074  * @param {jQuery|string|Function|null} [data.okLabel] Label of the OK button
2075  * @param {jQuery|string|Function|null} [data.cancelLabel] Label of the cancel button
2076  * @param {string|string[]} [data.okFlags="constructive"] Flags for the OK button
2077  * @param {string|string[]} [data.cancelFlags="destructive"] Flags for the cancel button
2078  * @return {OO.ui.Process} Setup process
2079  */
2080 OO.ui.ConfirmationDialog.prototype.getSetupProcess = function ( data ) {
2081         // Parent method
2082         return OO.ui.ConfirmationDialog.super.prototype.getSetupProcess.call( this, data )
2083                 .next( function () {
2084                         var prompt = data.prompt || OO.ui.deferMsg( 'ooui-dialog-confirm-default-prompt' ),
2085                                 okLabel = data.okLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-ok' ),
2086                                 cancelLabel = data.cancelLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-cancel' ),
2087                                 okFlags = data.okFlags || 'constructive',
2088                                 cancelFlags = data.cancelFlags || 'destructive';
2090                         if ( typeof prompt === 'string' ) {
2091                                 this.$promptContainer.text( prompt );
2092                         } else {
2093                                 this.$promptContainer.empty().append( prompt );
2094                         }
2096                         this.okButton.setLabel( okLabel ).clearFlags().setFlags( okFlags );
2097                         this.cancelButton.setLabel( cancelLabel ).clearFlags().setFlags( cancelFlags );
2098                 }, this );
2102  * @inheritdoc
2103  */
2104 OO.ui.ConfirmationDialog.prototype.getTeardownProcess = function ( data ) {
2105         // Parent method
2106         return OO.ui.ConfirmationDialog.super.prototype.getTeardownProcess.call( this, data )
2107                 .first( function () {
2108                         if ( data === 'ok' ) {
2109                                 this.opened.resolve();
2110                         } else { // data === 'cancel', or no data
2111                                 this.opened.reject();
2112                         }
2113                 }, this );
2117  * Element with a button.
2119  * @abstract
2120  * @class
2122  * @constructor
2123  * @param {jQuery} $button Button node, assigned to #$button
2124  * @param {Object} [config] Configuration options
2125  * @cfg {boolean} [frameless] Render button without a frame
2126  * @cfg {number} [tabIndex=0] Button's tab index, use -1 to prevent tab focusing
2127  */
2128 OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
2129         // Configuration initialization
2130         config = config || {};
2132         // Properties
2133         this.$button = $button;
2134         this.tabIndex = null;
2135         this.active = false;
2136         this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
2138         // Events
2139         this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
2141         // Initialization
2142         this.$element
2143                 .addClass( 'oo-ui-buttonedElement' )
2144                 .prop( 'tabIndex', config.tabIndex || 0 );
2145         this.$button
2146                 .addClass( 'oo-ui-buttonedElement-button' )
2147                 .attr( 'role', 'button' );
2148         if ( config.frameless ) {
2149                 this.$element.addClass( 'oo-ui-buttonedElement-frameless' );
2150         } else {
2151                 this.$element.addClass( 'oo-ui-buttonedElement-framed' );
2152         }
2155 /* Setup */
2157 OO.initClass( OO.ui.ButtonedElement );
2159 /* Static Properties */
2162  * Cancel mouse down events.
2164  * @static
2165  * @inheritable
2166  * @property {boolean}
2167  */
2168 OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true;
2170 /* Methods */
2173  * Handles mouse down events.
2175  * @param {jQuery.Event} e Mouse down event
2176  */
2177 OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
2178         if ( this.isDisabled() || e.which !== 1 ) {
2179                 return false;
2180         }
2181         // tabIndex should generally be interacted with via the property, but it's not possible to
2182         // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute
2183         this.tabIndex = this.$button.attr( 'tabindex' );
2184         this.$button
2185                 // Remove the tab-index while the button is down to prevent the button from stealing focus
2186                 .removeAttr( 'tabindex' )
2187                 .addClass( 'oo-ui-buttonedElement-pressed' );
2188         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2189         // reliably reapply the tabindex and remove the pressed class
2190         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
2191         // Prevent change of focus unless specifically configured otherwise
2192         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
2193                 return false;
2194         }
2198  * Handles mouse up events.
2200  * @param {jQuery.Event} e Mouse up event
2201  */
2202 OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
2203         if ( this.isDisabled() || e.which !== 1 ) {
2204                 return false;
2205         }
2206         this.$button
2207                 // Restore the tab-index after the button is up to restore the button's accesssibility
2208                 .attr( 'tabindex', this.tabIndex )
2209                 .removeClass( 'oo-ui-buttonedElement-pressed' );
2210         // Stop listening for mouseup, since we only needed this once
2211         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
2215  * Set active state.
2217  * @param {boolean} [value] Make button active
2218  * @chainable
2219  */
2220 OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
2221         this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value );
2222         return this;
2226  * Element that can be automatically clipped to visible boundaies.
2228  * @abstract
2229  * @class
2231  * @constructor
2232  * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
2233  * @param {Object} [config] Configuration options
2234  */
2235 OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
2236         // Configuration initialization
2237         config = config || {};
2239         // Properties
2240         this.$clippable = $clippable;
2241         this.clipping = false;
2242         this.clipped = false;
2243         this.$clippableContainer = null;
2244         this.$clippableScroller = null;
2245         this.$clippableWindow = null;
2246         this.idealWidth = null;
2247         this.idealHeight = null;
2248         this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
2249         this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
2251         // Initialization
2252         this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
2255 /* Methods */
2258  * Set clipping.
2260  * @param {boolean} value Enable clipping
2261  * @chainable
2262  */
2263 OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
2264         value = !!value;
2266         if ( this.clipping !== value ) {
2267                 this.clipping = value;
2268                 if ( this.clipping ) {
2269                         this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
2270                         // If the clippable container is the body, we have to listen to scroll events and check
2271                         // jQuery.scrollTop on the window because of browser inconsistencies
2272                         this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
2273                                 this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
2274                                 this.$clippableContainer;
2275                         this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
2276                         this.$clippableWindow = this.$( this.getElementWindow() )
2277                                 .on( 'resize', this.onClippableWindowResizeHandler );
2278                         // Initial clip after visible
2279                         setTimeout( OO.ui.bind( this.clip, this ) );
2280                 } else {
2281                         this.$clippableContainer = null;
2282                         this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
2283                         this.$clippableScroller = null;
2284                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
2285                         this.$clippableWindow = null;
2286                 }
2287         }
2289         return this;
2293  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
2295  * @return {boolean} Element will be clipped to the visible area
2296  */
2297 OO.ui.ClippableElement.prototype.isClipping = function () {
2298         return this.clipping;
2302  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
2304  * @return {boolean} Part of the element is being clipped
2305  */
2306 OO.ui.ClippableElement.prototype.isClipped = function () {
2307         return this.clipped;
2311  * Set the ideal size.
2313  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
2314  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
2315  */
2316 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
2317         this.idealWidth = width;
2318         this.idealHeight = height;
2322  * Clip element to visible boundaries and allow scrolling when needed.
2324  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
2325  * overlapped by, the visible area of the nearest scrollable container.
2327  * @chainable
2328  */
2329 OO.ui.ClippableElement.prototype.clip = function () {
2330         if ( !this.clipping ) {
2331                 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
2332                 return this;
2333         }
2335         var buffer = 10,
2336                 cOffset = this.$clippable.offset(),
2337                 ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 },
2338                 ccHeight = this.$clippableContainer.innerHeight() - buffer,
2339                 ccWidth = this.$clippableContainer.innerWidth() - buffer,
2340                 scrollTop = this.$clippableScroller.scrollTop(),
2341                 scrollLeft = this.$clippableScroller.scrollLeft(),
2342                 desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
2343                 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
2344                 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
2345                 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
2346                 clipWidth = desiredWidth < naturalWidth,
2347                 clipHeight = desiredHeight < naturalHeight;
2349         if ( clipWidth ) {
2350                 this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } );
2351         } else {
2352                 this.$clippable.css( 'width', this.idealWidth || '' );
2353                 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
2354                 this.$clippable.css( 'overflow-x', '' );
2355         }
2356         if ( clipHeight ) {
2357                 this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
2358         } else {
2359                 this.$clippable.css( 'height', this.idealHeight || '' );
2360                 this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
2361                 this.$clippable.css( 'overflow-y', '' );
2362         }
2364         this.clipped = clipWidth || clipHeight;
2366         return this;
2370  * Element with named flags that can be added, removed, listed and checked.
2372  * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with
2373  * the flag name. Flags are primarily useful for styling.
2375  * @abstract
2376  * @class
2378  * @constructor
2379  * @param {Object} [config] Configuration options
2380  * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
2381  */
2382 OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
2383         // Config initialization
2384         config = config || {};
2386         // Properties
2387         this.flags = {};
2389         // Initialization
2390         this.setFlags( config.flags );
2393 /* Methods */
2396  * Check if a flag is set.
2398  * @param {string} flag Name of flag
2399  * @return {boolean} Has flag
2400  */
2401 OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
2402         return flag in this.flags;
2406  * Get the names of all flags set.
2408  * @return {string[]} flags Flag names
2409  */
2410 OO.ui.FlaggableElement.prototype.getFlags = function () {
2411         return Object.keys( this.flags );
2415  * Clear all flags.
2417  * @chainable
2418  */
2419 OO.ui.FlaggableElement.prototype.clearFlags = function () {
2420         var flag,
2421                 classPrefix = 'oo-ui-flaggableElement-';
2423         for ( flag in this.flags ) {
2424                 delete this.flags[flag];
2425                 this.$element.removeClass( classPrefix + flag );
2426         }
2428         return this;
2432  * Add one or more flags.
2434  * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
2435  *  keyed by flag name containing boolean set/remove instructions.
2436  * @chainable
2437  */
2438 OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
2439         var i, len, flag,
2440                 classPrefix = 'oo-ui-flaggableElement-';
2442         if ( typeof flags === 'string' ) {
2443                 // Set
2444                 this.flags[flags] = true;
2445                 this.$element.addClass( classPrefix + flags );
2446         } else if ( $.isArray( flags ) ) {
2447                 for ( i = 0, len = flags.length; i < len; i++ ) {
2448                         flag = flags[i];
2449                         // Set
2450                         this.flags[flag] = true;
2451                         this.$element.addClass( classPrefix + flag );
2452                 }
2453         } else if ( OO.isPlainObject( flags ) ) {
2454                 for ( flag in flags ) {
2455                         if ( flags[flag] ) {
2456                                 // Set
2457                                 this.flags[flag] = true;
2458                                 this.$element.addClass( classPrefix + flag );
2459                         } else {
2460                                 // Remove
2461                                 delete this.flags[flag];
2462                                 this.$element.removeClass( classPrefix + flag );
2463                         }
2464                 }
2465         }
2466         return this;
2470  * Element containing a sequence of child elements.
2472  * @abstract
2473  * @class
2475  * @constructor
2476  * @param {jQuery} $group Container node, assigned to #$group
2477  * @param {Object} [config] Configuration options
2478  */
2479 OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
2480         // Configuration
2481         config = config || {};
2483         // Properties
2484         this.$group = $group;
2485         this.items = [];
2486         this.aggregateItemEvents = {};
2489 /* Methods */
2492  * Get items.
2494  * @return {OO.ui.Element[]} Items
2495  */
2496 OO.ui.GroupElement.prototype.getItems = function () {
2497         return this.items.slice( 0 );
2501  * Add an aggregate item event.
2503  * Aggregated events are listened to on each item and then emitted by the group under a new name,
2504  * and with an additional leading parameter containing the item that emitted the original event.
2505  * Other arguments that were emitted from the original event are passed through.
2507  * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
2508  *   event, use null value to remove aggregation
2509  * @throws {Error} If aggregation already exists
2510  */
2511 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
2512         var i, len, item, add, remove, itemEvent, groupEvent;
2514         for ( itemEvent in events ) {
2515                 groupEvent = events[itemEvent];
2517                 // Remove existing aggregated event
2518                 if ( itemEvent in this.aggregateItemEvents ) {
2519                         // Don't allow duplicate aggregations
2520                         if ( groupEvent ) {
2521                                 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
2522                         }
2523                         // Remove event aggregation from existing items
2524                         for ( i = 0, len = this.items.length; i < len; i++ ) {
2525                                 item = this.items[i];
2526                                 if ( item.connect && item.disconnect ) {
2527                                         remove = {};
2528                                         remove[itemEvent] = [ 'emit', groupEvent, item ];
2529                                         item.disconnect( this, remove );
2530                                 }
2531                         }
2532                         // Prevent future items from aggregating event
2533                         delete this.aggregateItemEvents[itemEvent];
2534                 }
2536                 // Add new aggregate event
2537                 if ( groupEvent ) {
2538                         // Make future items aggregate event
2539                         this.aggregateItemEvents[itemEvent] = groupEvent;
2540                         // Add event aggregation to existing items
2541                         for ( i = 0, len = this.items.length; i < len; i++ ) {
2542                                 item = this.items[i];
2543                                 if ( item.connect && item.disconnect ) {
2544                                         add = {};
2545                                         add[itemEvent] = [ 'emit', groupEvent, item ];
2546                                         item.connect( this, add );
2547                                 }
2548                         }
2549                 }
2550         }
2554  * Add items.
2556  * @param {OO.ui.Element[]} items Item
2557  * @param {number} [index] Index to insert items at
2558  * @chainable
2559  */
2560 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
2561         var i, len, item, event, events, currentIndex,
2562                 itemElements = [];
2564         for ( i = 0, len = items.length; i < len; i++ ) {
2565                 item = items[i];
2567                 // Check if item exists then remove it first, effectively "moving" it
2568                 currentIndex = $.inArray( item, this.items );
2569                 if ( currentIndex >= 0 ) {
2570                         this.removeItems( [ item ] );
2571                         // Adjust index to compensate for removal
2572                         if ( currentIndex < index ) {
2573                                 index--;
2574                         }
2575                 }
2576                 // Add the item
2577                 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
2578                         events = {};
2579                         for ( event in this.aggregateItemEvents ) {
2580                                 events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
2581                         }
2582                         item.connect( this, events );
2583                 }
2584                 item.setElementGroup( this );
2585                 itemElements.push( item.$element.get( 0 ) );
2586         }
2588         if ( index === undefined || index < 0 || index >= this.items.length ) {
2589                 this.$group.append( itemElements );
2590                 this.items.push.apply( this.items, items );
2591         } else if ( index === 0 ) {
2592                 this.$group.prepend( itemElements );
2593                 this.items.unshift.apply( this.items, items );
2594         } else {
2595                 this.items[index].$element.before( itemElements );
2596                 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
2597         }
2599         return this;
2603  * Remove items.
2605  * Items will be detached, not removed, so they can be used later.
2607  * @param {OO.ui.Element[]} items Items to remove
2608  * @chainable
2609  */
2610 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
2611         var i, len, item, index, remove, itemEvent;
2613         // Remove specific items
2614         for ( i = 0, len = items.length; i < len; i++ ) {
2615                 item = items[i];
2616                 index = $.inArray( item, this.items );
2617                 if ( index !== -1 ) {
2618                         if (
2619                                 item.connect && item.disconnect &&
2620                                 !$.isEmptyObject( this.aggregateItemEvents )
2621                         ) {
2622                                 remove = {};
2623                                 if ( itemEvent in this.aggregateItemEvents ) {
2624                                         remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
2625                                 }
2626                                 item.disconnect( this, remove );
2627                         }
2628                         item.setElementGroup( null );
2629                         this.items.splice( index, 1 );
2630                         item.$element.detach();
2631                 }
2632         }
2634         return this;
2638  * Clear all items.
2640  * Items will be detached, not removed, so they can be used later.
2642  * @chainable
2643  */
2644 OO.ui.GroupElement.prototype.clearItems = function () {
2645         var i, len, item, remove, itemEvent;
2647         // Remove all items
2648         for ( i = 0, len = this.items.length; i < len; i++ ) {
2649                 item = this.items[i];
2650                 if (
2651                         item.connect && item.disconnect &&
2652                         !$.isEmptyObject( this.aggregateItemEvents )
2653                 ) {
2654                         remove = {};
2655                         if ( itemEvent in this.aggregateItemEvents ) {
2656                                 remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
2657                         }
2658                         item.disconnect( this, remove );
2659                 }
2660                 item.setElementGroup( null );
2661                 item.$element.detach();
2662         }
2664         this.items = [];
2665         return this;
2669  * Element containing an icon.
2671  * @abstract
2672  * @class
2674  * @constructor
2675  * @param {jQuery} $icon Icon node, assigned to #$icon
2676  * @param {Object} [config] Configuration options
2677  * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
2678  *  use the 'default' key to specify the icon to be used when there is no icon in the user's
2679  *  language
2680  */
2681 OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) {
2682         // Config intialization
2683         config = config || {};
2685         // Properties
2686         this.$icon = $icon;
2687         this.icon = null;
2689         // Initialization
2690         this.$icon.addClass( 'oo-ui-iconedElement-icon' );
2691         this.setIcon( config.icon || this.constructor.static.icon );
2694 /* Setup */
2696 OO.initClass( OO.ui.IconedElement );
2698 /* Static Properties */
2701  * Icon.
2703  * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
2705  * For i18n purposes, this property can be an object containing a `default` icon name property and
2706  * additional icon names keyed by language code.
2708  * Example of i18n icon definition:
2709  *     { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' }
2711  * @static
2712  * @inheritable
2713  * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
2714  *  use the 'default' key to specify the icon to be used when there is no icon in the user's
2715  *  language
2716  */
2717 OO.ui.IconedElement.static.icon = null;
2719 /* Methods */
2722  * Set icon.
2724  * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID;
2725  *  use the 'default' key to specify the icon to be used when there is no icon in the user's
2726  *  language
2727  * @chainable
2728  */
2729 OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
2730         icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
2732         if ( this.icon ) {
2733                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
2734         }
2735         if ( typeof icon === 'string' ) {
2736                 icon = icon.trim();
2737                 if ( icon.length ) {
2738                         this.$icon.addClass( 'oo-ui-icon-' + icon );
2739                         this.icon = icon;
2740                 }
2741         }
2742         this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
2744         return this;
2748  * Get icon.
2750  * @return {string} Icon
2751  */
2752 OO.ui.IconedElement.prototype.getIcon = function () {
2753         return this.icon;
2757  * Element containing an indicator.
2759  * @abstract
2760  * @class
2762  * @constructor
2763  * @param {jQuery} $indicator Indicator node, assigned to #$indicator
2764  * @param {Object} [config] Configuration options
2765  * @cfg {string} [indicator] Symbolic indicator name
2766  * @cfg {string} [indicatorTitle] Indicator title text or a function that return text
2767  */
2768 OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
2769         // Config intialization
2770         config = config || {};
2772         // Properties
2773         this.$indicator = $indicator;
2774         this.indicator = null;
2775         this.indicatorLabel = null;
2777         // Initialization
2778         this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
2779         this.setIndicator( config.indicator || this.constructor.static.indicator );
2780         this.setIndicatorTitle( config.indicatorTitle  || this.constructor.static.indicatorTitle );
2783 /* Setup */
2785 OO.initClass( OO.ui.IndicatedElement );
2787 /* Static Properties */
2790  * indicator.
2792  * @static
2793  * @inheritable
2794  * @property {string|null} Symbolic indicator name or null for no indicator
2795  */
2796 OO.ui.IndicatedElement.static.indicator = null;
2799  * Indicator title.
2801  * @static
2802  * @inheritable
2803  * @property {string|Function|null} Indicator title text, a function that return text or null for no
2804  *  indicator title
2805  */
2806 OO.ui.IndicatedElement.static.indicatorTitle = null;
2808 /* Methods */
2811  * Set indicator.
2813  * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
2814  * @chainable
2815  */
2816 OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) {
2817         if ( this.indicator ) {
2818                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
2819                 this.indicator = null;
2820         }
2821         if ( typeof indicator === 'string' ) {
2822                 indicator = indicator.trim();
2823                 if ( indicator.length ) {
2824                         this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
2825                         this.indicator = indicator;
2826                 }
2827         }
2828         this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator );
2830         return this;
2834  * Set indicator label.
2836  * @param {string|Function|null} indicator Indicator title text, a function that return text or null
2837  *  for no indicator title
2838  * @chainable
2839  */
2840 OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
2841         this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
2843         if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
2844                 this.$indicator.attr( 'title', indicatorTitle );
2845         } else {
2846                 this.$indicator.removeAttr( 'title' );
2847         }
2849         return this;
2853  * Get indicator.
2855  * @return {string} title Symbolic name of indicator
2856  */
2857 OO.ui.IndicatedElement.prototype.getIndicator = function () {
2858         return this.indicator;
2862  * Get indicator title.
2864  * @return {string} Indicator title text
2865  */
2866 OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
2867         return this.indicatorTitle;
2871  * Element containing a label.
2873  * @abstract
2874  * @class
2876  * @constructor
2877  * @param {jQuery} $label Label node, assigned to #$label
2878  * @param {Object} [config] Configuration options
2879  * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
2880  * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
2881  */
2882 OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
2883         // Config intialization
2884         config = config || {};
2886         // Properties
2887         this.$label = $label;
2888         this.label = null;
2890         // Initialization
2891         this.$label.addClass( 'oo-ui-labeledElement-label' );
2892         this.setLabel( config.label || this.constructor.static.label );
2893         this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
2896 /* Setup */
2898 OO.initClass( OO.ui.LabeledElement );
2900 /* Static Properties */
2903  * Label.
2905  * @static
2906  * @inheritable
2907  * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
2908  *  no label
2909  */
2910 OO.ui.LabeledElement.static.label = null;
2912 /* Methods */
2915  * Set the label.
2917  * An empty string will result in the label being hidden. A string containing only whitespace will
2918  * be converted to a single &nbsp;
2920  * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
2921  *  text; or null for no label
2922  * @chainable
2923  */
2924 OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
2925         var empty = false;
2927         this.label = label = OO.ui.resolveMsg( label ) || null;
2928         if ( typeof label === 'string' && label.length ) {
2929                 if ( label.match( /^\s*$/ ) ) {
2930                         // Convert whitespace only string to a single non-breaking space
2931                         this.$label.html( '&nbsp;' );
2932                 } else {
2933                         this.$label.text( label );
2934                 }
2935         } else if ( label instanceof jQuery ) {
2936                 this.$label.empty().append( label );
2937         } else {
2938                 this.$label.empty();
2939                 empty = true;
2940         }
2941         this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
2942         this.$label.css( 'display', empty ? 'none' : '' );
2944         return this;
2948  * Get the label.
2950  * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2951  *  text; or null for no label
2952  */
2953 OO.ui.LabeledElement.prototype.getLabel = function () {
2954         return this.label;
2958  * Fit the label.
2960  * @chainable
2961  */
2962 OO.ui.LabeledElement.prototype.fitLabel = function () {
2963         if ( this.$label.autoEllipsis && this.autoFitLabel ) {
2964                 this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
2965         }
2966         return this;
2970  * Popuppable element.
2972  * @abstract
2973  * @class
2975  * @constructor
2976  * @param {Object} [config] Configuration options
2977  * @cfg {number} [popupWidth=320] Width of popup
2978  * @cfg {number} [popupHeight] Height of popup
2979  * @cfg {Object} [popup] Configuration to pass to popup
2980  */
2981 OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
2982         // Configuration initialization
2983         config = $.extend( { 'popupWidth': 320 }, config );
2985         // Properties
2986         this.popup = new OO.ui.PopupWidget( $.extend(
2987                 { 'align': 'center', 'autoClose': true },
2988                 config.popup,
2989                 { '$': this.$, '$autoCloseIgnore': this.$element }
2990         ) );
2991         this.popupWidth = config.popupWidth;
2992         this.popupHeight = config.popupHeight;
2995 /* Methods */
2998  * Get popup.
3000  * @return {OO.ui.PopupWidget} Popup widget
3001  */
3002 OO.ui.PopuppableElement.prototype.getPopup = function () {
3003         return this.popup;
3007  * Show popup.
3008  */
3009 OO.ui.PopuppableElement.prototype.showPopup = function () {
3010         this.popup.show().display( this.popupWidth, this.popupHeight );
3014  * Hide popup.
3015  */
3016 OO.ui.PopuppableElement.prototype.hidePopup = function () {
3017         this.popup.hide();
3021  * Element with a title.
3023  * @abstract
3024  * @class
3026  * @constructor
3027  * @param {jQuery} $label Titled node, assigned to #$titled
3028  * @param {Object} [config] Configuration options
3029  * @cfg {string|Function} [title] Title text or a function that returns text
3030  */
3031 OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
3032         // Config intialization
3033         config = config || {};
3035         // Properties
3036         this.$titled = $titled;
3037         this.title = null;
3039         // Initialization
3040         this.setTitle( config.title || this.constructor.static.title );
3043 /* Setup */
3045 OO.initClass( OO.ui.TitledElement );
3047 /* Static Properties */
3050  * Title.
3052  * @static
3053  * @inheritable
3054  * @property {string|Function} Title text or a function that returns text
3055  */
3056 OO.ui.TitledElement.static.title = null;
3058 /* Methods */
3061  * Set title.
3063  * @param {string|Function|null} title Title text, a function that returns text or null for no title
3064  * @chainable
3065  */
3066 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
3067         this.title = title = OO.ui.resolveMsg( title ) || null;
3069         if ( typeof title === 'string' && title.length ) {
3070                 this.$titled.attr( 'title', title );
3071         } else {
3072                 this.$titled.removeAttr( 'title' );
3073         }
3075         return this;
3079  * Get title.
3081  * @return {string} Title string
3082  */
3083 OO.ui.TitledElement.prototype.getTitle = function () {
3084         return this.title;
3088  * Generic toolbar tool.
3090  * @abstract
3091  * @class
3092  * @extends OO.ui.Widget
3093  * @mixins OO.ui.IconedElement
3095  * @constructor
3096  * @param {OO.ui.ToolGroup} toolGroup
3097  * @param {Object} [config] Configuration options
3098  * @cfg {string|Function} [title] Title text or a function that returns text
3099  */
3100 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
3101         // Config intialization
3102         config = config || {};
3104         // Parent constructor
3105         OO.ui.Tool.super.call( this, config );
3107         // Mixin constructors
3108         OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
3110         // Properties
3111         this.toolGroup = toolGroup;
3112         this.toolbar = this.toolGroup.getToolbar();
3113         this.active = false;
3114         this.$title = this.$( '<span>' );
3115         this.$link = this.$( '<a>' );
3116         this.title = null;
3118         // Events
3119         this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
3121         // Initialization
3122         this.$title.addClass( 'oo-ui-tool-title' );
3123         this.$link
3124                 .addClass( 'oo-ui-tool-link' )
3125                 .append( this.$icon, this.$title )
3126                 .prop( 'tabIndex', 0 )
3127                 .attr( 'role', 'button' );
3128         this.$element
3129                 .data( 'oo-ui-tool', this )
3130                 .addClass(
3131                         'oo-ui-tool ' + 'oo-ui-tool-name-' +
3132                         this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
3133                 )
3134                 .append( this.$link );
3135         this.setTitle( config.title || this.constructor.static.title );
3138 /* Setup */
3140 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
3141 OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
3143 /* Events */
3146  * @event select
3147  */
3149 /* Static Properties */
3152  * @static
3153  * @inheritdoc
3154  */
3155 OO.ui.Tool.static.tagName = 'span';
3158  * Symbolic name of tool.
3160  * @abstract
3161  * @static
3162  * @inheritable
3163  * @property {string}
3164  */
3165 OO.ui.Tool.static.name = '';
3168  * Tool group.
3170  * @abstract
3171  * @static
3172  * @inheritable
3173  * @property {string}
3174  */
3175 OO.ui.Tool.static.group = '';
3178  * Tool title.
3180  * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
3181  * is part of a list or menu tool group. If a trigger is associated with an action by the same name
3182  * as the tool, a description of its keyboard shortcut for the appropriate platform will be
3183  * appended to the title if the tool is part of a bar tool group.
3185  * @abstract
3186  * @static
3187  * @inheritable
3188  * @property {string|Function} Title text or a function that returns text
3189  */
3190 OO.ui.Tool.static.title = '';
3193  * Tool can be automatically added to catch-all groups.
3195  * @static
3196  * @inheritable
3197  * @property {boolean}
3198  */
3199 OO.ui.Tool.static.autoAddToCatchall = true;
3202  * Tool can be automatically added to named groups.
3204  * @static
3205  * @property {boolean}
3206  * @inheritable
3207  */
3208 OO.ui.Tool.static.autoAddToGroup = true;
3211  * Check if this tool is compatible with given data.
3213  * @static
3214  * @inheritable
3215  * @param {Mixed} data Data to check
3216  * @return {boolean} Tool can be used with data
3217  */
3218 OO.ui.Tool.static.isCompatibleWith = function () {
3219         return false;
3222 /* Methods */
3225  * Handle the toolbar state being updated.
3227  * This is an abstract method that must be overridden in a concrete subclass.
3229  * @abstract
3230  */
3231 OO.ui.Tool.prototype.onUpdateState = function () {
3232         throw new Error(
3233                 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
3234         );
3238  * Handle the tool being selected.
3240  * This is an abstract method that must be overridden in a concrete subclass.
3242  * @abstract
3243  */
3244 OO.ui.Tool.prototype.onSelect = function () {
3245         throw new Error(
3246                 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
3247         );
3251  * Check if the button is active.
3253  * @param {boolean} Button is active
3254  */
3255 OO.ui.Tool.prototype.isActive = function () {
3256         return this.active;
3260  * Make the button appear active or inactive.
3262  * @param {boolean} state Make button appear active
3263  */
3264 OO.ui.Tool.prototype.setActive = function ( state ) {
3265         this.active = !!state;
3266         if ( this.active ) {
3267                 this.$element.addClass( 'oo-ui-tool-active' );
3268         } else {
3269                 this.$element.removeClass( 'oo-ui-tool-active' );
3270         }
3274  * Get the tool title.
3276  * @param {string|Function} title Title text or a function that returns text
3277  * @chainable
3278  */
3279 OO.ui.Tool.prototype.setTitle = function ( title ) {
3280         this.title = OO.ui.resolveMsg( title );
3281         this.updateTitle();
3282         return this;
3286  * Get the tool title.
3288  * @return {string} Title text
3289  */
3290 OO.ui.Tool.prototype.getTitle = function () {
3291         return this.title;
3295  * Get the tool's symbolic name.
3297  * @return {string} Symbolic name of tool
3298  */
3299 OO.ui.Tool.prototype.getName = function () {
3300         return this.constructor.static.name;
3304  * Update the title.
3305  */
3306 OO.ui.Tool.prototype.updateTitle = function () {
3307         var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
3308                 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
3309                 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
3310                 tooltipParts = [];
3312         this.$title.empty()
3313                 .text( this.title )
3314                 .append(
3315                         this.$( '<span>' )
3316                                 .addClass( 'oo-ui-tool-accel' )
3317                                 .text( accel )
3318                 );
3320         if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
3321                 tooltipParts.push( this.title );
3322         }
3323         if ( accelTooltips && typeof accel === 'string' && accel.length ) {
3324                 tooltipParts.push( accel );
3325         }
3326         if ( tooltipParts.length ) {
3327                 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
3328         } else {
3329                 this.$link.removeAttr( 'title' );
3330         }
3334  * Destroy tool.
3335  */
3336 OO.ui.Tool.prototype.destroy = function () {
3337         this.toolbar.disconnect( this );
3338         this.$element.remove();
3342  * Collection of tool groups.
3344  * @class
3345  * @extends OO.ui.Element
3346  * @mixins OO.EventEmitter
3347  * @mixins OO.ui.GroupElement
3349  * @constructor
3350  * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
3351  * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
3352  * @param {Object} [config] Configuration options
3353  * @cfg {boolean} [actions] Add an actions section opposite to the tools
3354  * @cfg {boolean} [shadow] Add a shadow below the toolbar
3355  */
3356 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
3357         // Configuration initialization
3358         config = config || {};
3360         // Parent constructor
3361         OO.ui.Toolbar.super.call( this, config );
3363         // Mixin constructors
3364         OO.EventEmitter.call( this );
3365         OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
3367         // Properties
3368         this.toolFactory = toolFactory;
3369         this.toolGroupFactory = toolGroupFactory;
3370         this.groups = [];
3371         this.tools = {};
3372         this.$bar = this.$( '<div>' );
3373         this.$actions = this.$( '<div>' );
3374         this.initialized = false;
3376         // Events
3377         this.$element
3378                 .add( this.$bar ).add( this.$group ).add( this.$actions )
3379                 .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
3381         // Initialization
3382         this.$group.addClass( 'oo-ui-toolbar-tools' );
3383         this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
3384         if ( config.actions ) {
3385                 this.$actions.addClass( 'oo-ui-toolbar-actions' );
3386                 this.$bar.append( this.$actions );
3387         }
3388         this.$bar.append( '<div style="clear:both"></div>' );
3389         if ( config.shadow ) {
3390                 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
3391         }
3392         this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
3395 /* Setup */
3397 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
3398 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
3399 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
3401 /* Methods */
3404  * Get the tool factory.
3406  * @return {OO.ui.ToolFactory} Tool factory
3407  */
3408 OO.ui.Toolbar.prototype.getToolFactory = function () {
3409         return this.toolFactory;
3413  * Get the tool group factory.
3415  * @return {OO.Factory} Tool group factory
3416  */
3417 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
3418         return this.toolGroupFactory;
3422  * Handles mouse down events.
3424  * @param {jQuery.Event} e Mouse down event
3425  */
3426 OO.ui.Toolbar.prototype.onMouseDown = function ( e ) {
3427         var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
3428                 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
3429         if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
3430                 return false;
3431         }
3435  * Sets up handles and preloads required information for the toolbar to work.
3436  * This must be called immediately after it is attached to a visible document.
3437  */
3438 OO.ui.Toolbar.prototype.initialize = function () {
3439         this.initialized = true;
3443  * Setup toolbar.
3445  * Tools can be specified in the following ways:
3447  * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
3448  * - All tools in a group: `{ 'group': 'group-name' }`
3449  * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
3451  * @param {Object.<string,Array>} groups List of tool group configurations
3452  * @param {Array|string} [groups.include] Tools to include
3453  * @param {Array|string} [groups.exclude] Tools to exclude
3454  * @param {Array|string} [groups.promote] Tools to promote to the beginning
3455  * @param {Array|string} [groups.demote] Tools to demote to the end
3456  */
3457 OO.ui.Toolbar.prototype.setup = function ( groups ) {
3458         var i, len, type, group,
3459                 items = [],
3460                 defaultType = 'bar';
3462         // Cleanup previous groups
3463         this.reset();
3465         // Build out new groups
3466         for ( i = 0, len = groups.length; i < len; i++ ) {
3467                 group = groups[i];
3468                 if ( group.include === '*' ) {
3469                         // Apply defaults to catch-all groups
3470                         if ( group.type === undefined ) {
3471                                 group.type = 'list';
3472                         }
3473                         if ( group.label === undefined ) {
3474                                 group.label = 'ooui-toolbar-more';
3475                         }
3476                 }
3477                 // Check type has been registered
3478                 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
3479                 items.push(
3480                         this.getToolGroupFactory().create( type, this, $.extend( { '$': this.$ }, group ) )
3481                 );
3482         }
3483         this.addItems( items );
3487  * Remove all tools and groups from the toolbar.
3488  */
3489 OO.ui.Toolbar.prototype.reset = function () {
3490         var i, len;
3492         this.groups = [];
3493         this.tools = {};
3494         for ( i = 0, len = this.items.length; i < len; i++ ) {
3495                 this.items[i].destroy();
3496         }
3497         this.clearItems();
3501  * Destroys toolbar, removing event handlers and DOM elements.
3503  * Call this whenever you are done using a toolbar.
3504  */
3505 OO.ui.Toolbar.prototype.destroy = function () {
3506         this.reset();
3507         this.$element.remove();
3511  * Check if tool has not been used yet.
3513  * @param {string} name Symbolic name of tool
3514  * @return {boolean} Tool is available
3515  */
3516 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
3517         return !this.tools[name];
3521  * Prevent tool from being used again.
3523  * @param {OO.ui.Tool} tool Tool to reserve
3524  */
3525 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
3526         this.tools[tool.getName()] = tool;
3530  * Allow tool to be used again.
3532  * @param {OO.ui.Tool} tool Tool to release
3533  */
3534 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
3535         delete this.tools[tool.getName()];
3539  * Get accelerator label for tool.
3541  * This is a stub that should be overridden to provide access to accelerator information.
3543  * @param {string} name Symbolic name of tool
3544  * @return {string|undefined} Tool accelerator label if available
3545  */
3546 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
3547         return undefined;
3551  * Factory for tools.
3553  * @class
3554  * @extends OO.Factory
3555  * @constructor
3556  */
3557 OO.ui.ToolFactory = function OoUiToolFactory() {
3558         // Parent constructor
3559         OO.ui.ToolFactory.super.call( this );
3562 /* Setup */
3564 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3566 /* Methods */
3568 /** */
3569 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3570         var i, len, included, promoted, demoted,
3571                 auto = [],
3572                 used = {};
3574         // Collect included and not excluded tools
3575         included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3577         // Promotion
3578         promoted = this.extract( promote, used );
3579         demoted = this.extract( demote, used );
3581         // Auto
3582         for ( i = 0, len = included.length; i < len; i++ ) {
3583                 if ( !used[included[i]] ) {
3584                         auto.push( included[i] );
3585                 }
3586         }
3588         return promoted.concat( auto ).concat( demoted );
3592  * Get a flat list of names from a list of names or groups.
3594  * Tools can be specified in the following ways:
3596  * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
3597  * - All tools in a group: `{ 'group': 'group-name' }`
3598  * - All tools: `'*'`
3600  * @private
3601  * @param {Array|string} collection List of tools
3602  * @param {Object} [used] Object with names that should be skipped as properties; extracted
3603  *  names will be added as properties
3604  * @return {string[]} List of extracted names
3605  */
3606 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3607         var i, len, item, name, tool,
3608                 names = [];
3610         if ( collection === '*' ) {
3611                 for ( name in this.registry ) {
3612                         tool = this.registry[name];
3613                         if (
3614                                 // Only add tools by group name when auto-add is enabled
3615                                 tool.static.autoAddToCatchall &&
3616                                 // Exclude already used tools
3617                                 ( !used || !used[name] )
3618                         ) {
3619                                 names.push( name );
3620                                 if ( used ) {
3621                                         used[name] = true;
3622                                 }
3623                         }
3624                 }
3625         } else if ( $.isArray( collection ) ) {
3626                 for ( i = 0, len = collection.length; i < len; i++ ) {
3627                         item = collection[i];
3628                         // Allow plain strings as shorthand for named tools
3629                         if ( typeof item === 'string' ) {
3630                                 item = { 'name': item };
3631                         }
3632                         if ( OO.isPlainObject( item ) ) {
3633                                 if ( item.group ) {
3634                                         for ( name in this.registry ) {
3635                                                 tool = this.registry[name];
3636                                                 if (
3637                                                         // Include tools with matching group
3638                                                         tool.static.group === item.group &&
3639                                                         // Only add tools by group name when auto-add is enabled
3640                                                         tool.static.autoAddToGroup &&
3641                                                         // Exclude already used tools
3642                                                         ( !used || !used[name] )
3643                                                 ) {
3644                                                         names.push( name );
3645                                                         if ( used ) {
3646                                                                 used[name] = true;
3647                                                         }
3648                                                 }
3649                                         }
3650                                 // Include tools with matching name and exclude already used tools
3651                                 } else if ( item.name && ( !used || !used[item.name] ) ) {
3652                                         names.push( item.name );
3653                                         if ( used ) {
3654                                                 used[item.name] = true;
3655                                         }
3656                                 }
3657                         }
3658                 }
3659         }
3660         return names;
3664  * Collection of tools.
3666  * Tools can be specified in the following ways:
3668  * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
3669  * - All tools in a group: `{ 'group': 'group-name' }`
3670  * - All tools: `'*'`
3672  * @abstract
3673  * @class
3674  * @extends OO.ui.Widget
3675  * @mixins OO.ui.GroupElement
3677  * @constructor
3678  * @param {OO.ui.Toolbar} toolbar
3679  * @param {Object} [config] Configuration options
3680  * @cfg {Array|string} [include=[]] List of tools to include
3681  * @cfg {Array|string} [exclude=[]] List of tools to exclude
3682  * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
3683  * @cfg {Array|string} [demote=[]] List of tools to demote to the end
3684  */
3685 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
3686         // Configuration initialization
3687         config = config || {};
3689         // Parent constructor
3690         OO.ui.ToolGroup.super.call( this, config );
3692         // Mixin constructors
3693         OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
3695         // Properties
3696         this.toolbar = toolbar;
3697         this.tools = {};
3698         this.pressed = null;
3699         this.autoDisabled = false;
3700         this.include = config.include || [];
3701         this.exclude = config.exclude || [];
3702         this.promote = config.promote || [];
3703         this.demote = config.demote || [];
3704         this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
3706         // Events
3707         this.$element.on( {
3708                 'mousedown': OO.ui.bind( this.onMouseDown, this ),
3709                 'mouseup': OO.ui.bind( this.onMouseUp, this ),
3710                 'mouseover': OO.ui.bind( this.onMouseOver, this ),
3711                 'mouseout': OO.ui.bind( this.onMouseOut, this )
3712         } );
3713         this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } );
3714         this.aggregate( { 'disable': 'itemDisable' } );
3715         this.connect( this, { 'itemDisable': 'updateDisabled' } );
3717         // Initialization
3718         this.$group.addClass( 'oo-ui-toolGroup-tools' );
3719         this.$element
3720                 .addClass( 'oo-ui-toolGroup' )
3721                 .append( this.$group );
3722         this.populate();
3725 /* Setup */
3727 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
3728 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
3730 /* Events */
3733  * @event update
3734  */
3736 /* Static Properties */
3739  * Show labels in tooltips.
3741  * @static
3742  * @inheritable
3743  * @property {boolean}
3744  */
3745 OO.ui.ToolGroup.static.titleTooltips = false;
3748  * Show acceleration labels in tooltips.
3750  * @static
3751  * @inheritable
3752  * @property {boolean}
3753  */
3754 OO.ui.ToolGroup.static.accelTooltips = false;
3757  * Automatically disable the toolgroup when all tools are disabled
3759  * @static
3760  * @inheritable
3761  * @property {boolean}
3762  */
3763 OO.ui.ToolGroup.static.autoDisable = true;
3765 /* Methods */
3768  * @inheritdoc
3769  */
3770 OO.ui.ToolGroup.prototype.isDisabled = function () {
3771         return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
3775  * @inheritdoc
3776  */
3777 OO.ui.ToolGroup.prototype.updateDisabled = function () {
3778         var i, item, allDisabled = true;
3780         if ( this.constructor.static.autoDisable ) {
3781                 for ( i = this.items.length - 1; i >= 0; i-- ) {
3782                         item = this.items[i];
3783                         if ( !item.isDisabled() ) {
3784                                 allDisabled = false;
3785                                 break;
3786                         }
3787                 }
3788                 this.autoDisabled = allDisabled;
3789         }
3790         OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
3794  * Handle mouse down events.
3796  * @param {jQuery.Event} e Mouse down event
3797  */
3798 OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
3799         if ( !this.isDisabled() && e.which === 1 ) {
3800                 this.pressed = this.getTargetTool( e );
3801                 if ( this.pressed ) {
3802                         this.pressed.setActive( true );
3803                         this.getElementDocument().addEventListener(
3804                                 'mouseup', this.onCapturedMouseUpHandler, true
3805                         );
3806                         return false;
3807                 }
3808         }
3812  * Handle captured mouse up events.
3814  * @param {Event} e Mouse up event
3815  */
3816 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
3817         this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
3818         // onMouseUp may be called a second time, depending on where the mouse is when the button is
3819         // released, but since `this.pressed` will no longer be true, the second call will be ignored.
3820         this.onMouseUp( e );
3824  * Handle mouse up events.
3826  * @param {jQuery.Event} e Mouse up event
3827  */
3828 OO.ui.ToolGroup.prototype.onMouseUp = function ( e ) {
3829         var tool = this.getTargetTool( e );
3831         if ( !this.isDisabled() && e.which === 1 && this.pressed && this.pressed === tool ) {
3832                 this.pressed.onSelect();
3833         }
3835         this.pressed = null;
3836         return false;
3840  * Handle mouse over events.
3842  * @param {jQuery.Event} e Mouse over event
3843  */
3844 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
3845         var tool = this.getTargetTool( e );
3847         if ( this.pressed && this.pressed === tool ) {
3848                 this.pressed.setActive( true );
3849         }
3853  * Handle mouse out events.
3855  * @param {jQuery.Event} e Mouse out event
3856  */
3857 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
3858         var tool = this.getTargetTool( e );
3860         if ( this.pressed && this.pressed === tool ) {
3861                 this.pressed.setActive( false );
3862         }
3866  * Get the closest tool to a jQuery.Event.
3868  * Only tool links are considered, which prevents other elements in the tool such as popups from
3869  * triggering tool group interactions.
3871  * @private
3872  * @param {jQuery.Event} e
3873  * @return {OO.ui.Tool|null} Tool, `null` if none was found
3874  */
3875 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
3876         var tool,
3877                 $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
3879         if ( $item.length ) {
3880                 tool = $item.parent().data( 'oo-ui-tool' );
3881         }
3883         return tool && !tool.isDisabled() ? tool : null;
3887  * Handle tool registry register events.
3889  * If a tool is registered after the group is created, we must repopulate the list to account for:
3891  * - a tool being added that may be included
3892  * - a tool already included being overridden
3894  * @param {string} name Symbolic name of tool
3895  */
3896 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
3897         this.populate();
3901  * Get the toolbar this group is in.
3903  * @return {OO.ui.Toolbar} Toolbar of group
3904  */
3905 OO.ui.ToolGroup.prototype.getToolbar = function () {
3906         return this.toolbar;
3910  * Add and remove tools based on configuration.
3911  */
3912 OO.ui.ToolGroup.prototype.populate = function () {
3913         var i, len, name, tool,
3914                 toolFactory = this.toolbar.getToolFactory(),
3915                 names = {},
3916                 add = [],
3917                 remove = [],
3918                 list = this.toolbar.getToolFactory().getTools(
3919                         this.include, this.exclude, this.promote, this.demote
3920                 );
3922         // Build a list of needed tools
3923         for ( i = 0, len = list.length; i < len; i++ ) {
3924                 name = list[i];
3925                 if (
3926                         // Tool exists
3927                         toolFactory.lookup( name ) &&
3928                         // Tool is available or is already in this group
3929                         ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
3930                 ) {
3931                         tool = this.tools[name];
3932                         if ( !tool ) {
3933                                 // Auto-initialize tools on first use
3934                                 this.tools[name] = tool = toolFactory.create( name, this );
3935                                 tool.updateTitle();
3936                         }
3937                         this.toolbar.reserveTool( tool );
3938                         add.push( tool );
3939                         names[name] = true;
3940                 }
3941         }
3942         // Remove tools that are no longer needed
3943         for ( name in this.tools ) {
3944                 if ( !names[name] ) {
3945                         this.tools[name].destroy();
3946                         this.toolbar.releaseTool( this.tools[name] );
3947                         remove.push( this.tools[name] );
3948                         delete this.tools[name];
3949                 }
3950         }
3951         if ( remove.length ) {
3952                 this.removeItems( remove );
3953         }
3954         // Update emptiness state
3955         if ( add.length ) {
3956                 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
3957         } else {
3958                 this.$element.addClass( 'oo-ui-toolGroup-empty' );
3959         }
3960         // Re-add tools (moving existing ones to new locations)
3961         this.addItems( add );
3962         // Disabled state may depend on items
3963         this.updateDisabled();
3967  * Destroy tool group.
3968  */
3969 OO.ui.ToolGroup.prototype.destroy = function () {
3970         var name;
3972         this.clearItems();
3973         this.toolbar.getToolFactory().disconnect( this );
3974         for ( name in this.tools ) {
3975                 this.toolbar.releaseTool( this.tools[name] );
3976                 this.tools[name].disconnect( this ).destroy();
3977                 delete this.tools[name];
3978         }
3979         this.$element.remove();
3983  * Factory for tool groups.
3985  * @class
3986  * @extends OO.Factory
3987  * @constructor
3988  */
3989 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3990         // Parent constructor
3991         OO.Factory.call( this );
3993         var i, l,
3994                 defaultClasses = this.constructor.static.getDefaultClasses();
3996         // Register default toolgroups
3997         for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3998                 this.register( defaultClasses[i] );
3999         }
4002 /* Setup */
4004 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
4006 /* Static Methods */
4009  * Get a default set of classes to be registered on construction
4011  * @return {Function[]} Default classes
4012  */
4013 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
4014         return [
4015                 OO.ui.BarToolGroup,
4016                 OO.ui.ListToolGroup,
4017                 OO.ui.MenuToolGroup
4018         ];
4022  * Layout made of a fieldset and optional legend.
4024  * Just add OO.ui.FieldLayout items.
4026  * @class
4027  * @extends OO.ui.Layout
4028  * @mixins OO.ui.LabeledElement
4029  * @mixins OO.ui.IconedElement
4030  * @mixins OO.ui.GroupElement
4032  * @constructor
4033  * @param {Object} [config] Configuration options
4034  * @cfg {string} [icon] Symbolic icon name
4035  * @cfg {OO.ui.FieldLayout[]} [items] Items to add
4036  */
4037 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
4038         // Config initialization
4039         config = config || {};
4041         // Parent constructor
4042         OO.ui.FieldsetLayout.super.call( this, config );
4044         // Mixin constructors
4045         OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
4046         OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
4047         OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
4049         // Initialization
4050         this.$element
4051                 .addClass( 'oo-ui-fieldsetLayout' )
4052                 .prepend( this.$icon, this.$label, this.$group );
4053         if ( $.isArray( config.items ) ) {
4054                 this.addItems( config.items );
4055         }
4058 /* Setup */
4060 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
4061 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
4062 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
4063 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
4065 /* Static Properties */
4067 OO.ui.FieldsetLayout.static.tagName = 'div';
4070  * Layout made of a field and optional label.
4072  * @class
4073  * @extends OO.ui.Layout
4074  * @mixins OO.ui.LabeledElement
4076  * Available label alignment modes include:
4077  *  - 'left': Label is before the field and aligned away from it, best for when the user will be
4078  *    scanning for a specific label in a form with many fields
4079  *  - 'right': Label is before the field and aligned toward it, best for forms the user is very
4080  *    familiar with and will tab through field checking quickly to verify which field they are in
4081  *  - 'top': Label is before the field and above it, best for when the use will need to fill out all
4082  *    fields from top to bottom in a form with few fields
4083  *  - 'inline': Label is after the field and aligned toward it, best for small boolean fields like
4084  *    checkboxes or radio buttons
4086  * @constructor
4087  * @param {OO.ui.Widget} field Field widget
4088  * @param {Object} [config] Configuration options
4089  * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
4090  */
4091 OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
4092         // Config initialization
4093         config = $.extend( { 'align': 'left' }, config );
4095         // Parent constructor
4096         OO.ui.FieldLayout.super.call( this, config );
4098         // Mixin constructors
4099         OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
4101         // Properties
4102         this.$field = this.$( '<div>' );
4103         this.field = field;
4104         this.align = null;
4106         // Events
4107         if ( this.field instanceof OO.ui.InputWidget ) {
4108                 this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
4109         }
4110         this.field.connect( this, { 'disable': 'onFieldDisable' } );
4112         // Initialization
4113         this.$element.addClass( 'oo-ui-fieldLayout' );
4114         this.$field
4115                 .addClass( 'oo-ui-fieldLayout-field' )
4116                 .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() )
4117                 .append( this.field.$element );
4118         this.setAlignment( config.align );
4121 /* Setup */
4123 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
4124 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
4126 /* Methods */
4129  * Handle field disable events.
4131  * @param {boolean} value Field is disabled
4132  */
4133 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
4134         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
4138  * Handle label mouse click events.
4140  * @param {jQuery.Event} e Mouse click event
4141  */
4142 OO.ui.FieldLayout.prototype.onLabelClick = function () {
4143         this.field.simulateLabelClick();
4144         return false;
4148  * Get the field.
4150  * @return {OO.ui.Widget} Field widget
4151  */
4152 OO.ui.FieldLayout.prototype.getField = function () {
4153         return this.field;
4157  * Set the field alignment mode.
4159  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
4160  * @chainable
4161  */
4162 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
4163         if ( value !== this.align ) {
4164                 // Default to 'left'
4165                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
4166                         value = 'left';
4167                 }
4168                 // Reorder elements
4169                 if ( value === 'inline' ) {
4170                         this.$element.append( this.$field, this.$label );
4171                 } else {
4172                         this.$element.append( this.$label, this.$field );
4173                 }
4174                 // Set classes
4175                 if ( this.align ) {
4176                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
4177                 }
4178                 this.align = value;
4179                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
4180         }
4182         return this;
4186  * Layout made of proportionally sized columns and rows.
4188  * @class
4189  * @extends OO.ui.Layout
4191  * @constructor
4192  * @param {OO.ui.PanelLayout[]} panels Panels in the grid
4193  * @param {Object} [config] Configuration options
4194  * @cfg {number[]} [widths] Widths of columns as ratios
4195  * @cfg {number[]} [heights] Heights of columns as ratios
4196  */
4197 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
4198         var i, len, widths;
4200         // Config initialization
4201         config = config || {};
4203         // Parent constructor
4204         OO.ui.GridLayout.super.call( this, config );
4206         // Properties
4207         this.panels = [];
4208         this.widths = [];
4209         this.heights = [];
4211         // Initialization
4212         this.$element.addClass( 'oo-ui-gridLayout' );
4213         for ( i = 0, len = panels.length; i < len; i++ ) {
4214                 this.panels.push( panels[i] );
4215                 this.$element.append( panels[i].$element );
4216         }
4217         if ( config.widths || config.heights ) {
4218                 this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
4219         } else {
4220                 // Arrange in columns by default
4221                 widths = [];
4222                 for ( i = 0, len = this.panels.length; i < len; i++ ) {
4223                         widths[i] = 1;
4224                 }
4225                 this.layout( widths, [ 1 ] );
4226         }
4229 /* Setup */
4231 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
4233 /* Events */
4236  * @event layout
4237  */
4240  * @event update
4241  */
4243 /* Static Properties */
4245 OO.ui.GridLayout.static.tagName = 'div';
4247 /* Methods */
4250  * Set grid dimensions.
4252  * @param {number[]} widths Widths of columns as ratios
4253  * @param {number[]} heights Heights of rows as ratios
4254  * @fires layout
4255  * @throws {Error} If grid is not large enough to fit all panels
4256  */
4257 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
4258         var x, y,
4259                 xd = 0,
4260                 yd = 0,
4261                 cols = widths.length,
4262                 rows = heights.length;
4264         // Verify grid is big enough to fit panels
4265         if ( cols * rows < this.panels.length ) {
4266                 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
4267         }
4269         // Sum up denominators
4270         for ( x = 0; x < cols; x++ ) {
4271                 xd += widths[x];
4272         }
4273         for ( y = 0; y < rows; y++ ) {
4274                 yd += heights[y];
4275         }
4276         // Store factors
4277         this.widths = [];
4278         this.heights = [];
4279         for ( x = 0; x < cols; x++ ) {
4280                 this.widths[x] = widths[x] / xd;
4281         }
4282         for ( y = 0; y < rows; y++ ) {
4283                 this.heights[y] = heights[y] / yd;
4284         }
4285         // Synchronize view
4286         this.update();
4287         this.emit( 'layout' );
4291  * Update panel positions and sizes.
4293  * @fires update
4294  */
4295 OO.ui.GridLayout.prototype.update = function () {
4296         var x, y, panel,
4297                 i = 0,
4298                 left = 0,
4299                 top = 0,
4300                 dimensions,
4301                 width = 0,
4302                 height = 0,
4303                 cols = this.widths.length,
4304                 rows = this.heights.length;
4306         for ( y = 0; y < rows; y++ ) {
4307                 for ( x = 0; x < cols; x++ ) {
4308                         panel = this.panels[i];
4309                         width = this.widths[x];
4310                         height = this.heights[y];
4311                         dimensions = {
4312                                 'width': Math.round( width * 100 ) + '%',
4313                                 'height': Math.round( height * 100 ) + '%',
4314                                 'top': Math.round( top * 100 ) + '%'
4315                         };
4316                         // If RTL, reverse:
4317                         if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
4318                                 dimensions.right = Math.round( left * 100 ) + '%';
4319                         } else {
4320                                 dimensions.left = Math.round( left * 100 ) + '%';
4321                         }
4322                         panel.$element.css( dimensions );
4323                         i++;
4324                         left += width;
4325                 }
4326                 top += height;
4327                 left = 0;
4328         }
4330         this.emit( 'update' );
4334  * Get a panel at a given position.
4336  * The x and y position is affected by the current grid layout.
4338  * @param {number} x Horizontal position
4339  * @param {number} y Vertical position
4340  * @return {OO.ui.PanelLayout} The panel at the given postion
4341  */
4342 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
4343         return this.panels[( x * this.widths.length ) + y];
4347  * Layout containing a series of pages.
4349  * @class
4350  * @extends OO.ui.Layout
4352  * @constructor
4353  * @param {Object} [config] Configuration options
4354  * @cfg {boolean} [continuous=false] Show all pages, one after another
4355  * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
4356  * @cfg {boolean} [outlined=false] Show an outline
4357  * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
4358  */
4359 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
4360         // Initialize configuration
4361         config = config || {};
4363         // Parent constructor
4364         OO.ui.BookletLayout.super.call( this, config );
4366         // Properties
4367         this.currentPageName = null;
4368         this.pages = {};
4369         this.ignoreFocus = false;
4370         this.stackLayout = new OO.ui.StackLayout( { '$': this.$, 'continuous': !!config.continuous } );
4371         this.autoFocus = config.autoFocus === undefined ? true : !!config.autoFocus;
4372         this.outlineVisible = false;
4373         this.outlined = !!config.outlined;
4374         if ( this.outlined ) {
4375                 this.editable = !!config.editable;
4376                 this.outlineControlsWidget = null;
4377                 this.outlineWidget = new OO.ui.OutlineWidget( { '$': this.$ } );
4378                 this.outlinePanel = new OO.ui.PanelLayout( { '$': this.$, 'scrollable': true } );
4379                 this.gridLayout = new OO.ui.GridLayout(
4380                         [ this.outlinePanel, this.stackLayout ],
4381                         { '$': this.$, 'widths': [ 1, 2 ] }
4382                 );
4383                 this.outlineVisible = true;
4384                 if ( this.editable ) {
4385                         this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
4386                                 this.outlineWidget, { '$': this.$ }
4387                         );
4388                 }
4389         }
4391         // Events
4392         this.stackLayout.connect( this, { 'set': 'onStackLayoutSet' } );
4393         if ( this.outlined ) {
4394                 this.outlineWidget.connect( this, { 'select': 'onOutlineWidgetSelect' } );
4395         }
4396         if ( this.autoFocus ) {
4397                 // Event 'focus' does not bubble, but 'focusin' does
4398                 this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) );
4399         }
4401         // Initialization
4402         this.$element.addClass( 'oo-ui-bookletLayout' );
4403         this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
4404         if ( this.outlined ) {
4405                 this.outlinePanel.$element
4406                         .addClass( 'oo-ui-bookletLayout-outlinePanel' )
4407                         .append( this.outlineWidget.$element );
4408                 if ( this.editable ) {
4409                         this.outlinePanel.$element
4410                                 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
4411                                 .append( this.outlineControlsWidget.$element );
4412                 }
4413                 this.$element.append( this.gridLayout.$element );
4414         } else {
4415                 this.$element.append( this.stackLayout.$element );
4416         }
4419 /* Setup */
4421 OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout );
4423 /* Events */
4426  * @event set
4427  * @param {OO.ui.PageLayout} page Current page
4428  */
4431  * @event add
4432  * @param {OO.ui.PageLayout[]} page Added pages
4433  * @param {number} index Index pages were added at
4434  */
4437  * @event remove
4438  * @param {OO.ui.PageLayout[]} pages Removed pages
4439  */
4441 /* Methods */
4444  * Handle stack layout focus.
4446  * @param {jQuery.Event} e Focusin event
4447  */
4448 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
4449         var name, $target;
4451         // Find the page that an element was focused within
4452         $target = $( e.target ).closest( '.oo-ui-pageLayout' );
4453         for ( name in this.pages ) {
4454                 // Check for page match, exclude current page to find only page changes
4455                 if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) {
4456                         this.setPage( name );
4457                         break;
4458                 }
4459         }
4463  * Handle stack layout set events.
4465  * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
4466  */
4467 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
4468         if ( page ) {
4469                 page.scrollElementIntoView( { 'complete': OO.ui.bind( function () {
4470                         if ( this.autoFocus ) {
4471                                 // Set focus to the first input if nothing on the page is focused yet
4472                                 if ( !page.$element.find( ':focus' ).length ) {
4473                                         page.$element.find( ':input:first' ).focus();
4474                                 }
4475                         }
4476                 }, this ) } );
4477         }
4481  * Handle outline widget select events.
4483  * @param {OO.ui.OptionWidget|null} item Selected item
4484  */
4485 OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
4486         if ( item ) {
4487                 this.setPage( item.getData() );
4488         }
4492  * Check if booklet has an outline.
4494  * @return {boolean}
4495  */
4496 OO.ui.BookletLayout.prototype.isOutlined = function () {
4497         return this.outlined;
4501  * Check if booklet has editing controls.
4503  * @return {boolean}
4504  */
4505 OO.ui.BookletLayout.prototype.isEditable = function () {
4506         return this.editable;
4510  * Check if booklet has a visible outline.
4512  * @return {boolean}
4513  */
4514 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
4515         return this.outlined && this.outlineVisible;
4519  * Hide or show the outline.
4521  * @param {boolean} [show] Show outline, omit to invert current state
4522  * @chainable
4523  */
4524 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
4525         if ( this.outlined ) {
4526                 show = show === undefined ? !this.outlineVisible : !!show;
4527                 this.outlineVisible = show;
4528                 this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
4529         }
4531         return this;
4535  * Get the outline widget.
4537  * @param {OO.ui.PageLayout} page Page to be selected
4538  * @return {OO.ui.PageLayout|null} Closest page to another
4539  */
4540 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
4541         var next, prev, level,
4542                 pages = this.stackLayout.getItems(),
4543                 index = $.inArray( page, pages );
4545         if ( index !== -1 ) {
4546                 next = pages[index + 1];
4547                 prev = pages[index - 1];
4548                 // Prefer adjacent pages at the same level
4549                 if ( this.outlined ) {
4550                         level = this.outlineWidget.getItemFromData( page.getName() ).getLevel();
4551                         if (
4552                                 prev &&
4553                                 level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel()
4554                         ) {
4555                                 return prev;
4556                         }
4557                         if (
4558                                 next &&
4559                                 level === this.outlineWidget.getItemFromData( next.getName() ).getLevel()
4560                         ) {
4561                                 return next;
4562                         }
4563                 }
4564         }
4565         return prev || next || null;
4569  * Get the outline widget.
4571  * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
4572  */
4573 OO.ui.BookletLayout.prototype.getOutline = function () {
4574         return this.outlineWidget;
4578  * Get the outline controls widget. If the outline is not editable, null is returned.
4580  * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
4581  */
4582 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
4583         return this.outlineControlsWidget;
4587  * Get a page by name.
4589  * @param {string} name Symbolic name of page
4590  * @return {OO.ui.PageLayout|undefined} Page, if found
4591  */
4592 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
4593         return this.pages[name];
4597  * Get the current page name.
4599  * @return {string|null} Current page name
4600  */
4601 OO.ui.BookletLayout.prototype.getPageName = function () {
4602         return this.currentPageName;
4606  * Add a page to the layout.
4608  * When pages are added with the same names as existing pages, the existing pages will be
4609  * automatically removed before the new pages are added.
4611  * @param {OO.ui.PageLayout[]} pages Pages to add
4612  * @param {number} index Index to insert pages after
4613  * @fires add
4614  * @chainable
4615  */
4616 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
4617         var i, len, name, page, item, currentIndex,
4618                 stackLayoutPages = this.stackLayout.getItems(),
4619                 remove = [],
4620                 items = [];
4622         // Remove pages with same names
4623         for ( i = 0, len = pages.length; i < len; i++ ) {
4624                 page = pages[i];
4625                 name = page.getName();
4627                 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
4628                         // Correct the insertion index
4629                         currentIndex = $.inArray( this.pages[name], stackLayoutPages );
4630                         if ( currentIndex !== -1 && currentIndex + 1 < index ) {
4631                                 index--;
4632                         }
4633                         remove.push( this.pages[name] );
4634                 }
4635         }
4636         if ( remove.length ) {
4637                 this.removePages( remove );
4638         }
4640         // Add new pages
4641         for ( i = 0, len = pages.length; i < len; i++ ) {
4642                 page = pages[i];
4643                 name = page.getName();
4644                 this.pages[page.getName()] = page;
4645                 if ( this.outlined ) {
4646                         item = new OO.ui.OutlineItemWidget( name, page, { '$': this.$ } );
4647                         page.setOutlineItem( item );
4648                         items.push( item );
4649                 }
4650         }
4652         if ( this.outlined && items.length ) {
4653                 this.outlineWidget.addItems( items, index );
4654                 this.updateOutlineWidget();
4655         }
4656         this.stackLayout.addItems( pages, index );
4657         this.emit( 'add', pages, index );
4659         return this;
4663  * Remove a page from the layout.
4665  * @fires remove
4666  * @chainable
4667  */
4668 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
4669         var i, len, name, page,
4670                 items = [];
4672         for ( i = 0, len = pages.length; i < len; i++ ) {
4673                 page = pages[i];
4674                 name = page.getName();
4675                 delete this.pages[name];
4676                 if ( this.outlined ) {
4677                         items.push( this.outlineWidget.getItemFromData( name ) );
4678                         page.setOutlineItem( null );
4679                 }
4680         }
4681         if ( this.outlined && items.length ) {
4682                 this.outlineWidget.removeItems( items );
4683                 this.updateOutlineWidget();
4684         }
4685         this.stackLayout.removeItems( pages );
4686         this.emit( 'remove', pages );
4688         return this;
4692  * Clear all pages from the layout.
4694  * @fires remove
4695  * @chainable
4696  */
4697 OO.ui.BookletLayout.prototype.clearPages = function () {
4698         var i, len,
4699                 pages = this.stackLayout.getItems();
4701         this.pages = {};
4702         this.currentPageName = null;
4703         if ( this.outlined ) {
4704                 this.outlineWidget.clearItems();
4705                 for ( i = 0, len = pages.length; i < len; i++ ) {
4706                         pages[i].setOutlineItem( null );
4707                 }
4708         }
4709         this.stackLayout.clearItems();
4711         this.emit( 'remove', pages );
4713         return this;
4717  * Set the current page by name.
4719  * @fires set
4720  * @param {string} name Symbolic name of page
4721  */
4722 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
4723         var selectedItem,
4724                 page = this.pages[name];
4726         if ( name !== this.currentPageName ) {
4727                 if ( this.outlined ) {
4728                         selectedItem = this.outlineWidget.getSelectedItem();
4729                         if ( selectedItem && selectedItem.getData() !== name ) {
4730                                 this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
4731                         }
4732                 }
4733                 if ( page ) {
4734                         if ( this.currentPageName && this.pages[this.currentPageName] ) {
4735                                 this.pages[this.currentPageName].setActive( false );
4736                                 // Blur anything focused if the next page doesn't have anything focusable - this
4737                                 // is not needed if the next page has something focusable because once it is focused
4738                                 // this blur happens automatically
4739                                 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
4740                                         this.pages[this.currentPageName].$element.find( ':focus' ).blur();
4741                                 }
4742                         }
4743                         this.currentPageName = name;
4744                         this.stackLayout.setItem( page );
4745                         page.setActive( true );
4746                         this.emit( 'set', page );
4747                 }
4748         }
4752  * Call this after adding or removing items from the OutlineWidget.
4754  * @chainable
4755  */
4756 OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
4757         // Auto-select first item when nothing is selected anymore
4758         if ( !this.outlineWidget.getSelectedItem() ) {
4759                 this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
4760         }
4762         return this;
4766  * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
4768  * @class
4769  * @extends OO.ui.Layout
4771  * @constructor
4772  * @param {Object} [config] Configuration options
4773  * @cfg {boolean} [scrollable] Allow vertical scrolling
4774  * @cfg {boolean} [padded] Pad the content from the edges
4775  */
4776 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
4777         // Config initialization
4778         config = config || {};
4780         // Parent constructor
4781         OO.ui.PanelLayout.super.call( this, config );
4783         // Initialization
4784         this.$element.addClass( 'oo-ui-panelLayout' );
4785         if ( config.scrollable ) {
4786                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
4787         }
4789         if ( config.padded ) {
4790                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
4791         }
4794 /* Setup */
4796 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
4799  * Page within an booklet layout.
4801  * @class
4802  * @extends OO.ui.PanelLayout
4804  * @constructor
4805  * @param {string} name Unique symbolic name of page
4806  * @param {Object} [config] Configuration options
4807  * @param {string} [outlineItem] Outline item widget
4808  */
4809 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
4810         // Configuration initialization
4811         config = $.extend( { 'scrollable': true }, config );
4813         // Parent constructor
4814         OO.ui.PageLayout.super.call( this, config );
4816         // Properties
4817         this.name = name;
4818         this.outlineItem = config.outlineItem || null;
4819         this.active = false;
4821         // Initialization
4822         this.$element.addClass( 'oo-ui-pageLayout' );
4825 /* Setup */
4827 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
4829 /* Events */
4832  * @event active
4833  * @param {boolean} active Page is active
4834  */
4836 /* Methods */
4839  * Get page name.
4841  * @return {string} Symbolic name of page
4842  */
4843 OO.ui.PageLayout.prototype.getName = function () {
4844         return this.name;
4848  * Check if page is active.
4850  * @return {boolean} Page is active
4851  */
4852 OO.ui.PageLayout.prototype.isActive = function () {
4853         return this.active;
4857  * Get outline item.
4859  * @return {OO.ui.OutlineItemWidget|null} Outline item widget
4860  */
4861 OO.ui.PageLayout.prototype.getOutlineItem = function () {
4862         return this.outlineItem;
4866  * Get outline item.
4868  * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
4869  * @chainable
4870  */
4871 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
4872         this.outlineItem = outlineItem;
4873         return this;
4877  * Set page active state.
4879  * @param {boolean} Page is active
4880  * @fires active
4881  */
4882 OO.ui.PageLayout.prototype.setActive = function ( active ) {
4883         active = !!active;
4885         if ( active !== this.active ) {
4886                 this.active = active;
4887                 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
4888                 this.emit( 'active', this.active );
4889         }
4893  * Layout containing a series of mutually exclusive pages.
4895  * @class
4896  * @extends OO.ui.PanelLayout
4897  * @mixins OO.ui.GroupElement
4899  * @constructor
4900  * @param {Object} [config] Configuration options
4901  * @cfg {boolean} [continuous=false] Show all pages, one after another
4902  * @cfg {string} [icon=''] Symbolic icon name
4903  * @cfg {OO.ui.Layout[]} [items] Layouts to add
4904  */
4905 OO.ui.StackLayout = function OoUiStackLayout( config ) {
4906         // Config initialization
4907         config = $.extend( { 'scrollable': true }, config );
4909         // Parent constructor
4910         OO.ui.StackLayout.super.call( this, config );
4912         // Mixin constructors
4913         OO.ui.GroupElement.call( this, this.$element, config );
4915         // Properties
4916         this.currentItem = null;
4917         this.continuous = !!config.continuous;
4919         // Initialization
4920         this.$element.addClass( 'oo-ui-stackLayout' );
4921         if ( this.continuous ) {
4922                 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
4923         }
4924         if ( $.isArray( config.items ) ) {
4925                 this.addItems( config.items );
4926         }
4929 /* Setup */
4931 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
4932 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
4934 /* Events */
4937  * @event set
4938  * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
4939  */
4941 /* Methods */
4944  * Get the current item.
4946  * @return {OO.ui.Layout|null}
4947  */
4948 OO.ui.StackLayout.prototype.getCurrentItem = function () {
4949         return this.currentItem;
4953  * Unset the current item.
4955  * @private
4956  * @param {OO.ui.StackLayout} layout
4957  * @fires set
4958  */
4959 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
4960         var prevItem = this.currentItem;
4961         if ( prevItem === null ) {
4962                 return;
4963         }
4965         this.currentItem = null;
4966         this.emit( 'set', null );
4970  * Add items.
4972  * Adding an existing item (by value) will move it.
4974  * @param {OO.ui.Layout[]} items Items to add
4975  * @param {number} [index] Index to insert items after
4976  * @chainable
4977  */
4978 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
4979         // Mixin method
4980         OO.ui.GroupElement.prototype.addItems.call( this, items, index );
4982         if ( !this.currentItem && items.length ) {
4983                 this.setItem( items[0] );
4984         }
4986         return this;
4990  * Remove items.
4992  * Items will be detached, not removed, so they can be used later.
4994  * @param {OO.ui.Layout[]} items Items to remove
4995  * @chainable
4996  * @fires set
4997  */
4998 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
4999         // Mixin method
5000         OO.ui.GroupElement.prototype.removeItems.call( this, items );
5002         if ( $.inArray( this.currentItem, items  ) !== -1 ) {
5003                 if ( this.items.length ) {
5004                         this.setItem( this.items[0] );
5005                 } else {
5006                         this.unsetCurrentItem();
5007                 }
5008         }
5010         return this;
5014  * Clear all items.
5016  * Items will be detached, not removed, so they can be used later.
5018  * @chainable
5019  * @fires set
5020  */
5021 OO.ui.StackLayout.prototype.clearItems = function () {
5022         this.unsetCurrentItem();
5023         OO.ui.GroupElement.prototype.clearItems.call( this );
5025         return this;
5029  * Show item.
5031  * Any currently shown item will be hidden.
5033  * FIXME: If the passed item to show has not been added in the items list, then
5034  * this method drops it and unsets the current item.
5036  * @param {OO.ui.Layout} item Item to show
5037  * @chainable
5038  * @fires set
5039  */
5040 OO.ui.StackLayout.prototype.setItem = function ( item ) {
5041         var i, len;
5043         if ( item !== this.currentItem ) {
5044                 if ( !this.continuous ) {
5045                         for ( i = 0, len = this.items.length; i < len; i++ ) {
5046                                 this.items[i].$element.css( 'display', '' );
5047                         }
5048                 }
5049                 if ( $.inArray( item, this.items ) !== -1 ) {
5050                         if ( !this.continuous ) {
5051                                 item.$element.css( 'display', 'block' );
5052                         }
5053                         this.currentItem = item;
5054                         this.emit( 'set', item );
5055                 } else {
5056                         this.unsetCurrentItem();
5057                 }
5058         }
5060         return this;
5064  * Horizontal bar layout of tools as icon buttons.
5066  * @class
5067  * @extends OO.ui.ToolGroup
5069  * @constructor
5070  * @param {OO.ui.Toolbar} toolbar
5071  * @param {Object} [config] Configuration options
5072  */
5073 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
5074         // Parent constructor
5075         OO.ui.BarToolGroup.super.call( this, toolbar, config );
5077         // Initialization
5078         this.$element.addClass( 'oo-ui-barToolGroup' );
5081 /* Setup */
5083 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
5085 /* Static Properties */
5087 OO.ui.BarToolGroup.static.titleTooltips = true;
5089 OO.ui.BarToolGroup.static.accelTooltips = true;
5091 OO.ui.BarToolGroup.static.name = 'bar';
5094  * Popup list of tools with an icon and optional label.
5096  * @abstract
5097  * @class
5098  * @extends OO.ui.ToolGroup
5099  * @mixins OO.ui.IconedElement
5100  * @mixins OO.ui.IndicatedElement
5101  * @mixins OO.ui.LabeledElement
5102  * @mixins OO.ui.TitledElement
5103  * @mixins OO.ui.ClippableElement
5105  * @constructor
5106  * @param {OO.ui.Toolbar} toolbar
5107  * @param {Object} [config] Configuration options
5108  * @cfg {string} [header] Text to display at the top of the pop-up
5109  */
5110 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
5111         // Configuration initialization
5112         config = config || {};
5114         // Parent constructor
5115         OO.ui.PopupToolGroup.super.call( this, toolbar, config );
5117         // Mixin constructors
5118         OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
5119         OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
5120         OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
5121         OO.ui.TitledElement.call( this, this.$element, config );
5122         OO.ui.ClippableElement.call( this, this.$group, config );
5124         // Properties
5125         this.active = false;
5126         this.dragging = false;
5127         this.onBlurHandler = OO.ui.bind( this.onBlur, this );
5128         this.$handle = this.$( '<span>' );
5130         // Events
5131         this.$handle.on( {
5132                 'mousedown': OO.ui.bind( this.onHandleMouseDown, this ),
5133                 'mouseup': OO.ui.bind( this.onHandleMouseUp, this )
5134         } );
5136         // Initialization
5137         this.$handle
5138                 .addClass( 'oo-ui-popupToolGroup-handle' )
5139                 .append( this.$icon, this.$label, this.$indicator );
5140         // If the pop-up should have a header, add it to the top of the toolGroup.
5141         // Note: If this feature is useful for other widgets, we could abstract it into an
5142         // OO.ui.HeaderedElement mixin constructor.
5143         if ( config.header !== undefined ) {
5144                 this.$group
5145                         .prepend( this.$( '<span>' )
5146                                 .addClass( 'oo-ui-popupToolGroup-header' )
5147                                 .text( config.header )
5148                         );
5149         }
5150         this.$element
5151                 .addClass( 'oo-ui-popupToolGroup' )
5152                 .prepend( this.$handle );
5155 /* Setup */
5157 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
5158 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconedElement );
5159 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatedElement );
5160 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabeledElement );
5161 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
5162 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
5164 /* Static Properties */
5166 /* Methods */
5169  * @inheritdoc
5170  */
5171 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
5172         // Parent method
5173         OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
5175         if ( this.isDisabled() && this.isElementAttached() ) {
5176                 this.setActive( false );
5177         }
5181  * Handle focus being lost.
5183  * The event is actually generated from a mouseup, so it is not a normal blur event object.
5185  * @param {jQuery.Event} e Mouse up event
5186  */
5187 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
5188         // Only deactivate when clicking outside the dropdown element
5189         if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
5190                 this.setActive( false );
5191         }
5195  * @inheritdoc
5196  */
5197 OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) {
5198         if ( !this.isDisabled() && e.which === 1 ) {
5199                 this.setActive( false );
5200         }
5201         return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e );
5205  * Handle mouse up events.
5207  * @param {jQuery.Event} e Mouse up event
5208  */
5209 OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
5210         return false;
5214  * Handle mouse down events.
5216  * @param {jQuery.Event} e Mouse down event
5217  */
5218 OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
5219         if ( !this.isDisabled() && e.which === 1 ) {
5220                 this.setActive( !this.active );
5221         }
5222         return false;
5226  * Switch into active mode.
5228  * When active, mouseup events anywhere in the document will trigger deactivation.
5229  */
5230 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
5231         value = !!value;
5232         if ( this.active !== value ) {
5233                 this.active = value;
5234                 if ( value ) {
5235                         this.setClipping( true );
5236                         this.$element.addClass( 'oo-ui-popupToolGroup-active' );
5237                         this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
5238                 } else {
5239                         this.setClipping( false );
5240                         this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
5241                         this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
5242                 }
5243         }
5247  * Drop down list layout of tools as labeled icon buttons.
5249  * @class
5250  * @extends OO.ui.PopupToolGroup
5252  * @constructor
5253  * @param {OO.ui.Toolbar} toolbar
5254  * @param {Object} [config] Configuration options
5255  */
5256 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
5257         // Parent constructor
5258         OO.ui.ListToolGroup.super.call( this, toolbar, config );
5260         // Initialization
5261         this.$element.addClass( 'oo-ui-listToolGroup' );
5264 /* Setup */
5266 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
5268 /* Static Properties */
5270 OO.ui.ListToolGroup.static.accelTooltips = true;
5272 OO.ui.ListToolGroup.static.name = 'list';
5275  * Drop down menu layout of tools as selectable menu items.
5277  * @class
5278  * @extends OO.ui.PopupToolGroup
5280  * @constructor
5281  * @param {OO.ui.Toolbar} toolbar
5282  * @param {Object} [config] Configuration options
5283  */
5284 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
5285         // Configuration initialization
5286         config = config || {};
5288         // Parent constructor
5289         OO.ui.MenuToolGroup.super.call( this, toolbar, config );
5291         // Events
5292         this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
5294         // Initialization
5295         this.$element.addClass( 'oo-ui-menuToolGroup' );
5298 /* Setup */
5300 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
5302 /* Static Properties */
5304 OO.ui.MenuToolGroup.static.accelTooltips = true;
5306 OO.ui.MenuToolGroup.static.name = 'menu';
5308 /* Methods */
5311  * Handle the toolbar state being updated.
5313  * When the state changes, the title of each active item in the menu will be joined together and
5314  * used as a label for the group. The label will be empty if none of the items are active.
5315  */
5316 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
5317         var name,
5318                 labelTexts = [];
5320         for ( name in this.tools ) {
5321                 if ( this.tools[name].isActive() ) {
5322                         labelTexts.push( this.tools[name].getTitle() );
5323                 }
5324         }
5326         this.setLabel( labelTexts.join( ', ' ) || ' ' );
5330  * Tool that shows a popup when selected.
5332  * @abstract
5333  * @class
5334  * @extends OO.ui.Tool
5335  * @mixins OO.ui.PopuppableElement
5337  * @constructor
5338  * @param {OO.ui.Toolbar} toolbar
5339  * @param {Object} [config] Configuration options
5340  */
5341 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
5342         // Parent constructor
5343         OO.ui.PopupTool.super.call( this, toolbar, config );
5345         // Mixin constructors
5346         OO.ui.PopuppableElement.call( this, config );
5348         // Initialization
5349         this.$element
5350                 .addClass( 'oo-ui-popupTool' )
5351                 .append( this.popup.$element );
5354 /* Setup */
5356 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
5357 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
5359 /* Methods */
5362  * Handle the tool being selected.
5364  * @inheritdoc
5365  */
5366 OO.ui.PopupTool.prototype.onSelect = function () {
5367         if ( !this.isDisabled() ) {
5368                 if ( this.popup.isVisible() ) {
5369                         this.hidePopup();
5370                 } else {
5371                         this.showPopup();
5372                 }
5373         }
5374         this.setActive( false );
5375         return false;
5379  * Handle the toolbar state being updated.
5381  * @inheritdoc
5382  */
5383 OO.ui.PopupTool.prototype.onUpdateState = function () {
5384         this.setActive( false );
5388  * Group widget.
5390  * Mixin for OO.ui.Widget subclasses.
5392  * Use together with OO.ui.ItemWidget to make disabled state inheritable.
5394  * @abstract
5395  * @class
5396  * @extends OO.ui.GroupElement
5398  * @constructor
5399  * @param {jQuery} $group Container node, assigned to #$group
5400  * @param {Object} [config] Configuration options
5401  */
5402 OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
5403         // Parent constructor
5404         OO.ui.GroupWidget.super.call( this, $element, config );
5407 /* Setup */
5409 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
5411 /* Methods */
5414  * Set the disabled state of the widget.
5416  * This will also update the disabled state of child widgets.
5418  * @param {boolean} disabled Disable widget
5419  * @chainable
5420  */
5421 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
5422         var i, len;
5424         // Parent method
5425         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5426         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
5428         // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
5429         if ( this.items ) {
5430                 for ( i = 0, len = this.items.length; i < len; i++ ) {
5431                         this.items[i].updateDisabled();
5432                 }
5433         }
5435         return this;
5439  * Item widget.
5441  * Use together with OO.ui.GroupWidget to make disabled state inheritable.
5443  * @abstract
5444  * @class
5446  * @constructor
5447  */
5448 OO.ui.ItemWidget = function OoUiItemWidget() {
5449         //
5452 /* Methods */
5455  * Check if widget is disabled.
5457  * Checks parent if present, making disabled state inheritable.
5459  * @return {boolean} Widget is disabled
5460  */
5461 OO.ui.ItemWidget.prototype.isDisabled = function () {
5462         return this.disabled ||
5463                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
5467  * Set group element is in.
5469  * @param {OO.ui.GroupElement|null} group Group element, null if none
5470  * @chainable
5471  */
5472 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
5473         // Parent method
5474         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5475         OO.ui.Element.prototype.setElementGroup.call( this, group );
5477         // Initialize item disabled states
5478         this.updateDisabled();
5480         return this;
5484  * Icon widget.
5486  * @class
5487  * @extends OO.ui.Widget
5488  * @mixins OO.ui.IconedElement
5489  * @mixins OO.ui.TitledElement
5491  * @constructor
5492  * @param {Object} [config] Configuration options
5493  */
5494 OO.ui.IconWidget = function OoUiIconWidget( config ) {
5495         // Config intialization
5496         config = config || {};
5498         // Parent constructor
5499         OO.ui.IconWidget.super.call( this, config );
5501         // Mixin constructors
5502         OO.ui.IconedElement.call( this, this.$element, config );
5503         OO.ui.TitledElement.call( this, this.$element, config );
5505         // Initialization
5506         this.$element.addClass( 'oo-ui-iconWidget' );
5509 /* Setup */
5511 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
5512 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconedElement );
5513 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
5515 /* Static Properties */
5517 OO.ui.IconWidget.static.tagName = 'span';
5520  * Indicator widget.
5522  * @class
5523  * @extends OO.ui.Widget
5524  * @mixins OO.ui.IndicatedElement
5525  * @mixins OO.ui.TitledElement
5527  * @constructor
5528  * @param {Object} [config] Configuration options
5529  */
5530 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
5531         // Config intialization
5532         config = config || {};
5534         // Parent constructor
5535         OO.ui.IndicatorWidget.super.call( this, config );
5537         // Mixin constructors
5538         OO.ui.IndicatedElement.call( this, this.$element, config );
5539         OO.ui.TitledElement.call( this, this.$element, config );
5541         // Initialization
5542         this.$element.addClass( 'oo-ui-indicatorWidget' );
5545 /* Setup */
5547 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
5548 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatedElement );
5549 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
5551 /* Static Properties */
5553 OO.ui.IndicatorWidget.static.tagName = 'span';
5556  * Container for multiple related buttons.
5558  * Use together with OO.ui.ButtonWidget.
5560  * @class
5561  * @extends OO.ui.Widget
5562  * @mixins OO.ui.GroupElement
5564  * @constructor
5565  * @param {Object} [config] Configuration options
5566  * @cfg {OO.ui.ButtonWidget} [items] Buttons to add
5567  */
5568 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
5569         // Parent constructor
5570         OO.ui.ButtonGroupWidget.super.call( this, config );
5572         // Mixin constructors
5573         OO.ui.GroupElement.call( this, this.$element, config );
5575         // Initialization
5576         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
5577         if ( $.isArray( config.items ) ) {
5578                 this.addItems( config.items );
5579         }
5582 /* Setup */
5584 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
5585 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
5588  * Button widget.
5590  * @class
5591  * @extends OO.ui.Widget
5592  * @mixins OO.ui.ButtonedElement
5593  * @mixins OO.ui.IconedElement
5594  * @mixins OO.ui.IndicatedElement
5595  * @mixins OO.ui.LabeledElement
5596  * @mixins OO.ui.TitledElement
5597  * @mixins OO.ui.FlaggableElement
5599  * @constructor
5600  * @param {Object} [config] Configuration options
5601  * @cfg {string} [title=''] Title text
5602  * @cfg {string} [href] Hyperlink to visit when clicked
5603  * @cfg {string} [target] Target to open hyperlink in
5604  */
5605 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
5606         // Configuration initialization
5607         config = $.extend( { 'target': '_blank' }, config );
5609         // Parent constructor
5610         OO.ui.ButtonWidget.super.call( this, config );
5612         // Mixin constructors
5613         OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
5614         OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
5615         OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
5616         OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
5617         OO.ui.TitledElement.call( this, this.$button, config );
5618         OO.ui.FlaggableElement.call( this, config );
5620         // Properties
5621         this.isHyperlink = typeof config.href === 'string';
5623         // Events
5624         this.$button.on( {
5625                 'click': OO.ui.bind( this.onClick, this ),
5626                 'keypress': OO.ui.bind( this.onKeyPress, this )
5627         } );
5629         // Initialization
5630         this.$button
5631                 .append( this.$icon, this.$label, this.$indicator )
5632                 .attr( { 'href': config.href, 'target': config.target } );
5633         this.$element
5634                 .addClass( 'oo-ui-buttonWidget' )
5635                 .append( this.$button );
5638 /* Setup */
5640 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
5641 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonedElement );
5642 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconedElement );
5643 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatedElement );
5644 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabeledElement );
5645 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
5646 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
5648 /* Events */
5651  * @event click
5652  */
5654 /* Methods */
5657  * Handles mouse click events.
5659  * @param {jQuery.Event} e Mouse click event
5660  * @fires click
5661  */
5662 OO.ui.ButtonWidget.prototype.onClick = function () {
5663         if ( !this.isDisabled() ) {
5664                 this.emit( 'click' );
5665                 if ( this.isHyperlink ) {
5666                         return true;
5667                 }
5668         }
5669         return false;
5673  * Handles keypress events.
5675  * @param {jQuery.Event} e Keypress event
5676  * @fires click
5677  */
5678 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
5679         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
5680                 this.onClick();
5681                 if ( this.isHyperlink ) {
5682                         return true;
5683                 }
5684         }
5685         return false;
5689  * Input widget.
5691  * @abstract
5692  * @class
5693  * @extends OO.ui.Widget
5695  * @constructor
5696  * @param {Object} [config] Configuration options
5697  * @cfg {string} [name=''] HTML input name
5698  * @cfg {string} [value=''] Input value
5699  * @cfg {boolean} [readOnly=false] Prevent changes
5700  * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
5701  */
5702 OO.ui.InputWidget = function OoUiInputWidget( config ) {
5703         // Config intialization
5704         config = $.extend( { 'readOnly': false }, config );
5706         // Parent constructor
5707         OO.ui.InputWidget.super.call( this, config );
5709         // Properties
5710         this.$input = this.getInputElement( config );
5711         this.value = '';
5712         this.readOnly = false;
5713         this.inputFilter = config.inputFilter;
5715         // Events
5716         this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) );
5718         // Initialization
5719         this.$input
5720                 .attr( 'name', config.name )
5721                 .prop( 'disabled', this.isDisabled() );
5722         this.setReadOnly( config.readOnly );
5723         this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
5724         this.setValue( config.value );
5727 /* Setup */
5729 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
5731 /* Events */
5734  * @event change
5735  * @param value
5736  */
5738 /* Methods */
5741  * Get input element.
5743  * @param {Object} [config] Configuration options
5744  * @return {jQuery} Input element
5745  */
5746 OO.ui.InputWidget.prototype.getInputElement = function () {
5747         return this.$( '<input>' );
5751  * Handle potentially value-changing events.
5753  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
5754  */
5755 OO.ui.InputWidget.prototype.onEdit = function () {
5756         if ( !this.isDisabled() ) {
5757                 // Allow the stack to clear so the value will be updated
5758                 setTimeout( OO.ui.bind( function () {
5759                         this.setValue( this.$input.val() );
5760                 }, this ) );
5761         }
5765  * Get the value of the input.
5767  * @return {string} Input value
5768  */
5769 OO.ui.InputWidget.prototype.getValue = function () {
5770         return this.value;
5774  * Sets the direction of the current input, either RTL or LTR
5776  * @param {boolean} isRTL
5777  */
5778 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
5779         if ( isRTL ) {
5780                 this.$input.removeClass( 'oo-ui-ltr' );
5781                 this.$input.addClass( 'oo-ui-rtl' );
5782         } else {
5783                 this.$input.removeClass( 'oo-ui-rtl' );
5784                 this.$input.addClass( 'oo-ui-ltr' );
5785         }
5789  * Set the value of the input.
5791  * @param {string} value New value
5792  * @fires change
5793  * @chainable
5794  */
5795 OO.ui.InputWidget.prototype.setValue = function ( value ) {
5796         value = this.sanitizeValue( value );
5797         if ( this.value !== value ) {
5798                 this.value = value;
5799                 this.emit( 'change', this.value );
5800         }
5801         // Update the DOM if it has changed. Note that with sanitizeValue, it
5802         // is possible for the DOM value to change without this.value changing.
5803         if ( this.$input.val() !== this.value ) {
5804                 this.$input.val( this.value );
5805         }
5806         return this;
5810  * Sanitize incoming value.
5812  * Ensures value is a string, and converts undefined and null to empty strings.
5814  * @param {string} value Original value
5815  * @return {string} Sanitized value
5816  */
5817 OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
5818         if ( value === undefined || value === null ) {
5819                 return '';
5820         } else if ( this.inputFilter ) {
5821                 return this.inputFilter( String( value ) );
5822         } else {
5823                 return String( value );
5824         }
5828  * Simulate the behavior of clicking on a label bound to this input.
5829  */
5830 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
5831         if ( !this.isDisabled() ) {
5832                 if ( this.$input.is( ':checkbox,:radio' ) ) {
5833                         this.$input.click();
5834                 } else if ( this.$input.is( ':input' ) ) {
5835                         this.$input.focus();
5836                 }
5837         }
5841  * Check if the widget is read-only.
5843  * @return {boolean}
5844  */
5845 OO.ui.InputWidget.prototype.isReadOnly = function () {
5846         return this.readOnly;
5850  * Set the read-only state of the widget.
5852  * This should probably change the widgets's appearance and prevent it from being used.
5854  * @param {boolean} state Make input read-only
5855  * @chainable
5856  */
5857 OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
5858         this.readOnly = !!state;
5859         this.$input.prop( 'readOnly', this.readOnly );
5860         return this;
5864  * @inheritdoc
5865  */
5866 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
5867         OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
5868         if ( this.$input ) {
5869                 this.$input.prop( 'disabled', this.isDisabled() );
5870         }
5871         return this;
5875  * Focus the input.
5877  * @chainable
5878  */
5879 OO.ui.InputWidget.prototype.focus = function () {
5880         this.$input.focus();
5881         return this;
5885  * Checkbox widget.
5887  * @class
5888  * @extends OO.ui.InputWidget
5890  * @constructor
5891  * @param {Object} [config] Configuration options
5892  */
5893 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
5894         // Parent constructor
5895         OO.ui.CheckboxInputWidget.super.call( this, config );
5897         // Initialization
5898         this.$element.addClass( 'oo-ui-checkboxInputWidget' );
5901 /* Setup */
5903 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
5905 /* Events */
5907 /* Methods */
5910  * Get input element.
5912  * @return {jQuery} Input element
5913  */
5914 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
5915         return this.$( '<input type="checkbox" />' );
5919  * Get checked state of the checkbox
5921  * @return {boolean} If the checkbox is checked
5922  */
5923 OO.ui.CheckboxInputWidget.prototype.getValue = function () {
5924         return this.value;
5928  * Set value
5929  */
5930 OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
5931         value = !!value;
5932         if ( this.value !== value ) {
5933                 this.value = value;
5934                 this.$input.prop( 'checked', this.value );
5935                 this.emit( 'change', this.value );
5936         }
5940  * @inheritdoc
5941  */
5942 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
5943         if ( !this.isDisabled() ) {
5944                 // Allow the stack to clear so the value will be updated
5945                 setTimeout( OO.ui.bind( function () {
5946                         this.setValue( this.$input.prop( 'checked' ) );
5947                 }, this ) );
5948         }
5952  * Label widget.
5954  * @class
5955  * @extends OO.ui.Widget
5956  * @mixins OO.ui.LabeledElement
5958  * @constructor
5959  * @param {Object} [config] Configuration options
5960  */
5961 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
5962         // Config intialization
5963         config = config || {};
5965         // Parent constructor
5966         OO.ui.LabelWidget.super.call( this, config );
5968         // Mixin constructors
5969         OO.ui.LabeledElement.call( this, this.$element, config );
5971         // Properties
5972         this.input = config.input;
5974         // Events
5975         if ( this.input instanceof OO.ui.InputWidget ) {
5976                 this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
5977         }
5979         // Initialization
5980         this.$element.addClass( 'oo-ui-labelWidget' );
5983 /* Setup */
5985 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
5986 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
5988 /* Static Properties */
5990 OO.ui.LabelWidget.static.tagName = 'label';
5992 /* Methods */
5995  * Handles label mouse click events.
5997  * @param {jQuery.Event} e Mouse click event
5998  */
5999 OO.ui.LabelWidget.prototype.onClick = function () {
6000         this.input.simulateLabelClick();
6001         return false;
6005  * Lookup input widget.
6007  * Mixin that adds a menu showing suggested values to a text input. Subclasses must handle `select`
6008  * and `choose` events on #lookupMenu to make use of selections.
6010  * @class
6011  * @abstract
6013  * @constructor
6014  * @param {OO.ui.TextInputWidget} input Input widget
6015  * @param {Object} [config] Configuration options
6016  * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
6017  */
6018 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
6019         // Config intialization
6020         config = config || {};
6022         // Properties
6023         this.lookupInput = input;
6024         this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
6025         this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
6026                 '$': OO.ui.Element.getJQuery( this.$overlay ),
6027                 'input': this.lookupInput,
6028                 '$container': config.$container
6029         } );
6030         this.lookupCache = {};
6031         this.lookupQuery = null;
6032         this.lookupRequest = null;
6033         this.populating = false;
6035         // Events
6036         this.$overlay.append( this.lookupMenu.$element );
6038         this.lookupInput.$input.on( {
6039                 'focus': OO.ui.bind( this.onLookupInputFocus, this ),
6040                 'blur': OO.ui.bind( this.onLookupInputBlur, this ),
6041                 'mousedown': OO.ui.bind( this.onLookupInputMouseDown, this )
6042         } );
6043         this.lookupInput.connect( this, { 'change': 'onLookupInputChange' } );
6045         // Initialization
6046         this.$element.addClass( 'oo-ui-lookupWidget' );
6047         this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
6050 /* Methods */
6053  * Handle input focus event.
6055  * @param {jQuery.Event} e Input focus event
6056  */
6057 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
6058         this.openLookupMenu();
6062  * Handle input blur event.
6064  * @param {jQuery.Event} e Input blur event
6065  */
6066 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
6067         this.lookupMenu.hide();
6071  * Handle input mouse down event.
6073  * @param {jQuery.Event} e Input mouse down event
6074  */
6075 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
6076         this.openLookupMenu();
6080  * Handle input change event.
6082  * @param {string} value New input value
6083  */
6084 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
6085         this.openLookupMenu();
6089  * Get lookup menu.
6091  * @return {OO.ui.TextInputMenuWidget}
6092  */
6093 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
6094         return this.lookupMenu;
6098  * Open the menu.
6100  * @chainable
6101  */
6102 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
6103         var value = this.lookupInput.getValue();
6105         if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
6106                 this.populateLookupMenu();
6107                 if ( !this.lookupMenu.isVisible() ) {
6108                         this.lookupMenu.show();
6109                 }
6110         } else {
6111                 this.lookupMenu.clearItems();
6112                 this.lookupMenu.hide();
6113         }
6115         return this;
6119  * Populate lookup menu with current information.
6121  * @chainable
6122  */
6123 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
6124         if ( !this.populating ) {
6125                 this.populating = true;
6126                 this.getLookupMenuItems()
6127                         .done( OO.ui.bind( function ( items ) {
6128                                 this.lookupMenu.clearItems();
6129                                 if ( items.length ) {
6130                                         this.lookupMenu.show();
6131                                         this.lookupMenu.addItems( items );
6132                                         this.initializeLookupMenuSelection();
6133                                         this.openLookupMenu();
6134                                 } else {
6135                                         this.lookupMenu.hide();
6136                                 }
6137                                 this.populating = false;
6138                         }, this ) )
6139                         .fail( OO.ui.bind( function () {
6140                                 this.lookupMenu.clearItems();
6141                                 this.populating = false;
6142                         }, this ) );
6143         }
6145         return this;
6149  * Set selection in the lookup menu with current information.
6151  * @chainable
6152  */
6153 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
6154         if ( !this.lookupMenu.getSelectedItem() ) {
6155                 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
6156         }
6157         this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
6161  * Get lookup menu items for the current query.
6163  * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
6164  * of the done event
6165  */
6166 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
6167         var value = this.lookupInput.getValue(),
6168                 deferred = $.Deferred();
6170         if ( value && value !== this.lookupQuery ) {
6171                 // Abort current request if query has changed
6172                 if ( this.lookupRequest ) {
6173                         this.lookupRequest.abort();
6174                         this.lookupQuery = null;
6175                         this.lookupRequest = null;
6176                 }
6177                 if ( value in this.lookupCache ) {
6178                         deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
6179                 } else {
6180                         this.lookupQuery = value;
6181                         this.lookupRequest = this.getLookupRequest()
6182                                 .always( OO.ui.bind( function () {
6183                                         this.lookupQuery = null;
6184                                         this.lookupRequest = null;
6185                                 }, this ) )
6186                                 .done( OO.ui.bind( function ( data ) {
6187                                         this.lookupCache[value] = this.getLookupCacheItemFromData( data );
6188                                         deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
6189                                 }, this ) )
6190                                 .fail( function () {
6191                                         deferred.reject();
6192                                 } );
6193                         this.pushPending();
6194                         this.lookupRequest.always( OO.ui.bind( function () {
6195                                 this.popPending();
6196                         }, this ) );
6197                 }
6198         }
6199         return deferred.promise();
6203  * Get a new request object of the current lookup query value.
6205  * @abstract
6206  * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
6207  */
6208 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
6209         // Stub, implemented in subclass
6210         return null;
6214  * Handle successful lookup request.
6216  * Overriding methods should call #populateLookupMenu when results are available and cache results
6217  * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
6219  * @abstract
6220  * @param {Mixed} data Response from server
6221  */
6222 OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
6223         // Stub, implemented in subclass
6227  * Get a list of menu item widgets from the data stored by the lookup request's done handler.
6229  * @abstract
6230  * @param {Mixed} data Cached result data, usually an array
6231  * @return {OO.ui.MenuItemWidget[]} Menu items
6232  */
6233 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
6234         // Stub, implemented in subclass
6235         return [];
6239  * Option widget.
6241  * Use with OO.ui.SelectWidget.
6243  * @class
6244  * @extends OO.ui.Widget
6245  * @mixins OO.ui.IconedElement
6246  * @mixins OO.ui.LabeledElement
6247  * @mixins OO.ui.IndicatedElement
6248  * @mixins OO.ui.FlaggableElement
6250  * @constructor
6251  * @param {Mixed} data Option data
6252  * @param {Object} [config] Configuration options
6253  * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
6254  */
6255 OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
6256         // Config intialization
6257         config = config || {};
6259         // Parent constructor
6260         OO.ui.OptionWidget.super.call( this, config );
6262         // Mixin constructors
6263         OO.ui.ItemWidget.call( this );
6264         OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
6265         OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
6266         OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
6267         OO.ui.FlaggableElement.call( this, config );
6269         // Properties
6270         this.data = data;
6271         this.selected = false;
6272         this.highlighted = false;
6273         this.pressed = false;
6275         // Initialization
6276         this.$element
6277                 .data( 'oo-ui-optionWidget', this )
6278                 .attr( 'rel', config.rel )
6279                 .addClass( 'oo-ui-optionWidget' )
6280                 .append( this.$label );
6281         this.$element
6282                 .prepend( this.$icon )
6283                 .append( this.$indicator );
6286 /* Setup */
6288 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
6289 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
6290 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
6291 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
6292 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
6293 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
6295 /* Static Properties */
6297 OO.ui.OptionWidget.static.tagName = 'li';
6299 OO.ui.OptionWidget.static.selectable = true;
6301 OO.ui.OptionWidget.static.highlightable = true;
6303 OO.ui.OptionWidget.static.pressable = true;
6305 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
6307 /* Methods */
6310  * Check if option can be selected.
6312  * @return {boolean} Item is selectable
6313  */
6314 OO.ui.OptionWidget.prototype.isSelectable = function () {
6315         return this.constructor.static.selectable && !this.isDisabled();
6319  * Check if option can be highlighted.
6321  * @return {boolean} Item is highlightable
6322  */
6323 OO.ui.OptionWidget.prototype.isHighlightable = function () {
6324         return this.constructor.static.highlightable && !this.isDisabled();
6328  * Check if option can be pressed.
6330  * @return {boolean} Item is pressable
6331  */
6332 OO.ui.OptionWidget.prototype.isPressable = function () {
6333         return this.constructor.static.pressable && !this.isDisabled();
6337  * Check if option is selected.
6339  * @return {boolean} Item is selected
6340  */
6341 OO.ui.OptionWidget.prototype.isSelected = function () {
6342         return this.selected;
6346  * Check if option is highlighted.
6348  * @return {boolean} Item is highlighted
6349  */
6350 OO.ui.OptionWidget.prototype.isHighlighted = function () {
6351         return this.highlighted;
6355  * Check if option is pressed.
6357  * @return {boolean} Item is pressed
6358  */
6359 OO.ui.OptionWidget.prototype.isPressed = function () {
6360         return this.pressed;
6364  * Set selected state.
6366  * @param {boolean} [state=false] Select option
6367  * @chainable
6368  */
6369 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
6370         if ( this.constructor.static.selectable ) {
6371                 this.selected = !!state;
6372                 if ( this.selected ) {
6373                         this.$element.addClass( 'oo-ui-optionWidget-selected' );
6374                         if ( this.constructor.static.scrollIntoViewOnSelect ) {
6375                                 this.scrollElementIntoView();
6376                         }
6377                 } else {
6378                         this.$element.removeClass( 'oo-ui-optionWidget-selected' );
6379                 }
6380         }
6381         return this;
6385  * Set highlighted state.
6387  * @param {boolean} [state=false] Highlight option
6388  * @chainable
6389  */
6390 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
6391         if ( this.constructor.static.highlightable ) {
6392                 this.highlighted = !!state;
6393                 if ( this.highlighted ) {
6394                         this.$element.addClass( 'oo-ui-optionWidget-highlighted' );
6395                 } else {
6396                         this.$element.removeClass( 'oo-ui-optionWidget-highlighted' );
6397                 }
6398         }
6399         return this;
6403  * Set pressed state.
6405  * @param {boolean} [state=false] Press option
6406  * @chainable
6407  */
6408 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
6409         if ( this.constructor.static.pressable ) {
6410                 this.pressed = !!state;
6411                 if ( this.pressed ) {
6412                         this.$element.addClass( 'oo-ui-optionWidget-pressed' );
6413                 } else {
6414                         this.$element.removeClass( 'oo-ui-optionWidget-pressed' );
6415                 }
6416         }
6417         return this;
6421  * Make the option's highlight flash.
6423  * While flashing, the visual style of the pressed state is removed if present.
6425  * @return {jQuery.Promise} Promise resolved when flashing is done
6426  */
6427 OO.ui.OptionWidget.prototype.flash = function () {
6428         var $this = this.$element,
6429                 deferred = $.Deferred();
6431         if ( !this.isDisabled() && this.constructor.static.pressable ) {
6432                 $this.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
6433                 setTimeout( OO.ui.bind( function () {
6434                         // Restore original classes
6435                         $this
6436                                 .toggleClass( 'oo-ui-optionWidget-highlighted', this.highlighted )
6437                                 .toggleClass( 'oo-ui-optionWidget-pressed', this.pressed );
6438                         setTimeout( function () {
6439                                 deferred.resolve();
6440                         }, 100 );
6441                 }, this ), 100 );
6442         }
6444         return deferred.promise();
6448  * Get option data.
6450  * @return {Mixed} Option data
6451  */
6452 OO.ui.OptionWidget.prototype.getData = function () {
6453         return this.data;
6457  * Selection of options.
6459  * Use together with OO.ui.OptionWidget.
6461  * @class
6462  * @extends OO.ui.Widget
6463  * @mixins OO.ui.GroupElement
6465  * @constructor
6466  * @param {Object} [config] Configuration options
6467  * @cfg {OO.ui.OptionWidget[]} [items] Options to add
6468  */
6469 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
6470         // Config intialization
6471         config = config || {};
6473         // Parent constructor
6474         OO.ui.SelectWidget.super.call( this, config );
6476         // Mixin constructors
6477         OO.ui.GroupWidget.call( this, this.$element, config );
6479         // Properties
6480         this.pressed = false;
6481         this.selecting = null;
6482         this.hashes = {};
6483         this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
6484         this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this );
6486         // Events
6487         this.$element.on( {
6488                 'mousedown': OO.ui.bind( this.onMouseDown, this ),
6489                 'mouseover': OO.ui.bind( this.onMouseOver, this ),
6490                 'mouseleave': OO.ui.bind( this.onMouseLeave, this )
6491         } );
6493         // Initialization
6494         this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
6495         if ( $.isArray( config.items ) ) {
6496                 this.addItems( config.items );
6497         }
6500 /* Setup */
6502 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
6504 // Need to mixin base class as well
6505 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
6506 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
6508 /* Events */
6511  * @event highlight
6512  * @param {OO.ui.OptionWidget|null} item Highlighted item
6513  */
6516  * @event press
6517  * @param {OO.ui.OptionWidget|null} item Pressed item
6518  */
6521  * @event select
6522  * @param {OO.ui.OptionWidget|null} item Selected item
6523  */
6526  * @event choose
6527  * @param {OO.ui.OptionWidget|null} item Chosen item
6528  */
6531  * @event add
6532  * @param {OO.ui.OptionWidget[]} items Added items
6533  * @param {number} index Index items were added at
6534  */
6537  * @event remove
6538  * @param {OO.ui.OptionWidget[]} items Removed items
6539  */
6541 /* Static Properties */
6543 OO.ui.SelectWidget.static.tagName = 'ul';
6545 /* Methods */
6548  * Handle mouse down events.
6550  * @private
6551  * @param {jQuery.Event} e Mouse down event
6552  */
6553 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
6554         var item;
6556         if ( !this.isDisabled() && e.which === 1 ) {
6557                 this.togglePressed( true );
6558                 item = this.getTargetItem( e );
6559                 if ( item && item.isSelectable() ) {
6560                         this.pressItem( item );
6561                         this.selecting = item;
6562                         this.getElementDocument().addEventListener(
6563                                 'mouseup', this.onMouseUpHandler, true
6564                         );
6565                         this.getElementDocument().addEventListener(
6566                                 'mousemove', this.onMouseMoveHandler, true
6567                         );
6568                 }
6569         }
6570         return false;
6574  * Handle mouse up events.
6576  * @private
6577  * @param {jQuery.Event} e Mouse up event
6578  */
6579 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
6580         var item;
6582         this.togglePressed( false );
6583         if ( !this.selecting ) {
6584                 item = this.getTargetItem( e );
6585                 if ( item && item.isSelectable() ) {
6586                         this.selecting = item;
6587                 }
6588         }
6589         if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
6590                 this.pressItem( null );
6591                 this.chooseItem( this.selecting );
6592                 this.selecting = null;
6593         }
6595         this.getElementDocument().removeEventListener(
6596                 'mouseup', this.onMouseUpHandler, true
6597         );
6598         this.getElementDocument().removeEventListener(
6599                 'mousemove', this.onMouseMoveHandler, true
6600         );
6602         return false;
6606  * Handle mouse move events.
6608  * @private
6609  * @param {jQuery.Event} e Mouse move event
6610  */
6611 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
6612         var item;
6614         if ( !this.isDisabled() && this.pressed ) {
6615                 item = this.getTargetItem( e );
6616                 if ( item && item !== this.selecting && item.isSelectable() ) {
6617                         this.pressItem( item );
6618                         this.selecting = item;
6619                 }
6620         }
6621         return false;
6625  * Handle mouse over events.
6627  * @private
6628  * @param {jQuery.Event} e Mouse over event
6629  */
6630 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
6631         var item;
6633         if ( !this.isDisabled() ) {
6634                 item = this.getTargetItem( e );
6635                 this.highlightItem( item && item.isHighlightable() ? item : null );
6636         }
6637         return false;
6641  * Handle mouse leave events.
6643  * @private
6644  * @param {jQuery.Event} e Mouse over event
6645  */
6646 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
6647         if ( !this.isDisabled() ) {
6648                 this.highlightItem( null );
6649         }
6650         return false;
6654  * Get the closest item to a jQuery.Event.
6656  * @private
6657  * @param {jQuery.Event} e
6658  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6659  */
6660 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
6661         var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
6662         if ( $item.length ) {
6663                 return $item.data( 'oo-ui-optionWidget' );
6664         }
6665         return null;
6669  * Get selected item.
6671  * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6672  */
6673 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
6674         var i, len;
6676         for ( i = 0, len = this.items.length; i < len; i++ ) {
6677                 if ( this.items[i].isSelected() ) {
6678                         return this.items[i];
6679                 }
6680         }
6681         return null;
6685  * Get highlighted item.
6687  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6688  */
6689 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
6690         var i, len;
6692         for ( i = 0, len = this.items.length; i < len; i++ ) {
6693                 if ( this.items[i].isHighlighted() ) {
6694                         return this.items[i];
6695                 }
6696         }
6697         return null;
6701  * Get an existing item with equivilant data.
6703  * @param {Object} data Item data to search for
6704  * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
6705  */
6706 OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
6707         var hash = OO.getHash( data );
6709         if ( hash in this.hashes ) {
6710                 return this.hashes[hash];
6711         }
6713         return null;
6717  * Toggle pressed state.
6719  * @param {boolean} pressed An option is being pressed
6720  */
6721 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
6722         if ( pressed === undefined ) {
6723                 pressed = !this.pressed;
6724         }
6725         if ( pressed !== this.pressed ) {
6726                 this.$element.toggleClass( 'oo-ui-selectWidget-pressed', pressed );
6727                 this.$element.toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
6728                 this.pressed = pressed;
6729         }
6733  * Highlight an item.
6735  * Highlighting is mutually exclusive.
6737  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
6738  * @fires highlight
6739  * @chainable
6740  */
6741 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
6742         var i, len, highlighted,
6743                 changed = false;
6745         for ( i = 0, len = this.items.length; i < len; i++ ) {
6746                 highlighted = this.items[i] === item;
6747                 if ( this.items[i].isHighlighted() !== highlighted ) {
6748                         this.items[i].setHighlighted( highlighted );
6749                         changed = true;
6750                 }
6751         }
6752         if ( changed ) {
6753                 this.emit( 'highlight', item );
6754         }
6756         return this;
6760  * Select an item.
6762  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6763  * @fires select
6764  * @chainable
6765  */
6766 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
6767         var i, len, selected,
6768                 changed = false;
6770         for ( i = 0, len = this.items.length; i < len; i++ ) {
6771                 selected = this.items[i] === item;
6772                 if ( this.items[i].isSelected() !== selected ) {
6773                         this.items[i].setSelected( selected );
6774                         changed = true;
6775                 }
6776         }
6777         if ( changed ) {
6778                 this.emit( 'select', item );
6779         }
6781         return this;
6785  * Press an item.
6787  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6788  * @fires press
6789  * @chainable
6790  */
6791 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
6792         var i, len, pressed,
6793                 changed = false;
6795         for ( i = 0, len = this.items.length; i < len; i++ ) {
6796                 pressed = this.items[i] === item;
6797                 if ( this.items[i].isPressed() !== pressed ) {
6798                         this.items[i].setPressed( pressed );
6799                         changed = true;
6800                 }
6801         }
6802         if ( changed ) {
6803                 this.emit( 'press', item );
6804         }
6806         return this;
6810  * Choose an item.
6812  * Identical to #selectItem, but may vary in subclasses that want to take additional action when
6813  * an item is selected using the keyboard or mouse.
6815  * @param {OO.ui.OptionWidget} item Item to choose
6816  * @fires choose
6817  * @chainable
6818  */
6819 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
6820         this.selectItem( item );
6821         this.emit( 'choose', item );
6823         return this;
6827  * Get an item relative to another one.
6829  * @param {OO.ui.OptionWidget} item Item to start at
6830  * @param {number} direction Direction to move in
6831  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
6832  */
6833 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
6834         var inc = direction > 0 ? 1 : -1,
6835                 len = this.items.length,
6836                 index = item instanceof OO.ui.OptionWidget ?
6837                         $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
6838                 stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
6839                 i = inc > 0 ?
6840                         // Default to 0 instead of -1, if nothing is selected let's start at the beginning
6841                         Math.max( index, -1 ) :
6842                         // Default to n-1 instead of -1, if nothing is selected let's start at the end
6843                         Math.min( index, len );
6845         while ( true ) {
6846                 i = ( i + inc + len ) % len;
6847                 item = this.items[i];
6848                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
6849                         return item;
6850                 }
6851                 // Stop iterating when we've looped all the way around
6852                 if ( i === stopAt ) {
6853                         break;
6854                 }
6855         }
6856         return null;
6860  * Get the next selectable item.
6862  * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
6863  */
6864 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
6865         var i, len, item;
6867         for ( i = 0, len = this.items.length; i < len; i++ ) {
6868                 item = this.items[i];
6869                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
6870                         return item;
6871                 }
6872         }
6874         return null;
6878  * Add items.
6880  * When items are added with the same values as existing items, the existing items will be
6881  * automatically removed before the new items are added.
6883  * @param {OO.ui.OptionWidget[]} items Items to add
6884  * @param {number} [index] Index to insert items after
6885  * @fires add
6886  * @chainable
6887  */
6888 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
6889         var i, len, item, hash,
6890                 remove = [];
6892         for ( i = 0, len = items.length; i < len; i++ ) {
6893                 item = items[i];
6894                 hash = OO.getHash( item.getData() );
6895                 if ( hash in this.hashes ) {
6896                         // Remove item with same value
6897                         remove.push( this.hashes[hash] );
6898                 }
6899                 this.hashes[hash] = item;
6900         }
6901         if ( remove.length ) {
6902                 this.removeItems( remove );
6903         }
6905         // Mixin method
6906         OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
6908         // Always provide an index, even if it was omitted
6909         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
6911         return this;
6915  * Remove items.
6917  * Items will be detached, not removed, so they can be used later.
6919  * @param {OO.ui.OptionWidget[]} items Items to remove
6920  * @fires remove
6921  * @chainable
6922  */
6923 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
6924         var i, len, item, hash;
6926         for ( i = 0, len = items.length; i < len; i++ ) {
6927                 item = items[i];
6928                 hash = OO.getHash( item.getData() );
6929                 if ( hash in this.hashes ) {
6930                         // Remove existing item
6931                         delete this.hashes[hash];
6932                 }
6933                 if ( item.isSelected() ) {
6934                         this.selectItem( null );
6935                 }
6936         }
6938         // Mixin method
6939         OO.ui.GroupWidget.prototype.removeItems.call( this, items );
6941         this.emit( 'remove', items );
6943         return this;
6947  * Clear all items.
6949  * Items will be detached, not removed, so they can be used later.
6951  * @fires remove
6952  * @chainable
6953  */
6954 OO.ui.SelectWidget.prototype.clearItems = function () {
6955         var items = this.items.slice();
6957         // Clear all items
6958         this.hashes = {};
6959         // Mixin method
6960         OO.ui.GroupWidget.prototype.clearItems.call( this );
6961         this.selectItem( null );
6963         this.emit( 'remove', items );
6965         return this;
6969  * Menu item widget.
6971  * Use with OO.ui.MenuWidget.
6973  * @class
6974  * @extends OO.ui.OptionWidget
6976  * @constructor
6977  * @param {Mixed} data Item data
6978  * @param {Object} [config] Configuration options
6979  */
6980 OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
6981         // Configuration initialization
6982         config = $.extend( { 'icon': 'check' }, config );
6984         // Parent constructor
6985         OO.ui.MenuItemWidget.super.call( this, data, config );
6987         // Initialization
6988         this.$element.addClass( 'oo-ui-menuItemWidget' );
6991 /* Setup */
6993 OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.OptionWidget );
6996  * Menu widget.
6998  * Use together with OO.ui.MenuItemWidget.
7000  * @class
7001  * @extends OO.ui.SelectWidget
7002  * @mixins OO.ui.ClippableElement
7004  * @constructor
7005  * @param {Object} [config] Configuration options
7006  * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
7007  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
7008  */
7009 OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
7010         // Config intialization
7011         config = config || {};
7013         // Parent constructor
7014         OO.ui.MenuWidget.super.call( this, config );
7016         // Mixin constructors
7017         OO.ui.ClippableElement.call( this, this.$group, config );
7019         // Properties
7020         this.autoHide = config.autoHide === undefined || !!config.autoHide;
7021         this.newItems = null;
7022         this.$input = config.input ? config.input.$input : null;
7023         this.$previousFocus = null;
7024         this.isolated = !config.input;
7025         this.visible = false;
7026         this.flashing = false;
7027         this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
7028         this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this );
7030         // Initialization
7031         this.$element.hide().addClass( 'oo-ui-menuWidget' );
7034 /* Setup */
7036 OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
7037 OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
7039 /* Methods */
7042  * Handles document mouse down events.
7044  * @param {jQuery.Event} e Key down event
7045  */
7046 OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
7047         if ( !$.contains( this.$element[0], e.target ) ) {
7048                 this.hide();
7049         }
7053  * Handles key down events.
7055  * @param {jQuery.Event} e Key down event
7056  */
7057 OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
7058         var nextItem,
7059                 handled = false,
7060                 highlightItem = this.getHighlightedItem();
7062         if ( !this.isDisabled() && this.visible ) {
7063                 if ( !highlightItem ) {
7064                         highlightItem = this.getSelectedItem();
7065                 }
7066                 switch ( e.keyCode ) {
7067                         case OO.ui.Keys.ENTER:
7068                                 this.chooseItem( highlightItem );
7069                                 handled = true;
7070                                 break;
7071                         case OO.ui.Keys.UP:
7072                                 nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
7073                                 handled = true;
7074                                 break;
7075                         case OO.ui.Keys.DOWN:
7076                                 nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
7077                                 handled = true;
7078                                 break;
7079                         case OO.ui.Keys.ESCAPE:
7080                                 if ( highlightItem ) {
7081                                         highlightItem.setHighlighted( false );
7082                                 }
7083                                 this.hide();
7084                                 handled = true;
7085                                 break;
7086                 }
7088                 if ( nextItem ) {
7089                         this.highlightItem( nextItem );
7090                         nextItem.scrollElementIntoView();
7091                 }
7093                 if ( handled ) {
7094                         e.preventDefault();
7095                         e.stopPropagation();
7096                         return false;
7097                 }
7098         }
7102  * Check if the menu is visible.
7104  * @return {boolean} Menu is visible
7105  */
7106 OO.ui.MenuWidget.prototype.isVisible = function () {
7107         return this.visible;
7111  * Bind key down listener.
7112  */
7113 OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
7114         if ( this.$input ) {
7115                 this.$input.on( 'keydown', this.onKeyDownHandler );
7116         } else {
7117                 // Capture menu navigation keys
7118                 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
7119         }
7123  * Unbind key down listener.
7124  */
7125 OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
7126         if ( this.$input ) {
7127                 this.$input.off( 'keydown' );
7128         } else {
7129                 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
7130         }
7134  * Choose an item.
7136  * This will close the menu when done, unlike selectItem which only changes selection.
7138  * @param {OO.ui.OptionWidget} item Item to choose
7139  * @chainable
7140  */
7141 OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
7142         // Parent method
7143         OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
7145         if ( item && !this.flashing ) {
7146                 this.flashing = true;
7147                 item.flash().done( OO.ui.bind( function () {
7148                         this.hide();
7149                         this.flashing = false;
7150                 }, this ) );
7151         } else {
7152                 this.hide();
7153         }
7155         return this;
7159  * Add items.
7161  * Adding an existing item (by value) will move it.
7163  * @param {OO.ui.MenuItemWidget[]} items Items to add
7164  * @param {number} [index] Index to insert items after
7165  * @chainable
7166  */
7167 OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
7168         var i, len, item;
7170         // Parent method
7171         OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
7173         // Auto-initialize
7174         if ( !this.newItems ) {
7175                 this.newItems = [];
7176         }
7178         for ( i = 0, len = items.length; i < len; i++ ) {
7179                 item = items[i];
7180                 if ( this.visible ) {
7181                         // Defer fitting label until
7182                         item.fitLabel();
7183                 } else {
7184                         this.newItems.push( item );
7185                 }
7186         }
7188         return this;
7192  * Show the menu.
7194  * @chainable
7195  */
7196 OO.ui.MenuWidget.prototype.show = function () {
7197         var i, len;
7199         if ( this.items.length ) {
7200                 this.$element.show();
7201                 this.visible = true;
7202                 this.bindKeyDownListener();
7204                 // Change focus to enable keyboard navigation
7205                 if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
7206                         this.$previousFocus = this.$( ':focus' );
7207                         this.$input.focus();
7208                 }
7209                 if ( this.newItems && this.newItems.length ) {
7210                         for ( i = 0, len = this.newItems.length; i < len; i++ ) {
7211                                 this.newItems[i].fitLabel();
7212                         }
7213                         this.newItems = null;
7214                 }
7216                 this.setClipping( true );
7218                 // Auto-hide
7219                 if ( this.autoHide ) {
7220                         this.getElementDocument().addEventListener(
7221                                 'mousedown', this.onDocumentMouseDownHandler, true
7222                         );
7223                 }
7224         }
7226         return this;
7230  * Hide the menu.
7232  * @chainable
7233  */
7234 OO.ui.MenuWidget.prototype.hide = function () {
7235         this.$element.hide();
7236         this.visible = false;
7237         this.unbindKeyDownListener();
7239         if ( this.isolated && this.$previousFocus ) {
7240                 this.$previousFocus.focus();
7241                 this.$previousFocus = null;
7242         }
7244         this.getElementDocument().removeEventListener(
7245                 'mousedown', this.onDocumentMouseDownHandler, true
7246         );
7248         this.setClipping( false );
7250         return this;
7254  * Inline menu of options.
7256  * Use with OO.ui.MenuOptionWidget.
7258  * @class
7259  * @extends OO.ui.Widget
7260  * @mixins OO.ui.IconedElement
7261  * @mixins OO.ui.IndicatedElement
7262  * @mixins OO.ui.LabeledElement
7263  * @mixins OO.ui.TitledElement
7265  * @constructor
7266  * @param {Object} [config] Configuration options
7267  * @cfg {Object} [menu] Configuration options to pass to menu widget
7268  */
7269 OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
7270         // Configuration initialization
7271         config = $.extend( { 'indicator': 'down' }, config );
7273         // Parent constructor
7274         OO.ui.InlineMenuWidget.super.call( this, config );
7276         // Mixin constructors
7277         OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
7278         OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
7279         OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
7280         OO.ui.TitledElement.call( this, this.$label, config );
7282         // Properties
7283         this.menu = new OO.ui.MenuWidget( $.extend( { '$': this.$ }, config.menu ) );
7284         this.$handle = this.$( '<span>' );
7286         // Events
7287         this.$element.on( { 'click': OO.ui.bind( this.onClick, this ) } );
7288         this.menu.connect( this, { 'select': 'onMenuSelect' } );
7290         // Initialization
7291         this.$handle
7292                 .addClass( 'oo-ui-inlineMenuWidget-handle' )
7293                 .append( this.$icon, this.$label, this.$indicator );
7294         this.$element
7295                 .addClass( 'oo-ui-inlineMenuWidget' )
7296                 .append( this.$handle, this.menu.$element );
7299 /* Setup */
7301 OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
7302 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
7303 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
7304 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
7305 OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
7307 /* Methods */
7310  * Get the menu.
7312  * @return {OO.ui.MenuWidget} Menu of widget
7313  */
7314 OO.ui.InlineMenuWidget.prototype.getMenu = function () {
7315         return this.menu;
7319  * Handles menu select events.
7321  * @param {OO.ui.MenuItemWidget} item Selected menu item
7322  */
7323 OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
7324         var selectedLabel;
7326         if ( !item ) {
7327                 return;
7328         }
7330         selectedLabel = item.getLabel();
7332         // If the label is a DOM element, clone it, because setLabel will append() it
7333         if ( selectedLabel instanceof jQuery ) {
7334                 selectedLabel = selectedLabel.clone();
7335         }
7337         this.setLabel( selectedLabel );
7341  * Handles mouse click events.
7343  * @param {jQuery.Event} e Mouse click event
7344  */
7345 OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
7346         // Skip clicks within the menu
7347         if ( $.contains( this.menu.$element[0], e.target ) ) {
7348                 return;
7349         }
7351         if ( !this.isDisabled() ) {
7352                 if ( this.menu.isVisible() ) {
7353                         this.menu.hide();
7354                 } else {
7355                         this.menu.show();
7356                 }
7357         }
7358         return false;
7362  * Menu section item widget.
7364  * Use with OO.ui.MenuWidget.
7366  * @class
7367  * @extends OO.ui.OptionWidget
7369  * @constructor
7370  * @param {Mixed} data Item data
7371  * @param {Object} [config] Configuration options
7372  */
7373 OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
7374         // Parent constructor
7375         OO.ui.MenuSectionItemWidget.super.call( this, data, config );
7377         // Initialization
7378         this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
7381 /* Setup */
7383 OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.OptionWidget );
7385 /* Static Properties */
7387 OO.ui.MenuSectionItemWidget.static.selectable = false;
7389 OO.ui.MenuSectionItemWidget.static.highlightable = false;
7392  * Create an OO.ui.OutlineWidget object.
7394  * Use with OO.ui.OutlineItemWidget.
7396  * @class
7397  * @extends OO.ui.SelectWidget
7399  * @constructor
7400  * @param {Object} [config] Configuration options
7401  */
7402 OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
7403         // Config intialization
7404         config = config || {};
7406         // Parent constructor
7407         OO.ui.OutlineWidget.super.call( this, config );
7409         // Initialization
7410         this.$element.addClass( 'oo-ui-outlineWidget' );
7413 /* Setup */
7415 OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
7418  * Creates an OO.ui.OutlineControlsWidget object.
7420  * Use together with OO.ui.OutlineWidget.js
7422  * @class
7424  * @constructor
7425  * @param {OO.ui.OutlineWidget} outline Outline to control
7426  * @param {Object} [config] Configuration options
7427  */
7428 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
7429         // Configuration initialization
7430         config = $.extend( { 'icon': 'add-item' }, config );
7432         // Parent constructor
7433         OO.ui.OutlineControlsWidget.super.call( this, config );
7435         // Mixin constructors
7436         OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
7437         OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
7439         // Properties
7440         this.outline = outline;
7441         this.$movers = this.$( '<div>' );
7442         this.upButton = new OO.ui.ButtonWidget( {
7443                 '$': this.$,
7444                 'frameless': true,
7445                 'icon': 'collapse',
7446                 'title': OO.ui.msg( 'ooui-outline-control-move-up' )
7447         } );
7448         this.downButton = new OO.ui.ButtonWidget( {
7449                 '$': this.$,
7450                 'frameless': true,
7451                 'icon': 'expand',
7452                 'title': OO.ui.msg( 'ooui-outline-control-move-down' )
7453         } );
7454         this.removeButton = new OO.ui.ButtonWidget( {
7455                 '$': this.$,
7456                 'frameless': true,
7457                 'icon': 'remove',
7458                 'title': OO.ui.msg( 'ooui-outline-control-remove' )
7459         } );
7461         // Events
7462         outline.connect( this, {
7463                 'select': 'onOutlineChange',
7464                 'add': 'onOutlineChange',
7465                 'remove': 'onOutlineChange'
7466         } );
7467         this.upButton.connect( this, { 'click': [ 'emit', 'move', -1 ] } );
7468         this.downButton.connect( this, { 'click': [ 'emit', 'move', 1 ] } );
7469         this.removeButton.connect( this, { 'click': [ 'emit', 'remove' ] } );
7471         // Initialization
7472         this.$element.addClass( 'oo-ui-outlineControlsWidget' );
7473         this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
7474         this.$movers
7475                 .addClass( 'oo-ui-outlineControlsWidget-movers' )
7476                 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
7477         this.$element.append( this.$icon, this.$group, this.$movers );
7480 /* Setup */
7482 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
7483 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
7484 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconedElement );
7486 /* Events */
7489  * @event move
7490  * @param {number} places Number of places to move
7491  */
7494  * @event remove
7495  */
7497 /* Methods */
7500  * Handle outline change events.
7501  */
7502 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
7503         var i, len, firstMovable, lastMovable,
7504                 items = this.outline.getItems(),
7505                 selectedItem = this.outline.getSelectedItem(),
7506                 movable = selectedItem && selectedItem.isMovable(),
7507                 removable = selectedItem && selectedItem.isRemovable();
7509         if ( movable ) {
7510                 i = -1;
7511                 len = items.length;
7512                 while ( ++i < len ) {
7513                         if ( items[i].isMovable() ) {
7514                                 firstMovable = items[i];
7515                                 break;
7516                         }
7517                 }
7518                 i = len;
7519                 while ( i-- ) {
7520                         if ( items[i].isMovable() ) {
7521                                 lastMovable = items[i];
7522                                 break;
7523                         }
7524                 }
7525         }
7526         this.upButton.setDisabled( !movable || selectedItem === firstMovable );
7527         this.downButton.setDisabled( !movable || selectedItem === lastMovable );
7528         this.removeButton.setDisabled( !removable );
7532  * Creates an OO.ui.OutlineItemWidget object.
7534  * Use with OO.ui.OutlineWidget.
7536  * @class
7537  * @extends OO.ui.OptionWidget
7539  * @constructor
7540  * @param {Mixed} data Item data
7541  * @param {Object} [config] Configuration options
7542  * @cfg {number} [level] Indentation level
7543  * @cfg {boolean} [movable] Allow modification from outline controls
7544  */
7545 OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
7546         // Config intialization
7547         config = config || {};
7549         // Parent constructor
7550         OO.ui.OutlineItemWidget.super.call( this, data, config );
7552         // Properties
7553         this.level = 0;
7554         this.movable = !!config.movable;
7555         this.removable = !!config.removable;
7557         // Initialization
7558         this.$element.addClass( 'oo-ui-outlineItemWidget' );
7559         this.setLevel( config.level );
7562 /* Setup */
7564 OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.OptionWidget );
7566 /* Static Properties */
7568 OO.ui.OutlineItemWidget.static.highlightable = false;
7570 OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
7572 OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
7574 OO.ui.OutlineItemWidget.static.levels = 3;
7576 /* Methods */
7579  * Check if item is movable.
7581  * Movablilty is used by outline controls.
7583  * @return {boolean} Item is movable
7584  */
7585 OO.ui.OutlineItemWidget.prototype.isMovable = function () {
7586         return this.movable;
7590  * Check if item is removable.
7592  * Removablilty is used by outline controls.
7594  * @return {boolean} Item is removable
7595  */
7596 OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
7597         return this.removable;
7601  * Get indentation level.
7603  * @return {number} Indentation level
7604  */
7605 OO.ui.OutlineItemWidget.prototype.getLevel = function () {
7606         return this.level;
7610  * Set movability.
7612  * Movablilty is used by outline controls.
7614  * @param {boolean} movable Item is movable
7615  * @chainable
7616  */
7617 OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
7618         this.movable = !!movable;
7619         return this;
7623  * Set removability.
7625  * Removablilty is used by outline controls.
7627  * @param {boolean} movable Item is removable
7628  * @chainable
7629  */
7630 OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
7631         this.removable = !!removable;
7632         return this;
7636  * Set indentation level.
7638  * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
7639  * @chainable
7640  */
7641 OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
7642         var levels = this.constructor.static.levels,
7643                 levelClass = this.constructor.static.levelClass,
7644                 i = levels;
7646         this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
7647         while ( i-- ) {
7648                 if ( this.level === i ) {
7649                         this.$element.addClass( levelClass + i );
7650                 } else {
7651                         this.$element.removeClass( levelClass + i );
7652                 }
7653         }
7655         return this;
7659  * Option widget that looks like a button.
7661  * Use together with OO.ui.ButtonSelectWidget.
7663  * @class
7664  * @extends OO.ui.OptionWidget
7665  * @mixins OO.ui.ButtonedElement
7666  * @mixins OO.ui.FlaggableElement
7668  * @constructor
7669  * @param {Mixed} data Option data
7670  * @param {Object} [config] Configuration options
7671  */
7672 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
7673         // Parent constructor
7674         OO.ui.ButtonOptionWidget.super.call( this, data, config );
7676         // Mixin constructors
7677         OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
7678         OO.ui.FlaggableElement.call( this, config );
7680         // Initialization
7681         this.$element.addClass( 'oo-ui-buttonOptionWidget' );
7682         this.$button.append( this.$element.contents() );
7683         this.$element.append( this.$button );
7686 /* Setup */
7688 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget );
7689 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
7690 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.FlaggableElement );
7692 /* Static Properties */
7694 // Allow button mouse down events to pass through so they can be handled by the parent select widget
7695 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
7697 /* Methods */
7700  * @inheritdoc
7701  */
7702 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
7703         OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
7705         if ( this.constructor.static.selectable ) {
7706                 this.setActive( state );
7707         }
7709         return this;
7713  * Select widget containing button options.
7715  * Use together with OO.ui.ButtonOptionWidget.
7717  * @class
7718  * @extends OO.ui.SelectWidget
7720  * @constructor
7721  * @param {Object} [config] Configuration options
7722  */
7723 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
7724         // Parent constructor
7725         OO.ui.ButtonSelectWidget.super.call( this, config );
7727         // Initialization
7728         this.$element.addClass( 'oo-ui-buttonSelectWidget' );
7731 /* Setup */
7733 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
7736  * Container for content that is overlaid and positioned absolutely.
7738  * @class
7739  * @extends OO.ui.Widget
7740  * @mixins OO.ui.LabeledElement
7742  * @constructor
7743  * @param {Object} [config] Configuration options
7744  * @cfg {boolean} [tail=true] Show tail pointing to origin of popup
7745  * @cfg {string} [align='center'] Alignment of popup to origin
7746  * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
7747  * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
7748  * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
7749  * @cfg {boolean} [head] Show label and close button at the top
7750  */
7751 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
7752         // Config intialization
7753         config = config || {};
7755         // Parent constructor
7756         OO.ui.PopupWidget.super.call( this, config );
7758         // Mixin constructors
7759         OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
7760         OO.ui.ClippableElement.call( this, this.$( '<div>' ), config );
7762         // Properties
7763         this.visible = false;
7764         this.$popup = this.$( '<div>' );
7765         this.$head = this.$( '<div>' );
7766         this.$body = this.$clippable;
7767         this.$tail = this.$( '<div>' );
7768         this.$container = config.$container || this.$( 'body' );
7769         this.autoClose = !!config.autoClose;
7770         this.$autoCloseIgnore = config.$autoCloseIgnore;
7771         this.transitionTimeout = null;
7772         this.tail = false;
7773         this.align = config.align || 'center';
7774         this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'frameless': true, 'icon': 'close' } );
7775         this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
7777         // Events
7778         this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
7780         // Initialization
7781         this.useTail( config.tail !== undefined ? !!config.tail : true );
7782         this.$body.addClass( 'oo-ui-popupWidget-body' );
7783         this.$tail.addClass( 'oo-ui-popupWidget-tail' );
7784         this.$head
7785                 .addClass( 'oo-ui-popupWidget-head' )
7786                 .append( this.$label, this.closeButton.$element );
7787         if ( !config.head ) {
7788                 this.$head.hide();
7789         }
7790         this.$popup
7791                 .addClass( 'oo-ui-popupWidget-popup' )
7792                 .append( this.$head, this.$body );
7793         this.$element.hide()
7794                 .addClass( 'oo-ui-popupWidget' )
7795                 .append( this.$popup, this.$tail );
7798 /* Setup */
7800 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
7801 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
7802 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
7804 /* Events */
7807  * @event hide
7808  */
7811  * @event show
7812  */
7814 /* Methods */
7817  * Handles mouse down events.
7819  * @param {jQuery.Event} e Mouse down event
7820  */
7821 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
7822         if (
7823                 this.visible &&
7824                 !$.contains( this.$element[0], e.target ) &&
7825                 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
7826         ) {
7827                 this.hide();
7828         }
7832  * Bind mouse down listener.
7833  */
7834 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
7835         // Capture clicks outside popup
7836         this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
7840  * Handles close button click events.
7841  */
7842 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
7843         if ( this.visible ) {
7844                 this.hide();
7845         }
7849  * Unbind mouse down listener.
7850  */
7851 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
7852         this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
7856  * Check if the popup is visible.
7858  * @return {boolean} Popup is visible
7859  */
7860 OO.ui.PopupWidget.prototype.isVisible = function () {
7861         return this.visible;
7865  * Set whether to show a tail.
7867  * @return {boolean} Make tail visible
7868  */
7869 OO.ui.PopupWidget.prototype.useTail = function ( value ) {
7870         value = !!value;
7871         if ( this.tail !== value ) {
7872                 this.tail = value;
7873                 if ( value ) {
7874                         this.$element.addClass( 'oo-ui-popupWidget-tailed' );
7875                 } else {
7876                         this.$element.removeClass( 'oo-ui-popupWidget-tailed' );
7877                 }
7878         }
7882  * Check if showing a tail.
7884  * @return {boolean} tail is visible
7885  */
7886 OO.ui.PopupWidget.prototype.hasTail = function () {
7887         return this.tail;
7891  * Show the context.
7893  * @fires show
7894  * @chainable
7895  */
7896 OO.ui.PopupWidget.prototype.show = function () {
7897         if ( !this.visible ) {
7898                 this.setClipping( true );
7899                 this.$element.show();
7900                 this.visible = true;
7901                 this.emit( 'show' );
7902                 if ( this.autoClose ) {
7903                         this.bindMouseDownListener();
7904                 }
7905         }
7906         return this;
7910  * Hide the context.
7912  * @fires hide
7913  * @chainable
7914  */
7915 OO.ui.PopupWidget.prototype.hide = function () {
7916         if ( this.visible ) {
7917                 this.setClipping( false );
7918                 this.$element.hide();
7919                 this.visible = false;
7920                 this.emit( 'hide' );
7921                 if ( this.autoClose ) {
7922                         this.unbindMouseDownListener();
7923                 }
7924         }
7925         return this;
7929  * Updates the position and size.
7931  * @param {number} width Width
7932  * @param {number} height Height
7933  * @param {boolean} [transition=false] Use a smooth transition
7934  * @chainable
7935  */
7936 OO.ui.PopupWidget.prototype.display = function ( width, height, transition ) {
7937         var padding = 10,
7938                 originOffset = Math.round( this.$element.offset().left ),
7939                 containerLeft = Math.round( this.$container.offset().left ),
7940                 containerWidth = this.$container.innerWidth(),
7941                 containerRight = containerLeft + containerWidth,
7942                 popupOffset = width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align],
7943                 popupLeft = popupOffset - padding,
7944                 popupRight = popupOffset + padding + width + padding,
7945                 overlapLeft = ( originOffset + popupLeft ) - containerLeft,
7946                 overlapRight = containerRight - ( originOffset + popupRight );
7948         // Prevent transition from being interrupted
7949         clearTimeout( this.transitionTimeout );
7950         if ( transition ) {
7951                 // Enable transition
7952                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
7953         }
7955         if ( overlapRight < 0 ) {
7956                 popupOffset += overlapRight;
7957         } else if ( overlapLeft < 0 ) {
7958                 popupOffset -= overlapLeft;
7959         }
7961         // Position body relative to anchor and resize
7962         this.$popup.css( {
7963                 'left': popupOffset,
7964                 'width': width,
7965                 'height': height === undefined ? 'auto' : height
7966         } );
7968         if ( transition ) {
7969                 // Prevent transitioning after transition is complete
7970                 this.transitionTimeout = setTimeout( OO.ui.bind( function () {
7971                         this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
7972                 }, this ), 200 );
7973         } else {
7974                 // Prevent transitioning immediately
7975                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
7976         }
7978         return this;
7982  * Button that shows and hides a popup.
7984  * @class
7985  * @extends OO.ui.ButtonWidget
7986  * @mixins OO.ui.PopuppableElement
7988  * @constructor
7989  * @param {Object} [config] Configuration options
7990  */
7991 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
7992         // Parent constructor
7993         OO.ui.PopupButtonWidget.super.call( this, config );
7995         // Mixin constructors
7996         OO.ui.PopuppableElement.call( this, config );
7998         // Initialization
7999         this.$element
8000                 .addClass( 'oo-ui-popupButtonWidget' )
8001                 .append( this.popup.$element );
8004 /* Setup */
8006 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
8007 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
8009 /* Methods */
8012  * Handles mouse click events.
8014  * @param {jQuery.Event} e Mouse click event
8015  */
8016 OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
8017         // Skip clicks within the popup
8018         if ( $.contains( this.popup.$element[0], e.target ) ) {
8019                 return;
8020         }
8022         if ( !this.isDisabled() ) {
8023                 if ( this.popup.isVisible() ) {
8024                         this.hidePopup();
8025                 } else {
8026                         this.showPopup();
8027                 }
8028                 OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
8029         }
8030         return false;
8034  * Search widget.
8036  * Combines query and results selection widgets.
8038  * @class
8039  * @extends OO.ui.Widget
8041  * @constructor
8042  * @param {Object} [config] Configuration options
8043  * @cfg {string|jQuery} [placeholder] Placeholder text for query input
8044  * @cfg {string} [value] Initial query value
8045  */
8046 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
8047         // Configuration intialization
8048         config = config || {};
8050         // Parent constructor
8051         OO.ui.SearchWidget.super.call( this, config );
8053         // Properties
8054         this.query = new OO.ui.TextInputWidget( {
8055                 '$': this.$,
8056                 'icon': 'search',
8057                 'placeholder': config.placeholder,
8058                 'value': config.value
8059         } );
8060         this.results = new OO.ui.SelectWidget( { '$': this.$ } );
8061         this.$query = this.$( '<div>' );
8062         this.$results = this.$( '<div>' );
8064         // Events
8065         this.query.connect( this, {
8066                 'change': 'onQueryChange',
8067                 'enter': 'onQueryEnter'
8068         } );
8069         this.results.connect( this, {
8070                 'highlight': 'onResultsHighlight',
8071                 'select': 'onResultsSelect'
8072         } );
8073         this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) );
8075         // Initialization
8076         this.$query
8077                 .addClass( 'oo-ui-searchWidget-query' )
8078                 .append( this.query.$element );
8079         this.$results
8080                 .addClass( 'oo-ui-searchWidget-results' )
8081                 .append( this.results.$element );
8082         this.$element
8083                 .addClass( 'oo-ui-searchWidget' )
8084                 .append( this.$results, this.$query );
8087 /* Setup */
8089 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
8091 /* Events */
8094  * @event highlight
8095  * @param {Object|null} item Item data or null if no item is highlighted
8096  */
8099  * @event select
8100  * @param {Object|null} item Item data or null if no item is selected
8101  */
8103 /* Methods */
8106  * Handle query key down events.
8108  * @param {jQuery.Event} e Key down event
8109  */
8110 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
8111         var highlightedItem, nextItem,
8112                 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
8114         if ( dir ) {
8115                 highlightedItem = this.results.getHighlightedItem();
8116                 if ( !highlightedItem ) {
8117                         highlightedItem = this.results.getSelectedItem();
8118                 }
8119                 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
8120                 this.results.highlightItem( nextItem );
8121                 nextItem.scrollElementIntoView();
8122         }
8126  * Handle select widget select events.
8128  * Clears existing results. Subclasses should repopulate items according to new query.
8130  * @param {string} value New value
8131  */
8132 OO.ui.SearchWidget.prototype.onQueryChange = function () {
8133         // Reset
8134         this.results.clearItems();
8138  * Handle select widget enter key events.
8140  * Selects highlighted item.
8142  * @param {string} value New value
8143  */
8144 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
8145         // Reset
8146         this.results.selectItem( this.results.getHighlightedItem() );
8150  * Handle select widget highlight events.
8152  * @param {OO.ui.OptionWidget} item Highlighted item
8153  * @fires highlight
8154  */
8155 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
8156         this.emit( 'highlight', item ? item.getData() : null );
8160  * Handle select widget select events.
8162  * @param {OO.ui.OptionWidget} item Selected item
8163  * @fires select
8164  */
8165 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
8166         this.emit( 'select', item ? item.getData() : null );
8170  * Get the query input.
8172  * @return {OO.ui.TextInputWidget} Query input
8173  */
8174 OO.ui.SearchWidget.prototype.getQuery = function () {
8175         return this.query;
8179  * Get the results list.
8181  * @return {OO.ui.SelectWidget} Select list
8182  */
8183 OO.ui.SearchWidget.prototype.getResults = function () {
8184         return this.results;
8188  * Text input widget.
8190  * @class
8191  * @extends OO.ui.InputWidget
8193  * @constructor
8194  * @param {Object} [config] Configuration options
8195  * @cfg {string} [placeholder] Placeholder text
8196  * @cfg {string} [icon] Symbolic name of icon
8197  * @cfg {boolean} [multiline=false] Allow multiple lines of text
8198  * @cfg {boolean} [autosize=false] Automatically resize to fit content
8199  * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
8200  */
8201 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
8202         config = $.extend( { 'maxRows': 10 }, config );
8204         // Parent constructor
8205         OO.ui.TextInputWidget.super.call( this, config );
8207         // Properties
8208         this.pending = 0;
8209         this.multiline = !!config.multiline;
8210         this.autosize = !!config.autosize;
8211         this.maxRows = config.maxRows;
8213         // Events
8214         this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
8215         this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
8217         // Initialization
8218         this.$element.addClass( 'oo-ui-textInputWidget' );
8219         if ( config.icon ) {
8220                 this.$element.addClass( 'oo-ui-textInputWidget-decorated' );
8221                 this.$element.append(
8222                         this.$( '<span>' )
8223                                 .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon )
8224                                 .mousedown( OO.ui.bind( function () {
8225                                         this.$input.focus();
8226                                         return false;
8227                                 }, this ) )
8228                 );
8229         }
8230         if ( config.placeholder ) {
8231                 this.$input.attr( 'placeholder', config.placeholder );
8232         }
8235 /* Setup */
8237 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
8239 /* Events */
8242  * User presses enter inside the text box.
8244  * Not called if input is multiline.
8246  * @event enter
8247  */
8249 /* Methods */
8252  * Handle key press events.
8254  * @param {jQuery.Event} e Key press event
8255  * @fires enter If enter key is pressed and input is not multiline
8256  */
8257 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
8258         if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
8259                 this.emit( 'enter' );
8260         }
8264  * Handle element attach events.
8266  * @param {jQuery.Event} e Element attach event
8267  */
8268 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
8269         this.adjustSize();
8273  * @inheritdoc
8274  */
8275 OO.ui.TextInputWidget.prototype.onEdit = function () {
8276         this.adjustSize();
8278         // Parent method
8279         return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
8283  * Automatically adjust the size of the text input.
8285  * This only affects multi-line inputs that are auto-sized.
8287  * @chainable
8288  */
8289 OO.ui.TextInputWidget.prototype.adjustSize = function () {
8290         var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
8292         if ( this.multiline && this.autosize ) {
8293                 $clone = this.$input.clone()
8294                         .val( this.$input.val() )
8295                         .css( { 'height': 0 } )
8296                         .insertAfter( this.$input );
8297                 // Set inline height property to 0 to measure scroll height
8298                 scrollHeight = $clone[0].scrollHeight;
8299                 // Remove inline height property to measure natural heights
8300                 $clone.css( 'height', '' );
8301                 innerHeight = $clone.innerHeight();
8302                 outerHeight = $clone.outerHeight();
8303                 // Measure max rows height
8304                 $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
8305                 maxInnerHeight = $clone.innerHeight();
8306                 $clone.removeAttr( 'rows' ).css( 'height', '' );
8307                 $clone.remove();
8308                 idealHeight = Math.min( maxInnerHeight, scrollHeight );
8309                 // Only apply inline height when expansion beyond natural height is needed
8310                 this.$input.css(
8311                         'height',
8312                         // Use the difference between the inner and outer height as a buffer
8313                         idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
8314                 );
8315         }
8316         return this;
8320  * Get input element.
8322  * @param {Object} [config] Configuration options
8323  * @return {jQuery} Input element
8324  */
8325 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
8326         return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
8329 /* Methods */
8332  * Check if input supports multiple lines.
8334  * @return {boolean}
8335  */
8336 OO.ui.TextInputWidget.prototype.isMultiline = function () {
8337         return !!this.multiline;
8341  * Check if input automatically adjusts its size.
8343  * @return {boolean}
8344  */
8345 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
8346         return !!this.autosize;
8350  * Check if input is pending.
8352  * @return {boolean}
8353  */
8354 OO.ui.TextInputWidget.prototype.isPending = function () {
8355         return !!this.pending;
8359  * Increase the pending stack.
8361  * @chainable
8362  */
8363 OO.ui.TextInputWidget.prototype.pushPending = function () {
8364         if ( this.pending === 0 ) {
8365                 this.$element.addClass( 'oo-ui-textInputWidget-pending' );
8366                 this.$input.addClass( 'oo-ui-texture-pending' );
8367         }
8368         this.pending++;
8370         return this;
8374  * Reduce the pending stack.
8376  * Clamped at zero.
8378  * @chainable
8379  */
8380 OO.ui.TextInputWidget.prototype.popPending = function () {
8381         if ( this.pending === 1 ) {
8382                 this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
8383                 this.$input.removeClass( 'oo-ui-texture-pending' );
8384         }
8385         this.pending = Math.max( 0, this.pending - 1 );
8387         return this;
8391  * Select the contents of the input.
8393  * @chainable
8394  */
8395 OO.ui.TextInputWidget.prototype.select = function () {
8396         this.$input.select();
8397         return this;
8401  * Menu for a text input widget.
8403  * @class
8404  * @extends OO.ui.MenuWidget
8406  * @constructor
8407  * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
8408  * @param {Object} [config] Configuration options
8409  * @cfg {jQuery} [$container=input.$element] Element to render menu under
8410  */
8411 OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
8412         // Parent constructor
8413         OO.ui.TextInputMenuWidget.super.call( this, config );
8415         // Properties
8416         this.input = input;
8417         this.$container = config.$container || this.input.$element;
8418         this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
8420         // Initialization
8421         this.$element.addClass( 'oo-ui-textInputMenuWidget' );
8424 /* Setup */
8426 OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
8428 /* Methods */
8431  * Handle window resize event.
8433  * @param {jQuery.Event} e Window resize event
8434  */
8435 OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
8436         this.position();
8440  * Show the menu.
8442  * @chainable
8443  */
8444 OO.ui.TextInputMenuWidget.prototype.show = function () {
8445         // Parent method
8446         OO.ui.TextInputMenuWidget.super.prototype.show.call( this );
8448         this.position();
8449         this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
8450         return this;
8454  * Hide the menu.
8456  * @chainable
8457  */
8458 OO.ui.TextInputMenuWidget.prototype.hide = function () {
8459         // Parent method
8460         OO.ui.TextInputMenuWidget.super.prototype.hide.call( this );
8462         this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
8463         return this;
8467  * Position the menu.
8469  * @chainable
8470  */
8471 OO.ui.TextInputMenuWidget.prototype.position = function () {
8472         var frameOffset,
8473                 $container = this.$container,
8474                 dimensions = $container.offset();
8476         // Position under input
8477         dimensions.top += $container.height();
8479         // Compensate for frame position if in a differnt frame
8480         if ( this.input.$.frame && this.input.$.context !== this.$element[0].ownerDocument ) {
8481                 frameOffset = OO.ui.Element.getRelativePosition(
8482                         this.input.$.frame.$element, this.$element.offsetParent()
8483                 );
8484                 dimensions.left += frameOffset.left;
8485                 dimensions.top += frameOffset.top;
8486         } else {
8487                 // Fix for RTL (for some reason, no need to fix if the frameoffset is set)
8488                 if ( this.$element.css( 'direction' ) === 'rtl' ) {
8489                         dimensions.right = this.$element.parent().position().left -
8490                                 $container.width() - dimensions.left;
8491                         // Erase the value for 'left':
8492                         delete dimensions.left;
8493                 }
8494         }
8496         this.$element.css( dimensions );
8497         this.setIdealSize( $container.width() );
8498         return this;
8502  * Width with on and off states.
8504  * Mixin for widgets with a boolean state.
8506  * @abstract
8507  * @class
8509  * @constructor
8510  * @param {Object} [config] Configuration options
8511  * @cfg {boolean} [value=false] Initial value
8512  */
8513 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
8514         // Configuration initialization
8515         config = config || {};
8517         // Properties
8518         this.value = null;
8520         // Initialization
8521         this.$element.addClass( 'oo-ui-toggleWidget' );
8522         this.setValue( !!config.value );
8525 /* Events */
8528  * @event change
8529  * @param {boolean} value Changed value
8530  */
8532 /* Methods */
8535  * Get the value of the toggle.
8537  * @return {boolean}
8538  */
8539 OO.ui.ToggleWidget.prototype.getValue = function () {
8540         return this.value;
8544  * Set the value of the toggle.
8546  * @param {boolean} value New value
8547  * @fires change
8548  * @chainable
8549  */
8550 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
8551         value = !!value;
8552         if ( this.value !== value ) {
8553                 this.value = value;
8554                 this.emit( 'change', value );
8555                 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
8556                 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
8557         }
8558         return this;
8562  * Button that toggles on and off.
8564  * @class
8565  * @extends OO.ui.ButtonWidget
8566  * @mixins OO.ui.ToggleWidget
8568  * @constructor
8569  * @param {Object} [config] Configuration options
8570  * @cfg {boolean} [value=false] Initial value
8571  */
8572 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
8573         // Configuration initialization
8574         config = config || {};
8576         // Parent constructor
8577         OO.ui.ToggleButtonWidget.super.call( this, config );
8579         // Mixin constructors
8580         OO.ui.ToggleWidget.call( this, config );
8582         // Initialization
8583         this.$element.addClass( 'oo-ui-toggleButtonWidget' );
8586 /* Setup */
8588 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
8589 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
8591 /* Methods */
8594  * @inheritdoc
8595  */
8596 OO.ui.ToggleButtonWidget.prototype.onClick = function () {
8597         if ( !this.isDisabled() ) {
8598                 this.setValue( !this.value );
8599         }
8601         // Parent method
8602         return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
8606  * @inheritdoc
8607  */
8608 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
8609         value = !!value;
8610         if ( value !== this.value ) {
8611                 this.setActive( value );
8612         }
8614         // Parent method (from mixin)
8615         OO.ui.ToggleWidget.prototype.setValue.call( this, value );
8617         return this;
8621  * Switch that slides on and off.
8623  * @class
8624  * @extends OO.ui.Widget
8625  * @mixins OO.ui.ToggleWidget
8627  * @constructor
8628  * @param {Object} [config] Configuration options
8629  * @cfg {boolean} [value=false] Initial value
8630  */
8631 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
8632         // Parent constructor
8633         OO.ui.ToggleSwitchWidget.super.call( this, config );
8635         // Mixin constructors
8636         OO.ui.ToggleWidget.call( this, config );
8638         // Properties
8639         this.dragging = false;
8640         this.dragStart = null;
8641         this.sliding = false;
8642         this.$glow = this.$( '<span>' );
8643         this.$grip = this.$( '<span>' );
8645         // Events
8646         this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
8648         // Initialization
8649         this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
8650         this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
8651         this.$element
8652                 .addClass( 'oo-ui-toggleSwitchWidget' )
8653                 .append( this.$glow, this.$grip );
8656 /* Setup */
8658 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
8659 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
8661 /* Methods */
8664  * Handle mouse down events.
8666  * @param {jQuery.Event} e Mouse down event
8667  */
8668 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
8669         if ( !this.isDisabled() && e.which === 1 ) {
8670                 this.setValue( !this.value );
8671         }
8674 }( OO ) );