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