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