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