2 * OOjs UI v0.1.0-pre (0d358b167a)
3 * https://www.mediawiki.org/wiki/OOjs_UI
5 * Copyright 2011–2014 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2014-10-17T23:41:06Z
16 * Namespace for all classes, static methods and static properties.
48 * Get the user's language and any fallback languages.
50 * These language codes are used to localize user interface elements in the user's language.
52 * In environments that provide a localization system, this function should be overridden to
53 * return the user's language(s). The default implementation returns English (en) only.
55 * @return {string[]} Language codes, in descending order of priority
57 OO
.ui
.getUserLanguages = function () {
62 * Get a value in an object keyed by language code.
64 * @param {Object.<string,Mixed>} obj Object keyed by language code
65 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
66 * @param {string} [fallback] Fallback code, used if no matching language can be found
67 * @return {Mixed} Local value
69 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
76 // Known user language
77 langs
= OO
.ui
.getUserLanguages();
78 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
85 if ( obj
[fallback
] ) {
88 // First existing language
98 * Message store for the default implementation of OO.ui.msg
100 * Environments that provide a localization system should not use this, but should override
101 * OO.ui.msg altogether.
106 // Tool tip for a button that moves items in a list down one place
107 'ooui-outline-control-move-down': 'Move item down',
108 // Tool tip for a button that moves items in a list up one place
109 'ooui-outline-control-move-up': 'Move item up',
110 // Tool tip for a button that removes items from a list
111 'ooui-outline-control-remove': 'Remove item',
112 // Label for the toolbar group that contains a list of all other available tools
113 'ooui-toolbar-more': 'More',
114 // Default label for the accept button of a confirmation dialog
115 'ooui-dialog-message-accept': 'OK',
116 // Default label for the reject button of a confirmation dialog
117 'ooui-dialog-message-reject': 'Cancel',
118 // Title for process dialog error description
119 'ooui-dialog-process-error': 'Something went wrong',
120 // Label for process dialog dismiss error button, visible when describing errors
121 'ooui-dialog-process-dismiss': 'Dismiss',
122 // Label for process dialog retry action button, visible when describing recoverable errors
123 'ooui-dialog-process-retry': 'Try again'
127 * Get a localized message.
129 * In environments that provide a localization system, this function should be overridden to
130 * return the message translated in the user's language. The default implementation always returns
133 * After the message key, message parameters may optionally be passed. In the default implementation,
134 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
135 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
136 * they support unnamed, ordered message parameters.
139 * @param {string} key Message key
140 * @param {Mixed...} [params] Message parameters
141 * @return {string} Translated message with parameters substituted
143 OO
.ui
.msg = function ( key
) {
144 var message
= messages
[key
], params
= Array
.prototype.slice
.call( arguments
, 1 );
145 if ( typeof message
=== 'string' ) {
146 // Perform $1 substitution
147 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
148 var i
= parseInt( n
, 10 );
149 return params
[i
- 1] !== undefined ? params
[i
- 1] : '$' + n
;
152 // Return placeholder if message not found
153 message
= '[' + key
+ ']';
159 * Package a message and arguments for deferred resolution.
161 * Use this when you are statically specifying a message and the message may not yet be present.
163 * @param {string} key Message key
164 * @param {Mixed...} [params] Message parameters
165 * @return {Function} Function that returns the resolved message when executed
167 OO
.ui
.deferMsg = function () {
168 var args
= arguments
;
170 return OO
.ui
.msg
.apply( OO
.ui
, args
);
177 * If the message is a function it will be executed, otherwise it will pass through directly.
179 * @param {Function|string} msg Deferred message, or message text
180 * @return {string} Resolved message
182 OO
.ui
.resolveMsg = function ( msg
) {
183 if ( $.isFunction( msg
) ) {
192 * Element that can be marked as pending.
198 * @param {Object} [config] Configuration options
200 OO
.ui
.PendingElement
= function OoUiPendingElement( config
) {
201 // Config initialisation
202 config
= config
|| {};
206 this.$pending
= null;
209 this.setPendingElement( config
.$pending
|| this.$element
);
214 OO
.initClass( OO
.ui
.PendingElement
);
219 * Set the pending element (and clean up any existing one).
221 * @param {jQuery} $pending The element to set to pending.
223 OO
.ui
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
224 if ( this.$pending
) {
225 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
228 this.$pending
= $pending
;
229 if ( this.pending
> 0 ) {
230 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
235 * Check if input is pending.
239 OO
.ui
.PendingElement
.prototype.isPending = function () {
240 return !!this.pending
;
244 * Increase the pending stack.
248 OO
.ui
.PendingElement
.prototype.pushPending = function () {
249 if ( this.pending
=== 0 ) {
250 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
251 this.updateThemeClasses();
259 * Reduce the pending stack.
265 OO
.ui
.PendingElement
.prototype.popPending = function () {
266 if ( this.pending
=== 1 ) {
267 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
268 this.updateThemeClasses();
270 this.pending
= Math
.max( 0, this.pending
- 1 );
280 * @mixins OO.EventEmitter
283 * @param {Object} [config] Configuration options
285 OO
.ui
.ActionSet
= function OoUiActionSet( config
) {
286 // Configuration intialization
287 config
= config
|| {};
289 // Mixin constructors
290 OO
.EventEmitter
.call( this );
295 actions
: 'getAction',
299 this.categorized
= {};
302 this.organized
= false;
303 this.changing
= false;
304 this.changed
= false;
309 OO
.mixinClass( OO
.ui
.ActionSet
, OO
.EventEmitter
);
311 /* Static Properties */
314 * Symbolic name of dialog.
321 OO
.ui
.ActionSet
.static.specialFlags
= [ 'safe', 'primary' ];
327 * @param {OO.ui.ActionWidget} action Action that was clicked
332 * @param {OO.ui.ActionWidget} action Action that was resized
337 * @param {OO.ui.ActionWidget[]} added Actions added
342 * @param {OO.ui.ActionWidget[]} added Actions removed
352 * Handle action change events.
356 OO
.ui
.ActionSet
.prototype.onActionChange = function () {
357 this.organized
= false;
358 if ( this.changing
) {
361 this.emit( 'change' );
366 * Check if a action is one of the special actions.
368 * @param {OO.ui.ActionWidget} action Action to check
369 * @return {boolean} Action is special
371 OO
.ui
.ActionSet
.prototype.isSpecial = function ( action
) {
374 for ( flag
in this.special
) {
375 if ( action
=== this.special
[flag
] ) {
386 * @param {Object} [filters] Filters to use, omit to get all actions
387 * @param {string|string[]} [filters.actions] Actions that actions must have
388 * @param {string|string[]} [filters.flags] Flags that actions must have
389 * @param {string|string[]} [filters.modes] Modes that actions must have
390 * @param {boolean} [filters.visible] Actions must be visible
391 * @param {boolean} [filters.disabled] Actions must be disabled
392 * @return {OO.ui.ActionWidget[]} Actions matching all criteria
394 OO
.ui
.ActionSet
.prototype.get = function ( filters
) {
395 var i
, len
, list
, category
, actions
, index
, match
, matches
;
400 // Collect category candidates
402 for ( category
in this.categorized
) {
403 list
= filters
[category
];
405 if ( !Array
.isArray( list
) ) {
408 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
409 actions
= this.categorized
[category
][list
[i
]];
410 if ( Array
.isArray( actions
) ) {
411 matches
.push
.apply( matches
, actions
);
416 // Remove by boolean filters
417 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
420 ( filters
.visible
!== undefined && match
.isVisible() !== filters
.visible
) ||
421 ( filters
.disabled
!== undefined && match
.isDisabled() !== filters
.disabled
)
423 matches
.splice( i
, 1 );
429 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
431 index
= matches
.lastIndexOf( match
);
432 while ( index
!== i
) {
433 matches
.splice( index
, 1 );
435 index
= matches
.lastIndexOf( match
);
440 return this.list
.slice();
444 * Get special actions.
446 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
447 * Special flags can be configured by changing #static-specialFlags in a subclass.
449 * @return {OO.ui.ActionWidget|null} Safe action
451 OO
.ui
.ActionSet
.prototype.getSpecial = function () {
453 return $.extend( {}, this.special
);
459 * Other actions include all non-special visible actions.
461 * @return {OO.ui.ActionWidget[]} Other actions
463 OO
.ui
.ActionSet
.prototype.getOthers = function () {
465 return this.others
.slice();
469 * Toggle actions based on their modes.
471 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
472 * visibility; matching actions will be shown, non-matching actions will be hidden.
474 * @param {string} mode Mode actions must have
479 OO
.ui
.ActionSet
.prototype.setMode = function ( mode
) {
482 this.changing
= true;
483 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
484 action
= this.list
[i
];
485 action
.toggle( action
.hasMode( mode
) );
488 this.organized
= false;
489 this.changing
= false;
490 this.emit( 'change' );
496 * Change which actions are able to be performed.
498 * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
500 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
501 * indicate actions are able to be performed
504 OO
.ui
.ActionSet
.prototype.setAbilities = function ( actions
) {
505 var i
, len
, action
, item
;
507 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
509 action
= item
.getAction();
510 if ( actions
[action
] !== undefined ) {
511 item
.setDisabled( !actions
[action
] );
519 * Executes a function once per action.
521 * When making changes to multiple actions, use this method instead of iterating over the actions
522 * manually to defer emitting a change event until after all actions have been changed.
524 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
525 * @param {Function} callback Callback to run for each action; callback is invoked with three
526 * arguments: the action, the action's index, the list of actions being iterated over
529 OO
.ui
.ActionSet
.prototype.forEach = function ( filter
, callback
) {
530 this.changed
= false;
531 this.changing
= true;
532 this.get( filter
).forEach( callback
);
533 this.changing
= false;
534 if ( this.changed
) {
535 this.emit( 'change' );
544 * @param {OO.ui.ActionWidget[]} actions Actions to add
549 OO
.ui
.ActionSet
.prototype.add = function ( actions
) {
552 this.changing
= true;
553 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
555 action
.connect( this, {
556 click
: [ 'emit', 'click', action
],
557 resize
: [ 'emit', 'resize', action
],
558 toggle
: [ 'onActionChange' ]
560 this.list
.push( action
);
562 this.organized
= false;
563 this.emit( 'add', actions
);
564 this.changing
= false;
565 this.emit( 'change' );
573 * @param {OO.ui.ActionWidget[]} actions Actions to remove
578 OO
.ui
.ActionSet
.prototype.remove = function ( actions
) {
579 var i
, len
, index
, action
;
581 this.changing
= true;
582 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
584 index
= this.list
.indexOf( action
);
585 if ( index
!== -1 ) {
586 action
.disconnect( this );
587 this.list
.splice( index
, 1 );
590 this.organized
= false;
591 this.emit( 'remove', actions
);
592 this.changing
= false;
593 this.emit( 'change' );
599 * Remove all actions.
605 OO
.ui
.ActionSet
.prototype.clear = function () {
607 removed
= this.list
.slice();
609 this.changing
= true;
610 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
611 action
= this.list
[i
];
612 action
.disconnect( this );
617 this.organized
= false;
618 this.emit( 'remove', removed
);
619 this.changing
= false;
620 this.emit( 'change' );
628 * This is called whenver organized information is requested. It will only reorganize the actions
629 * if something has changed since the last time it ran.
634 OO
.ui
.ActionSet
.prototype.organize = function () {
635 var i
, iLen
, j
, jLen
, flag
, action
, category
, list
, item
, special
,
636 specialFlags
= this.constructor.static.specialFlags
;
638 if ( !this.organized
) {
639 this.categorized
= {};
642 for ( i
= 0, iLen
= this.list
.length
; i
< iLen
; i
++ ) {
643 action
= this.list
[i
];
644 if ( action
.isVisible() ) {
645 // Populate catgeories
646 for ( category
in this.categories
) {
647 if ( !this.categorized
[category
] ) {
648 this.categorized
[category
] = {};
650 list
= action
[this.categories
[category
]]();
651 if ( !Array
.isArray( list
) ) {
654 for ( j
= 0, jLen
= list
.length
; j
< jLen
; j
++ ) {
656 if ( !this.categorized
[category
][item
] ) {
657 this.categorized
[category
][item
] = [];
659 this.categorized
[category
][item
].push( action
);
662 // Populate special/others
664 for ( j
= 0, jLen
= specialFlags
.length
; j
< jLen
; j
++ ) {
665 flag
= specialFlags
[j
];
666 if ( !this.special
[flag
] && action
.hasFlag( flag
) ) {
667 this.special
[flag
] = action
;
673 this.others
.push( action
);
677 this.organized
= true;
684 * DOM element abstraction.
690 * @param {Object} [config] Configuration options
691 * @cfg {Function} [$] jQuery for the frame the widget is in
692 * @cfg {string[]} [classes] CSS class names
693 * @cfg {string} [text] Text to insert
694 * @cfg {jQuery} [$content] Content elements to append (after text)
696 OO
.ui
.Element
= function OoUiElement( config
) {
697 // Configuration initialization
698 config
= config
|| {};
701 this.$ = config
.$ || OO
.ui
.Element
.getJQuery( document
);
702 this.$element
= this.$( this.$.context
.createElement( this.getTagName() ) );
703 this.elementGroup
= null;
704 this.debouncedUpdateThemeClassesHandler
= this.debouncedUpdateThemeClasses
.bind( this );
705 this.updateThemeClassesPending
= false;
708 if ( $.isArray( config
.classes
) ) {
709 this.$element
.addClass( config
.classes
.join( ' ' ) );
712 this.$element
.text( config
.text
);
714 if ( config
.$content
) {
715 this.$element
.append( config
.$content
);
721 OO
.initClass( OO
.ui
.Element
);
723 /* Static Properties */
728 * This may be ignored if #getTagName is overridden.
734 OO
.ui
.Element
.static.tagName
= 'div';
739 * Get a jQuery function within a specific document.
742 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
743 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
745 * @return {Function} Bound jQuery function
747 OO
.ui
.Element
.getJQuery = function ( context
, $iframe
) {
748 function wrapper( selector
) {
749 return $( selector
, wrapper
.context
);
752 wrapper
.context
= this.getDocument( context
);
755 wrapper
.$iframe
= $iframe
;
762 * Get the document of an element.
765 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
766 * @return {HTMLDocument|null} Document object
768 OO
.ui
.Element
.getDocument = function ( obj
) {
769 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
770 return ( obj
[0] && obj
[0].ownerDocument
) ||
771 // Empty jQuery selections might have a context
778 ( obj
.nodeType
=== 9 && obj
) ||
783 * Get the window of an element or document.
786 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
787 * @return {Window} Window object
789 OO
.ui
.Element
.getWindow = function ( obj
) {
790 var doc
= this.getDocument( obj
);
791 return doc
.parentWindow
|| doc
.defaultView
;
795 * Get the direction of an element or document.
798 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
799 * @return {string} Text direction, either `ltr` or `rtl`
801 OO
.ui
.Element
.getDir = function ( obj
) {
804 if ( obj
instanceof jQuery
) {
807 isDoc
= obj
.nodeType
=== 9;
808 isWin
= obj
.document
!== undefined;
809 if ( isDoc
|| isWin
) {
815 return $( obj
).css( 'direction' );
819 * Get the offset between two frames.
821 * TODO: Make this function not use recursion.
824 * @param {Window} from Window of the child frame
825 * @param {Window} [to=window] Window of the parent frame
826 * @param {Object} [offset] Offset to start with, used internally
827 * @return {Object} Offset object, containing left and top properties
829 OO
.ui
.Element
.getFrameOffset = function ( from, to
, offset
) {
830 var i
, len
, frames
, frame
, rect
;
836 offset
= { top
: 0, left
: 0 };
838 if ( from.parent
=== from ) {
842 // Get iframe element
843 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
844 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
845 if ( frames
[i
].contentWindow
=== from ) {
851 // Recursively accumulate offset values
853 rect
= frame
.getBoundingClientRect();
854 offset
.left
+= rect
.left
;
855 offset
.top
+= rect
.top
;
857 this.getFrameOffset( from.parent
, offset
);
864 * Get the offset between two elements.
866 * The two elements may be in a different frame, but in that case the frame $element is in must
867 * be contained in the frame $anchor is in.
870 * @param {jQuery} $element Element whose position to get
871 * @param {jQuery} $anchor Element to get $element's position relative to
872 * @return {Object} Translated position coordinates, containing top and left properties
874 OO
.ui
.Element
.getRelativePosition = function ( $element
, $anchor
) {
875 var iframe
, iframePos
,
876 pos
= $element
.offset(),
877 anchorPos
= $anchor
.offset(),
878 elementDocument
= this.getDocument( $element
),
879 anchorDocument
= this.getDocument( $anchor
);
881 // If $element isn't in the same document as $anchor, traverse up
882 while ( elementDocument
!== anchorDocument
) {
883 iframe
= elementDocument
.defaultView
.frameElement
;
885 throw new Error( '$element frame is not contained in $anchor frame' );
887 iframePos
= $( iframe
).offset();
888 pos
.left
+= iframePos
.left
;
889 pos
.top
+= iframePos
.top
;
890 elementDocument
= iframe
.ownerDocument
;
892 pos
.left
-= anchorPos
.left
;
893 pos
.top
-= anchorPos
.top
;
898 * Get element border sizes.
901 * @param {HTMLElement} el Element to measure
902 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
904 OO
.ui
.Element
.getBorders = function ( el
) {
905 var doc
= el
.ownerDocument
,
906 win
= doc
.parentWindow
|| doc
.defaultView
,
907 style
= win
&& win
.getComputedStyle
?
908 win
.getComputedStyle( el
, null ) :
911 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
912 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
913 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
914 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
917 top
: Math
.round( top
),
918 left
: Math
.round( left
),
919 bottom
: Math
.round( bottom
),
920 right
: Math
.round( right
)
925 * Get dimensions of an element or window.
928 * @param {HTMLElement|Window} el Element to measure
929 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
931 OO
.ui
.Element
.getDimensions = function ( el
) {
933 doc
= el
.ownerDocument
|| el
.document
,
934 win
= doc
.parentWindow
|| doc
.defaultView
;
936 if ( win
=== el
|| el
=== doc
.documentElement
) {
939 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
941 top
: $win
.scrollTop(),
942 left
: $win
.scrollLeft()
944 scrollbar
: { right
: 0, bottom
: 0 },
948 bottom
: $win
.innerHeight(),
949 right
: $win
.innerWidth()
955 borders
: this.getBorders( el
),
957 top
: $el
.scrollTop(),
958 left
: $el
.scrollLeft()
961 right
: $el
.innerWidth() - el
.clientWidth
,
962 bottom
: $el
.innerHeight() - el
.clientHeight
964 rect
: el
.getBoundingClientRect()
970 * Get closest scrollable container.
972 * Traverses up until either a scrollable element or the root is reached, in which case the window
976 * @param {HTMLElement} el Element to find scrollable container for
977 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
978 * @return {HTMLElement} Closest scrollable container
980 OO
.ui
.Element
.getClosestScrollableContainer = function ( el
, dimension
) {
982 props
= [ 'overflow' ],
983 $parent
= $( el
).parent();
985 if ( dimension
=== 'x' || dimension
=== 'y' ) {
986 props
.push( 'overflow-' + dimension
);
989 while ( $parent
.length
) {
990 if ( $parent
[0] === el
.ownerDocument
.body
) {
995 val
= $parent
.css( props
[i
] );
996 if ( val
=== 'auto' || val
=== 'scroll' ) {
1000 $parent
= $parent
.parent();
1002 return this.getDocument( el
).body
;
1006 * Scroll element into view.
1009 * @param {HTMLElement} el Element to scroll into view
1010 * @param {Object} [config={}] Configuration config
1011 * @param {string} [config.duration] jQuery animation duration value
1012 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1013 * to scroll in both directions
1014 * @param {Function} [config.complete] Function to call when scrolling completes
1016 OO
.ui
.Element
.scrollIntoView = function ( el
, config
) {
1017 // Configuration initialization
1018 config
= config
|| {};
1021 callback
= typeof config
.complete
=== 'function' && config
.complete
,
1022 sc
= this.getClosestScrollableContainer( el
, config
.direction
),
1024 eld
= this.getDimensions( el
),
1025 scd
= this.getDimensions( sc
),
1026 $win
= $( this.getWindow( el
) );
1028 // Compute the distances between the edges of el and the edges of the scroll viewport
1029 if ( $sc
.is( 'body' ) ) {
1030 // If the scrollable container is the <body> this is easy
1033 bottom
: $win
.innerHeight() - eld
.rect
.bottom
,
1034 left
: eld
.rect
.left
,
1035 right
: $win
.innerWidth() - eld
.rect
.right
1038 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1040 top
: eld
.rect
.top
- ( scd
.rect
.top
+ scd
.borders
.top
),
1041 bottom
: scd
.rect
.bottom
- scd
.borders
.bottom
- scd
.scrollbar
.bottom
- eld
.rect
.bottom
,
1042 left
: eld
.rect
.left
- ( scd
.rect
.left
+ scd
.borders
.left
),
1043 right
: scd
.rect
.right
- scd
.borders
.right
- scd
.scrollbar
.right
- eld
.rect
.right
1047 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1048 if ( rel
.top
< 0 ) {
1049 anim
.scrollTop
= scd
.scroll
.top
+ rel
.top
;
1050 } else if ( rel
.top
> 0 && rel
.bottom
< 0 ) {
1051 anim
.scrollTop
= scd
.scroll
.top
+ Math
.min( rel
.top
, -rel
.bottom
);
1054 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1055 if ( rel
.left
< 0 ) {
1056 anim
.scrollLeft
= scd
.scroll
.left
+ rel
.left
;
1057 } else if ( rel
.left
> 0 && rel
.right
< 0 ) {
1058 anim
.scrollLeft
= scd
.scroll
.left
+ Math
.min( rel
.left
, -rel
.right
);
1061 if ( !$.isEmptyObject( anim
) ) {
1062 $sc
.stop( true ).animate( anim
, config
.duration
|| 'fast' );
1064 $sc
.queue( function ( next
) {
1077 * Bind a handler for an event on a DOM element.
1079 * Used to be for working around a jQuery bug (jqbug.com/14180),
1080 * but obsolete as of jQuery 1.11.0.
1083 * @deprecated Use jQuery#on instead.
1084 * @param {HTMLElement|jQuery} el DOM element
1085 * @param {string} event Event to bind
1086 * @param {Function} callback Callback to call when the event fires
1088 OO
.ui
.Element
.onDOMEvent = function ( el
, event
, callback
) {
1089 $( el
).on( event
, callback
);
1093 * Unbind a handler bound with #static-method-onDOMEvent.
1095 * @deprecated Use jQuery#off instead.
1097 * @param {HTMLElement|jQuery} el DOM element
1098 * @param {string} event Event to unbind
1099 * @param {Function} [callback] Callback to unbind
1101 OO
.ui
.Element
.offDOMEvent = function ( el
, event
, callback
) {
1102 $( el
).off( event
, callback
);
1108 * Check if element supports one or more methods.
1110 * @param {string|string[]} methods Method or list of methods to check
1111 * @return boolean All methods are supported
1113 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1117 methods
= $.isArray( methods
) ? methods
: [ methods
];
1118 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1119 if ( $.isFunction( this[methods
[i
]] ) ) {
1124 return methods
.length
=== support
;
1128 * Update the theme-provided classes.
1130 * @localdoc This is called in element mixins and widget classes anytime state changes.
1131 * Updating is debounced, minimizing overhead of changing multiple attributes and
1132 * guaranteeing that theme updates do not occur within an element's constructor
1134 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1135 if ( !this.updateThemeClassesPending
) {
1136 this.updateThemeClassesPending
= true;
1137 setTimeout( this.debouncedUpdateThemeClassesHandler
);
1144 OO
.ui
.Element
.prototype.debouncedUpdateThemeClasses = function () {
1145 OO
.ui
.theme
.updateElementClasses( this );
1146 this.updateThemeClassesPending
= false;
1150 * Get the HTML tag name.
1152 * Override this method to base the result on instance information.
1154 * @return {string} HTML tag name
1156 OO
.ui
.Element
.prototype.getTagName = function () {
1157 return this.constructor.static.tagName
;
1161 * Check if the element is attached to the DOM
1162 * @return {boolean} The element is attached to the DOM
1164 OO
.ui
.Element
.prototype.isElementAttached = function () {
1165 return $.contains( this.getElementDocument(), this.$element
[0] );
1169 * Get the DOM document.
1171 * @return {HTMLDocument} Document object
1173 OO
.ui
.Element
.prototype.getElementDocument = function () {
1174 return OO
.ui
.Element
.getDocument( this.$element
);
1178 * Get the DOM window.
1180 * @return {Window} Window object
1182 OO
.ui
.Element
.prototype.getElementWindow = function () {
1183 return OO
.ui
.Element
.getWindow( this.$element
);
1187 * Get closest scrollable container.
1189 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1190 return OO
.ui
.Element
.getClosestScrollableContainer( this.$element
[0] );
1194 * Get group element is in.
1196 * @return {OO.ui.GroupElement|null} Group element, null if none
1198 OO
.ui
.Element
.prototype.getElementGroup = function () {
1199 return this.elementGroup
;
1203 * Set group element is in.
1205 * @param {OO.ui.GroupElement|null} group Group element, null if none
1208 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1209 this.elementGroup
= group
;
1214 * Scroll element into view.
1216 * @param {Object} [config={}]
1218 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1219 return OO
.ui
.Element
.scrollIntoView( this.$element
[0], config
);
1223 * Bind a handler for an event on this.$element
1225 * @deprecated Use jQuery#on instead.
1226 * @param {string} event
1227 * @param {Function} callback
1229 OO
.ui
.Element
.prototype.onDOMEvent = function ( event
, callback
) {
1230 OO
.ui
.Element
.onDOMEvent( this.$element
, event
, callback
);
1234 * Unbind a handler bound with #offDOMEvent
1236 * @deprecated Use jQuery#off instead.
1237 * @param {string} event
1238 * @param {Function} callback
1240 OO
.ui
.Element
.prototype.offDOMEvent = function ( event
, callback
) {
1241 OO
.ui
.Element
.offDOMEvent( this.$element
, event
, callback
);
1245 * Container for elements.
1249 * @extends OO.ui.Element
1250 * @mixins OO.EventEmitter
1253 * @param {Object} [config] Configuration options
1255 OO
.ui
.Layout
= function OoUiLayout( config
) {
1256 // Initialize config
1257 config
= config
|| {};
1259 // Parent constructor
1260 OO
.ui
.Layout
.super.call( this, config
);
1262 // Mixin constructors
1263 OO
.EventEmitter
.call( this );
1266 this.$element
.addClass( 'oo-ui-layout' );
1271 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1272 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1275 * User interface control.
1279 * @extends OO.ui.Element
1280 * @mixins OO.EventEmitter
1283 * @param {Object} [config] Configuration options
1284 * @cfg {boolean} [disabled=false] Disable
1286 OO
.ui
.Widget
= function OoUiWidget( config
) {
1287 // Initialize config
1288 config
= $.extend( { disabled
: false }, config
);
1290 // Parent constructor
1291 OO
.ui
.Widget
.super.call( this, config
);
1293 // Mixin constructors
1294 OO
.EventEmitter
.call( this );
1297 this.visible
= true;
1298 this.disabled
= null;
1299 this.wasDisabled
= null;
1302 this.$element
.addClass( 'oo-ui-widget' );
1303 this.setDisabled( !!config
.disabled
);
1308 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1309 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1315 * @param {boolean} disabled Widget is disabled
1320 * @param {boolean} visible Widget is visible
1326 * Check if the widget is disabled.
1328 * @param {boolean} Button is disabled
1330 OO
.ui
.Widget
.prototype.isDisabled = function () {
1331 return this.disabled
;
1335 * Check if widget is visible.
1337 * @return {boolean} Widget is visible
1339 OO
.ui
.Widget
.prototype.isVisible = function () {
1340 return this.visible
;
1344 * Set the disabled state of the widget.
1346 * This should probably change the widgets' appearance and prevent it from being used.
1348 * @param {boolean} disabled Disable widget
1351 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1354 this.disabled
= !!disabled
;
1355 isDisabled
= this.isDisabled();
1356 if ( isDisabled
!== this.wasDisabled
) {
1357 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1358 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1359 this.emit( 'disable', isDisabled
);
1360 this.updateThemeClasses();
1362 this.wasDisabled
= isDisabled
;
1368 * Toggle visibility of widget.
1370 * @param {boolean} [show] Make widget visible, omit to toggle visibility
1374 OO
.ui
.Widget
.prototype.toggle = function ( show
) {
1375 show
= show
=== undefined ? !this.visible
: !!show
;
1377 if ( show
!== this.isVisible() ) {
1378 this.visible
= show
;
1379 this.$element
.toggle( show
);
1380 this.emit( 'toggle', show
);
1387 * Update the disabled state, in case of changes in parent widget.
1391 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1392 this.setDisabled( this.disabled
);
1397 * Container for elements in a child frame.
1399 * Use together with OO.ui.WindowManager.
1403 * @extends OO.ui.Element
1404 * @mixins OO.EventEmitter
1406 * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
1407 * teardown processes are executed when the window is closed.
1409 * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
1410 * - Window manager begins opening window
1411 * - {@link #getSetupProcess} method is called and its result executed
1412 * - {@link #getReadyProcess} method is called and its result executed
1413 * - Window is now open
1415 * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
1416 * - Window manager begins closing window
1417 * - {@link #getHoldProcess} method is called and its result executed
1418 * - {@link #getTeardownProcess} method is called and its result executed
1419 * - Window is now closed
1421 * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
1422 * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
1423 * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous
1424 * processing can complete. Always assume window processes are executed asychronously. See
1425 * OO.ui.Process for more details about how to work with processes. Some events, as well as the
1426 * #open and #close methods, provide promises which are resolved when the window enters a new state.
1428 * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
1429 * If the requested size is not recognized, the window manager will choose a sensible fallback.
1432 * @param {Object} [config] Configuration options
1433 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
1437 OO
.ui
.Window
= function OoUiWindow( config
) {
1438 // Configuration initialization
1439 config
= config
|| {};
1441 // Parent constructor
1442 OO
.ui
.Window
.super.call( this, config
);
1444 // Mixin constructors
1445 OO
.EventEmitter
.call( this );
1448 this.manager
= null;
1449 this.initialized
= false;
1450 this.visible
= false;
1451 this.opening
= null;
1452 this.closing
= null;
1455 this.loading
= null;
1456 this.size
= config
.size
|| this.constructor.static.size
;
1457 this.$frame
= this.$( '<div>' );
1458 this.$overlay
= this.$( '<div>' );
1462 .addClass( 'oo-ui-window' )
1463 .append( this.$frame
, this.$overlay
);
1464 this.$frame
.addClass( 'oo-ui-window-frame' );
1465 this.$overlay
.addClass( 'oo-ui-window-overlay' );
1467 // NOTE: Additional intitialization will occur when #setManager is called
1472 OO
.inheritClass( OO
.ui
.Window
, OO
.ui
.Element
);
1473 OO
.mixinClass( OO
.ui
.Window
, OO
.EventEmitter
);
1475 /* Static Properties */
1478 * Symbolic name of size.
1480 * Size is used if no size is configured during construction.
1484 * @property {string}
1486 OO
.ui
.Window
.static.size
= 'medium';
1488 /* Static Methods */
1491 * Transplant the CSS styles from as parent document to a frame's document.
1493 * This loops over the style sheets in the parent document, and copies their nodes to the
1494 * frame's document. It then polls the document to see when all styles have loaded, and once they
1495 * have, resolves the promise.
1497 * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
1498 * and resolve the promise anyway. This protects against cases like a display: none; iframe in
1499 * Firefox, where the styles won't load until the iframe becomes visible.
1501 * For details of how we arrived at the strategy used in this function, see #load.
1505 * @param {HTMLDocument} parentDoc Document to transplant styles from
1506 * @param {HTMLDocument} frameDoc Document to transplant styles to
1507 * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
1508 * @return {jQuery.Promise} Promise resolved when styles have loaded
1510 OO
.ui
.Window
.static.transplantStyles = function ( parentDoc
, frameDoc
, timeout
) {
1511 var i
, numSheets
, styleNode
, styleText
, newNode
, timeoutID
, pollNodeId
, $pendingPollNodes
,
1512 $pollNodes
= $( [] ),
1513 // Fake font-family value
1514 fontFamily
= 'oo-ui-frame-transplantStyles-loaded',
1515 nextIndex
= parentDoc
.oouiFrameTransplantStylesNextIndex
|| 0,
1516 deferred
= $.Deferred();
1518 for ( i
= 0, numSheets
= parentDoc
.styleSheets
.length
; i
< numSheets
; i
++ ) {
1519 styleNode
= parentDoc
.styleSheets
[i
].ownerNode
;
1520 if ( styleNode
.disabled
) {
1524 if ( styleNode
.nodeName
.toLowerCase() === 'link' ) {
1525 // External stylesheet; use @import
1526 styleText
= '@import url(' + styleNode
.href
+ ');';
1528 // Internal stylesheet; just copy the text
1529 // For IE10 we need to fall back to .cssText, BUT that's undefined in
1530 // other browsers, so fall back to '' rather than 'undefined'
1531 styleText
= styleNode
.textContent
|| parentDoc
.styleSheets
[i
].cssText
|| '';
1534 // Create a node with a unique ID that we're going to monitor to see when the CSS
1536 if ( styleNode
.oouiFrameTransplantStylesId
) {
1537 // If we're nesting transplantStyles operations and this node already has
1538 // a CSS rule to wait for loading, reuse it
1539 pollNodeId
= styleNode
.oouiFrameTransplantStylesId
;
1541 // Otherwise, create a new ID
1542 pollNodeId
= 'oo-ui-frame-transplantStyles-loaded-' + nextIndex
;
1545 // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import
1546 // The font-family rule will only take effect once the @import finishes
1547 styleText
+= '\n' + '#' + pollNodeId
+ ' { font-family: ' + fontFamily
+ '; }';
1550 // Create a node with id=pollNodeId
1551 $pollNodes
= $pollNodes
.add( $( '<div>', frameDoc
)
1552 .attr( 'id', pollNodeId
)
1553 .appendTo( frameDoc
.body
)
1556 // Add our modified CSS as a <style> tag
1557 newNode
= frameDoc
.createElement( 'style' );
1558 newNode
.textContent
= styleText
;
1559 newNode
.oouiFrameTransplantStylesId
= pollNodeId
;
1560 frameDoc
.head
.appendChild( newNode
);
1562 frameDoc
.oouiFrameTransplantStylesNextIndex
= nextIndex
;
1564 // Poll every 100ms until all external stylesheets have loaded
1565 $pendingPollNodes
= $pollNodes
;
1566 timeoutID
= setTimeout( function pollExternalStylesheets() {
1568 $pendingPollNodes
.length
> 0 &&
1569 $pendingPollNodes
.eq( 0 ).css( 'font-family' ) === fontFamily
1571 $pendingPollNodes
= $pendingPollNodes
.slice( 1 );
1574 if ( $pendingPollNodes
.length
=== 0 ) {
1576 if ( timeoutID
!== null ) {
1578 $pollNodes
.remove();
1582 timeoutID
= setTimeout( pollExternalStylesheets
, 100 );
1585 // ...but give up after a while
1586 if ( timeout
!== 0 ) {
1587 setTimeout( function () {
1589 clearTimeout( timeoutID
);
1591 $pollNodes
.remove();
1594 }, timeout
|| 5000 );
1597 return deferred
.promise();
1603 * Handle mouse down events.
1605 * @param {jQuery.Event} e Mouse down event
1607 OO
.ui
.Window
.prototype.onMouseDown = function ( e
) {
1608 // Prevent clicking on the click-block from stealing focus
1609 if ( e
.target
=== this.$element
[0] ) {
1615 * Check if window has been initialized.
1617 * @return {boolean} Window has been initialized
1619 OO
.ui
.Window
.prototype.isInitialized = function () {
1620 return this.initialized
;
1624 * Check if window is visible.
1626 * @return {boolean} Window is visible
1628 OO
.ui
.Window
.prototype.isVisible = function () {
1629 return this.visible
;
1633 * Check if window is loading.
1635 * @return {boolean} Window is loading
1637 OO
.ui
.Window
.prototype.isLoading = function () {
1638 return this.loading
&& this.loading
.state() === 'pending';
1642 * Check if window is loaded.
1644 * @return {boolean} Window is loaded
1646 OO
.ui
.Window
.prototype.isLoaded = function () {
1647 return this.loading
&& this.loading
.state() === 'resolved';
1651 * Check if window is opening.
1653 * This is a wrapper around OO.ui.WindowManager#isOpening.
1655 * @return {boolean} Window is opening
1657 OO
.ui
.Window
.prototype.isOpening = function () {
1658 return this.manager
.isOpening( this );
1662 * Check if window is closing.
1664 * This is a wrapper around OO.ui.WindowManager#isClosing.
1666 * @return {boolean} Window is closing
1668 OO
.ui
.Window
.prototype.isClosing = function () {
1669 return this.manager
.isClosing( this );
1673 * Check if window is opened.
1675 * This is a wrapper around OO.ui.WindowManager#isOpened.
1677 * @return {boolean} Window is opened
1679 OO
.ui
.Window
.prototype.isOpened = function () {
1680 return this.manager
.isOpened( this );
1684 * Get the window manager.
1686 * @return {OO.ui.WindowManager} Manager of window
1688 OO
.ui
.Window
.prototype.getManager = function () {
1689 return this.manager
;
1693 * Get the window size.
1695 * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1697 OO
.ui
.Window
.prototype.getSize = function () {
1702 * Get the height of the dialog contents.
1704 * @return {number} Content height
1706 OO
.ui
.Window
.prototype.getContentHeight = function () {
1707 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements
1708 var bodyHeight
, oldHeight
= this.$frame
[0].style
.height
;
1709 this.$frame
[0].style
.height
= '1px';
1710 bodyHeight
= this.getBodyHeight();
1711 this.$frame
[0].style
.height
= oldHeight
;
1714 // Add buffer for border
1715 ( this.$frame
.outerHeight() - this.$frame
.innerHeight() ) +
1716 // Use combined heights of children
1717 ( this.$head
.outerHeight( true ) + bodyHeight
+ this.$foot
.outerHeight( true ) )
1722 * Get the height of the dialog contents.
1724 * When this function is called, the dialog will temporarily have been resized
1725 * to height=1px, so .scrollHeight measurements can be taken accurately.
1727 * @return {number} Height of content
1729 OO
.ui
.Window
.prototype.getBodyHeight = function () {
1730 return this.$body
[0].scrollHeight
;
1734 * Get the directionality of the frame
1736 * @return {string} Directionality, 'ltr' or 'rtl'
1738 OO
.ui
.Window
.prototype.getDir = function () {
1743 * Get a process for setting up a window for use.
1745 * Each time the window is opened this process will set it up for use in a particular context, based
1746 * on the `data` argument.
1748 * When you override this method, you can add additional setup steps to the process the parent
1749 * method provides using the 'first' and 'next' methods.
1752 * @param {Object} [data] Window opening data
1753 * @return {OO.ui.Process} Setup process
1755 OO
.ui
.Window
.prototype.getSetupProcess = function () {
1756 return new OO
.ui
.Process();
1760 * Get a process for readying a window for use.
1762 * Each time the window is open and setup, this process will ready it up for use in a particular
1763 * context, based on the `data` argument.
1765 * When you override this method, you can add additional setup steps to the process the parent
1766 * method provides using the 'first' and 'next' methods.
1769 * @param {Object} [data] Window opening data
1770 * @return {OO.ui.Process} Setup process
1772 OO
.ui
.Window
.prototype.getReadyProcess = function () {
1773 return new OO
.ui
.Process();
1777 * Get a process for holding a window from use.
1779 * Each time the window is closed, this process will hold it from use in a particular context, based
1780 * on the `data` argument.
1782 * When you override this method, you can add additional setup steps to the process the parent
1783 * method provides using the 'first' and 'next' methods.
1786 * @param {Object} [data] Window closing data
1787 * @return {OO.ui.Process} Hold process
1789 OO
.ui
.Window
.prototype.getHoldProcess = function () {
1790 return new OO
.ui
.Process();
1794 * Get a process for tearing down a window after use.
1796 * Each time the window is closed this process will tear it down and do something with the user's
1797 * interactions within the window, based on the `data` argument.
1799 * When you override this method, you can add additional teardown steps to the process the parent
1800 * method provides using the 'first' and 'next' methods.
1803 * @param {Object} [data] Window closing data
1804 * @return {OO.ui.Process} Teardown process
1806 OO
.ui
.Window
.prototype.getTeardownProcess = function () {
1807 return new OO
.ui
.Process();
1811 * Toggle visibility of window.
1813 * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used
1814 * instead of display.
1816 * @param {boolean} [show] Make window visible, omit to toggle visibility
1820 OO
.ui
.Window
.prototype.toggle = function ( show
) {
1821 show
= show
=== undefined ? !this.visible
: !!show
;
1823 if ( show
!== this.isVisible() ) {
1824 this.visible
= show
;
1826 if ( this.isolated
&& !this.isLoaded() ) {
1827 // Hide the window using visibility instead of display until loading is complete
1828 // Can't use display: none; because that prevents the iframe from loading in Firefox
1829 this.$element
.css( 'visibility', show
? 'visible' : 'hidden' );
1831 this.$element
.toggle( show
).css( 'visibility', '' );
1833 this.emit( 'toggle', show
);
1840 * Set the window manager.
1842 * This must be called before initialize. Calling it more than once will cause an error.
1844 * @param {OO.ui.WindowManager} manager Manager for this window
1845 * @throws {Error} If called more than once
1848 OO
.ui
.Window
.prototype.setManager = function ( manager
) {
1849 if ( this.manager
) {
1850 throw new Error( 'Cannot set window manager, window already has a manager' );
1854 this.manager
= manager
;
1855 this.isolated
= manager
.shouldIsolate();
1858 if ( this.isolated
) {
1859 this.$iframe
= this.$( '<iframe>' );
1860 this.$iframe
.attr( { frameborder
: 0, scrolling
: 'no' } );
1861 this.$frame
.append( this.$iframe
);
1862 this.$ = function () {
1863 throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
1865 // WARNING: Do not use this.$ again until #initialize is called
1867 this.$content
= this.$( '<div>' );
1868 this.$document
= $( this.getElementDocument() );
1869 this.$content
.addClass( 'oo-ui-window-content' );
1870 this.$frame
.append( this.$content
);
1872 this.toggle( false );
1874 // Figure out directionality:
1875 this.dir
= OO
.ui
.Element
.getDir( this.$iframe
|| this.$content
) || 'ltr';
1881 * Set the window size.
1883 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1886 OO
.ui
.Window
.prototype.setSize = function ( size
) {
1888 this.manager
.updateWindowSize( this );
1893 * Set window dimensions.
1895 * Properties are applied to the frame container.
1897 * @param {Object} dim CSS dimension properties
1898 * @param {string|number} [dim.width] Width
1899 * @param {string|number} [dim.minWidth] Minimum width
1900 * @param {string|number} [dim.maxWidth] Maximum width
1901 * @param {string|number} [dim.width] Height, omit to set based on height of contents
1902 * @param {string|number} [dim.minWidth] Minimum height
1903 * @param {string|number} [dim.maxWidth] Maximum height
1906 OO
.ui
.Window
.prototype.setDimensions = function ( dim
) {
1907 // Apply width before height so height is not based on wrapping content using the wrong width
1909 width
: dim
.width
|| '',
1910 minWidth
: dim
.minWidth
|| '',
1911 maxWidth
: dim
.maxWidth
|| ''
1914 height
: ( dim
.height
!== undefined ? dim
.height
: this.getContentHeight() ) || '',
1915 minHeight
: dim
.minHeight
|| '',
1916 maxHeight
: dim
.maxHeight
|| ''
1922 * Initialize window contents.
1924 * The first time the window is opened, #initialize is called when it's safe to begin populating
1925 * its contents. See #getSetupProcess for a way to make changes each time the window opens.
1927 * Once this method is called, this.$ can be used to create elements within the frame.
1929 * @throws {Error} If not attached to a manager
1932 OO
.ui
.Window
.prototype.initialize = function () {
1933 if ( !this.manager
) {
1934 throw new Error( 'Cannot initialize window, must be attached to a manager' );
1938 this.$head
= this.$( '<div>' );
1939 this.$body
= this.$( '<div>' );
1940 this.$foot
= this.$( '<div>' );
1941 this.$innerOverlay
= this.$( '<div>' );
1944 this.$element
.on( 'mousedown', this.onMouseDown
.bind( this ) );
1947 this.$head
.addClass( 'oo-ui-window-head' );
1948 this.$body
.addClass( 'oo-ui-window-body' );
1949 this.$foot
.addClass( 'oo-ui-window-foot' );
1950 this.$innerOverlay
.addClass( 'oo-ui-window-inner-overlay' );
1951 this.$content
.append( this.$head
, this.$body
, this.$foot
, this.$innerOverlay
);
1959 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
1960 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
1962 * @param {Object} [data] Window opening data
1963 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
1964 * first argument will be a promise which will be resolved when the window begins closing
1966 OO
.ui
.Window
.prototype.open = function ( data
) {
1967 return this.manager
.openWindow( this, data
);
1973 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
1974 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
1976 * @param {Object} [data] Window closing data
1977 * @return {jQuery.Promise} Promise resolved when window is closed
1979 OO
.ui
.Window
.prototype.close = function ( data
) {
1980 return this.manager
.closeWindow( this, data
);
1986 * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
1989 * @param {Object} [data] Window opening data
1990 * @return {jQuery.Promise} Promise resolved when window is setup
1992 OO
.ui
.Window
.prototype.setup = function ( data
) {
1994 deferred
= $.Deferred();
1996 this.$element
.show();
1997 this.visible
= true;
1998 this.getSetupProcess( data
).execute().done( function () {
1999 // Force redraw by asking the browser to measure the elements' widths
2000 win
.$element
.addClass( 'oo-ui-window-setup' ).width();
2001 win
.$content
.addClass( 'oo-ui-window-content-setup' ).width();
2005 return deferred
.promise();
2011 * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
2014 * @param {Object} [data] Window opening data
2015 * @return {jQuery.Promise} Promise resolved when window is ready
2017 OO
.ui
.Window
.prototype.ready = function ( data
) {
2019 deferred
= $.Deferred();
2021 this.$content
.focus();
2022 this.getReadyProcess( data
).execute().done( function () {
2023 // Force redraw by asking the browser to measure the elements' widths
2024 win
.$element
.addClass( 'oo-ui-window-ready' ).width();
2025 win
.$content
.addClass( 'oo-ui-window-content-ready' ).width();
2029 return deferred
.promise();
2035 * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
2038 * @param {Object} [data] Window closing data
2039 * @return {jQuery.Promise} Promise resolved when window is held
2041 OO
.ui
.Window
.prototype.hold = function ( data
) {
2043 deferred
= $.Deferred();
2045 this.getHoldProcess( data
).execute().done( function () {
2046 // Get the focused element within the window's content
2047 var $focus
= win
.$content
.find( OO
.ui
.Element
.getDocument( win
.$content
).activeElement
);
2049 // Blur the focused element
2050 if ( $focus
.length
) {
2054 // Force redraw by asking the browser to measure the elements' widths
2055 win
.$element
.removeClass( 'oo-ui-window-ready' ).width();
2056 win
.$content
.removeClass( 'oo-ui-window-content-ready' ).width();
2060 return deferred
.promise();
2066 * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
2069 * @param {Object} [data] Window closing data
2070 * @return {jQuery.Promise} Promise resolved when window is torn down
2072 OO
.ui
.Window
.prototype.teardown = function ( data
) {
2074 deferred
= $.Deferred();
2076 this.getTeardownProcess( data
).execute().done( function () {
2077 // Force redraw by asking the browser to measure the elements' widths
2078 win
.$element
.removeClass( 'oo-ui-window-setup' ).width();
2079 win
.$content
.removeClass( 'oo-ui-window-content-setup' ).width();
2080 win
.$element
.hide();
2081 win
.visible
= false;
2085 return deferred
.promise();
2089 * Load the frame contents.
2091 * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
2092 * promise will be resolved. Calling while loading will return a promise but not trigger a new
2093 * loading cycle. Calling after loading is complete will return a promise that's already been
2096 * Sounds simple right? Read on...
2098 * When you create a dynamic iframe using open/write/close, the window.load event for the
2099 * iframe is triggered when you call close, and there's no further load event to indicate that
2100 * everything is actually loaded.
2102 * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
2103 * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
2104 * are added to document.styleSheets immediately, and the only way you can determine whether they've
2105 * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
2106 * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
2108 * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>`
2109 * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets
2110 * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the
2111 * `@import` has finished. And because the contents of the `<style>` tag are from the same origin,
2112 * accessing .cssRules is allowed.
2114 * However, now that we control the styles we're injecting, we might as well do away with
2115 * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
2116 * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
2117 * and wait for its font-family to change to someValue. Because `@import` is blocking, the
2118 * font-family rule is not applied until after the `@import` finishes.
2120 * All this stylesheet injection and polling magic is in #transplantStyles.
2122 * @return {jQuery.Promise} Promise resolved when loading is complete
2125 OO
.ui
.Window
.prototype.load = function () {
2126 var sub
, doc
, loading
,
2129 // Non-isolated windows are already "loaded"
2130 if ( !this.loading
&& !this.isolated
) {
2131 this.loading
= $.Deferred().resolve();
2133 // Set initialized state after so sub-classes aren't confused by it being set by calling
2134 // their parent initialize method
2135 this.initialized
= true;
2138 // Return existing promise if already loading or loaded
2139 if ( this.loading
) {
2140 return this.loading
.promise();
2144 loading
= this.loading
= $.Deferred();
2145 sub
= this.$iframe
.prop( 'contentWindow' );
2148 // Initialize contents
2153 '<body class="oo-ui-window-isolated oo-ui-' + this.dir
+ '"' +
2154 ' style="direction:' + this.dir
+ ';" dir="' + this.dir
+ '">' +
2155 '<div class="oo-ui-window-content"></div>' +
2162 this.$ = OO
.ui
.Element
.getJQuery( doc
, this.$iframe
);
2163 this.$content
= this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
2164 this.$document
= this.$( doc
);
2167 this.constructor.static.transplantStyles( this.getElementDocument(), this.$document
[0] )
2168 .always( function () {
2169 // Initialize isolated windows
2171 // Set initialized state after so sub-classes aren't confused by it being set by calling
2172 // their parent initialize method
2173 win
.initialized
= true;
2174 // Undo the visibility: hidden; hack and apply display: none;
2175 // We can do this safely now that the iframe has initialized
2176 // (don't do this from within #initialize because it has to happen
2177 // after the all subclasses have been handled as well).
2178 win
.toggle( win
.isVisible() );
2183 return loading
.promise();
2187 * Base class for all dialogs.
2190 * - Manage the window (open and close, etc.).
2191 * - Store the internal name and display title.
2192 * - A stack to track one or more pending actions.
2193 * - Manage a set of actions that can be performed.
2194 * - Configure and create action widgets.
2197 * - Close the dialog with Escape key.
2198 * - Visually lock the dialog while an action is in
2199 * progress (aka "pending").
2201 * Subclass responsibilities:
2202 * - Display the title somewhere.
2203 * - Add content to the dialog.
2204 * - Provide a UI to close the dialog.
2205 * - Display the action widgets somewhere.
2209 * @extends OO.ui.Window
2210 * @mixins OO.ui.PendingElement
2213 * @param {Object} [config] Configuration options
2215 OO
.ui
.Dialog
= function OoUiDialog( config
) {
2216 // Parent constructor
2217 OO
.ui
.Dialog
.super.call( this, config
);
2219 // Mixin constructors
2220 OO
.ui
.PendingElement
.call( this );
2223 this.actions
= new OO
.ui
.ActionSet();
2224 this.attachedActions
= [];
2225 this.currentAction
= null;
2228 this.actions
.connect( this, {
2229 click
: 'onActionClick',
2230 resize
: 'onActionResize',
2231 change
: 'onActionsChange'
2236 .addClass( 'oo-ui-dialog' )
2237 .attr( 'role', 'dialog' );
2242 OO
.inheritClass( OO
.ui
.Dialog
, OO
.ui
.Window
);
2243 OO
.mixinClass( OO
.ui
.Dialog
, OO
.ui
.PendingElement
);
2245 /* Static Properties */
2248 * Symbolic name of dialog.
2253 * @property {string}
2255 OO
.ui
.Dialog
.static.name
= '';
2263 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2265 OO
.ui
.Dialog
.static.title
= '';
2268 * List of OO.ui.ActionWidget configuration options.
2272 * @property {Object[]}
2274 OO
.ui
.Dialog
.static.actions
= [];
2277 * Close dialog when the escape key is pressed.
2282 * @property {boolean}
2284 OO
.ui
.Dialog
.static.escapable
= true;
2289 * Handle frame document key down events.
2291 * @param {jQuery.Event} e Key down event
2293 OO
.ui
.Dialog
.prototype.onDocumentKeyDown = function ( e
) {
2294 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
) {
2301 * Handle action resized events.
2303 * @param {OO.ui.ActionWidget} action Action that was resized
2305 OO
.ui
.Dialog
.prototype.onActionResize = function () {
2306 // Override in subclass
2310 * Handle action click events.
2312 * @param {OO.ui.ActionWidget} action Action that was clicked
2314 OO
.ui
.Dialog
.prototype.onActionClick = function ( action
) {
2315 if ( !this.isPending() ) {
2316 this.currentAction
= action
;
2317 this.executeAction( action
.getAction() );
2322 * Handle actions change event.
2324 OO
.ui
.Dialog
.prototype.onActionsChange = function () {
2325 this.detachActions();
2326 if ( !this.isClosing() ) {
2327 this.attachActions();
2332 * Get set of actions.
2334 * @return {OO.ui.ActionSet}
2336 OO
.ui
.Dialog
.prototype.getActions = function () {
2337 return this.actions
;
2341 * Get a process for taking action.
2343 * When you override this method, you can add additional accept steps to the process the parent
2344 * method provides using the 'first' and 'next' methods.
2347 * @param {string} [action] Symbolic name of action
2348 * @return {OO.ui.Process} Action process
2350 OO
.ui
.Dialog
.prototype.getActionProcess = function ( action
) {
2351 return new OO
.ui
.Process()
2352 .next( function () {
2354 // An empty action always closes the dialog without data, which should always be
2355 // safe and make no changes
2364 * @param {Object} [data] Dialog opening data
2365 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2366 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2367 * action item, omit to use #static-actions
2369 OO
.ui
.Dialog
.prototype.getSetupProcess = function ( data
) {
2373 return OO
.ui
.Dialog
.super.prototype.getSetupProcess
.call( this, data
)
2374 .next( function () {
2377 config
= this.constructor.static,
2378 actions
= data
.actions
!== undefined ? data
.actions
: config
.actions
;
2380 this.title
.setLabel(
2381 data
.title
!== undefined ? data
.title
: this.constructor.static.title
2383 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
2385 new OO
.ui
.ActionWidget( $.extend( { $: this.$ }, actions
[i
] ) )
2388 this.actions
.add( items
);
2395 OO
.ui
.Dialog
.prototype.getTeardownProcess = function ( data
) {
2397 return OO
.ui
.Dialog
.super.prototype.getTeardownProcess
.call( this, data
)
2398 .first( function () {
2399 this.actions
.clear();
2400 this.currentAction
= null;
2407 OO
.ui
.Dialog
.prototype.initialize = function () {
2409 OO
.ui
.Dialog
.super.prototype.initialize
.call( this );
2412 this.title
= new OO
.ui
.LabelWidget( { $: this.$ } );
2415 if ( this.constructor.static.escapable
) {
2416 this.$document
.on( 'keydown', this.onDocumentKeyDown
.bind( this ) );
2420 this.$content
.addClass( 'oo-ui-dialog-content' );
2421 this.setPendingElement( this.$head
);
2425 * Attach action actions.
2427 OO
.ui
.Dialog
.prototype.attachActions = function () {
2428 // Remember the list of potentially attached actions
2429 this.attachedActions
= this.actions
.get();
2433 * Detach action actions.
2437 OO
.ui
.Dialog
.prototype.detachActions = function () {
2440 // Detach all actions that may have been previously attached
2441 for ( i
= 0, len
= this.attachedActions
.length
; i
< len
; i
++ ) {
2442 this.attachedActions
[i
].$element
.detach();
2444 this.attachedActions
= [];
2448 * Execute an action.
2450 * @param {string} action Symbolic name of action to execute
2451 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2453 OO
.ui
.Dialog
.prototype.executeAction = function ( action
) {
2455 return this.getActionProcess( action
).execute()
2456 .always( this.popPending
.bind( this ) );
2460 * Collection of windows.
2463 * @extends OO.ui.Element
2464 * @mixins OO.EventEmitter
2466 * Managed windows are mutually exclusive. If a window is opened while there is a current window
2467 * already opening or opened, the current window will be closed without data. Empty closing data
2468 * should always result in the window being closed without causing constructive or destructive
2471 * As a window is opened and closed, it passes through several stages and the manager emits several
2472 * corresponding events.
2474 * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
2475 * - {@link #event-opening} is emitted with `opening` promise
2476 * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
2477 * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
2478 * - `setup` progress notification is emitted from opening promise
2479 * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
2480 * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
2481 * - `ready` progress notification is emitted from opening promise
2482 * - `opening` promise is resolved with `opened` promise
2483 * - Window is now open
2485 * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
2486 * - `opened` promise is resolved with `closing` promise
2487 * - {@link #event-closing} is emitted with `closing` promise
2488 * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
2489 * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
2490 * - `hold` progress notification is emitted from opening promise
2491 * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
2492 * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
2493 * - `teardown` progress notification is emitted from opening promise
2494 * - Closing promise is resolved
2495 * - Window is now closed
2498 * @param {Object} [config] Configuration options
2499 * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames
2500 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2501 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2503 OO
.ui
.WindowManager
= function OoUiWindowManager( config
) {
2504 // Configuration initialization
2505 config
= config
|| {};
2507 // Parent constructor
2508 OO
.ui
.WindowManager
.super.call( this, config
);
2510 // Mixin constructors
2511 OO
.EventEmitter
.call( this );
2514 this.factory
= config
.factory
;
2515 this.modal
= config
.modal
=== undefined || !!config
.modal
;
2516 this.isolate
= !!config
.isolate
;
2518 this.opening
= null;
2520 this.closing
= null;
2521 this.preparingToOpen
= null;
2522 this.preparingToClose
= null;
2524 this.currentWindow
= null;
2525 this.$ariaHidden
= null;
2526 this.requestedSize
= null;
2527 this.onWindowResizeTimeout
= null;
2528 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
2529 this.afterWindowResizeHandler
= this.afterWindowResize
.bind( this );
2530 this.onWindowMouseWheelHandler
= this.onWindowMouseWheel
.bind( this );
2531 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
2535 .addClass( 'oo-ui-windowManager' )
2536 .toggleClass( 'oo-ui-windowManager-modal', this.modal
);
2541 OO
.inheritClass( OO
.ui
.WindowManager
, OO
.ui
.Element
);
2542 OO
.mixinClass( OO
.ui
.WindowManager
, OO
.EventEmitter
);
2547 * Window is opening.
2549 * Fired when the window begins to be opened.
2552 * @param {OO.ui.Window} win Window that's being opened
2553 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2554 * resolved the first argument will be a promise which will be resolved when the window begins
2555 * closing, the second argument will be the opening data; progress notifications will be fired on
2556 * the promise for `setup` and `ready` when those processes are completed respectively.
2557 * @param {Object} data Window opening data
2561 * Window is closing.
2563 * Fired when the window begins to be closed.
2566 * @param {OO.ui.Window} win Window that's being closed
2567 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2568 * is resolved the first argument will be a the closing data; progress notifications will be fired
2569 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2570 * @param {Object} data Window closing data
2574 * Window was resized.
2577 * @param {OO.ui.Window} win Window that was resized
2580 /* Static Properties */
2583 * Map of symbolic size names and CSS properties.
2587 * @property {Object}
2589 OO
.ui
.WindowManager
.static.sizes
= {
2600 // These can be non-numeric because they are never used in calculations
2607 * Symbolic name of default size.
2609 * Default size is used if the window's requested size is not recognized.
2613 * @property {string}
2615 OO
.ui
.WindowManager
.static.defaultSize
= 'medium';
2620 * Handle window resize events.
2622 * @param {jQuery.Event} e Window resize event
2624 OO
.ui
.WindowManager
.prototype.onWindowResize = function () {
2625 clearTimeout( this.onWindowResizeTimeout
);
2626 this.onWindowResizeTimeout
= setTimeout( this.afterWindowResizeHandler
, 200 );
2630 * Handle window resize events.
2632 * @param {jQuery.Event} e Window resize event
2634 OO
.ui
.WindowManager
.prototype.afterWindowResize = function () {
2635 if ( this.currentWindow
) {
2636 this.updateWindowSize( this.currentWindow
);
2641 * Handle window mouse wheel events.
2643 * @param {jQuery.Event} e Mouse wheel event
2645 OO
.ui
.WindowManager
.prototype.onWindowMouseWheel = function ( e
) {
2646 // Kill all events in the parent window if the child window is isolated,
2647 // or if the event didn't come from the child window
2648 return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame
[0], e
.target
) );
2652 * Handle document key down events.
2654 * @param {jQuery.Event} e Key down event
2656 OO
.ui
.WindowManager
.prototype.onDocumentKeyDown = function ( e
) {
2657 switch ( e
.which
) {
2658 case OO
.ui
.Keys
.PAGEUP
:
2659 case OO
.ui
.Keys
.PAGEDOWN
:
2660 case OO
.ui
.Keys
.END
:
2661 case OO
.ui
.Keys
.HOME
:
2662 case OO
.ui
.Keys
.LEFT
:
2664 case OO
.ui
.Keys
.RIGHT
:
2665 case OO
.ui
.Keys
.DOWN
:
2666 // Kill all events in the parent window if the child window is isolated,
2667 // or if the event didn't come from the child window
2668 return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame
[0], e
.target
) );
2673 * Check if window is opening.
2675 * @return {boolean} Window is opening
2677 OO
.ui
.WindowManager
.prototype.isOpening = function ( win
) {
2678 return win
=== this.currentWindow
&& !!this.opening
&& this.opening
.state() === 'pending';
2682 * Check if window is closing.
2684 * @return {boolean} Window is closing
2686 OO
.ui
.WindowManager
.prototype.isClosing = function ( win
) {
2687 return win
=== this.currentWindow
&& !!this.closing
&& this.closing
.state() === 'pending';
2691 * Check if window is opened.
2693 * @return {boolean} Window is opened
2695 OO
.ui
.WindowManager
.prototype.isOpened = function ( win
) {
2696 return win
=== this.currentWindow
&& !!this.opened
&& this.opened
.state() === 'pending';
2700 * Check if window contents should be isolated.
2702 * Window content isolation is done using inline frames.
2704 * @return {boolean} Window contents should be isolated
2706 OO
.ui
.WindowManager
.prototype.shouldIsolate = function () {
2707 return this.isolate
;
2711 * Check if a window is being managed.
2713 * @param {OO.ui.Window} win Window to check
2714 * @return {boolean} Window is being managed
2716 OO
.ui
.WindowManager
.prototype.hasWindow = function ( win
) {
2719 for ( name
in this.windows
) {
2720 if ( this.windows
[name
] === win
) {
2729 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2731 * @param {OO.ui.Window} win Window being opened
2732 * @param {Object} [data] Window opening data
2733 * @return {number} Milliseconds to wait
2735 OO
.ui
.WindowManager
.prototype.getSetupDelay = function () {
2740 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2742 * @param {OO.ui.Window} win Window being opened
2743 * @param {Object} [data] Window opening data
2744 * @return {number} Milliseconds to wait
2746 OO
.ui
.WindowManager
.prototype.getReadyDelay = function () {
2751 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2753 * @param {OO.ui.Window} win Window being closed
2754 * @param {Object} [data] Window closing data
2755 * @return {number} Milliseconds to wait
2757 OO
.ui
.WindowManager
.prototype.getHoldDelay = function () {
2762 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2764 * @param {OO.ui.Window} win Window being closed
2765 * @param {Object} [data] Window closing data
2766 * @return {number} Milliseconds to wait
2768 OO
.ui
.WindowManager
.prototype.getTeardownDelay = function () {
2769 return this.modal
? 250 : 0;
2773 * Get managed window by symbolic name.
2775 * If window is not yet instantiated, it will be instantiated and added automatically.
2777 * @param {string} name Symbolic window name
2778 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2779 * @throws {Error} If the symbolic name is unrecognized by the factory
2780 * @throws {Error} If the symbolic name unrecognized as a managed window
2782 OO
.ui
.WindowManager
.prototype.getWindow = function ( name
) {
2783 var deferred
= $.Deferred(),
2784 win
= this.windows
[name
];
2786 if ( !( win
instanceof OO
.ui
.Window
) ) {
2787 if ( this.factory
) {
2788 if ( !this.factory
.lookup( name
) ) {
2789 deferred
.reject( new OO
.ui
.Error(
2790 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2793 win
= this.factory
.create( name
, this, { $: this.$ } );
2794 this.addWindows( [ win
] );
2795 deferred
.resolve( win
);
2798 deferred
.reject( new OO
.ui
.Error(
2799 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2803 deferred
.resolve( win
);
2806 return deferred
.promise();
2810 * Get current window.
2812 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2814 OO
.ui
.WindowManager
.prototype.getCurrentWindow = function () {
2815 return this.currentWindow
;
2821 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2822 * @param {Object} [data] Window opening data
2823 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2824 * for more details about the `opening` promise
2827 OO
.ui
.WindowManager
.prototype.openWindow = function ( win
, data
) {
2830 opening
= $.Deferred();
2832 // Argument handling
2833 if ( typeof win
=== 'string' ) {
2834 return this.getWindow( win
).then( function ( win
) {
2835 return manager
.openWindow( win
, data
);
2840 if ( !this.hasWindow( win
) ) {
2841 opening
.reject( new OO
.ui
.Error(
2842 'Cannot open window: window is not attached to manager'
2844 } else if ( this.preparingToOpen
|| this.opening
|| this.opened
) {
2845 opening
.reject( new OO
.ui
.Error(
2846 'Cannot open window: another window is opening or open'
2851 if ( opening
.state() !== 'rejected' ) {
2852 // Begin loading the window if it's not loading or loaded already - may take noticable time
2853 // and we want to do this in paralell with any other preparatory actions
2854 if ( !win
.isLoading() && !win
.isLoaded() ) {
2855 // Finish initializing the window (must be done after manager is attached to DOM)
2856 win
.setManager( this );
2857 preparing
.push( win
.load() );
2860 if ( this.closing
) {
2861 // If a window is currently closing, wait for it to complete
2862 preparing
.push( this.closing
);
2865 this.preparingToOpen
= $.when
.apply( $, preparing
);
2866 // Ensure handlers get called after preparingToOpen is set
2867 this.preparingToOpen
.done( function () {
2868 if ( manager
.modal
) {
2869 manager
.toggleGlobalEvents( true );
2870 manager
.toggleAriaIsolation( true );
2872 manager
.currentWindow
= win
;
2873 manager
.opening
= opening
;
2874 manager
.preparingToOpen
= null;
2875 manager
.emit( 'opening', win
, opening
, data
);
2876 setTimeout( function () {
2877 win
.setup( data
).then( function () {
2878 manager
.updateWindowSize( win
);
2879 manager
.opening
.notify( { state
: 'setup' } );
2880 setTimeout( function () {
2881 win
.ready( data
).then( function () {
2882 manager
.opening
.notify( { state
: 'ready' } );
2883 manager
.opening
= null;
2884 manager
.opened
= $.Deferred();
2885 opening
.resolve( manager
.opened
.promise(), data
);
2887 }, manager
.getReadyDelay() );
2889 }, manager
.getSetupDelay() );
2893 return opening
.promise();
2899 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2900 * @param {Object} [data] Window closing data
2901 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2902 * for more details about the `closing` promise
2903 * @throws {Error} If no window by that name is being managed
2906 OO
.ui
.WindowManager
.prototype.closeWindow = function ( win
, data
) {
2909 closing
= $.Deferred(),
2912 // Argument handling
2913 if ( typeof win
=== 'string' ) {
2914 win
= this.windows
[win
];
2915 } else if ( !this.hasWindow( win
) ) {
2921 closing
.reject( new OO
.ui
.Error(
2922 'Cannot close window: window is not attached to manager'
2924 } else if ( win
!== this.currentWindow
) {
2925 closing
.reject( new OO
.ui
.Error(
2926 'Cannot close window: window already closed with different data'
2928 } else if ( this.preparingToClose
|| this.closing
) {
2929 closing
.reject( new OO
.ui
.Error(
2930 'Cannot close window: window already closing with different data'
2935 if ( closing
.state() !== 'rejected' ) {
2936 if ( this.opening
) {
2937 // If the window is currently opening, close it when it's done
2938 preparing
.push( this.opening
);
2941 this.preparingToClose
= $.when
.apply( $, preparing
);
2942 // Ensure handlers get called after preparingToClose is set
2943 this.preparingToClose
.done( function () {
2944 manager
.closing
= closing
;
2945 manager
.preparingToClose
= null;
2946 manager
.emit( 'closing', win
, closing
, data
);
2947 opened
= manager
.opened
;
2948 manager
.opened
= null;
2949 opened
.resolve( closing
.promise(), data
);
2950 setTimeout( function () {
2951 win
.hold( data
).then( function () {
2952 closing
.notify( { state
: 'hold' } );
2953 setTimeout( function () {
2954 win
.teardown( data
).then( function () {
2955 closing
.notify( { state
: 'teardown' } );
2956 if ( manager
.modal
) {
2957 manager
.toggleGlobalEvents( false );
2958 manager
.toggleAriaIsolation( false );
2960 manager
.closing
= null;
2961 manager
.currentWindow
= null;
2962 closing
.resolve( data
);
2964 }, manager
.getTeardownDelay() );
2966 }, manager
.getHoldDelay() );
2970 return closing
.promise();
2976 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
2977 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
2978 * a statically configured symbolic name
2980 OO
.ui
.WindowManager
.prototype.addWindows = function ( windows
) {
2981 var i
, len
, win
, name
, list
;
2983 if ( $.isArray( windows
) ) {
2984 // Convert to map of windows by looking up symbolic names from static configuration
2986 for ( i
= 0, len
= windows
.length
; i
< len
; i
++ ) {
2987 name
= windows
[i
].constructor.static.name
;
2988 if ( typeof name
!== 'string' ) {
2989 throw new Error( 'Cannot add window' );
2991 list
[name
] = windows
[i
];
2993 } else if ( $.isPlainObject( windows
) ) {
2998 for ( name
in list
) {
3000 this.windows
[name
] = win
;
3001 this.$element
.append( win
.$element
);
3008 * Windows will be closed before they are removed.
3010 * @param {string} name Symbolic name of window to remove
3011 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3012 * @throws {Error} If windows being removed are not being managed
3014 OO
.ui
.WindowManager
.prototype.removeWindows = function ( names
) {
3015 var i
, len
, win
, name
,
3018 cleanup = function ( name
, win
) {
3019 delete manager
.windows
[name
];
3020 win
.$element
.detach();
3023 for ( i
= 0, len
= names
.length
; i
< len
; i
++ ) {
3025 win
= this.windows
[name
];
3027 throw new Error( 'Cannot remove window' );
3029 promises
.push( this.closeWindow( name
).then( cleanup
.bind( null, name
, win
) ) );
3032 return $.when
.apply( $, promises
);
3036 * Remove all windows.
3038 * Windows will be closed before they are removed.
3040 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3042 OO
.ui
.WindowManager
.prototype.clearWindows = function () {
3043 return this.removeWindows( Object
.keys( this.windows
) );
3049 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3053 OO
.ui
.WindowManager
.prototype.updateWindowSize = function ( win
) {
3054 // Bypass for non-current, and thus invisible, windows
3055 if ( win
!== this.currentWindow
) {
3059 var viewport
= OO
.ui
.Element
.getDimensions( win
.getElementWindow() ),
3060 sizes
= this.constructor.static.sizes
,
3061 size
= win
.getSize();
3063 if ( !sizes
[size
] ) {
3064 size
= this.constructor.static.defaultSize
;
3066 if ( size
!== 'full' && viewport
.rect
.right
- viewport
.rect
.left
< sizes
[size
].width
) {
3070 this.$element
.toggleClass( 'oo-ui-windowManager-fullscreen', size
=== 'full' );
3071 this.$element
.toggleClass( 'oo-ui-windowManager-floating', size
!== 'full' );
3072 win
.setDimensions( sizes
[size
] );
3074 this.emit( 'resize', win
);
3080 * Bind or unbind global events for scrolling.
3082 * @param {boolean} [on] Bind global events
3085 OO
.ui
.WindowManager
.prototype.toggleGlobalEvents = function ( on
) {
3086 on
= on
=== undefined ? !!this.globalEvents
: !!on
;
3089 if ( !this.globalEvents
) {
3090 this.$( this.getElementDocument() ).on( {
3091 // Prevent scrolling by keys in top-level window
3092 keydown
: this.onDocumentKeyDownHandler
3094 this.$( this.getElementWindow() ).on( {
3095 // Prevent scrolling by wheel in top-level window
3096 mousewheel
: this.onWindowMouseWheelHandler
,
3097 // Start listening for top-level window dimension changes
3098 'orientationchange resize': this.onWindowResizeHandler
3100 this.globalEvents
= true;
3102 } else if ( this.globalEvents
) {
3103 // Unbind global events
3104 this.$( this.getElementDocument() ).off( {
3105 // Allow scrolling by keys in top-level window
3106 keydown
: this.onDocumentKeyDownHandler
3108 this.$( this.getElementWindow() ).off( {
3109 // Allow scrolling by wheel in top-level window
3110 mousewheel
: this.onWindowMouseWheelHandler
,
3111 // Stop listening for top-level window dimension changes
3112 'orientationchange resize': this.onWindowResizeHandler
3114 this.globalEvents
= false;
3121 * Toggle screen reader visibility of content other than the window manager.
3123 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3126 OO
.ui
.WindowManager
.prototype.toggleAriaIsolation = function ( isolate
) {
3127 isolate
= isolate
=== undefined ? !this.$ariaHidden
: !!isolate
;
3130 if ( !this.$ariaHidden
) {
3131 // Hide everything other than the window manager from screen readers
3132 this.$ariaHidden
= $( 'body' )
3134 .not( this.$element
.parentsUntil( 'body' ).last() )
3135 .attr( 'aria-hidden', '' );
3137 } else if ( this.$ariaHidden
) {
3138 // Restore screen reader visiblity
3139 this.$ariaHidden
.removeAttr( 'aria-hidden' );
3140 this.$ariaHidden
= null;
3147 * Destroy window manager.
3149 * Windows will not be closed, only removed from the DOM.
3151 OO
.ui
.WindowManager
.prototype.destroy = function () {
3152 this.toggleGlobalEvents( false );
3153 this.toggleAriaIsolation( false );
3154 this.$element
.remove();
3162 * @param {string|jQuery} message Description of error
3163 * @param {Object} [config] Configuration options
3164 * @cfg {boolean} [recoverable=true] Error is recoverable
3166 OO
.ui
.Error
= function OoUiElement( message
, config
) {
3167 // Configuration initialization
3168 config
= config
|| {};
3171 this.message
= message
instanceof jQuery
? message
: String( message
);
3172 this.recoverable
= config
.recoverable
=== undefined || !!config
.recoverable
;
3177 OO
.initClass( OO
.ui
.Error
);
3182 * Check if error can be recovered from.
3184 * @return {boolean} Error is recoverable
3186 OO
.ui
.Error
.prototype.isRecoverable = function () {
3187 return this.recoverable
;
3191 * Get error message as DOM nodes.
3193 * @return {jQuery} Error message in DOM nodes
3195 OO
.ui
.Error
.prototype.getMessage = function () {
3196 return this.message
instanceof jQuery
?
3197 this.message
.clone() :
3198 $( '<div>' ).text( this.message
).contents();
3202 * Get error message as text.
3204 * @return {string} Error message
3206 OO
.ui
.Error
.prototype.getMessageText = function () {
3207 return this.message
instanceof jQuery
? this.message
.text() : this.message
;
3211 * A list of functions, called in sequence.
3213 * If a function added to a process returns boolean false the process will stop; if it returns an
3214 * object with a `promise` method the process will use the promise to either continue to the next
3215 * step when the promise is resolved or stop when the promise is rejected.
3220 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3221 * call, see #createStep for more information
3222 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3224 * @return {Object} Step object, with `callback` and `context` properties
3226 OO
.ui
.Process = function ( step
, context
) {
3231 if ( step
!== undefined ) {
3232 this.next( step
, context
);
3238 OO
.initClass( OO
.ui
.Process
);
3243 * Start the process.
3245 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3246 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3247 * process, the remaining steps will not be taken
3249 OO
.ui
.Process
.prototype.execute = function () {
3250 var i
, len
, promise
;
3253 * Continue execution.
3256 * @param {Array} step A function and the context it should be called in
3257 * @return {Function} Function that continues the process
3259 function proceed( step
) {
3260 return function () {
3261 // Execute step in the correct context
3263 result
= step
.callback
.call( step
.context
);
3265 if ( result
=== false ) {
3266 // Use rejected promise for boolean false results
3267 return $.Deferred().reject( [] ).promise();
3269 if ( typeof result
=== 'number' ) {
3271 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3273 // Use a delayed promise for numbers, expecting them to be in milliseconds
3274 deferred
= $.Deferred();
3275 setTimeout( deferred
.resolve
, result
);
3276 return deferred
.promise();
3278 if ( result
instanceof OO
.ui
.Error
) {
3279 // Use rejected promise for error
3280 return $.Deferred().reject( [ result
] ).promise();
3282 if ( $.isArray( result
) && result
.length
&& result
[0] instanceof OO
.ui
.Error
) {
3283 // Use rejected promise for list of errors
3284 return $.Deferred().reject( result
).promise();
3286 // Duck-type the object to see if it can produce a promise
3287 if ( result
&& $.isFunction( result
.promise
) ) {
3288 // Use a promise generated from the result
3289 return result
.promise();
3291 // Use resolved promise for other results
3292 return $.Deferred().resolve().promise();
3296 if ( this.steps
.length
) {
3297 // Generate a chain reaction of promises
3298 promise
= proceed( this.steps
[0] )();
3299 for ( i
= 1, len
= this.steps
.length
; i
< len
; i
++ ) {
3300 promise
= promise
.then( proceed( this.steps
[i
] ) );
3303 promise
= $.Deferred().resolve().promise();
3310 * Create a process step.
3313 * @param {number|jQuery.Promise|Function} step
3315 * - Number of milliseconds to wait; or
3316 * - Promise to wait to be resolved; or
3317 * - Function to execute
3318 * - If it returns boolean false the process will stop
3319 * - If it returns an object with a `promise` method the process will use the promise to either
3320 * continue to the next step when the promise is resolved or stop when the promise is rejected
3321 * - If it returns a number, the process will wait for that number of milliseconds before
3323 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3325 * @return {Object} Step object, with `callback` and `context` properties
3327 OO
.ui
.Process
.prototype.createStep = function ( step
, context
) {
3328 if ( typeof step
=== 'number' || $.isFunction( step
.promise
) ) {
3330 callback: function () {
3336 if ( $.isFunction( step
) ) {
3342 throw new Error( 'Cannot create process step: number, promise or function expected' );
3346 * Add step to the beginning of the process.
3348 * @inheritdoc #createStep
3349 * @return {OO.ui.Process} this
3352 OO
.ui
.Process
.prototype.first = function ( step
, context
) {
3353 this.steps
.unshift( this.createStep( step
, context
) );
3358 * Add step to the end of the process.
3360 * @inheritdoc #createStep
3361 * @return {OO.ui.Process} this
3364 OO
.ui
.Process
.prototype.next = function ( step
, context
) {
3365 this.steps
.push( this.createStep( step
, context
) );
3370 * Factory for tools.
3373 * @extends OO.Factory
3376 OO
.ui
.ToolFactory
= function OoUiToolFactory() {
3377 // Parent constructor
3378 OO
.ui
.ToolFactory
.super.call( this );
3383 OO
.inheritClass( OO
.ui
.ToolFactory
, OO
.Factory
);
3388 OO
.ui
.ToolFactory
.prototype.getTools = function ( include
, exclude
, promote
, demote
) {
3389 var i
, len
, included
, promoted
, demoted
,
3393 // Collect included and not excluded tools
3394 included
= OO
.simpleArrayDifference( this.extract( include
), this.extract( exclude
) );
3397 promoted
= this.extract( promote
, used
);
3398 demoted
= this.extract( demote
, used
);
3401 for ( i
= 0, len
= included
.length
; i
< len
; i
++ ) {
3402 if ( !used
[included
[i
]] ) {
3403 auto
.push( included
[i
] );
3407 return promoted
.concat( auto
).concat( demoted
);
3411 * Get a flat list of names from a list of names or groups.
3413 * Tools can be specified in the following ways:
3415 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3416 * - All tools in a group: `{ group: 'group-name' }`
3417 * - All tools: `'*'`
3420 * @param {Array|string} collection List of tools
3421 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3422 * names will be added as properties
3423 * @return {string[]} List of extracted names
3425 OO
.ui
.ToolFactory
.prototype.extract = function ( collection
, used
) {
3426 var i
, len
, item
, name
, tool
,
3429 if ( collection
=== '*' ) {
3430 for ( name
in this.registry
) {
3431 tool
= this.registry
[name
];
3433 // Only add tools by group name when auto-add is enabled
3434 tool
.static.autoAddToCatchall
&&
3435 // Exclude already used tools
3436 ( !used
|| !used
[name
] )
3444 } else if ( $.isArray( collection
) ) {
3445 for ( i
= 0, len
= collection
.length
; i
< len
; i
++ ) {
3446 item
= collection
[i
];
3447 // Allow plain strings as shorthand for named tools
3448 if ( typeof item
=== 'string' ) {
3449 item
= { name
: item
};
3451 if ( OO
.isPlainObject( item
) ) {
3453 for ( name
in this.registry
) {
3454 tool
= this.registry
[name
];
3456 // Include tools with matching group
3457 tool
.static.group
=== item
.group
&&
3458 // Only add tools by group name when auto-add is enabled
3459 tool
.static.autoAddToGroup
&&
3460 // Exclude already used tools
3461 ( !used
|| !used
[name
] )
3469 // Include tools with matching name and exclude already used tools
3470 } else if ( item
.name
&& ( !used
|| !used
[item
.name
] ) ) {
3471 names
.push( item
.name
);
3473 used
[item
.name
] = true;
3483 * Factory for tool groups.
3486 * @extends OO.Factory
3489 OO
.ui
.ToolGroupFactory
= function OoUiToolGroupFactory() {
3490 // Parent constructor
3491 OO
.Factory
.call( this );
3494 defaultClasses
= this.constructor.static.getDefaultClasses();
3496 // Register default toolgroups
3497 for ( i
= 0, l
= defaultClasses
.length
; i
< l
; i
++ ) {
3498 this.register( defaultClasses
[i
] );
3504 OO
.inheritClass( OO
.ui
.ToolGroupFactory
, OO
.Factory
);
3506 /* Static Methods */
3509 * Get a default set of classes to be registered on construction
3511 * @return {Function[]} Default classes
3513 OO
.ui
.ToolGroupFactory
.static.getDefaultClasses = function () {
3516 OO
.ui
.ListToolGroup
,
3528 * @param {Object} [config] Configuration options
3530 OO
.ui
.Theme
= function OoUiTheme( config
) {
3531 // Initialize config
3532 config
= config
|| {};
3537 OO
.initClass( OO
.ui
.Theme
);
3542 * Get a list of classes to be applied to a widget.
3544 * @localdoc The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or
3545 * removes, otherwise state transitions will not work properly.
3547 * @param {OO.ui.Element} element Element for which to get classes
3548 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3550 OO
.ui
.Theme
.prototype.getElementClasses = function ( /* element */ ) {
3551 return { on
: [], off
: [] };
3555 * Update CSS classes provided by the theme.
3557 * For elements with theme logic hooks, this should be called anytime there's a state change.
3559 * @param {OO.ui.Element} Element for which to update classes
3560 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3562 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
3563 var classes
= this.getElementClasses( element
);
3566 .removeClass( classes
.off
.join( ' ' ) )
3567 .addClass( classes
.on
.join( ' ' ) );
3571 * Element with a button.
3573 * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
3574 * and access keys for accessibility purposes.
3580 * @param {Object} [config] Configuration options
3581 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3582 * @cfg {boolean} [framed=true] Render button with a frame
3583 * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex
3584 * @cfg {string} [accessKey] Button's access key
3586 OO
.ui
.ButtonElement
= function OoUiButtonElement( config
) {
3587 // Configuration initialization
3588 config
= config
|| {};
3591 this.$button
= null;
3593 this.tabIndex
= null;
3594 this.accessKey
= null;
3595 this.active
= false;
3596 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
3597 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
3600 this.$element
.addClass( 'oo-ui-buttonElement' );
3601 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
3602 this.setTabIndex( config
.tabIndex
|| 0 );
3603 this.setAccessKey( config
.accessKey
);
3604 this.setButtonElement( config
.$button
|| this.$( '<a>' ) );
3609 OO
.initClass( OO
.ui
.ButtonElement
);
3611 /* Static Properties */
3614 * Cancel mouse down events.
3618 * @property {boolean}
3620 OO
.ui
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
3625 * Set the button element.
3627 * If an element is already set, it will be cleaned up before setting up the new element.
3629 * @param {jQuery} $button Element to use as button
3631 OO
.ui
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
3632 if ( this.$button
) {
3634 .removeClass( 'oo-ui-buttonElement-button' )
3635 .removeAttr( 'role accesskey tabindex' )
3636 .off( this.onMouseDownHandler
);
3639 this.$button
= $button
3640 .addClass( 'oo-ui-buttonElement-button' )
3641 .attr( { role
: 'button', accesskey
: this.accessKey
, tabindex
: this.tabIndex
} )
3642 .on( 'mousedown', this.onMouseDownHandler
);
3646 * Handles mouse down events.
3648 * @param {jQuery.Event} e Mouse down event
3650 OO
.ui
.ButtonElement
.prototype.onMouseDown = function ( e
) {
3651 if ( this.isDisabled() || e
.which
!== 1 ) {
3654 // Remove the tab-index while the button is down to prevent the button from stealing focus
3655 this.$button
.removeAttr( 'tabindex' );
3656 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
3657 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3658 // reliably reapply the tabindex and remove the pressed class
3659 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
3660 // Prevent change of focus unless specifically configured otherwise
3661 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
3667 * Handles mouse up events.
3669 * @param {jQuery.Event} e Mouse up event
3671 OO
.ui
.ButtonElement
.prototype.onMouseUp = function ( e
) {
3672 if ( this.isDisabled() || e
.which
!== 1 ) {
3675 // Restore the tab-index after the button is up to restore the button's accesssibility
3676 this.$button
.attr( 'tabindex', this.tabIndex
);
3677 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
3678 // Stop listening for mouseup, since we only needed this once
3679 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
3683 * Check if button has a frame.
3685 * @return {boolean} Button is framed
3687 OO
.ui
.ButtonElement
.prototype.isFramed = function () {
3694 * @param {boolean} [framed] Make button framed, omit to toggle
3697 OO
.ui
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
3698 framed
= framed
=== undefined ? !this.framed
: !!framed
;
3699 if ( framed
!== this.framed
) {
3700 this.framed
= framed
;
3702 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
3703 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
3704 this.updateThemeClasses();
3713 * @param {number|null} tabIndex Button's tab index, use null to remove
3716 OO
.ui
.ButtonElement
.prototype.setTabIndex = function ( tabIndex
) {
3717 tabIndex
= typeof tabIndex
=== 'number' && tabIndex
>= 0 ? tabIndex
: null;
3719 if ( this.tabIndex
!== tabIndex
) {
3720 if ( this.$button
) {
3721 if ( tabIndex
!== null ) {
3722 this.$button
.attr( 'tabindex', tabIndex
);
3724 this.$button
.removeAttr( 'tabindex' );
3727 this.tabIndex
= tabIndex
;
3736 * @param {string} accessKey Button's access key, use empty string to remove
3739 OO
.ui
.ButtonElement
.prototype.setAccessKey = function ( accessKey
) {
3740 accessKey
= typeof accessKey
=== 'string' && accessKey
.length
? accessKey
: null;
3742 if ( this.accessKey
!== accessKey
) {
3743 if ( this.$button
) {
3744 if ( accessKey
!== null ) {
3745 this.$button
.attr( 'accesskey', accessKey
);
3747 this.$button
.removeAttr( 'accesskey' );
3750 this.accessKey
= accessKey
;
3759 * @param {boolean} [value] Make button active
3762 OO
.ui
.ButtonElement
.prototype.setActive = function ( value
) {
3763 this.$element
.toggleClass( 'oo-ui-buttonElement-active', !!value
);
3768 * Element containing a sequence of child elements.
3774 * @param {Object} [config] Configuration options
3775 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
3777 OO
.ui
.GroupElement
= function OoUiGroupElement( config
) {
3779 config
= config
|| {};
3784 this.aggregateItemEvents
= {};
3787 this.setGroupElement( config
.$group
|| this.$( '<div>' ) );
3793 * Set the group element.
3795 * If an element is already set, items will be moved to the new element.
3797 * @param {jQuery} $group Element to use as group
3799 OO
.ui
.GroupElement
.prototype.setGroupElement = function ( $group
) {
3802 this.$group
= $group
;
3803 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
3804 this.$group
.append( this.items
[i
].$element
);
3809 * Check if there are no items.
3811 * @return {boolean} Group is empty
3813 OO
.ui
.GroupElement
.prototype.isEmpty = function () {
3814 return !this.items
.length
;
3820 * @return {OO.ui.Element[]} Items
3822 OO
.ui
.GroupElement
.prototype.getItems = function () {
3823 return this.items
.slice( 0 );
3827 * Add an aggregate item event.
3829 * Aggregated events are listened to on each item and then emitted by the group under a new name,
3830 * and with an additional leading parameter containing the item that emitted the original event.
3831 * Other arguments that were emitted from the original event are passed through.
3833 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
3834 * event, use null value to remove aggregation
3835 * @throws {Error} If aggregation already exists
3837 OO
.ui
.GroupElement
.prototype.aggregate = function ( events
) {
3838 var i
, len
, item
, add
, remove
, itemEvent
, groupEvent
;
3840 for ( itemEvent
in events
) {
3841 groupEvent
= events
[itemEvent
];
3843 // Remove existing aggregated event
3844 if ( itemEvent
in this.aggregateItemEvents
) {
3845 // Don't allow duplicate aggregations
3847 throw new Error( 'Duplicate item event aggregation for ' + itemEvent
);
3849 // Remove event aggregation from existing items
3850 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
3851 item
= this.items
[i
];
3852 if ( item
.connect
&& item
.disconnect
) {
3854 remove
[itemEvent
] = [ 'emit', groupEvent
, item
];
3855 item
.disconnect( this, remove
);
3858 // Prevent future items from aggregating event
3859 delete this.aggregateItemEvents
[itemEvent
];
3862 // Add new aggregate event
3864 // Make future items aggregate event
3865 this.aggregateItemEvents
[itemEvent
] = groupEvent
;
3866 // Add event aggregation to existing items
3867 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
3868 item
= this.items
[i
];
3869 if ( item
.connect
&& item
.disconnect
) {
3871 add
[itemEvent
] = [ 'emit', groupEvent
, item
];
3872 item
.connect( this, add
);
3882 * Adding an existing item (by value) will move it.
3884 * @param {OO.ui.Element[]} items Items
3885 * @param {number} [index] Index to insert items at
3888 OO
.ui
.GroupElement
.prototype.addItems = function ( items
, index
) {
3889 var i
, len
, item
, event
, events
, currentIndex
,
3892 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
3895 // Check if item exists then remove it first, effectively "moving" it
3896 currentIndex
= $.inArray( item
, this.items
);
3897 if ( currentIndex
>= 0 ) {
3898 this.removeItems( [ item
] );
3899 // Adjust index to compensate for removal
3900 if ( currentIndex
< index
) {
3905 if ( item
.connect
&& item
.disconnect
&& !$.isEmptyObject( this.aggregateItemEvents
) ) {
3907 for ( event
in this.aggregateItemEvents
) {
3908 events
[event
] = [ 'emit', this.aggregateItemEvents
[event
], item
];
3910 item
.connect( this, events
);
3912 item
.setElementGroup( this );
3913 itemElements
.push( item
.$element
.get( 0 ) );
3916 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
3917 this.$group
.append( itemElements
);
3918 this.items
.push
.apply( this.items
, items
);
3919 } else if ( index
=== 0 ) {
3920 this.$group
.prepend( itemElements
);
3921 this.items
.unshift
.apply( this.items
, items
);
3923 this.items
[index
].$element
.before( itemElements
);
3924 this.items
.splice
.apply( this.items
, [ index
, 0 ].concat( items
) );
3933 * Items will be detached, not removed, so they can be used later.
3935 * @param {OO.ui.Element[]} items Items to remove
3938 OO
.ui
.GroupElement
.prototype.removeItems = function ( items
) {
3939 var i
, len
, item
, index
, remove
, itemEvent
;
3941 // Remove specific items
3942 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
3944 index
= $.inArray( item
, this.items
);
3945 if ( index
!== -1 ) {
3947 item
.connect
&& item
.disconnect
&&
3948 !$.isEmptyObject( this.aggregateItemEvents
)
3951 if ( itemEvent
in this.aggregateItemEvents
) {
3952 remove
[itemEvent
] = [ 'emit', this.aggregateItemEvents
[itemEvent
], item
];
3954 item
.disconnect( this, remove
);
3956 item
.setElementGroup( null );
3957 this.items
.splice( index
, 1 );
3958 item
.$element
.detach();
3968 * Items will be detached, not removed, so they can be used later.
3972 OO
.ui
.GroupElement
.prototype.clearItems = function () {
3973 var i
, len
, item
, remove
, itemEvent
;
3976 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
3977 item
= this.items
[i
];
3979 item
.connect
&& item
.disconnect
&&
3980 !$.isEmptyObject( this.aggregateItemEvents
)
3983 if ( itemEvent
in this.aggregateItemEvents
) {
3984 remove
[itemEvent
] = [ 'emit', this.aggregateItemEvents
[itemEvent
], item
];
3986 item
.disconnect( this, remove
);
3988 item
.setElementGroup( null );
3989 item
.$element
.detach();
3997 * Element containing an icon.
3999 * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
4000 * a control or convey information in a more space efficient way. Icons should rarely be used
4001 * without labels; such as in a toolbar where space is at a premium or within a context where the
4002 * meaning is very clear to the user.
4008 * @param {Object} [config] Configuration options
4009 * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
4010 * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
4011 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4013 * @cfg {string} [iconTitle] Icon title text or a function that returns text
4015 OO
.ui
.IconElement
= function OoUiIconElement( config
) {
4016 // Config intialization
4017 config
= config
|| {};
4022 this.iconTitle
= null;
4025 this.setIcon( config
.icon
|| this.constructor.static.icon
);
4026 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
4027 this.setIconElement( config
.$icon
|| this.$( '<span>' ) );
4032 OO
.initClass( OO
.ui
.IconElement
);
4034 /* Static Properties */
4039 * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
4041 * For i18n purposes, this property can be an object containing a `default` icon name property and
4042 * additional icon names keyed by language code.
4044 * Example of i18n icon definition:
4045 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4049 * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
4050 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4053 OO
.ui
.IconElement
.static.icon
= null;
4060 * @property {string|Function|null} Icon title text, a function that returns text or null for no
4063 OO
.ui
.IconElement
.static.iconTitle
= null;
4068 * Set the icon element.
4070 * If an element is already set, it will be cleaned up before setting up the new element.
4072 * @param {jQuery} $icon Element to use as icon
4074 OO
.ui
.IconElement
.prototype.setIconElement = function ( $icon
) {
4077 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
4078 .removeAttr( 'title' );
4082 .addClass( 'oo-ui-iconElement-icon' )
4083 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
4084 if ( this.iconTitle
!== null ) {
4085 this.$icon
.attr( 'title', this.iconTitle
);
4092 * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
4093 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4094 * language, use null to remove icon
4097 OO
.ui
.IconElement
.prototype.setIcon = function ( icon
) {
4098 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
4099 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
4101 if ( this.icon
!== icon
) {
4103 if ( this.icon
!== null ) {
4104 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
4106 if ( icon
!== null ) {
4107 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
4113 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
4114 this.updateThemeClasses();
4122 * @param {string|Function|null} icon Icon title text, a function that returns text or null
4126 OO
.ui
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
4127 iconTitle
= typeof iconTitle
=== 'function' ||
4128 ( typeof iconTitle
=== 'string' && iconTitle
.length
) ?
4129 OO
.ui
.resolveMsg( iconTitle
) : null;
4131 if ( this.iconTitle
!== iconTitle
) {
4132 this.iconTitle
= iconTitle
;
4134 if ( this.iconTitle
!== null ) {
4135 this.$icon
.attr( 'title', iconTitle
);
4137 this.$icon
.removeAttr( 'title' );
4148 * @return {string} Icon
4150 OO
.ui
.IconElement
.prototype.getIcon = function () {
4155 * Element containing an indicator.
4157 * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
4158 * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
4159 * instead of performing an action directly, or an item in a list which has errors that need to be
4166 * @param {Object} [config] Configuration options
4167 * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
4169 * @cfg {string} [indicator] Symbolic indicator name
4170 * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
4172 OO
.ui
.IndicatorElement
= function OoUiIndicatorElement( config
) {
4173 // Config intialization
4174 config
= config
|| {};
4177 this.$indicator
= null;
4178 this.indicator
= null;
4179 this.indicatorTitle
= null;
4182 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
4183 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
4184 this.setIndicatorElement( config
.$indicator
|| this.$( '<span>' ) );
4189 OO
.initClass( OO
.ui
.IndicatorElement
);
4191 /* Static Properties */
4198 * @property {string|null} Symbolic indicator name or null for no indicator
4200 OO
.ui
.IndicatorElement
.static.indicator
= null;
4207 * @property {string|Function|null} Indicator title text, a function that returns text or null for no
4210 OO
.ui
.IndicatorElement
.static.indicatorTitle
= null;
4215 * Set the indicator element.
4217 * If an element is already set, it will be cleaned up before setting up the new element.
4219 * @param {jQuery} $indicator Element to use as indicator
4221 OO
.ui
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
4222 if ( this.$indicator
) {
4224 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
4225 .removeAttr( 'title' );
4228 this.$indicator
= $indicator
4229 .addClass( 'oo-ui-indicatorElement-indicator' )
4230 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
4231 if ( this.indicatorTitle
!== null ) {
4232 this.$indicatorTitle
.attr( 'title', this.indicatorTitle
);
4239 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
4242 OO
.ui
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
4243 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
4245 if ( this.indicator
!== indicator
) {
4246 if ( this.$indicator
) {
4247 if ( this.indicator
!== null ) {
4248 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
4250 if ( indicator
!== null ) {
4251 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
4254 this.indicator
= indicator
;
4257 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
4258 this.updateThemeClasses();
4264 * Set indicator title.
4266 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
4267 * null for no indicator title
4270 OO
.ui
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
4271 indicatorTitle
= typeof indicatorTitle
=== 'function' ||
4272 ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ?
4273 OO
.ui
.resolveMsg( indicatorTitle
) : null;
4275 if ( this.indicatorTitle
!== indicatorTitle
) {
4276 this.indicatorTitle
= indicatorTitle
;
4277 if ( this.$indicator
) {
4278 if ( this.indicatorTitle
!== null ) {
4279 this.$indicator
.attr( 'title', indicatorTitle
);
4281 this.$indicator
.removeAttr( 'title' );
4292 * @return {string} title Symbolic name of indicator
4294 OO
.ui
.IndicatorElement
.prototype.getIndicator = function () {
4295 return this.indicator
;
4299 * Get indicator title.
4301 * @return {string} Indicator title text
4303 OO
.ui
.IndicatorElement
.prototype.getIndicatorTitle = function () {
4304 return this.indicatorTitle
;
4308 * Element containing a label.
4314 * @param {Object} [config] Configuration options
4315 * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
4316 * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
4317 * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
4319 OO
.ui
.LabelElement
= function OoUiLabelElement( config
) {
4320 // Config intialization
4321 config
= config
|| {};
4326 this.autoFitLabel
= config
.autoFitLabel
=== undefined || !!config
.autoFitLabel
;
4329 this.setLabel( config
.label
|| this.constructor.static.label
);
4330 this.setLabelElement( config
.$label
|| this.$( '<span>' ) );
4335 OO
.initClass( OO
.ui
.LabelElement
);
4337 /* Static Properties */
4344 * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
4347 OO
.ui
.LabelElement
.static.label
= null;
4352 * Set the label element.
4354 * If an element is already set, it will be cleaned up before setting up the new element.
4356 * @param {jQuery} $label Element to use as label
4358 OO
.ui
.LabelElement
.prototype.setLabelElement = function ( $label
) {
4359 if ( this.$label
) {
4360 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
4363 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
4364 this.setLabelContent( this.label
);
4370 * An empty string will result in the label being hidden. A string containing only whitespace will
4371 * be converted to a single
4373 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4374 * text; or null for no label
4377 OO
.ui
.LabelElement
.prototype.setLabel = function ( label
) {
4378 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
4379 label
= ( typeof label
=== 'string' && label
.length
) || label
instanceof jQuery
? label
: null;
4381 if ( this.label
!== label
) {
4382 if ( this.$label
) {
4383 this.setLabelContent( label
);
4388 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
4396 * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4397 * text; or null for no label
4399 OO
.ui
.LabelElement
.prototype.getLabel = function () {
4408 OO
.ui
.LabelElement
.prototype.fitLabel = function () {
4409 if ( this.$label
&& this.$label
.autoEllipsis
&& this.autoFitLabel
) {
4410 this.$label
.autoEllipsis( { hasSpan
: false, tooltip
: true } );
4417 * Set the content of the label.
4419 * Do not call this method until after the label element has been set by #setLabelElement.
4422 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4423 * text; or null for no label
4425 OO
.ui
.LabelElement
.prototype.setLabelContent = function ( label
) {
4426 if ( typeof label
=== 'string' ) {
4427 if ( label
.match( /^\s*$/ ) ) {
4428 // Convert whitespace only string to a single non-breaking space
4429 this.$label
.html( ' ' );
4431 this.$label
.text( label
);
4433 } else if ( label
instanceof jQuery
) {
4434 this.$label
.empty().append( label
);
4436 this.$label
.empty();
4438 this.$label
.css( 'display', !label
? 'none' : '' );
4442 * Element containing an OO.ui.PopupWidget object.
4448 * @param {Object} [config] Configuration options
4449 * @cfg {Object} [popup] Configuration to pass to popup
4450 * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
4452 OO
.ui
.PopupElement
= function OoUiPopupElement( config
) {
4453 // Configuration initialization
4454 config
= config
|| {};
4457 this.popup
= new OO
.ui
.PopupWidget( $.extend(
4458 { autoClose
: true },
4460 { $: this.$, $autoCloseIgnore
: this.$element
}
4469 * @return {OO.ui.PopupWidget} Popup widget
4471 OO
.ui
.PopupElement
.prototype.getPopup = function () {
4476 * Element with named flags that can be added, removed, listed and checked.
4478 * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
4479 * the flag name. Flags are primarily useful for styling.
4485 * @param {Object} [config] Configuration options
4486 * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
4487 * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
4489 OO
.ui
.FlaggedElement
= function OoUiFlaggedElement( config
) {
4490 // Config initialization
4491 config
= config
|| {};
4495 this.$flagged
= null;
4498 this.setFlags( config
.flags
);
4499 this.setFlaggedElement( config
.$flagged
|| this.$element
);
4506 * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
4507 * added/removed properties
4513 * Set the flagged element.
4515 * If an element is already set, it will be cleaned up before setting up the new element.
4517 * @param {jQuery} $flagged Element to add flags to
4519 OO
.ui
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
4520 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
4521 return 'oo-ui-flaggedElement-' + flag
;
4524 if ( this.$flagged
) {
4525 this.$flagged
.removeClass( classNames
);
4528 this.$flagged
= $flagged
.addClass( classNames
);
4532 * Check if a flag is set.
4534 * @param {string} flag Name of flag
4535 * @return {boolean} Has flag
4537 OO
.ui
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
4538 return flag
in this.flags
;
4542 * Get the names of all flags set.
4544 * @return {string[]} flags Flag names
4546 OO
.ui
.FlaggedElement
.prototype.getFlags = function () {
4547 return Object
.keys( this.flags
);
4556 OO
.ui
.FlaggedElement
.prototype.clearFlags = function () {
4557 var flag
, className
,
4560 classPrefix
= 'oo-ui-flaggedElement-';
4562 for ( flag
in this.flags
) {
4563 className
= classPrefix
+ flag
;
4564 changes
[flag
] = false;
4565 delete this.flags
[flag
];
4566 remove
.push( className
);
4569 if ( this.$flagged
) {
4570 this.$flagged
.removeClass( remove
.join( ' ' ) );
4573 this.updateThemeClasses();
4574 this.emit( 'flag', changes
);
4580 * Add one or more flags.
4582 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
4583 * keyed by flag name containing boolean set/remove instructions.
4587 OO
.ui
.FlaggedElement
.prototype.setFlags = function ( flags
) {
4588 var i
, len
, flag
, className
,
4592 classPrefix
= 'oo-ui-flaggedElement-';
4594 if ( typeof flags
=== 'string' ) {
4595 className
= classPrefix
+ flags
;
4597 if ( !this.flags
[flags
] ) {
4598 this.flags
[flags
] = true;
4599 add
.push( className
);
4601 } else if ( $.isArray( flags
) ) {
4602 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
4604 className
= classPrefix
+ flag
;
4606 if ( !this.flags
[flag
] ) {
4607 changes
[flag
] = true;
4608 this.flags
[flag
] = true;
4609 add
.push( className
);
4612 } else if ( OO
.isPlainObject( flags
) ) {
4613 for ( flag
in flags
) {
4614 className
= classPrefix
+ flag
;
4615 if ( flags
[flag
] ) {
4617 if ( !this.flags
[flag
] ) {
4618 changes
[flag
] = true;
4619 this.flags
[flag
] = true;
4620 add
.push( className
);
4624 if ( this.flags
[flag
] ) {
4625 changes
[flag
] = false;
4626 delete this.flags
[flag
];
4627 remove
.push( className
);
4633 if ( this.$flagged
) {
4635 .addClass( add
.join( ' ' ) )
4636 .removeClass( remove
.join( ' ' ) );
4639 this.updateThemeClasses();
4640 this.emit( 'flag', changes
);
4646 * Element with a title.
4648 * Titles are rendered by the browser and are made visible when hovering the element. Titles are
4649 * not visible on touch devices.
4655 * @param {Object} [config] Configuration options
4656 * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
4657 * @cfg {string|Function} [title] Title text or a function that returns text
4659 OO
.ui
.TitledElement
= function OoUiTitledElement( config
) {
4660 // Config intialization
4661 config
= config
|| {};
4664 this.$titled
= null;
4668 this.setTitle( config
.title
|| this.constructor.static.title
);
4669 this.setTitledElement( config
.$titled
|| this.$element
);
4674 OO
.initClass( OO
.ui
.TitledElement
);
4676 /* Static Properties */
4683 * @property {string|Function} Title text or a function that returns text
4685 OO
.ui
.TitledElement
.static.title
= null;
4690 * Set the titled element.
4692 * If an element is already set, it will be cleaned up before setting up the new element.
4694 * @param {jQuery} $titled Element to set title on
4696 OO
.ui
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
4697 if ( this.$titled
) {
4698 this.$titled
.removeAttr( 'title' );
4701 this.$titled
= $titled
;
4703 this.$titled
.attr( 'title', this.title
);
4710 * @param {string|Function|null} title Title text, a function that returns text or null for no title
4713 OO
.ui
.TitledElement
.prototype.setTitle = function ( title
) {
4714 title
= typeof title
=== 'string' ? OO
.ui
.resolveMsg( title
) : null;
4716 if ( this.title
!== title
) {
4717 if ( this.$titled
) {
4718 if ( title
!== null ) {
4719 this.$titled
.attr( 'title', title
);
4721 this.$titled
.removeAttr( 'title' );
4733 * @return {string} Title string
4735 OO
.ui
.TitledElement
.prototype.getTitle = function () {
4740 * Element that can be automatically clipped to visible boundaries.
4742 * Whenever the element's natural height changes, you have to call
4743 * #clip to make sure it's still clipping correctly.
4749 * @param {Object} [config] Configuration options
4750 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
4752 OO
.ui
.ClippableElement
= function OoUiClippableElement( config
) {
4753 // Configuration initialization
4754 config
= config
|| {};
4757 this.$clippable
= null;
4758 this.clipping
= false;
4759 this.clippedHorizontally
= false;
4760 this.clippedVertically
= false;
4761 this.$clippableContainer
= null;
4762 this.$clippableScroller
= null;
4763 this.$clippableWindow
= null;
4764 this.idealWidth
= null;
4765 this.idealHeight
= null;
4766 this.onClippableContainerScrollHandler
= this.clip
.bind( this );
4767 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4770 this.setClippableElement( config
.$clippable
|| this.$element
);
4776 * Set clippable element.
4778 * If an element is already set, it will be cleaned up before setting up the new element.
4780 * @param {jQuery} $clippable Element to make clippable
4782 OO
.ui
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4783 if ( this.$clippable
) {
4784 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4785 this.$clippable
.css( { width
: '', height
: '' } );
4786 this.$clippable
.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4787 this.$clippable
.css( { overflowX
: '', overflowY
: '' } );
4790 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4797 * Do not turn clipping on until after the element is attached to the DOM and visible.
4799 * @param {boolean} [clipping] Enable clipping, omit to toggle
4802 OO
.ui
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4803 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4805 if ( this.clipping
!== clipping
) {
4806 this.clipping
= clipping
;
4808 this.$clippableContainer
= this.$( this.getClosestScrollableElementContainer() );
4809 // If the clippable container is the body, we have to listen to scroll events and check
4810 // jQuery.scrollTop on the window because of browser inconsistencies
4811 this.$clippableScroller
= this.$clippableContainer
.is( 'body' ) ?
4812 this.$( OO
.ui
.Element
.getWindow( this.$clippableContainer
) ) :
4813 this.$clippableContainer
;
4814 this.$clippableScroller
.on( 'scroll', this.onClippableContainerScrollHandler
);
4815 this.$clippableWindow
= this.$( this.getElementWindow() )
4816 .on( 'resize', this.onClippableWindowResizeHandler
);
4817 // Initial clip after visible
4820 this.$clippable
.css( { width
: '', height
: '' } );
4821 this.$clippable
.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4822 this.$clippable
.css( { overflowX
: '', overflowY
: '' } );
4824 this.$clippableContainer
= null;
4825 this.$clippableScroller
.off( 'scroll', this.onClippableContainerScrollHandler
);
4826 this.$clippableScroller
= null;
4827 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4828 this.$clippableWindow
= null;
4836 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4838 * @return {boolean} Element will be clipped to the visible area
4840 OO
.ui
.ClippableElement
.prototype.isClipping = function () {
4841 return this.clipping
;
4845 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4847 * @return {boolean} Part of the element is being clipped
4849 OO
.ui
.ClippableElement
.prototype.isClipped = function () {
4850 return this.clippedHorizontally
|| this.clippedVertically
;
4854 * Check if the right of the element is being clipped by the nearest scrollable container.
4856 * @return {boolean} Part of the element is being clipped
4858 OO
.ui
.ClippableElement
.prototype.isClippedHorizontally = function () {
4859 return this.clippedHorizontally
;
4863 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4865 * @return {boolean} Part of the element is being clipped
4867 OO
.ui
.ClippableElement
.prototype.isClippedVertically = function () {
4868 return this.clippedVertically
;
4872 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4874 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4875 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4877 OO
.ui
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4878 this.idealWidth
= width
;
4879 this.idealHeight
= height
;
4881 if ( !this.clipping
) {
4882 // Update dimensions
4883 this.$clippable
.css( { width
: width
, height
: height
} );
4885 // While clipping, idealWidth and idealHeight are not considered
4889 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
4890 * the element's natural height changes.
4892 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4893 * overlapped by, the visible area of the nearest scrollable container.
4897 OO
.ui
.ClippableElement
.prototype.clip = function () {
4898 if ( !this.clipping
) {
4899 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
4904 cOffset
= this.$clippable
.offset(),
4905 $container
= this.$clippableContainer
.is( 'body' ) ?
4906 this.$clippableWindow
: this.$clippableContainer
,
4907 ccOffset
= $container
.offset() || { top
: 0, left
: 0 },
4908 ccHeight
= $container
.innerHeight() - buffer
,
4909 ccWidth
= $container
.innerWidth() - buffer
,
4910 scrollTop
= this.$clippableScroller
.scrollTop(),
4911 scrollLeft
= this.$clippableScroller
.scrollLeft(),
4912 desiredWidth
= ( ccOffset
.left
+ scrollLeft
+ ccWidth
) - cOffset
.left
,
4913 desiredHeight
= ( ccOffset
.top
+ scrollTop
+ ccHeight
) - cOffset
.top
,
4914 naturalWidth
= this.$clippable
.prop( 'scrollWidth' ),
4915 naturalHeight
= this.$clippable
.prop( 'scrollHeight' ),
4916 clipWidth
= desiredWidth
< naturalWidth
,
4917 clipHeight
= desiredHeight
< naturalHeight
;
4920 this.$clippable
.css( { overflowX
: 'scroll', width
: desiredWidth
} );
4922 this.$clippable
.css( 'width', this.idealWidth
|| '' );
4923 this.$clippable
.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4924 this.$clippable
.css( 'overflowX', '' );
4927 this.$clippable
.css( { overflowY
: 'scroll', height
: desiredHeight
} );
4929 this.$clippable
.css( 'height', this.idealHeight
|| '' );
4930 this.$clippable
.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4931 this.$clippable
.css( 'overflowY', '' );
4934 this.clippedHorizontally
= clipWidth
;
4935 this.clippedVertically
= clipHeight
;
4941 * Generic toolbar tool.
4945 * @extends OO.ui.Widget
4946 * @mixins OO.ui.IconElement
4947 * @mixins OO.ui.FlaggedElement
4950 * @param {OO.ui.ToolGroup} toolGroup
4951 * @param {Object} [config] Configuration options
4952 * @cfg {string|Function} [title] Title text or a function that returns text
4954 OO
.ui
.Tool
= function OoUiTool( toolGroup
, config
) {
4955 // Config intialization
4956 config
= config
|| {};
4958 // Parent constructor
4959 OO
.ui
.Tool
.super.call( this, config
);
4961 // Mixin constructors
4962 OO
.ui
.IconElement
.call( this, config
);
4963 OO
.ui
.FlaggedElement
.call( this, config
);
4966 this.toolGroup
= toolGroup
;
4967 this.toolbar
= this.toolGroup
.getToolbar();
4968 this.active
= false;
4969 this.$title
= this.$( '<span>' );
4970 this.$link
= this.$( '<a>' );
4974 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
4977 this.$title
.addClass( 'oo-ui-tool-title' );
4979 .addClass( 'oo-ui-tool-link' )
4980 .append( this.$icon
, this.$title
)
4981 .prop( 'tabIndex', 0 )
4982 .attr( 'role', 'button' );
4984 .data( 'oo-ui-tool', this )
4986 'oo-ui-tool ' + 'oo-ui-tool-name-' +
4987 this.constructor.static.name
.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
4989 .append( this.$link
);
4990 this.setTitle( config
.title
|| this.constructor.static.title
);
4995 OO
.inheritClass( OO
.ui
.Tool
, OO
.ui
.Widget
);
4996 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.IconElement
);
4997 OO
.mixinClass( OO
.ui
.Tool
, OO
.ui
.FlaggedElement
);
5005 /* Static Properties */
5011 OO
.ui
.Tool
.static.tagName
= 'span';
5014 * Symbolic name of tool.
5019 * @property {string}
5021 OO
.ui
.Tool
.static.name
= '';
5029 * @property {string}
5031 OO
.ui
.Tool
.static.group
= '';
5036 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
5037 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
5038 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
5039 * appended to the title if the tool is part of a bar tool group.
5044 * @property {string|Function} Title text or a function that returns text
5046 OO
.ui
.Tool
.static.title
= '';
5049 * Tool can be automatically added to catch-all groups.
5053 * @property {boolean}
5055 OO
.ui
.Tool
.static.autoAddToCatchall
= true;
5058 * Tool can be automatically added to named groups.
5061 * @property {boolean}
5064 OO
.ui
.Tool
.static.autoAddToGroup
= true;
5067 * Check if this tool is compatible with given data.
5071 * @param {Mixed} data Data to check
5072 * @return {boolean} Tool can be used with data
5074 OO
.ui
.Tool
.static.isCompatibleWith = function () {
5081 * Handle the toolbar state being updated.
5083 * This is an abstract method that must be overridden in a concrete subclass.
5087 OO
.ui
.Tool
.prototype.onUpdateState = function () {
5089 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
5094 * Handle the tool being selected.
5096 * This is an abstract method that must be overridden in a concrete subclass.
5100 OO
.ui
.Tool
.prototype.onSelect = function () {
5102 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
5107 * Check if the button is active.
5109 * @param {boolean} Button is active
5111 OO
.ui
.Tool
.prototype.isActive = function () {
5116 * Make the button appear active or inactive.
5118 * @param {boolean} state Make button appear active
5120 OO
.ui
.Tool
.prototype.setActive = function ( state
) {
5121 this.active
= !!state
;
5122 if ( this.active
) {
5123 this.$element
.addClass( 'oo-ui-tool-active' );
5125 this.$element
.removeClass( 'oo-ui-tool-active' );
5130 * Get the tool title.
5132 * @param {string|Function} title Title text or a function that returns text
5135 OO
.ui
.Tool
.prototype.setTitle = function ( title
) {
5136 this.title
= OO
.ui
.resolveMsg( title
);
5142 * Get the tool title.
5144 * @return {string} Title text
5146 OO
.ui
.Tool
.prototype.getTitle = function () {
5151 * Get the tool's symbolic name.
5153 * @return {string} Symbolic name of tool
5155 OO
.ui
.Tool
.prototype.getName = function () {
5156 return this.constructor.static.name
;
5162 OO
.ui
.Tool
.prototype.updateTitle = function () {
5163 var titleTooltips
= this.toolGroup
.constructor.static.titleTooltips
,
5164 accelTooltips
= this.toolGroup
.constructor.static.accelTooltips
,
5165 accel
= this.toolbar
.getToolAccelerator( this.constructor.static.name
),
5172 .addClass( 'oo-ui-tool-accel' )
5176 if ( titleTooltips
&& typeof this.title
=== 'string' && this.title
.length
) {
5177 tooltipParts
.push( this.title
);
5179 if ( accelTooltips
&& typeof accel
=== 'string' && accel
.length
) {
5180 tooltipParts
.push( accel
);
5182 if ( tooltipParts
.length
) {
5183 this.$link
.attr( 'title', tooltipParts
.join( ' ' ) );
5185 this.$link
.removeAttr( 'title' );
5192 OO
.ui
.Tool
.prototype.destroy = function () {
5193 this.toolbar
.disconnect( this );
5194 this.$element
.remove();
5198 * Collection of tool groups.
5201 * @extends OO.ui.Element
5202 * @mixins OO.EventEmitter
5203 * @mixins OO.ui.GroupElement
5206 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
5207 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
5208 * @param {Object} [config] Configuration options
5209 * @cfg {boolean} [actions] Add an actions section opposite to the tools
5210 * @cfg {boolean} [shadow] Add a shadow below the toolbar
5212 OO
.ui
.Toolbar
= function OoUiToolbar( toolFactory
, toolGroupFactory
, config
) {
5213 // Configuration initialization
5214 config
= config
|| {};
5216 // Parent constructor
5217 OO
.ui
.Toolbar
.super.call( this, config
);
5219 // Mixin constructors
5220 OO
.EventEmitter
.call( this );
5221 OO
.ui
.GroupElement
.call( this, config
);
5224 this.toolFactory
= toolFactory
;
5225 this.toolGroupFactory
= toolGroupFactory
;
5228 this.$bar
= this.$( '<div>' );
5229 this.$actions
= this.$( '<div>' );
5230 this.initialized
= false;
5234 .add( this.$bar
).add( this.$group
).add( this.$actions
)
5235 .on( 'mousedown touchstart', this.onPointerDown
.bind( this ) );
5238 this.$group
.addClass( 'oo-ui-toolbar-tools' );
5239 this.$bar
.addClass( 'oo-ui-toolbar-bar' ).append( this.$group
);
5240 if ( config
.actions
) {
5241 this.$actions
.addClass( 'oo-ui-toolbar-actions' );
5242 this.$bar
.append( this.$actions
);
5244 this.$bar
.append( '<div style="clear:both"></div>' );
5245 if ( config
.shadow
) {
5246 this.$bar
.append( '<div class="oo-ui-toolbar-shadow"></div>' );
5248 this.$element
.addClass( 'oo-ui-toolbar' ).append( this.$bar
);
5253 OO
.inheritClass( OO
.ui
.Toolbar
, OO
.ui
.Element
);
5254 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.EventEmitter
);
5255 OO
.mixinClass( OO
.ui
.Toolbar
, OO
.ui
.GroupElement
);
5260 * Get the tool factory.
5262 * @return {OO.ui.ToolFactory} Tool factory
5264 OO
.ui
.Toolbar
.prototype.getToolFactory = function () {
5265 return this.toolFactory
;
5269 * Get the tool group factory.
5271 * @return {OO.Factory} Tool group factory
5273 OO
.ui
.Toolbar
.prototype.getToolGroupFactory = function () {
5274 return this.toolGroupFactory
;
5278 * Handles mouse down events.
5280 * @param {jQuery.Event} e Mouse down event
5282 OO
.ui
.Toolbar
.prototype.onPointerDown = function ( e
) {
5283 var $closestWidgetToEvent
= this.$( e
.target
).closest( '.oo-ui-widget' ),
5284 $closestWidgetToToolbar
= this.$element
.closest( '.oo-ui-widget' );
5285 if ( !$closestWidgetToEvent
.length
|| $closestWidgetToEvent
[0] === $closestWidgetToToolbar
[0] ) {
5291 * Sets up handles and preloads required information for the toolbar to work.
5292 * This must be called immediately after it is attached to a visible document.
5294 OO
.ui
.Toolbar
.prototype.initialize = function () {
5295 this.initialized
= true;
5301 * Tools can be specified in the following ways:
5303 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
5304 * - All tools in a group: `{ group: 'group-name' }`
5305 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
5307 * @param {Object.<string,Array>} groups List of tool group configurations
5308 * @param {Array|string} [groups.include] Tools to include
5309 * @param {Array|string} [groups.exclude] Tools to exclude
5310 * @param {Array|string} [groups.promote] Tools to promote to the beginning
5311 * @param {Array|string} [groups.demote] Tools to demote to the end
5313 OO
.ui
.Toolbar
.prototype.setup = function ( groups
) {
5314 var i
, len
, type
, group
,
5316 defaultType
= 'bar';
5318 // Cleanup previous groups
5321 // Build out new groups
5322 for ( i
= 0, len
= groups
.length
; i
< len
; i
++ ) {
5324 if ( group
.include
=== '*' ) {
5325 // Apply defaults to catch-all groups
5326 if ( group
.type
=== undefined ) {
5327 group
.type
= 'list';
5329 if ( group
.label
=== undefined ) {
5330 group
.label
= OO
.ui
.msg( 'ooui-toolbar-more' );
5333 // Check type has been registered
5334 type
= this.getToolGroupFactory().lookup( group
.type
) ? group
.type
: defaultType
;
5336 this.getToolGroupFactory().create( type
, this, $.extend( { $: this.$ }, group
) )
5339 this.addItems( items
);
5343 * Remove all tools and groups from the toolbar.
5345 OO
.ui
.Toolbar
.prototype.reset = function () {
5350 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5351 this.items
[i
].destroy();
5357 * Destroys toolbar, removing event handlers and DOM elements.
5359 * Call this whenever you are done using a toolbar.
5361 OO
.ui
.Toolbar
.prototype.destroy = function () {
5363 this.$element
.remove();
5367 * Check if tool has not been used yet.
5369 * @param {string} name Symbolic name of tool
5370 * @return {boolean} Tool is available
5372 OO
.ui
.Toolbar
.prototype.isToolAvailable = function ( name
) {
5373 return !this.tools
[name
];
5377 * Prevent tool from being used again.
5379 * @param {OO.ui.Tool} tool Tool to reserve
5381 OO
.ui
.Toolbar
.prototype.reserveTool = function ( tool
) {
5382 this.tools
[tool
.getName()] = tool
;
5386 * Allow tool to be used again.
5388 * @param {OO.ui.Tool} tool Tool to release
5390 OO
.ui
.Toolbar
.prototype.releaseTool = function ( tool
) {
5391 delete this.tools
[tool
.getName()];
5395 * Get accelerator label for tool.
5397 * This is a stub that should be overridden to provide access to accelerator information.
5399 * @param {string} name Symbolic name of tool
5400 * @return {string|undefined} Tool accelerator label if available
5402 OO
.ui
.Toolbar
.prototype.getToolAccelerator = function () {
5407 * Collection of tools.
5409 * Tools can be specified in the following ways:
5411 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
5412 * - All tools in a group: `{ group: 'group-name' }`
5413 * - All tools: `'*'`
5417 * @extends OO.ui.Widget
5418 * @mixins OO.ui.GroupElement
5421 * @param {OO.ui.Toolbar} toolbar
5422 * @param {Object} [config] Configuration options
5423 * @cfg {Array|string} [include=[]] List of tools to include
5424 * @cfg {Array|string} [exclude=[]] List of tools to exclude
5425 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
5426 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
5428 OO
.ui
.ToolGroup
= function OoUiToolGroup( toolbar
, config
) {
5429 // Configuration initialization
5430 config
= config
|| {};
5432 // Parent constructor
5433 OO
.ui
.ToolGroup
.super.call( this, config
);
5435 // Mixin constructors
5436 OO
.ui
.GroupElement
.call( this, config
);
5439 this.toolbar
= toolbar
;
5441 this.pressed
= null;
5442 this.autoDisabled
= false;
5443 this.include
= config
.include
|| [];
5444 this.exclude
= config
.exclude
|| [];
5445 this.promote
= config
.promote
|| [];
5446 this.demote
= config
.demote
|| [];
5447 this.onCapturedMouseUpHandler
= this.onCapturedMouseUp
.bind( this );
5451 'mousedown touchstart': this.onPointerDown
.bind( this ),
5452 'mouseup touchend': this.onPointerUp
.bind( this ),
5453 mouseover
: this.onMouseOver
.bind( this ),
5454 mouseout
: this.onMouseOut
.bind( this )
5456 this.toolbar
.getToolFactory().connect( this, { register
: 'onToolFactoryRegister' } );
5457 this.aggregate( { disable
: 'itemDisable' } );
5458 this.connect( this, { itemDisable
: 'updateDisabled' } );
5461 this.$group
.addClass( 'oo-ui-toolGroup-tools' );
5463 .addClass( 'oo-ui-toolGroup' )
5464 .append( this.$group
);
5470 OO
.inheritClass( OO
.ui
.ToolGroup
, OO
.ui
.Widget
);
5471 OO
.mixinClass( OO
.ui
.ToolGroup
, OO
.ui
.GroupElement
);
5479 /* Static Properties */
5482 * Show labels in tooltips.
5486 * @property {boolean}
5488 OO
.ui
.ToolGroup
.static.titleTooltips
= false;
5491 * Show acceleration labels in tooltips.
5495 * @property {boolean}
5497 OO
.ui
.ToolGroup
.static.accelTooltips
= false;
5500 * Automatically disable the toolgroup when all tools are disabled
5504 * @property {boolean}
5506 OO
.ui
.ToolGroup
.static.autoDisable
= true;
5513 OO
.ui
.ToolGroup
.prototype.isDisabled = function () {
5514 return this.autoDisabled
|| OO
.ui
.ToolGroup
.super.prototype.isDisabled
.apply( this, arguments
);
5520 OO
.ui
.ToolGroup
.prototype.updateDisabled = function () {
5521 var i
, item
, allDisabled
= true;
5523 if ( this.constructor.static.autoDisable
) {
5524 for ( i
= this.items
.length
- 1; i
>= 0; i
-- ) {
5525 item
= this.items
[i
];
5526 if ( !item
.isDisabled() ) {
5527 allDisabled
= false;
5531 this.autoDisabled
= allDisabled
;
5533 OO
.ui
.ToolGroup
.super.prototype.updateDisabled
.apply( this, arguments
);
5537 * Handle mouse down events.
5539 * @param {jQuery.Event} e Mouse down event
5541 OO
.ui
.ToolGroup
.prototype.onPointerDown = function ( e
) {
5542 // e.which is 0 for touch events, 1 for left mouse button
5543 if ( !this.isDisabled() && e
.which
<= 1 ) {
5544 this.pressed
= this.getTargetTool( e
);
5545 if ( this.pressed
) {
5546 this.pressed
.setActive( true );
5547 this.getElementDocument().addEventListener(
5548 'mouseup', this.onCapturedMouseUpHandler
, true
5556 * Handle captured mouse up events.
5558 * @param {Event} e Mouse up event
5560 OO
.ui
.ToolGroup
.prototype.onCapturedMouseUp = function ( e
) {
5561 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler
, true );
5562 // onPointerUp may be called a second time, depending on where the mouse is when the button is
5563 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
5564 this.onPointerUp( e
);
5568 * Handle mouse up events.
5570 * @param {jQuery.Event} e Mouse up event
5572 OO
.ui
.ToolGroup
.prototype.onPointerUp = function ( e
) {
5573 var tool
= this.getTargetTool( e
);
5575 // e.which is 0 for touch events, 1 for left mouse button
5576 if ( !this.isDisabled() && e
.which
<= 1 && this.pressed
&& this.pressed
=== tool
) {
5577 this.pressed
.onSelect();
5580 this.pressed
= null;
5585 * Handle mouse over events.
5587 * @param {jQuery.Event} e Mouse over event
5589 OO
.ui
.ToolGroup
.prototype.onMouseOver = function ( e
) {
5590 var tool
= this.getTargetTool( e
);
5592 if ( this.pressed
&& this.pressed
=== tool
) {
5593 this.pressed
.setActive( true );
5598 * Handle mouse out events.
5600 * @param {jQuery.Event} e Mouse out event
5602 OO
.ui
.ToolGroup
.prototype.onMouseOut = function ( e
) {
5603 var tool
= this.getTargetTool( e
);
5605 if ( this.pressed
&& this.pressed
=== tool
) {
5606 this.pressed
.setActive( false );
5611 * Get the closest tool to a jQuery.Event.
5613 * Only tool links are considered, which prevents other elements in the tool such as popups from
5614 * triggering tool group interactions.
5617 * @param {jQuery.Event} e
5618 * @return {OO.ui.Tool|null} Tool, `null` if none was found
5620 OO
.ui
.ToolGroup
.prototype.getTargetTool = function ( e
) {
5622 $item
= this.$( e
.target
).closest( '.oo-ui-tool-link' );
5624 if ( $item
.length
) {
5625 tool
= $item
.parent().data( 'oo-ui-tool' );
5628 return tool
&& !tool
.isDisabled() ? tool
: null;
5632 * Handle tool registry register events.
5634 * If a tool is registered after the group is created, we must repopulate the list to account for:
5636 * - a tool being added that may be included
5637 * - a tool already included being overridden
5639 * @param {string} name Symbolic name of tool
5641 OO
.ui
.ToolGroup
.prototype.onToolFactoryRegister = function () {
5646 * Get the toolbar this group is in.
5648 * @return {OO.ui.Toolbar} Toolbar of group
5650 OO
.ui
.ToolGroup
.prototype.getToolbar = function () {
5651 return this.toolbar
;
5655 * Add and remove tools based on configuration.
5657 OO
.ui
.ToolGroup
.prototype.populate = function () {
5658 var i
, len
, name
, tool
,
5659 toolFactory
= this.toolbar
.getToolFactory(),
5663 list
= this.toolbar
.getToolFactory().getTools(
5664 this.include
, this.exclude
, this.promote
, this.demote
5667 // Build a list of needed tools
5668 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
5672 toolFactory
.lookup( name
) &&
5673 // Tool is available or is already in this group
5674 ( this.toolbar
.isToolAvailable( name
) || this.tools
[name
] )
5676 tool
= this.tools
[name
];
5678 // Auto-initialize tools on first use
5679 this.tools
[name
] = tool
= toolFactory
.create( name
, this );
5682 this.toolbar
.reserveTool( tool
);
5687 // Remove tools that are no longer needed
5688 for ( name
in this.tools
) {
5689 if ( !names
[name
] ) {
5690 this.tools
[name
].destroy();
5691 this.toolbar
.releaseTool( this.tools
[name
] );
5692 remove
.push( this.tools
[name
] );
5693 delete this.tools
[name
];
5696 if ( remove
.length
) {
5697 this.removeItems( remove
);
5699 // Update emptiness state
5701 this.$element
.removeClass( 'oo-ui-toolGroup-empty' );
5703 this.$element
.addClass( 'oo-ui-toolGroup-empty' );
5705 // Re-add tools (moving existing ones to new locations)
5706 this.addItems( add
);
5707 // Disabled state may depend on items
5708 this.updateDisabled();
5712 * Destroy tool group.
5714 OO
.ui
.ToolGroup
.prototype.destroy = function () {
5718 this.toolbar
.getToolFactory().disconnect( this );
5719 for ( name
in this.tools
) {
5720 this.toolbar
.releaseTool( this.tools
[name
] );
5721 this.tools
[name
].disconnect( this ).destroy();
5722 delete this.tools
[name
];
5724 this.$element
.remove();
5728 * Dialog for showing a message.
5731 * - Registers two actions by default (safe and primary).
5732 * - Renders action widgets in the footer.
5735 * @extends OO.ui.Dialog
5738 * @param {Object} [config] Configuration options
5740 OO
.ui
.MessageDialog
= function OoUiMessageDialog( config
) {
5741 // Parent constructor
5742 OO
.ui
.MessageDialog
.super.call( this, config
);
5745 this.verticalActionLayout
= null;
5748 this.$element
.addClass( 'oo-ui-messageDialog' );
5753 OO
.inheritClass( OO
.ui
.MessageDialog
, OO
.ui
.Dialog
);
5755 /* Static Properties */
5757 OO
.ui
.MessageDialog
.static.name
= 'message';
5759 OO
.ui
.MessageDialog
.static.size
= 'small';
5761 OO
.ui
.MessageDialog
.static.verbose
= false;
5766 * A confirmation dialog's title should describe what the progressive action will do. An alert
5767 * dialog's title should describe what event occured.
5771 * @property {jQuery|string|Function|null}
5773 OO
.ui
.MessageDialog
.static.title
= null;
5776 * A confirmation dialog's message should describe the consequences of the progressive action. An
5777 * alert dialog's message should describe why the event occured.
5781 * @property {jQuery|string|Function|null}
5783 OO
.ui
.MessageDialog
.static.message
= null;
5785 OO
.ui
.MessageDialog
.static.actions
= [
5786 { action
: 'accept', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-accept' ), flags
: 'primary' },
5787 { action
: 'reject', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-reject' ), flags
: 'safe' }
5795 OO
.ui
.MessageDialog
.prototype.onActionResize = function ( action
) {
5797 return OO
.ui
.ProcessDialog
.super.prototype.onActionResize
.call( this, action
);
5801 * Toggle action layout between vertical and horizontal.
5803 * @param {boolean} [value] Layout actions vertically, omit to toggle
5806 OO
.ui
.MessageDialog
.prototype.toggleVerticalActionLayout = function ( value
) {
5807 value
= value
=== undefined ? !this.verticalActionLayout
: !!value
;
5809 if ( value
!== this.verticalActionLayout
) {
5810 this.verticalActionLayout
= value
;
5812 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value
)
5813 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value
);
5822 OO
.ui
.MessageDialog
.prototype.getActionProcess = function ( action
) {
5824 return new OO
.ui
.Process( function () {
5825 this.close( { action
: action
} );
5828 return OO
.ui
.MessageDialog
.super.prototype.getActionProcess
.call( this, action
);
5834 * @param {Object} [data] Dialog opening data
5835 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
5836 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
5837 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
5838 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
5841 OO
.ui
.MessageDialog
.prototype.getSetupProcess = function ( data
) {
5845 return OO
.ui
.MessageDialog
.super.prototype.getSetupProcess
.call( this, data
)
5846 .next( function () {
5847 this.title
.setLabel(
5848 data
.title
!== undefined ? data
.title
: this.constructor.static.title
5850 this.message
.setLabel(
5851 data
.message
!== undefined ? data
.message
: this.constructor.static.message
5853 this.message
.$element
.toggleClass(
5854 'oo-ui-messageDialog-message-verbose',
5855 data
.verbose
!== undefined ? data
.verbose
: this.constructor.static.verbose
5863 OO
.ui
.MessageDialog
.prototype.getBodyHeight = function () {
5864 return Math
.round( this.text
.$element
.outerHeight( true ) );
5870 OO
.ui
.MessageDialog
.prototype.initialize = function () {
5872 OO
.ui
.MessageDialog
.super.prototype.initialize
.call( this );
5875 this.$actions
= this.$( '<div>' );
5876 this.container
= new OO
.ui
.PanelLayout( {
5877 $: this.$, scrollable
: true, classes
: [ 'oo-ui-messageDialog-container' ]
5879 this.text
= new OO
.ui
.PanelLayout( {
5880 $: this.$, padded
: true, expanded
: false, classes
: [ 'oo-ui-messageDialog-text' ]
5882 this.message
= new OO
.ui
.LabelWidget( {
5883 $: this.$, classes
: [ 'oo-ui-messageDialog-message' ]
5887 this.title
.$element
.addClass( 'oo-ui-messageDialog-title' );
5888 this.$content
.addClass( 'oo-ui-messageDialog-content' );
5889 this.container
.$element
.append( this.text
.$element
);
5890 this.text
.$element
.append( this.title
.$element
, this.message
.$element
);
5891 this.$body
.append( this.container
.$element
);
5892 this.$actions
.addClass( 'oo-ui-messageDialog-actions' );
5893 this.$foot
.append( this.$actions
);
5899 OO
.ui
.MessageDialog
.prototype.attachActions = function () {
5900 var i
, len
, other
, special
, others
;
5903 OO
.ui
.MessageDialog
.super.prototype.attachActions
.call( this );
5905 special
= this.actions
.getSpecial();
5906 others
= this.actions
.getOthers();
5907 if ( special
.safe
) {
5908 this.$actions
.append( special
.safe
.$element
);
5909 special
.safe
.toggleFramed( false );
5911 if ( others
.length
) {
5912 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
5914 this.$actions
.append( other
.$element
);
5915 other
.toggleFramed( false );
5918 if ( special
.primary
) {
5919 this.$actions
.append( special
.primary
.$element
);
5920 special
.primary
.toggleFramed( false );
5924 if ( !this.isOpening() ) {
5925 this.manager
.updateWindowSize( this );
5927 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
5931 * Fit action actions into columns or rows.
5933 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
5935 OO
.ui
.MessageDialog
.prototype.fitActions = function () {
5937 actions
= this.actions
.get();
5940 this.toggleVerticalActionLayout( false );
5941 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
5942 action
= actions
[i
];
5943 if ( action
.$element
.innerWidth() < action
.$label
.outerWidth( true ) ) {
5944 this.toggleVerticalActionLayout( true );
5951 * Navigation dialog window.
5954 * - Show and hide errors.
5955 * - Retry an action.
5958 * - Renders header with dialog title and one action widget on either side
5959 * (a 'safe' button on the left, and a 'primary' button on the right, both of
5960 * which close the dialog).
5961 * - Displays any action widgets in the footer (none by default).
5962 * - Ability to dismiss errors.
5964 * Subclass responsibilities:
5965 * - Register a 'safe' action.
5966 * - Register a 'primary' action.
5967 * - Add content to the dialog.
5971 * @extends OO.ui.Dialog
5974 * @param {Object} [config] Configuration options
5976 OO
.ui
.ProcessDialog
= function OoUiProcessDialog( config
) {
5977 // Parent constructor
5978 OO
.ui
.ProcessDialog
.super.call( this, config
);
5981 this.$element
.addClass( 'oo-ui-processDialog' );
5986 OO
.inheritClass( OO
.ui
.ProcessDialog
, OO
.ui
.Dialog
);
5991 * Handle dismiss button click events.
5995 OO
.ui
.ProcessDialog
.prototype.onDismissErrorButtonClick = function () {
6000 * Handle retry button click events.
6002 * Hides errors and then tries again.
6004 OO
.ui
.ProcessDialog
.prototype.onRetryButtonClick = function () {
6006 this.executeAction( this.currentAction
.getAction() );
6012 OO
.ui
.ProcessDialog
.prototype.onActionResize = function ( action
) {
6013 if ( this.actions
.isSpecial( action
) ) {
6016 return OO
.ui
.ProcessDialog
.super.prototype.onActionResize
.call( this, action
);
6022 OO
.ui
.ProcessDialog
.prototype.initialize = function () {
6024 OO
.ui
.ProcessDialog
.super.prototype.initialize
.call( this );
6027 this.$navigation
= this.$( '<div>' );
6028 this.$location
= this.$( '<div>' );
6029 this.$safeActions
= this.$( '<div>' );
6030 this.$primaryActions
= this.$( '<div>' );
6031 this.$otherActions
= this.$( '<div>' );
6032 this.dismissButton
= new OO
.ui
.ButtonWidget( {
6034 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' )
6036 this.retryButton
= new OO
.ui
.ButtonWidget( {
6038 label
: OO
.ui
.msg( 'ooui-dialog-process-retry' )
6040 this.$errors
= this.$( '<div>' );
6041 this.$errorsTitle
= this.$( '<div>' );
6044 this.dismissButton
.connect( this, { click
: 'onDismissErrorButtonClick' } );
6045 this.retryButton
.connect( this, { click
: 'onRetryButtonClick' } );
6048 this.title
.$element
.addClass( 'oo-ui-processDialog-title' );
6050 .append( this.title
.$element
)
6051 .addClass( 'oo-ui-processDialog-location' );
6052 this.$safeActions
.addClass( 'oo-ui-processDialog-actions-safe' );
6053 this.$primaryActions
.addClass( 'oo-ui-processDialog-actions-primary' );
6054 this.$otherActions
.addClass( 'oo-ui-processDialog-actions-other' );
6056 .addClass( 'oo-ui-processDialog-errors-title' )
6057 .text( OO
.ui
.msg( 'ooui-dialog-process-error' ) );
6059 .addClass( 'oo-ui-processDialog-errors' )
6060 .append( this.$errorsTitle
, this.dismissButton
.$element
, this.retryButton
.$element
);
6062 .addClass( 'oo-ui-processDialog-content' )
6063 .append( this.$errors
);
6065 .addClass( 'oo-ui-processDialog-navigation' )
6066 .append( this.$safeActions
, this.$location
, this.$primaryActions
);
6067 this.$head
.append( this.$navigation
);
6068 this.$foot
.append( this.$otherActions
);
6074 OO
.ui
.ProcessDialog
.prototype.attachActions = function () {
6075 var i
, len
, other
, special
, others
;
6078 OO
.ui
.ProcessDialog
.super.prototype.attachActions
.call( this );
6080 special
= this.actions
.getSpecial();
6081 others
= this.actions
.getOthers();
6082 if ( special
.primary
) {
6083 this.$primaryActions
.append( special
.primary
.$element
);
6084 special
.primary
.toggleFramed( true );
6086 if ( others
.length
) {
6087 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
6089 this.$otherActions
.append( other
.$element
);
6090 other
.toggleFramed( true );
6093 if ( special
.safe
) {
6094 this.$safeActions
.append( special
.safe
.$element
);
6095 special
.safe
.toggleFramed( true );
6099 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
6105 OO
.ui
.ProcessDialog
.prototype.executeAction = function ( action
) {
6106 OO
.ui
.ProcessDialog
.super.prototype.executeAction
.call( this, action
)
6107 .fail( this.showErrors
.bind( this ) );
6111 * Fit label between actions.
6115 OO
.ui
.ProcessDialog
.prototype.fitLabel = function () {
6116 var width
= Math
.max(
6117 this.$safeActions
.is( ':visible' ) ? this.$safeActions
.width() : 0,
6118 this.$primaryActions
.is( ':visible' ) ? this.$primaryActions
.width() : 0
6120 this.$location
.css( { paddingLeft
: width
, paddingRight
: width
} );
6126 * Handle errors that occured durring accept or reject processes.
6128 * @param {OO.ui.Error[]} errors Errors to be handled
6130 OO
.ui
.ProcessDialog
.prototype.showErrors = function ( errors
) {
6135 for ( i
= 0, len
= errors
.length
; i
< len
; i
++ ) {
6136 if ( !errors
[i
].isRecoverable() ) {
6137 recoverable
= false;
6139 $item
= this.$( '<div>' )
6140 .addClass( 'oo-ui-processDialog-error' )
6141 .append( errors
[i
].getMessage() );
6142 items
.push( $item
[0] );
6144 this.$errorItems
= this.$( items
);
6145 if ( recoverable
) {
6146 this.retryButton
.clearFlags().setFlags( this.currentAction
.getFlags() );
6148 this.currentAction
.setDisabled( true );
6150 this.retryButton
.toggle( recoverable
);
6151 this.$errorsTitle
.after( this.$errorItems
);
6152 this.$errors
.show().scrollTop( 0 );
6158 OO
.ui
.ProcessDialog
.prototype.hideErrors = function () {
6159 this.$errors
.hide();
6160 this.$errorItems
.remove();
6161 this.$errorItems
= null;
6165 * Layout containing a series of pages.
6168 * @extends OO.ui.Layout
6171 * @param {Object} [config] Configuration options
6172 * @cfg {boolean} [continuous=false] Show all pages, one after another
6173 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
6174 * @cfg {boolean} [outlined=false] Show an outline
6175 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
6177 OO
.ui
.BookletLayout
= function OoUiBookletLayout( config
) {
6178 // Initialize configuration
6179 config
= config
|| {};
6181 // Parent constructor
6182 OO
.ui
.BookletLayout
.super.call( this, config
);
6185 this.currentPageName
= null;
6187 this.ignoreFocus
= false;
6188 this.stackLayout
= new OO
.ui
.StackLayout( { $: this.$, continuous
: !!config
.continuous
} );
6189 this.autoFocus
= config
.autoFocus
=== undefined || !!config
.autoFocus
;
6190 this.outlineVisible
= false;
6191 this.outlined
= !!config
.outlined
;
6192 if ( this.outlined
) {
6193 this.editable
= !!config
.editable
;
6194 this.outlineControlsWidget
= null;
6195 this.outlineWidget
= new OO
.ui
.OutlineWidget( { $: this.$ } );
6196 this.outlinePanel
= new OO
.ui
.PanelLayout( { $: this.$, scrollable
: true } );
6197 this.gridLayout
= new OO
.ui
.GridLayout(
6198 [ this.outlinePanel
, this.stackLayout
],
6199 { $: this.$, widths
: [ 1, 2 ] }
6201 this.outlineVisible
= true;
6202 if ( this.editable
) {
6203 this.outlineControlsWidget
= new OO
.ui
.OutlineControlsWidget(
6204 this.outlineWidget
, { $: this.$ }
6210 this.stackLayout
.connect( this, { set: 'onStackLayoutSet' } );
6211 if ( this.outlined
) {
6212 this.outlineWidget
.connect( this, { select
: 'onOutlineWidgetSelect' } );
6214 if ( this.autoFocus
) {
6215 // Event 'focus' does not bubble, but 'focusin' does
6216 this.stackLayout
.onDOMEvent( 'focusin', this.onStackLayoutFocus
.bind( this ) );
6220 this.$element
.addClass( 'oo-ui-bookletLayout' );
6221 this.stackLayout
.$element
.addClass( 'oo-ui-bookletLayout-stackLayout' );
6222 if ( this.outlined
) {
6223 this.outlinePanel
.$element
6224 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
6225 .append( this.outlineWidget
.$element
);
6226 if ( this.editable
) {
6227 this.outlinePanel
.$element
6228 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
6229 .append( this.outlineControlsWidget
.$element
);
6231 this.$element
.append( this.gridLayout
.$element
);
6233 this.$element
.append( this.stackLayout
.$element
);
6239 OO
.inheritClass( OO
.ui
.BookletLayout
, OO
.ui
.Layout
);
6245 * @param {OO.ui.PageLayout} page Current page
6250 * @param {OO.ui.PageLayout[]} page Added pages
6251 * @param {number} index Index pages were added at
6256 * @param {OO.ui.PageLayout[]} pages Removed pages
6262 * Handle stack layout focus.
6264 * @param {jQuery.Event} e Focusin event
6266 OO
.ui
.BookletLayout
.prototype.onStackLayoutFocus = function ( e
) {
6269 // Find the page that an element was focused within
6270 $target
= $( e
.target
).closest( '.oo-ui-pageLayout' );
6271 for ( name
in this.pages
) {
6272 // Check for page match, exclude current page to find only page changes
6273 if ( this.pages
[name
].$element
[0] === $target
[0] && name
!== this.currentPageName
) {
6274 this.setPage( name
);
6281 * Handle stack layout set events.
6283 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
6285 OO
.ui
.BookletLayout
.prototype.onStackLayoutSet = function ( page
) {
6286 var $input
, layout
= this;
6288 page
.scrollElementIntoView( { complete: function () {
6289 if ( layout
.autoFocus
) {
6290 // Set focus to the first input if nothing on the page is focused yet
6291 if ( !page
.$element
.find( ':focus' ).length
) {
6292 $input
= page
.$element
.find( ':input:first' );
6293 if ( $input
.length
) {
6303 * Handle outline widget select events.
6305 * @param {OO.ui.OptionWidget|null} item Selected item
6307 OO
.ui
.BookletLayout
.prototype.onOutlineWidgetSelect = function ( item
) {
6309 this.setPage( item
.getData() );
6314 * Check if booklet has an outline.
6318 OO
.ui
.BookletLayout
.prototype.isOutlined = function () {
6319 return this.outlined
;
6323 * Check if booklet has editing controls.
6327 OO
.ui
.BookletLayout
.prototype.isEditable = function () {
6328 return this.editable
;
6332 * Check if booklet has a visible outline.
6336 OO
.ui
.BookletLayout
.prototype.isOutlineVisible = function () {
6337 return this.outlined
&& this.outlineVisible
;
6341 * Hide or show the outline.
6343 * @param {boolean} [show] Show outline, omit to invert current state
6346 OO
.ui
.BookletLayout
.prototype.toggleOutline = function ( show
) {
6347 if ( this.outlined
) {
6348 show
= show
=== undefined ? !this.outlineVisible
: !!show
;
6349 this.outlineVisible
= show
;
6350 this.gridLayout
.layout( show
? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
6357 * Get the outline widget.
6359 * @param {OO.ui.PageLayout} page Page to be selected
6360 * @return {OO.ui.PageLayout|null} Closest page to another
6362 OO
.ui
.BookletLayout
.prototype.getClosestPage = function ( page
) {
6363 var next
, prev
, level
,
6364 pages
= this.stackLayout
.getItems(),
6365 index
= $.inArray( page
, pages
);
6367 if ( index
!== -1 ) {
6368 next
= pages
[index
+ 1];
6369 prev
= pages
[index
- 1];
6370 // Prefer adjacent pages at the same level
6371 if ( this.outlined
) {
6372 level
= this.outlineWidget
.getItemFromData( page
.getName() ).getLevel();
6375 level
=== this.outlineWidget
.getItemFromData( prev
.getName() ).getLevel()
6381 level
=== this.outlineWidget
.getItemFromData( next
.getName() ).getLevel()
6387 return prev
|| next
|| null;
6391 * Get the outline widget.
6393 * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
6395 OO
.ui
.BookletLayout
.prototype.getOutline = function () {
6396 return this.outlineWidget
;
6400 * Get the outline controls widget. If the outline is not editable, null is returned.
6402 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
6404 OO
.ui
.BookletLayout
.prototype.getOutlineControls = function () {
6405 return this.outlineControlsWidget
;
6409 * Get a page by name.
6411 * @param {string} name Symbolic name of page
6412 * @return {OO.ui.PageLayout|undefined} Page, if found
6414 OO
.ui
.BookletLayout
.prototype.getPage = function ( name
) {
6415 return this.pages
[name
];
6419 * Get the current page name.
6421 * @return {string|null} Current page name
6423 OO
.ui
.BookletLayout
.prototype.getPageName = function () {
6424 return this.currentPageName
;
6428 * Add a page to the layout.
6430 * When pages are added with the same names as existing pages, the existing pages will be
6431 * automatically removed before the new pages are added.
6433 * @param {OO.ui.PageLayout[]} pages Pages to add
6434 * @param {number} index Index to insert pages after
6438 OO
.ui
.BookletLayout
.prototype.addPages = function ( pages
, index
) {
6439 var i
, len
, name
, page
, item
, currentIndex
,
6440 stackLayoutPages
= this.stackLayout
.getItems(),
6444 // Remove pages with same names
6445 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
6447 name
= page
.getName();
6449 if ( Object
.prototype.hasOwnProperty
.call( this.pages
, name
) ) {
6450 // Correct the insertion index
6451 currentIndex
= $.inArray( this.pages
[name
], stackLayoutPages
);
6452 if ( currentIndex
!== -1 && currentIndex
+ 1 < index
) {
6455 remove
.push( this.pages
[name
] );
6458 if ( remove
.length
) {
6459 this.removePages( remove
);
6463 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
6465 name
= page
.getName();
6466 this.pages
[page
.getName()] = page
;
6467 if ( this.outlined
) {
6468 item
= new OO
.ui
.OutlineItemWidget( name
, page
, { $: this.$ } );
6469 page
.setOutlineItem( item
);
6474 if ( this.outlined
&& items
.length
) {
6475 this.outlineWidget
.addItems( items
, index
);
6476 this.updateOutlineWidget();
6478 this.stackLayout
.addItems( pages
, index
);
6479 this.emit( 'add', pages
, index
);
6485 * Remove a page from the layout.
6490 OO
.ui
.BookletLayout
.prototype.removePages = function ( pages
) {
6491 var i
, len
, name
, page
,
6494 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
6496 name
= page
.getName();
6497 delete this.pages
[name
];
6498 if ( this.outlined
) {
6499 items
.push( this.outlineWidget
.getItemFromData( name
) );
6500 page
.setOutlineItem( null );
6503 if ( this.outlined
&& items
.length
) {
6504 this.outlineWidget
.removeItems( items
);
6505 this.updateOutlineWidget();
6507 this.stackLayout
.removeItems( pages
);
6508 this.emit( 'remove', pages
);
6514 * Clear all pages from the layout.
6519 OO
.ui
.BookletLayout
.prototype.clearPages = function () {
6521 pages
= this.stackLayout
.getItems();
6524 this.currentPageName
= null;
6525 if ( this.outlined
) {
6526 this.outlineWidget
.clearItems();
6527 for ( i
= 0, len
= pages
.length
; i
< len
; i
++ ) {
6528 pages
[i
].setOutlineItem( null );
6531 this.stackLayout
.clearItems();
6533 this.emit( 'remove', pages
);
6539 * Set the current page by name.
6542 * @param {string} name Symbolic name of page
6544 OO
.ui
.BookletLayout
.prototype.setPage = function ( name
) {
6547 page
= this.pages
[name
];
6549 if ( name
!== this.currentPageName
) {
6550 if ( this.outlined
) {
6551 selectedItem
= this.outlineWidget
.getSelectedItem();
6552 if ( selectedItem
&& selectedItem
.getData() !== name
) {
6553 this.outlineWidget
.selectItem( this.outlineWidget
.getItemFromData( name
) );
6557 if ( this.currentPageName
&& this.pages
[this.currentPageName
] ) {
6558 this.pages
[this.currentPageName
].setActive( false );
6559 // Blur anything focused if the next page doesn't have anything focusable - this
6560 // is not needed if the next page has something focusable because once it is focused
6561 // this blur happens automatically
6562 if ( this.autoFocus
&& !page
.$element
.find( ':input' ).length
) {
6563 $focused
= this.pages
[this.currentPageName
].$element
.find( ':focus' );
6564 if ( $focused
.length
) {
6569 this.currentPageName
= name
;
6570 this.stackLayout
.setItem( page
);
6571 page
.setActive( true );
6572 this.emit( 'set', page
);
6578 * Call this after adding or removing items from the OutlineWidget.
6582 OO
.ui
.BookletLayout
.prototype.updateOutlineWidget = function () {
6583 // Auto-select first item when nothing is selected anymore
6584 if ( !this.outlineWidget
.getSelectedItem() ) {
6585 this.outlineWidget
.selectItem( this.outlineWidget
.getFirstSelectableItem() );
6592 * Layout made of a field and optional label.
6595 * @extends OO.ui.Layout
6596 * @mixins OO.ui.LabelElement
6598 * Available label alignment modes include:
6599 * - left: Label is before the field and aligned away from it, best for when the user will be
6600 * scanning for a specific label in a form with many fields
6601 * - right: Label is before the field and aligned toward it, best for forms the user is very
6602 * familiar with and will tab through field checking quickly to verify which field they are in
6603 * - top: Label is before the field and above it, best for when the user will need to fill out all
6604 * fields from top to bottom in a form with few fields
6605 * - inline: Label is after the field and aligned toward it, best for small boolean fields like
6606 * checkboxes or radio buttons
6609 * @param {OO.ui.Widget} fieldWidget Field widget
6610 * @param {Object} [config] Configuration options
6611 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
6612 * @cfg {string} [help] Explanatory text shown as a '?' icon.
6614 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
6615 // Config initialization
6616 config
= $.extend( { align
: 'left' }, config
);
6618 // Parent constructor
6619 OO
.ui
.FieldLayout
.super.call( this, config
);
6621 // Mixin constructors
6622 OO
.ui
.LabelElement
.call( this, config
);
6625 this.$field
= this.$( '<div>' );
6626 this.fieldWidget
= fieldWidget
;
6628 if ( config
.help
) {
6629 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
6631 classes
: [ 'oo-ui-fieldLayout-help' ],
6636 this.popupButtonWidget
.getPopup().$body
.append(
6638 .text( config
.help
)
6639 .addClass( 'oo-ui-fieldLayout-help-content' )
6641 this.$help
= this.popupButtonWidget
.$element
;
6643 this.$help
= this.$( [] );
6647 if ( this.fieldWidget
instanceof OO
.ui
.InputWidget
) {
6648 this.$label
.on( 'click', this.onLabelClick
.bind( this ) );
6650 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
6653 this.$element
.addClass( 'oo-ui-fieldLayout' );
6655 .addClass( 'oo-ui-fieldLayout-field' )
6656 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget
.isDisabled() )
6657 .append( this.fieldWidget
.$element
);
6658 this.setAlignment( config
.align
);
6663 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
6664 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.LabelElement
);
6669 * Handle field disable events.
6671 * @param {boolean} value Field is disabled
6673 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
6674 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
6678 * Handle label mouse click events.
6680 * @param {jQuery.Event} e Mouse click event
6682 OO
.ui
.FieldLayout
.prototype.onLabelClick = function () {
6683 this.fieldWidget
.simulateLabelClick();
6690 * @return {OO.ui.Widget} Field widget
6692 OO
.ui
.FieldLayout
.prototype.getField = function () {
6693 return this.fieldWidget
;
6697 * Set the field alignment mode.
6699 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
6702 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
6703 if ( value
!== this.align
) {
6704 // Default to 'left'
6705 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
6709 if ( value
=== 'inline' ) {
6710 this.$element
.append( this.$field
, this.$label
, this.$help
);
6712 this.$element
.append( this.$help
, this.$label
, this.$field
);
6714 // Set classes. The following classes can be used here:
6715 // * oo-ui-fieldLayout-align-left
6716 // * oo-ui-fieldLayout-align-right
6717 // * oo-ui-fieldLayout-align-top
6718 // * oo-ui-fieldLayout-align-inline
6720 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
6722 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
6730 * Layout made of a fieldset and optional legend.
6732 * Just add OO.ui.FieldLayout items.
6735 * @extends OO.ui.Layout
6736 * @mixins OO.ui.LabelElement
6737 * @mixins OO.ui.IconElement
6738 * @mixins OO.ui.GroupElement
6741 * @param {Object} [config] Configuration options
6742 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
6744 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
6745 // Config initialization
6746 config
= config
|| {};
6748 // Parent constructor
6749 OO
.ui
.FieldsetLayout
.super.call( this, config
);
6751 // Mixin constructors
6752 OO
.ui
.IconElement
.call( this, config
);
6753 OO
.ui
.LabelElement
.call( this, config
);
6754 OO
.ui
.GroupElement
.call( this, config
);
6758 .addClass( 'oo-ui-fieldsetLayout' )
6759 .prepend( this.$icon
, this.$label
, this.$group
);
6760 if ( $.isArray( config
.items
) ) {
6761 this.addItems( config
.items
);
6767 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
6768 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.IconElement
);
6769 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.LabelElement
);
6770 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.GroupElement
);
6773 * Layout with an HTML form.
6776 * @extends OO.ui.Layout
6779 * @param {Object} [config] Configuration options
6781 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
6782 // Configuration initialization
6783 config
= config
|| {};
6785 // Parent constructor
6786 OO
.ui
.FormLayout
.super.call( this, config
);
6789 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
6792 this.$element
.addClass( 'oo-ui-formLayout' );
6797 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
6805 /* Static Properties */
6807 OO
.ui
.FormLayout
.static.tagName
= 'form';
6812 * Handle form submit events.
6814 * @param {jQuery.Event} e Submit event
6817 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
6818 this.emit( 'submit' );
6823 * Layout made of proportionally sized columns and rows.
6826 * @extends OO.ui.Layout
6829 * @param {OO.ui.PanelLayout[]} panels Panels in the grid
6830 * @param {Object} [config] Configuration options
6831 * @cfg {number[]} [widths] Widths of columns as ratios
6832 * @cfg {number[]} [heights] Heights of rows as ratios
6834 OO
.ui
.GridLayout
= function OoUiGridLayout( panels
, config
) {
6837 // Config initialization
6838 config
= config
|| {};
6840 // Parent constructor
6841 OO
.ui
.GridLayout
.super.call( this, config
);
6849 this.$element
.addClass( 'oo-ui-gridLayout' );
6850 for ( i
= 0, len
= panels
.length
; i
< len
; i
++ ) {
6851 this.panels
.push( panels
[i
] );
6852 this.$element
.append( panels
[i
].$element
);
6854 if ( config
.widths
|| config
.heights
) {
6855 this.layout( config
.widths
|| [ 1 ], config
.heights
|| [ 1 ] );
6857 // Arrange in columns by default
6858 widths
= this.panels
.map( function () { return 1; } );
6859 this.layout( widths
, [ 1 ] );
6865 OO
.inheritClass( OO
.ui
.GridLayout
, OO
.ui
.Layout
);
6880 * Set grid dimensions.
6882 * @param {number[]} widths Widths of columns as ratios
6883 * @param {number[]} heights Heights of rows as ratios
6885 * @throws {Error} If grid is not large enough to fit all panels
6887 OO
.ui
.GridLayout
.prototype.layout = function ( widths
, heights
) {
6891 cols
= widths
.length
,
6892 rows
= heights
.length
;
6894 // Verify grid is big enough to fit panels
6895 if ( cols
* rows
< this.panels
.length
) {
6896 throw new Error( 'Grid is not large enough to fit ' + this.panels
.length
+ 'panels' );
6899 // Sum up denominators
6900 for ( x
= 0; x
< cols
; x
++ ) {
6903 for ( y
= 0; y
< rows
; y
++ ) {
6909 for ( x
= 0; x
< cols
; x
++ ) {
6910 this.widths
[x
] = widths
[x
] / xd
;
6912 for ( y
= 0; y
< rows
; y
++ ) {
6913 this.heights
[y
] = heights
[y
] / yd
;
6917 this.emit( 'layout' );
6921 * Update panel positions and sizes.
6925 OO
.ui
.GridLayout
.prototype.update = function () {
6926 var x
, y
, panel
, width
, height
, dimensions
,
6930 cols
= this.widths
.length
,
6931 rows
= this.heights
.length
;
6933 for ( y
= 0; y
< rows
; y
++ ) {
6934 height
= this.heights
[y
];
6935 for ( x
= 0; x
< cols
; x
++ ) {
6936 width
= this.widths
[x
];
6937 panel
= this.panels
[i
];
6939 width
: Math
.round( width
* 100 ) + '%',
6940 height
: Math
.round( height
* 100 ) + '%',
6941 top
: Math
.round( top
* 100 ) + '%'
6944 if ( OO
.ui
.Element
.getDir( this.$.context
) === 'rtl' ) {
6945 dimensions
.right
= Math
.round( left
* 100 ) + '%';
6947 dimensions
.left
= Math
.round( left
* 100 ) + '%';
6949 // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
6950 if ( width
=== 0 || height
=== 0 ) {
6951 dimensions
.visibility
= 'hidden';
6953 panel
.$element
.css( dimensions
);
6961 this.emit( 'update' );
6965 * Get a panel at a given position.
6967 * The x and y position is affected by the current grid layout.
6969 * @param {number} x Horizontal position
6970 * @param {number} y Vertical position
6971 * @return {OO.ui.PanelLayout} The panel at the given postion
6973 OO
.ui
.GridLayout
.prototype.getPanel = function ( x
, y
) {
6974 return this.panels
[ ( x
* this.widths
.length
) + y
];
6978 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
6981 * @extends OO.ui.Layout
6984 * @param {Object} [config] Configuration options
6985 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
6986 * @cfg {boolean} [padded=false] Pad the content from the edges
6987 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
6989 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
6990 // Config initialization
6991 config
= $.extend( {
6997 // Parent constructor
6998 OO
.ui
.PanelLayout
.super.call( this, config
);
7001 this.$element
.addClass( 'oo-ui-panelLayout' );
7002 if ( config
.scrollable
) {
7003 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
7005 if ( config
.padded
) {
7006 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
7008 if ( config
.expanded
) {
7009 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
7015 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
7018 * Page within an booklet layout.
7021 * @extends OO.ui.PanelLayout
7024 * @param {string} name Unique symbolic name of page
7025 * @param {Object} [config] Configuration options
7026 * @param {string} [outlineItem] Outline item widget
7028 OO
.ui
.PageLayout
= function OoUiPageLayout( name
, config
) {
7029 // Configuration initialization
7030 config
= $.extend( { scrollable
: true }, config
);
7032 // Parent constructor
7033 OO
.ui
.PageLayout
.super.call( this, config
);
7037 this.outlineItem
= config
.outlineItem
|| null;
7038 this.active
= false;
7041 this.$element
.addClass( 'oo-ui-pageLayout' );
7046 OO
.inheritClass( OO
.ui
.PageLayout
, OO
.ui
.PanelLayout
);
7052 * @param {boolean} active Page is active
7060 * @return {string} Symbolic name of page
7062 OO
.ui
.PageLayout
.prototype.getName = function () {
7067 * Check if page is active.
7069 * @return {boolean} Page is active
7071 OO
.ui
.PageLayout
.prototype.isActive = function () {
7078 * @return {OO.ui.OutlineItemWidget|null} Outline item widget
7080 OO
.ui
.PageLayout
.prototype.getOutlineItem = function () {
7081 return this.outlineItem
;
7087 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
7088 * outline item as desired; this method is called for setting (with an object) and unsetting
7089 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
7090 * operating on null instead of an OO.ui.OutlineItemWidget object.
7092 * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
7095 OO
.ui
.PageLayout
.prototype.setOutlineItem = function ( outlineItem
) {
7096 this.outlineItem
= outlineItem
|| null;
7097 if ( outlineItem
) {
7098 this.setupOutlineItem();
7104 * Setup outline item.
7106 * @localdoc Subclasses should override this method to adjust the outline item as desired.
7108 * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup
7111 OO
.ui
.PageLayout
.prototype.setupOutlineItem = function () {
7116 * Set page active state.
7118 * @param {boolean} Page is active
7121 OO
.ui
.PageLayout
.prototype.setActive = function ( active
) {
7124 if ( active
!== this.active
) {
7125 this.active
= active
;
7126 this.$element
.toggleClass( 'oo-ui-pageLayout-active', active
);
7127 this.emit( 'active', this.active
);
7132 * Layout containing a series of mutually exclusive pages.
7135 * @extends OO.ui.PanelLayout
7136 * @mixins OO.ui.GroupElement
7139 * @param {Object} [config] Configuration options
7140 * @cfg {boolean} [continuous=false] Show all pages, one after another
7141 * @cfg {string} [icon=''] Symbolic icon name
7142 * @cfg {OO.ui.Layout[]} [items] Layouts to add
7144 OO
.ui
.StackLayout
= function OoUiStackLayout( config
) {
7145 // Config initialization
7146 config
= $.extend( { scrollable
: true }, config
);
7148 // Parent constructor
7149 OO
.ui
.StackLayout
.super.call( this, config
);
7151 // Mixin constructors
7152 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
7155 this.currentItem
= null;
7156 this.continuous
= !!config
.continuous
;
7159 this.$element
.addClass( 'oo-ui-stackLayout' );
7160 if ( this.continuous
) {
7161 this.$element
.addClass( 'oo-ui-stackLayout-continuous' );
7163 if ( $.isArray( config
.items
) ) {
7164 this.addItems( config
.items
);
7170 OO
.inheritClass( OO
.ui
.StackLayout
, OO
.ui
.PanelLayout
);
7171 OO
.mixinClass( OO
.ui
.StackLayout
, OO
.ui
.GroupElement
);
7177 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
7183 * Get the current item.
7185 * @return {OO.ui.Layout|null}
7187 OO
.ui
.StackLayout
.prototype.getCurrentItem = function () {
7188 return this.currentItem
;
7192 * Unset the current item.
7195 * @param {OO.ui.StackLayout} layout
7198 OO
.ui
.StackLayout
.prototype.unsetCurrentItem = function () {
7199 var prevItem
= this.currentItem
;
7200 if ( prevItem
=== null ) {
7204 this.currentItem
= null;
7205 this.emit( 'set', null );
7211 * Adding an existing item (by value) will move it.
7213 * @param {OO.ui.Layout[]} items Items to add
7214 * @param {number} [index] Index to insert items after
7217 OO
.ui
.StackLayout
.prototype.addItems = function ( items
, index
) {
7219 OO
.ui
.GroupElement
.prototype.addItems
.call( this, items
, index
);
7221 if ( !this.currentItem
&& items
.length
) {
7222 this.setItem( items
[0] );
7231 * Items will be detached, not removed, so they can be used later.
7233 * @param {OO.ui.Layout[]} items Items to remove
7237 OO
.ui
.StackLayout
.prototype.removeItems = function ( items
) {
7239 OO
.ui
.GroupElement
.prototype.removeItems
.call( this, items
);
7241 if ( $.inArray( this.currentItem
, items
) !== -1 ) {
7242 if ( this.items
.length
) {
7243 this.setItem( this.items
[0] );
7245 this.unsetCurrentItem();
7255 * Items will be detached, not removed, so they can be used later.
7260 OO
.ui
.StackLayout
.prototype.clearItems = function () {
7261 this.unsetCurrentItem();
7262 OO
.ui
.GroupElement
.prototype.clearItems
.call( this );
7270 * Any currently shown item will be hidden.
7272 * FIXME: If the passed item to show has not been added in the items list, then
7273 * this method drops it and unsets the current item.
7275 * @param {OO.ui.Layout} item Item to show
7279 OO
.ui
.StackLayout
.prototype.setItem = function ( item
) {
7282 if ( item
!== this.currentItem
) {
7283 if ( !this.continuous
) {
7284 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7285 this.items
[i
].$element
.css( 'display', '' );
7288 if ( $.inArray( item
, this.items
) !== -1 ) {
7289 if ( !this.continuous
) {
7290 item
.$element
.css( 'display', 'block' );
7292 this.currentItem
= item
;
7293 this.emit( 'set', item
);
7295 this.unsetCurrentItem();
7303 * Horizontal bar layout of tools as icon buttons.
7306 * @extends OO.ui.ToolGroup
7309 * @param {OO.ui.Toolbar} toolbar
7310 * @param {Object} [config] Configuration options
7312 OO
.ui
.BarToolGroup
= function OoUiBarToolGroup( toolbar
, config
) {
7313 // Parent constructor
7314 OO
.ui
.BarToolGroup
.super.call( this, toolbar
, config
);
7317 this.$element
.addClass( 'oo-ui-barToolGroup' );
7322 OO
.inheritClass( OO
.ui
.BarToolGroup
, OO
.ui
.ToolGroup
);
7324 /* Static Properties */
7326 OO
.ui
.BarToolGroup
.static.titleTooltips
= true;
7328 OO
.ui
.BarToolGroup
.static.accelTooltips
= true;
7330 OO
.ui
.BarToolGroup
.static.name
= 'bar';
7333 * Popup list of tools with an icon and optional label.
7337 * @extends OO.ui.ToolGroup
7338 * @mixins OO.ui.IconElement
7339 * @mixins OO.ui.IndicatorElement
7340 * @mixins OO.ui.LabelElement
7341 * @mixins OO.ui.TitledElement
7342 * @mixins OO.ui.ClippableElement
7345 * @param {OO.ui.Toolbar} toolbar
7346 * @param {Object} [config] Configuration options
7347 * @cfg {string} [header] Text to display at the top of the pop-up
7349 OO
.ui
.PopupToolGroup
= function OoUiPopupToolGroup( toolbar
, config
) {
7350 // Configuration initialization
7351 config
= config
|| {};
7353 // Parent constructor
7354 OO
.ui
.PopupToolGroup
.super.call( this, toolbar
, config
);
7356 // Mixin constructors
7357 OO
.ui
.IconElement
.call( this, config
);
7358 OO
.ui
.IndicatorElement
.call( this, config
);
7359 OO
.ui
.LabelElement
.call( this, config
);
7360 OO
.ui
.TitledElement
.call( this, config
);
7361 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7364 this.active
= false;
7365 this.dragging
= false;
7366 this.onBlurHandler
= this.onBlur
.bind( this );
7367 this.$handle
= this.$( '<span>' );
7371 'mousedown touchstart': this.onHandlePointerDown
.bind( this ),
7372 'mouseup touchend': this.onHandlePointerUp
.bind( this )
7377 .addClass( 'oo-ui-popupToolGroup-handle' )
7378 .append( this.$icon
, this.$label
, this.$indicator
);
7379 // If the pop-up should have a header, add it to the top of the toolGroup.
7380 // Note: If this feature is useful for other widgets, we could abstract it into an
7381 // OO.ui.HeaderedElement mixin constructor.
7382 if ( config
.header
!== undefined ) {
7384 .prepend( this.$( '<span>' )
7385 .addClass( 'oo-ui-popupToolGroup-header' )
7386 .text( config
.header
)
7390 .addClass( 'oo-ui-popupToolGroup' )
7391 .prepend( this.$handle
);
7396 OO
.inheritClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ToolGroup
);
7397 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.IconElement
);
7398 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.IndicatorElement
);
7399 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.LabelElement
);
7400 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.TitledElement
);
7401 OO
.mixinClass( OO
.ui
.PopupToolGroup
, OO
.ui
.ClippableElement
);
7403 /* Static Properties */
7410 OO
.ui
.PopupToolGroup
.prototype.setDisabled = function () {
7412 OO
.ui
.PopupToolGroup
.super.prototype.setDisabled
.apply( this, arguments
);
7414 if ( this.isDisabled() && this.isElementAttached() ) {
7415 this.setActive( false );
7420 * Handle focus being lost.
7422 * The event is actually generated from a mouseup, so it is not a normal blur event object.
7424 * @param {jQuery.Event} e Mouse up event
7426 OO
.ui
.PopupToolGroup
.prototype.onBlur = function ( e
) {
7427 // Only deactivate when clicking outside the dropdown element
7428 if ( this.$( e
.target
).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element
[0] ) {
7429 this.setActive( false );
7436 OO
.ui
.PopupToolGroup
.prototype.onPointerUp = function ( e
) {
7437 // e.which is 0 for touch events, 1 for left mouse button
7438 if ( !this.isDisabled() && e
.which
<= 1 ) {
7439 this.setActive( false );
7441 return OO
.ui
.PopupToolGroup
.super.prototype.onPointerUp
.call( this, e
);
7445 * Handle mouse up events.
7447 * @param {jQuery.Event} e Mouse up event
7449 OO
.ui
.PopupToolGroup
.prototype.onHandlePointerUp = function () {
7454 * Handle mouse down events.
7456 * @param {jQuery.Event} e Mouse down event
7458 OO
.ui
.PopupToolGroup
.prototype.onHandlePointerDown = function ( e
) {
7459 // e.which is 0 for touch events, 1 for left mouse button
7460 if ( !this.isDisabled() && e
.which
<= 1 ) {
7461 this.setActive( !this.active
);
7467 * Switch into active mode.
7469 * When active, mouseup events anywhere in the document will trigger deactivation.
7471 OO
.ui
.PopupToolGroup
.prototype.setActive = function ( value
) {
7473 if ( this.active
!== value
) {
7474 this.active
= value
;
7476 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler
, true );
7478 // Try anchoring the popup to the left first
7479 this.$element
.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
7480 this.toggleClipping( true );
7481 if ( this.isClippedHorizontally() ) {
7482 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
7483 this.toggleClipping( false );
7485 .removeClass( 'oo-ui-popupToolGroup-left' )
7486 .addClass( 'oo-ui-popupToolGroup-right' );
7487 this.toggleClipping( true );
7490 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler
, true );
7491 this.$element
.removeClass(
7492 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
7494 this.toggleClipping( false );
7500 * Drop down list layout of tools as labeled icon buttons.
7503 * @extends OO.ui.PopupToolGroup
7506 * @param {OO.ui.Toolbar} toolbar
7507 * @param {Object} [config] Configuration options
7509 OO
.ui
.ListToolGroup
= function OoUiListToolGroup( toolbar
, config
) {
7510 // Parent constructor
7511 OO
.ui
.ListToolGroup
.super.call( this, toolbar
, config
);
7514 this.$element
.addClass( 'oo-ui-listToolGroup' );
7519 OO
.inheritClass( OO
.ui
.ListToolGroup
, OO
.ui
.PopupToolGroup
);
7521 /* Static Properties */
7523 OO
.ui
.ListToolGroup
.static.accelTooltips
= true;
7525 OO
.ui
.ListToolGroup
.static.name
= 'list';
7528 * Drop down menu layout of tools as selectable menu items.
7531 * @extends OO.ui.PopupToolGroup
7534 * @param {OO.ui.Toolbar} toolbar
7535 * @param {Object} [config] Configuration options
7537 OO
.ui
.MenuToolGroup
= function OoUiMenuToolGroup( toolbar
, config
) {
7538 // Configuration initialization
7539 config
= config
|| {};
7541 // Parent constructor
7542 OO
.ui
.MenuToolGroup
.super.call( this, toolbar
, config
);
7545 this.toolbar
.connect( this, { updateState
: 'onUpdateState' } );
7548 this.$element
.addClass( 'oo-ui-menuToolGroup' );
7553 OO
.inheritClass( OO
.ui
.MenuToolGroup
, OO
.ui
.PopupToolGroup
);
7555 /* Static Properties */
7557 OO
.ui
.MenuToolGroup
.static.accelTooltips
= true;
7559 OO
.ui
.MenuToolGroup
.static.name
= 'menu';
7564 * Handle the toolbar state being updated.
7566 * When the state changes, the title of each active item in the menu will be joined together and
7567 * used as a label for the group. The label will be empty if none of the items are active.
7569 OO
.ui
.MenuToolGroup
.prototype.onUpdateState = function () {
7573 for ( name
in this.tools
) {
7574 if ( this.tools
[name
].isActive() ) {
7575 labelTexts
.push( this.tools
[name
].getTitle() );
7579 this.setLabel( labelTexts
.join( ', ' ) || ' ' );
7583 * Tool that shows a popup when selected.
7587 * @extends OO.ui.Tool
7588 * @mixins OO.ui.PopupElement
7591 * @param {OO.ui.Toolbar} toolbar
7592 * @param {Object} [config] Configuration options
7594 OO
.ui
.PopupTool
= function OoUiPopupTool( toolbar
, config
) {
7595 // Parent constructor
7596 OO
.ui
.PopupTool
.super.call( this, toolbar
, config
);
7598 // Mixin constructors
7599 OO
.ui
.PopupElement
.call( this, config
);
7603 .addClass( 'oo-ui-popupTool' )
7604 .append( this.popup
.$element
);
7609 OO
.inheritClass( OO
.ui
.PopupTool
, OO
.ui
.Tool
);
7610 OO
.mixinClass( OO
.ui
.PopupTool
, OO
.ui
.PopupElement
);
7615 * Handle the tool being selected.
7619 OO
.ui
.PopupTool
.prototype.onSelect = function () {
7620 if ( !this.isDisabled() ) {
7621 this.popup
.toggle();
7623 this.setActive( false );
7628 * Handle the toolbar state being updated.
7632 OO
.ui
.PopupTool
.prototype.onUpdateState = function () {
7633 this.setActive( false );
7637 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
7639 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
7643 * @extends OO.ui.GroupElement
7646 * @param {Object} [config] Configuration options
7648 OO
.ui
.GroupWidget
= function OoUiGroupWidget( config
) {
7649 // Parent constructor
7650 OO
.ui
.GroupWidget
.super.call( this, config
);
7655 OO
.inheritClass( OO
.ui
.GroupWidget
, OO
.ui
.GroupElement
);
7660 * Set the disabled state of the widget.
7662 * This will also update the disabled state of child widgets.
7664 * @param {boolean} disabled Disable widget
7667 OO
.ui
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
7671 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
7672 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
7674 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
7676 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7677 this.items
[i
].updateDisabled();
7685 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
7687 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
7688 * allows bidrectional communication.
7690 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
7697 OO
.ui
.ItemWidget
= function OoUiItemWidget() {
7704 * Check if widget is disabled.
7706 * Checks parent if present, making disabled state inheritable.
7708 * @return {boolean} Widget is disabled
7710 OO
.ui
.ItemWidget
.prototype.isDisabled = function () {
7711 return this.disabled
||
7712 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
7716 * Set group element is in.
7718 * @param {OO.ui.GroupElement|null} group Group element, null if none
7721 OO
.ui
.ItemWidget
.prototype.setElementGroup = function ( group
) {
7723 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
7724 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
7726 // Initialize item disabled states
7727 this.updateDisabled();
7733 * Mixin that adds a menu showing suggested values for a text input.
7735 * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
7741 * @param {OO.ui.TextInputWidget} input Input widget
7742 * @param {Object} [config] Configuration options
7743 * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay.
7745 OO
.ui
.LookupInputWidget
= function OoUiLookupInputWidget( input
, config
) {
7746 // Config intialization
7747 config
= config
|| {};
7750 this.lookupInput
= input
;
7751 this.$overlay
= config
.$overlay
|| ( this.$.$iframe
|| this.$element
).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' );
7752 if ( this.$overlay
.length
=== 0 ) {
7753 this.$overlay
= this.$( 'body' );
7755 this.lookupMenu
= new OO
.ui
.TextInputMenuWidget( this, {
7756 $: OO
.ui
.Element
.getJQuery( this.$overlay
),
7757 input
: this.lookupInput
,
7758 $container
: config
.$container
7760 this.lookupCache
= {};
7761 this.lookupQuery
= null;
7762 this.lookupRequest
= null;
7763 this.populating
= false;
7766 this.$overlay
.append( this.lookupMenu
.$element
);
7768 this.lookupInput
.$input
.on( {
7769 focus
: this.onLookupInputFocus
.bind( this ),
7770 blur
: this.onLookupInputBlur
.bind( this ),
7771 mousedown
: this.onLookupInputMouseDown
.bind( this )
7773 this.lookupInput
.connect( this, { change
: 'onLookupInputChange' } );
7776 this.$element
.addClass( 'oo-ui-lookupWidget' );
7777 this.lookupMenu
.$element
.addClass( 'oo-ui-lookupWidget-menu' );
7783 * Handle input focus event.
7785 * @param {jQuery.Event} e Input focus event
7787 OO
.ui
.LookupInputWidget
.prototype.onLookupInputFocus = function () {
7788 this.openLookupMenu();
7792 * Handle input blur event.
7794 * @param {jQuery.Event} e Input blur event
7796 OO
.ui
.LookupInputWidget
.prototype.onLookupInputBlur = function () {
7797 this.lookupMenu
.toggle( false );
7801 * Handle input mouse down event.
7803 * @param {jQuery.Event} e Input mouse down event
7805 OO
.ui
.LookupInputWidget
.prototype.onLookupInputMouseDown = function () {
7806 this.openLookupMenu();
7810 * Handle input change event.
7812 * @param {string} value New input value
7814 OO
.ui
.LookupInputWidget
.prototype.onLookupInputChange = function () {
7815 this.openLookupMenu();
7821 * @return {OO.ui.TextInputMenuWidget}
7823 OO
.ui
.LookupInputWidget
.prototype.getLookupMenu = function () {
7824 return this.lookupMenu
;
7832 OO
.ui
.LookupInputWidget
.prototype.openLookupMenu = function () {
7833 var value
= this.lookupInput
.getValue();
7835 if ( this.lookupMenu
.$input
.is( ':focus' ) && $.trim( value
) !== '' ) {
7836 this.populateLookupMenu();
7837 this.lookupMenu
.toggle( true );
7848 * Populate lookup menu with current information.
7852 OO
.ui
.LookupInputWidget
.prototype.populateLookupMenu = function () {
7855 if ( !this.populating
) {
7856 this.populating
= true;
7857 this.getLookupMenuItems()
7858 .done( function ( items
) {
7859 widget
.lookupMenu
.clearItems();
7860 if ( items
.length
) {
7864 widget
.initializeLookupMenuSelection();
7865 widget
.openLookupMenu();
7867 widget
.lookupMenu
.toggle( true );
7869 widget
.populating
= false;
7871 .fail( function () {
7872 widget
.lookupMenu
.clearItems();
7873 widget
.populating
= false;
7881 * Set selection in the lookup menu with current information.
7885 OO
.ui
.LookupInputWidget
.prototype.initializeLookupMenuSelection = function () {
7886 if ( !this.lookupMenu
.getSelectedItem() ) {
7887 this.lookupMenu
.selectItem( this.lookupMenu
.getFirstSelectableItem() );
7889 this.lookupMenu
.highlightItem( this.lookupMenu
.getSelectedItem() );
7893 * Get lookup menu items for the current query.
7895 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
7898 OO
.ui
.LookupInputWidget
.prototype.getLookupMenuItems = function () {
7900 value
= this.lookupInput
.getValue(),
7901 deferred
= $.Deferred();
7903 if ( value
&& value
!== this.lookupQuery
) {
7904 // Abort current request if query has changed
7905 if ( this.lookupRequest
) {
7906 this.lookupRequest
.abort();
7907 this.lookupQuery
= null;
7908 this.lookupRequest
= null;
7910 if ( value
in this.lookupCache
) {
7911 deferred
.resolve( this.getLookupMenuItemsFromData( this.lookupCache
[value
] ) );
7913 this.lookupQuery
= value
;
7914 this.lookupRequest
= this.getLookupRequest()
7915 .always( function () {
7916 widget
.lookupQuery
= null;
7917 widget
.lookupRequest
= null;
7919 .done( function ( data
) {
7920 widget
.lookupCache
[value
] = widget
.getLookupCacheItemFromData( data
);
7921 deferred
.resolve( widget
.getLookupMenuItemsFromData( widget
.lookupCache
[value
] ) );
7923 .fail( function () {
7927 this.lookupRequest
.always( function () {
7928 widget
.popPending();
7932 return deferred
.promise();
7936 * Get a new request object of the current lookup query value.
7939 * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
7941 OO
.ui
.LookupInputWidget
.prototype.getLookupRequest = function () {
7942 // Stub, implemented in subclass
7947 * Handle successful lookup request.
7949 * Overriding methods should call #populateLookupMenu when results are available and cache results
7950 * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
7953 * @param {Mixed} data Response from server
7955 OO
.ui
.LookupInputWidget
.prototype.onLookupRequestDone = function () {
7956 // Stub, implemented in subclass
7960 * Get a list of menu item widgets from the data stored by the lookup request's done handler.
7963 * @param {Mixed} data Cached result data, usually an array
7964 * @return {OO.ui.MenuItemWidget[]} Menu items
7966 OO
.ui
.LookupInputWidget
.prototype.getLookupMenuItemsFromData = function () {
7967 // Stub, implemented in subclass
7972 * Set of controls for an OO.ui.OutlineWidget.
7974 * Controls include moving items up and down, removing items, and adding different kinds of items.
7977 * @extends OO.ui.Widget
7978 * @mixins OO.ui.GroupElement
7979 * @mixins OO.ui.IconElement
7982 * @param {OO.ui.OutlineWidget} outline Outline to control
7983 * @param {Object} [config] Configuration options
7985 OO
.ui
.OutlineControlsWidget
= function OoUiOutlineControlsWidget( outline
, config
) {
7986 // Configuration initialization
7987 config
= $.extend( { icon
: 'add' }, config
);
7989 // Parent constructor
7990 OO
.ui
.OutlineControlsWidget
.super.call( this, config
);
7992 // Mixin constructors
7993 OO
.ui
.GroupElement
.call( this, config
);
7994 OO
.ui
.IconElement
.call( this, config
);
7997 this.outline
= outline
;
7998 this.$movers
= this.$( '<div>' );
7999 this.upButton
= new OO
.ui
.ButtonWidget( {
8003 title
: OO
.ui
.msg( 'ooui-outline-control-move-up' )
8005 this.downButton
= new OO
.ui
.ButtonWidget( {
8009 title
: OO
.ui
.msg( 'ooui-outline-control-move-down' )
8011 this.removeButton
= new OO
.ui
.ButtonWidget( {
8015 title
: OO
.ui
.msg( 'ooui-outline-control-remove' )
8019 outline
.connect( this, {
8020 select
: 'onOutlineChange',
8021 add
: 'onOutlineChange',
8022 remove
: 'onOutlineChange'
8024 this.upButton
.connect( this, { click
: [ 'emit', 'move', -1 ] } );
8025 this.downButton
.connect( this, { click
: [ 'emit', 'move', 1 ] } );
8026 this.removeButton
.connect( this, { click
: [ 'emit', 'remove' ] } );
8029 this.$element
.addClass( 'oo-ui-outlineControlsWidget' );
8030 this.$group
.addClass( 'oo-ui-outlineControlsWidget-items' );
8032 .addClass( 'oo-ui-outlineControlsWidget-movers' )
8033 .append( this.removeButton
.$element
, this.upButton
.$element
, this.downButton
.$element
);
8034 this.$element
.append( this.$icon
, this.$group
, this.$movers
);
8039 OO
.inheritClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.Widget
);
8040 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.GroupElement
);
8041 OO
.mixinClass( OO
.ui
.OutlineControlsWidget
, OO
.ui
.IconElement
);
8047 * @param {number} places Number of places to move
8057 * Handle outline change events.
8059 OO
.ui
.OutlineControlsWidget
.prototype.onOutlineChange = function () {
8060 var i
, len
, firstMovable
, lastMovable
,
8061 items
= this.outline
.getItems(),
8062 selectedItem
= this.outline
.getSelectedItem(),
8063 movable
= selectedItem
&& selectedItem
.isMovable(),
8064 removable
= selectedItem
&& selectedItem
.isRemovable();
8069 while ( ++i
< len
) {
8070 if ( items
[i
].isMovable() ) {
8071 firstMovable
= items
[i
];
8077 if ( items
[i
].isMovable() ) {
8078 lastMovable
= items
[i
];
8083 this.upButton
.setDisabled( !movable
|| selectedItem
=== firstMovable
);
8084 this.downButton
.setDisabled( !movable
|| selectedItem
=== lastMovable
);
8085 this.removeButton
.setDisabled( !removable
);
8089 * Mixin for widgets with a boolean on/off state.
8095 * @param {Object} [config] Configuration options
8096 * @cfg {boolean} [value=false] Initial value
8098 OO
.ui
.ToggleWidget
= function OoUiToggleWidget( config
) {
8099 // Configuration initialization
8100 config
= config
|| {};
8106 this.$element
.addClass( 'oo-ui-toggleWidget' );
8107 this.setValue( !!config
.value
);
8114 * @param {boolean} value Changed value
8120 * Get the value of the toggle.
8124 OO
.ui
.ToggleWidget
.prototype.getValue = function () {
8129 * Set the value of the toggle.
8131 * @param {boolean} value New value
8135 OO
.ui
.ToggleWidget
.prototype.setValue = function ( value
) {
8137 if ( this.value
!== value
) {
8139 this.emit( 'change', value
);
8140 this.$element
.toggleClass( 'oo-ui-toggleWidget-on', value
);
8141 this.$element
.toggleClass( 'oo-ui-toggleWidget-off', !value
);
8147 * Group widget for multiple related buttons.
8149 * Use together with OO.ui.ButtonWidget.
8152 * @extends OO.ui.Widget
8153 * @mixins OO.ui.GroupElement
8156 * @param {Object} [config] Configuration options
8157 * @cfg {OO.ui.ButtonWidget} [items] Buttons to add
8159 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
8160 // Parent constructor
8161 OO
.ui
.ButtonGroupWidget
.super.call( this, config
);
8163 // Mixin constructors
8164 OO
.ui
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
8167 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
8168 if ( $.isArray( config
.items
) ) {
8169 this.addItems( config
.items
);
8175 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
8176 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.GroupElement
);
8179 * Generic widget for buttons.
8182 * @extends OO.ui.Widget
8183 * @mixins OO.ui.ButtonElement
8184 * @mixins OO.ui.IconElement
8185 * @mixins OO.ui.IndicatorElement
8186 * @mixins OO.ui.LabelElement
8187 * @mixins OO.ui.TitledElement
8188 * @mixins OO.ui.FlaggedElement
8191 * @param {Object} [config] Configuration options
8192 * @cfg {string} [href] Hyperlink to visit when clicked
8193 * @cfg {string} [target] Target to open hyperlink in
8195 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
8196 // Configuration initialization
8197 config
= $.extend( { target
: '_blank' }, config
);
8199 // Parent constructor
8200 OO
.ui
.ButtonWidget
.super.call( this, config
);
8202 // Mixin constructors
8203 OO
.ui
.ButtonElement
.call( this, config
);
8204 OO
.ui
.IconElement
.call( this, config
);
8205 OO
.ui
.IndicatorElement
.call( this, config
);
8206 OO
.ui
.LabelElement
.call( this, config
);
8207 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
8208 OO
.ui
.FlaggedElement
.call( this, config
);
8213 this.isHyperlink
= false;
8217 click
: this.onClick
.bind( this ),
8218 keypress
: this.onKeyPress
.bind( this )
8222 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
8224 .addClass( 'oo-ui-buttonWidget' )
8225 .append( this.$button
);
8226 this.setHref( config
.href
);
8227 this.setTarget( config
.target
);
8232 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
8233 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.ButtonElement
);
8234 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.IconElement
);
8235 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.IndicatorElement
);
8236 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.LabelElement
);
8237 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.TitledElement
);
8238 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.FlaggedElement
);
8249 * Handles mouse click events.
8251 * @param {jQuery.Event} e Mouse click event
8254 OO
.ui
.ButtonWidget
.prototype.onClick = function () {
8255 if ( !this.isDisabled() ) {
8256 this.emit( 'click' );
8257 if ( this.isHyperlink
) {
8265 * Handles keypress events.
8267 * @param {jQuery.Event} e Keypress event
8270 OO
.ui
.ButtonWidget
.prototype.onKeyPress = function ( e
) {
8271 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
8273 if ( this.isHyperlink
) {
8281 * Get hyperlink location.
8283 * @return {string} Hyperlink location
8285 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
8290 * Get hyperlink target.
8292 * @return {string} Hyperlink target
8294 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
8299 * Set hyperlink location.
8301 * @param {string|null} href Hyperlink location, null to remove
8303 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
8304 href
= typeof href
=== 'string' ? href
: null;
8306 if ( href
!== this.href
) {
8308 if ( href
!== null ) {
8309 this.$button
.attr( 'href', href
);
8310 this.isHyperlink
= true;
8312 this.$button
.removeAttr( 'href' );
8313 this.isHyperlink
= false;
8321 * Set hyperlink target.
8323 * @param {string|null} target Hyperlink target, null to remove
8325 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
8326 target
= typeof target
=== 'string' ? target
: null;
8328 if ( target
!== this.target
) {
8329 this.target
= target
;
8330 if ( target
!== null ) {
8331 this.$button
.attr( 'target', target
);
8333 this.$button
.removeAttr( 'target' );
8341 * Button widget that executes an action and is managed by an OO.ui.ActionSet.
8344 * @extends OO.ui.ButtonWidget
8345 * @mixins OO.ui.PendingElement
8348 * @param {Object} [config] Configuration options
8349 * @cfg {string} [action] Symbolic action name
8350 * @cfg {string[]} [modes] Symbolic mode names
8351 * @cfg {boolean} [framed=false] Render button with a frame
8353 OO
.ui
.ActionWidget
= function OoUiActionWidget( config
) {
8354 // Config intialization
8355 config
= $.extend( { framed
: false }, config
);
8357 // Parent constructor
8358 OO
.ui
.ActionWidget
.super.call( this, config
);
8360 // Mixin constructors
8361 OO
.ui
.PendingElement
.call( this, config
);
8364 this.action
= config
.action
|| '';
8365 this.modes
= config
.modes
|| [];
8370 this.$element
.addClass( 'oo-ui-actionWidget' );
8375 OO
.inheritClass( OO
.ui
.ActionWidget
, OO
.ui
.ButtonWidget
);
8376 OO
.mixinClass( OO
.ui
.ActionWidget
, OO
.ui
.PendingElement
);
8387 * Check if action is available in a certain mode.
8389 * @param {string} mode Name of mode
8390 * @return {boolean} Has mode
8392 OO
.ui
.ActionWidget
.prototype.hasMode = function ( mode
) {
8393 return this.modes
.indexOf( mode
) !== -1;
8397 * Get symbolic action name.
8401 OO
.ui
.ActionWidget
.prototype.getAction = function () {
8406 * Get symbolic action name.
8410 OO
.ui
.ActionWidget
.prototype.getModes = function () {
8411 return this.modes
.slice();
8415 * Emit a resize event if the size has changed.
8419 OO
.ui
.ActionWidget
.prototype.propagateResize = function () {
8422 if ( this.isElementAttached() ) {
8423 width
= this.$element
.width();
8424 height
= this.$element
.height();
8426 if ( width
!== this.width
|| height
!== this.height
) {
8428 this.height
= height
;
8429 this.emit( 'resize' );
8439 OO
.ui
.ActionWidget
.prototype.setIcon = function () {
8441 OO
.ui
.IconElement
.prototype.setIcon
.apply( this, arguments
);
8442 this.propagateResize();
8450 OO
.ui
.ActionWidget
.prototype.setLabel = function () {
8452 OO
.ui
.LabelElement
.prototype.setLabel
.apply( this, arguments
);
8453 this.propagateResize();
8461 OO
.ui
.ActionWidget
.prototype.setFlags = function () {
8463 OO
.ui
.FlaggedElement
.prototype.setFlags
.apply( this, arguments
);
8464 this.propagateResize();
8472 OO
.ui
.ActionWidget
.prototype.clearFlags = function () {
8474 OO
.ui
.FlaggedElement
.prototype.clearFlags
.apply( this, arguments
);
8475 this.propagateResize();
8481 * Toggle visibility of button.
8483 * @param {boolean} [show] Show button, omit to toggle visibility
8486 OO
.ui
.ActionWidget
.prototype.toggle = function () {
8488 OO
.ui
.ActionWidget
.super.prototype.toggle
.apply( this, arguments
);
8489 this.propagateResize();
8495 * Button that shows and hides a popup.
8498 * @extends OO.ui.ButtonWidget
8499 * @mixins OO.ui.PopupElement
8502 * @param {Object} [config] Configuration options
8504 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
8505 // Parent constructor
8506 OO
.ui
.PopupButtonWidget
.super.call( this, config
);
8508 // Mixin constructors
8509 OO
.ui
.PopupElement
.call( this, config
);
8513 .addClass( 'oo-ui-popupButtonWidget' )
8514 .append( this.popup
.$element
);
8519 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
8520 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.PopupElement
);
8525 * Handles mouse click events.
8527 * @param {jQuery.Event} e Mouse click event
8529 OO
.ui
.PopupButtonWidget
.prototype.onClick = function ( e
) {
8530 // Skip clicks within the popup
8531 if ( $.contains( this.popup
.$element
[0], e
.target
) ) {
8535 if ( !this.isDisabled() ) {
8536 this.popup
.toggle();
8538 OO
.ui
.PopupButtonWidget
.super.prototype.onClick
.call( this );
8544 * Button that toggles on and off.
8547 * @extends OO.ui.ButtonWidget
8548 * @mixins OO.ui.ToggleWidget
8551 * @param {Object} [config] Configuration options
8552 * @cfg {boolean} [value=false] Initial value
8554 OO
.ui
.ToggleButtonWidget
= function OoUiToggleButtonWidget( config
) {
8555 // Configuration initialization
8556 config
= config
|| {};
8558 // Parent constructor
8559 OO
.ui
.ToggleButtonWidget
.super.call( this, config
);
8561 // Mixin constructors
8562 OO
.ui
.ToggleWidget
.call( this, config
);
8565 this.$element
.addClass( 'oo-ui-toggleButtonWidget' );
8570 OO
.inheritClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ButtonWidget
);
8571 OO
.mixinClass( OO
.ui
.ToggleButtonWidget
, OO
.ui
.ToggleWidget
);
8578 OO
.ui
.ToggleButtonWidget
.prototype.onClick = function () {
8579 if ( !this.isDisabled() ) {
8580 this.setValue( !this.value
);
8584 return OO
.ui
.ToggleButtonWidget
.super.prototype.onClick
.call( this );
8590 OO
.ui
.ToggleButtonWidget
.prototype.setValue = function ( value
) {
8592 if ( value
!== this.value
) {
8593 this.setActive( value
);
8596 // Parent method (from mixin)
8597 OO
.ui
.ToggleWidget
.prototype.setValue
.call( this, value
);
8605 * See OO.ui.IconElement for more information.
8608 * @extends OO.ui.Widget
8609 * @mixins OO.ui.IconElement
8610 * @mixins OO.ui.TitledElement
8613 * @param {Object} [config] Configuration options
8615 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
8616 // Config intialization
8617 config
= config
|| {};
8619 // Parent constructor
8620 OO
.ui
.IconWidget
.super.call( this, config
);
8622 // Mixin constructors
8623 OO
.ui
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
8624 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
8627 this.$element
.addClass( 'oo-ui-iconWidget' );
8632 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
8633 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.IconElement
);
8634 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.TitledElement
);
8636 /* Static Properties */
8638 OO
.ui
.IconWidget
.static.tagName
= 'span';
8643 * See OO.ui.IndicatorElement for more information.
8646 * @extends OO.ui.Widget
8647 * @mixins OO.ui.IndicatorElement
8648 * @mixins OO.ui.TitledElement
8651 * @param {Object} [config] Configuration options
8653 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
8654 // Config intialization
8655 config
= config
|| {};
8657 // Parent constructor
8658 OO
.ui
.IndicatorWidget
.super.call( this, config
);
8660 // Mixin constructors
8661 OO
.ui
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
8662 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
8665 this.$element
.addClass( 'oo-ui-indicatorWidget' );
8670 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
8671 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.IndicatorElement
);
8672 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.TitledElement
);
8674 /* Static Properties */
8676 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
8679 * Inline menu of options.
8681 * Inline menus provide a control for accessing a menu and compose a menu within the widget, which
8682 * can be accessed using the #getMenu method.
8684 * Use with OO.ui.MenuItemWidget.
8687 * @extends OO.ui.Widget
8688 * @mixins OO.ui.IconElement
8689 * @mixins OO.ui.IndicatorElement
8690 * @mixins OO.ui.LabelElement
8691 * @mixins OO.ui.TitledElement
8694 * @param {Object} [config] Configuration options
8695 * @cfg {Object} [menu] Configuration options to pass to menu widget
8697 OO
.ui
.InlineMenuWidget
= function OoUiInlineMenuWidget( config
) {
8698 // Configuration initialization
8699 config
= $.extend( { indicator
: 'down' }, config
);
8701 // Parent constructor
8702 OO
.ui
.InlineMenuWidget
.super.call( this, config
);
8704 // Mixin constructors
8705 OO
.ui
.IconElement
.call( this, config
);
8706 OO
.ui
.IndicatorElement
.call( this, config
);
8707 OO
.ui
.LabelElement
.call( this, config
);
8708 OO
.ui
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
8711 this.menu
= new OO
.ui
.MenuWidget( $.extend( { $: this.$, widget
: this }, config
.menu
) );
8712 this.$handle
= this.$( '<span>' );
8715 this.$element
.on( { click
: this.onClick
.bind( this ) } );
8716 this.menu
.connect( this, { select
: 'onMenuSelect' } );
8720 .addClass( 'oo-ui-inlineMenuWidget-handle' )
8721 .append( this.$icon
, this.$label
, this.$indicator
);
8723 .addClass( 'oo-ui-inlineMenuWidget' )
8724 .append( this.$handle
, this.menu
.$element
);
8729 OO
.inheritClass( OO
.ui
.InlineMenuWidget
, OO
.ui
.Widget
);
8730 OO
.mixinClass( OO
.ui
.InlineMenuWidget
, OO
.ui
.IconElement
);
8731 OO
.mixinClass( OO
.ui
.InlineMenuWidget
, OO
.ui
.IndicatorElement
);
8732 OO
.mixinClass( OO
.ui
.InlineMenuWidget
, OO
.ui
.LabelElement
);
8733 OO
.mixinClass( OO
.ui
.InlineMenuWidget
, OO
.ui
.TitledElement
);
8740 * @return {OO.ui.MenuWidget} Menu of widget
8742 OO
.ui
.InlineMenuWidget
.prototype.getMenu = function () {
8747 * Handles menu select events.
8749 * @param {OO.ui.MenuItemWidget} item Selected menu item
8751 OO
.ui
.InlineMenuWidget
.prototype.onMenuSelect = function ( item
) {
8758 selectedLabel
= item
.getLabel();
8760 // If the label is a DOM element, clone it, because setLabel will append() it
8761 if ( selectedLabel
instanceof jQuery
) {
8762 selectedLabel
= selectedLabel
.clone();
8765 this.setLabel( selectedLabel
);
8769 * Handles mouse click events.
8771 * @param {jQuery.Event} e Mouse click event
8773 OO
.ui
.InlineMenuWidget
.prototype.onClick = function ( e
) {
8774 // Skip clicks within the menu
8775 if ( $.contains( this.menu
.$element
[0], e
.target
) ) {
8779 if ( !this.isDisabled() ) {
8780 if ( this.menu
.isVisible() ) {
8781 this.menu
.toggle( false );
8783 this.menu
.toggle( true );
8790 * Base class for input widgets.
8794 * @extends OO.ui.Widget
8795 * @mixins OO.ui.FlaggedElement
8798 * @param {Object} [config] Configuration options
8799 * @cfg {string} [name=''] HTML input name
8800 * @cfg {string} [value=''] Input value
8801 * @cfg {boolean} [readOnly=false] Prevent changes
8802 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
8804 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8805 // Config intialization
8806 config
= $.extend( { readOnly
: false }, config
);
8808 // Parent constructor
8809 OO
.ui
.InputWidget
.super.call( this, config
);
8811 // Mixin constructors
8812 OO
.ui
.FlaggedElement
.call( this, config
);
8815 this.$input
= this.getInputElement( config
);
8817 this.readOnly
= false;
8818 this.inputFilter
= config
.inputFilter
;
8821 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8825 .attr( 'name', config
.name
)
8826 .prop( 'disabled', this.isDisabled() );
8827 this.setReadOnly( config
.readOnly
);
8828 this.$element
.addClass( 'oo-ui-inputWidget' ).append( this.$input
);
8829 this.setValue( config
.value
);
8834 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8835 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.FlaggedElement
);
8847 * Get input element.
8849 * @param {Object} [config] Configuration options
8850 * @return {jQuery} Input element
8852 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8853 return this.$( '<input>' );
8857 * Handle potentially value-changing events.
8859 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8861 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8863 if ( !this.isDisabled() ) {
8864 // Allow the stack to clear so the value will be updated
8865 setTimeout( function () {
8866 widget
.setValue( widget
.$input
.val() );
8872 * Get the value of the input.
8874 * @return {string} Input value
8876 OO
.ui
.InputWidget
.prototype.getValue = function () {
8881 * Sets the direction of the current input, either RTL or LTR
8883 * @param {boolean} isRTL
8885 OO
.ui
.InputWidget
.prototype.setRTL = function ( isRTL
) {
8887 this.$input
.removeClass( 'oo-ui-ltr' );
8888 this.$input
.addClass( 'oo-ui-rtl' );
8890 this.$input
.removeClass( 'oo-ui-rtl' );
8891 this.$input
.addClass( 'oo-ui-ltr' );
8896 * Set the value of the input.
8898 * @param {string} value New value
8902 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8903 value
= this.sanitizeValue( value
);
8904 if ( this.value
!== value
) {
8906 this.emit( 'change', this.value
);
8908 // Update the DOM if it has changed. Note that with sanitizeValue, it
8909 // is possible for the DOM value to change without this.value changing.
8910 if ( this.$input
.val() !== this.value
) {
8911 this.$input
.val( this.value
);
8917 * Sanitize incoming value.
8919 * Ensures value is a string, and converts undefined and null to empty strings.
8921 * @param {string} value Original value
8922 * @return {string} Sanitized value
8924 OO
.ui
.InputWidget
.prototype.sanitizeValue = function ( value
) {
8925 if ( value
=== undefined || value
=== null ) {
8927 } else if ( this.inputFilter
) {
8928 return this.inputFilter( String( value
) );
8930 return String( value
);
8935 * Simulate the behavior of clicking on a label bound to this input.
8937 OO
.ui
.InputWidget
.prototype.simulateLabelClick = function () {
8938 if ( !this.isDisabled() ) {
8939 if ( this.$input
.is( ':checkbox,:radio' ) ) {
8940 this.$input
.click();
8941 } else if ( this.$input
.is( ':input' ) ) {
8942 this.$input
[0].focus();
8948 * Check if the widget is read-only.
8952 OO
.ui
.InputWidget
.prototype.isReadOnly = function () {
8953 return this.readOnly
;
8957 * Set the read-only state of the widget.
8959 * This should probably change the widgets's appearance and prevent it from being used.
8961 * @param {boolean} state Make input read-only
8964 OO
.ui
.InputWidget
.prototype.setReadOnly = function ( state
) {
8965 this.readOnly
= !!state
;
8966 this.$input
.prop( 'readOnly', this.readOnly
);
8973 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8974 OO
.ui
.InputWidget
.super.prototype.setDisabled
.call( this, state
);
8975 if ( this.$input
) {
8976 this.$input
.prop( 'disabled', this.isDisabled() );
8986 OO
.ui
.InputWidget
.prototype.focus = function () {
8987 this.$input
[0].focus();
8996 OO
.ui
.InputWidget
.prototype.blur = function () {
8997 this.$input
[0].blur();
9002 * Checkbox input widget.
9005 * @extends OO.ui.InputWidget
9008 * @param {Object} [config] Configuration options
9010 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9011 // Parent constructor
9012 OO
.ui
.CheckboxInputWidget
.super.call( this, config
);
9015 this.$element
.addClass( 'oo-ui-checkboxInputWidget' );
9020 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9025 * Get input element.
9027 * @return {jQuery} Input element
9029 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9030 return this.$( '<input type="checkbox" />' );
9034 * Get checked state of the checkbox
9036 * @return {boolean} If the checkbox is checked
9038 OO
.ui
.CheckboxInputWidget
.prototype.getValue = function () {
9043 * Set checked state of the checkbox
9045 * @param {boolean} value New value
9047 OO
.ui
.CheckboxInputWidget
.prototype.setValue = function ( value
) {
9049 if ( this.value
!== value
) {
9051 this.$input
.prop( 'checked', this.value
);
9052 this.emit( 'change', this.value
);
9059 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9061 if ( !this.isDisabled() ) {
9062 // Allow the stack to clear so the value will be updated
9063 setTimeout( function () {
9064 widget
.setValue( widget
.$input
.prop( 'checked' ) );
9070 * Input widget with a text field.
9073 * @extends OO.ui.InputWidget
9074 * @mixins OO.ui.IconElement
9075 * @mixins OO.ui.IndicatorElement
9076 * @mixins OO.ui.PendingElement
9079 * @param {Object} [config] Configuration options
9080 * @cfg {string} [placeholder] Placeholder text
9081 * @cfg {boolean} [multiline=false] Allow multiple lines of text
9082 * @cfg {boolean} [autosize=false] Automatically resize to fit content
9083 * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
9084 * @cfg {RegExp|string} [validate] Regular expression (or symbolic name referencing
9085 * one, see #static-validationPatterns)
9087 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
9088 // Configuration initialization
9089 config
= config
|| {};
9091 // Parent constructor
9092 OO
.ui
.TextInputWidget
.super.call( this, config
);
9094 // Mixin constructors
9095 OO
.ui
.IconElement
.call( this, config
);
9096 OO
.ui
.IndicatorElement
.call( this, config
);
9097 OO
.ui
.PendingElement
.call( this, config
);
9100 this.multiline
= !!config
.multiline
;
9101 this.autosize
= !!config
.autosize
;
9102 this.maxRows
= config
.maxRows
!== undefined ? config
.maxRows
: 10;
9103 this.validate
= null;
9105 this.setValidation( config
.validate
);
9109 keypress
: this.onKeyPress
.bind( this ),
9110 blur
: this.setValidityFlag
.bind( this )
9112 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
9113 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
9114 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
9118 .addClass( 'oo-ui-textInputWidget' )
9119 .append( this.$icon
, this.$indicator
);
9120 if ( config
.placeholder
) {
9121 this.$input
.attr( 'placeholder', config
.placeholder
);
9123 this.$element
.attr( 'role', 'textbox' );
9128 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
9129 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.IconElement
);
9130 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.IndicatorElement
);
9131 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.PendingElement
);
9133 /* Static properties */
9135 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
9143 * User presses enter inside the text box.
9145 * Not called if input is multiline.
9151 * User clicks the icon.
9157 * User clicks the indicator.
9165 * Handle icon mouse down events.
9167 * @param {jQuery.Event} e Mouse down event
9170 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
9171 if ( e
.which
=== 1 ) {
9172 this.$input
[0].focus();
9173 this.emit( 'icon' );
9179 * Handle indicator mouse down events.
9181 * @param {jQuery.Event} e Mouse down event
9184 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
9185 if ( e
.which
=== 1 ) {
9186 this.$input
[0].focus();
9187 this.emit( 'indicator' );
9193 * Handle key press events.
9195 * @param {jQuery.Event} e Key press event
9196 * @fires enter If enter key is pressed and input is not multiline
9198 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
9199 if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& !this.multiline
) {
9200 this.emit( 'enter' );
9205 * Handle element attach events.
9207 * @param {jQuery.Event} e Element attach event
9209 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
9216 OO
.ui
.TextInputWidget
.prototype.onEdit = function () {
9220 return OO
.ui
.TextInputWidget
.super.prototype.onEdit
.call( this );
9226 OO
.ui
.TextInputWidget
.prototype.setValue = function ( value
) {
9228 OO
.ui
.TextInputWidget
.super.prototype.setValue
.call( this, value
);
9230 this.setValidityFlag();
9236 * Automatically adjust the size of the text input.
9238 * This only affects multi-line inputs that are auto-sized.
9242 OO
.ui
.TextInputWidget
.prototype.adjustSize = function () {
9243 var $clone
, scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
, idealHeight
;
9245 if ( this.multiline
&& this.autosize
) {
9246 $clone
= this.$input
.clone()
9247 .val( this.$input
.val() )
9248 // Set inline height property to 0 to measure scroll height
9249 .css( { height
: 0 } )
9250 .insertAfter( this.$input
);
9251 scrollHeight
= $clone
[0].scrollHeight
;
9252 // Remove inline height property to measure natural heights
9253 $clone
.css( 'height', '' );
9254 innerHeight
= $clone
.innerHeight();
9255 outerHeight
= $clone
.outerHeight();
9256 // Measure max rows height
9257 $clone
.attr( 'rows', this.maxRows
).css( 'height', 'auto' ).val( '' );
9258 maxInnerHeight
= $clone
.innerHeight();
9259 // Difference between reported innerHeight and scrollHeight with no scrollbars present
9260 // Equals 1 on Blink-based browsers and 0 everywhere else
9261 measurementError
= maxInnerHeight
- $clone
[0].scrollHeight
;
9263 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
9264 // Only apply inline height when expansion beyond natural height is needed
9265 if ( idealHeight
> innerHeight
) {
9266 // Use the difference between the inner and outer height as a buffer
9267 this.$input
.css( 'height', idealHeight
+ ( outerHeight
- innerHeight
) );
9269 this.$input
.css( 'height', '' );
9276 * Get input element.
9278 * @param {Object} [config] Configuration options
9279 * @return {jQuery} Input element
9281 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
9282 return config
.multiline
? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
9288 * Check if input supports multiple lines.
9292 OO
.ui
.TextInputWidget
.prototype.isMultiline = function () {
9293 return !!this.multiline
;
9297 * Check if input automatically adjusts its size.
9301 OO
.ui
.TextInputWidget
.prototype.isAutosizing = function () {
9302 return !!this.autosize
;
9306 * Select the contents of the input.
9310 OO
.ui
.TextInputWidget
.prototype.select = function () {
9311 this.$input
.select();
9316 * Sets the validation pattern to use.
9317 * @param validate {RegExp|string|null} Regular expression (or symbolic name referencing
9318 * one, see #static-validationPatterns)
9320 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
9321 if ( validate
instanceof RegExp
) {
9322 this.validate
= validate
;
9324 this.validate
= this.constructor.static.validationPatterns
[validate
] || /.*/;
9329 * Sets the 'invalid' flag appropriately.
9331 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function () {
9333 this.isValid().done( function ( valid
) {
9334 widget
.setFlags( { invalid
: !valid
} );
9339 * Returns whether or not the current value is considered valid, according to the
9340 * supplied validation pattern.
9342 * @return {jQuery.Deferred}
9344 OO
.ui
.TextInputWidget
.prototype.isValid = function () {
9345 return $.Deferred().resolve( !!this.getValue().match( this.validate
) ).promise();
9349 * Text input with a menu of optional values.
9352 * @extends OO.ui.Widget
9355 * @param {Object} [config] Configuration options
9356 * @cfg {Object} [menu] Configuration options to pass to menu widget
9357 * @cfg {Object} [input] Configuration options to pass to input widget
9358 * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay.
9360 OO
.ui
.ComboBoxWidget
= function OoUiComboBoxWidget( config
) {
9361 // Configuration initialization
9362 config
= config
|| {};
9364 // Parent constructor
9365 OO
.ui
.ComboBoxWidget
.super.call( this, config
);
9368 this.$overlay
= config
.$overlay
|| ( this.$.$iframe
|| this.$element
).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' );
9369 if ( this.$overlay
.length
=== 0 ) {
9370 this.$overlay
= this.$( 'body' );
9372 this.input
= new OO
.ui
.TextInputWidget( $.extend(
9373 { $: this.$, indicator
: 'down', disabled
: this.isDisabled() },
9376 this.menu
= new OO
.ui
.TextInputMenuWidget( this.input
, $.extend(
9377 { $: this.$, widget
: this, input
: this.input
, disabled
: this.isDisabled() },
9382 this.input
.connect( this, {
9383 change
: 'onInputChange',
9384 indicator
: 'onInputIndicator',
9385 enter
: 'onInputEnter'
9387 this.menu
.connect( this, {
9388 choose
: 'onMenuChoose',
9389 add
: 'onMenuItemsChange',
9390 remove
: 'onMenuItemsChange'
9394 this.$element
.addClass( 'oo-ui-comboBoxWidget' ).append( this.input
.$element
);
9395 this.$overlay
.append( this.menu
.$element
);
9396 this.onMenuItemsChange();
9401 OO
.inheritClass( OO
.ui
.ComboBoxWidget
, OO
.ui
.Widget
);
9406 * Handle input change events.
9408 * @param {string} value New value
9410 OO
.ui
.ComboBoxWidget
.prototype.onInputChange = function ( value
) {
9411 var match
= this.menu
.getItemFromData( value
);
9413 this.menu
.selectItem( match
);
9415 if ( !this.isDisabled() ) {
9416 this.menu
.toggle( true );
9421 * Handle input indicator events.
9423 OO
.ui
.ComboBoxWidget
.prototype.onInputIndicator = function () {
9424 if ( !this.isDisabled() ) {
9430 * Handle input enter events.
9432 OO
.ui
.ComboBoxWidget
.prototype.onInputEnter = function () {
9433 if ( !this.isDisabled() ) {
9434 this.menu
.toggle( false );
9439 * Handle menu choose events.
9441 * @param {OO.ui.OptionWidget} item Chosen item
9443 OO
.ui
.ComboBoxWidget
.prototype.onMenuChoose = function ( item
) {
9445 this.input
.setValue( item
.getData() );
9450 * Handle menu item change events.
9452 OO
.ui
.ComboBoxWidget
.prototype.onMenuItemsChange = function () {
9453 this.$element
.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu
.isEmpty() );
9459 OO
.ui
.ComboBoxWidget
.prototype.setDisabled = function ( disabled
) {
9461 OO
.ui
.ComboBoxWidget
.super.prototype.setDisabled
.call( this, disabled
);
9464 this.input
.setDisabled( this.isDisabled() );
9467 this.menu
.setDisabled( this.isDisabled() );
9477 * @extends OO.ui.Widget
9478 * @mixins OO.ui.LabelElement
9481 * @param {Object} [config] Configuration options
9483 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
9484 // Config intialization
9485 config
= config
|| {};
9487 // Parent constructor
9488 OO
.ui
.LabelWidget
.super.call( this, config
);
9490 // Mixin constructors
9491 OO
.ui
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
9492 OO
.ui
.TitledElement
.call( this, config
);
9495 this.input
= config
.input
;
9498 if ( this.input
instanceof OO
.ui
.InputWidget
) {
9499 this.$element
.on( 'click', this.onClick
.bind( this ) );
9503 this.$element
.addClass( 'oo-ui-labelWidget' );
9508 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
9509 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.LabelElement
);
9510 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.TitledElement
);
9512 /* Static Properties */
9514 OO
.ui
.LabelWidget
.static.tagName
= 'span';
9519 * Handles label mouse click events.
9521 * @param {jQuery.Event} e Mouse click event
9523 OO
.ui
.LabelWidget
.prototype.onClick = function () {
9524 this.input
.simulateLabelClick();
9529 * Generic option widget for use with OO.ui.SelectWidget.
9532 * @extends OO.ui.Widget
9533 * @mixins OO.ui.LabelElement
9534 * @mixins OO.ui.FlaggedElement
9537 * @param {Mixed} data Option data
9538 * @param {Object} [config] Configuration options
9539 * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
9541 OO
.ui
.OptionWidget
= function OoUiOptionWidget( data
, config
) {
9542 // Config intialization
9543 config
= config
|| {};
9545 // Parent constructor
9546 OO
.ui
.OptionWidget
.super.call( this, config
);
9548 // Mixin constructors
9549 OO
.ui
.ItemWidget
.call( this );
9550 OO
.ui
.LabelElement
.call( this, config
);
9551 OO
.ui
.FlaggedElement
.call( this, config
);
9555 this.selected
= false;
9556 this.highlighted
= false;
9557 this.pressed
= false;
9561 .data( 'oo-ui-optionWidget', this )
9562 .attr( 'rel', config
.rel
)
9563 .attr( 'role', 'option' )
9564 .addClass( 'oo-ui-optionWidget' )
9565 .append( this.$label
);
9567 .prepend( this.$icon
)
9568 .append( this.$indicator
);
9573 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
9574 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.ItemWidget
);
9575 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.LabelElement
);
9576 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.FlaggedElement
);
9578 /* Static Properties */
9580 OO
.ui
.OptionWidget
.static.selectable
= true;
9582 OO
.ui
.OptionWidget
.static.highlightable
= true;
9584 OO
.ui
.OptionWidget
.static.pressable
= true;
9586 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
9591 * Check if option can be selected.
9593 * @return {boolean} Item is selectable
9595 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
9596 return this.constructor.static.selectable
&& !this.isDisabled();
9600 * Check if option can be highlighted.
9602 * @return {boolean} Item is highlightable
9604 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
9605 return this.constructor.static.highlightable
&& !this.isDisabled();
9609 * Check if option can be pressed.
9611 * @return {boolean} Item is pressable
9613 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
9614 return this.constructor.static.pressable
&& !this.isDisabled();
9618 * Check if option is selected.
9620 * @return {boolean} Item is selected
9622 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
9623 return this.selected
;
9627 * Check if option is highlighted.
9629 * @return {boolean} Item is highlighted
9631 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
9632 return this.highlighted
;
9636 * Check if option is pressed.
9638 * @return {boolean} Item is pressed
9640 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
9641 return this.pressed
;
9645 * Set selected state.
9647 * @param {boolean} [state=false] Select option
9650 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
9651 if ( this.constructor.static.selectable
) {
9652 this.selected
= !!state
;
9653 this.$element
.toggleClass( 'oo-ui-optionWidget-selected', state
);
9654 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
9655 this.scrollElementIntoView();
9657 this.updateThemeClasses();
9663 * Set highlighted state.
9665 * @param {boolean} [state=false] Highlight option
9668 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
9669 if ( this.constructor.static.highlightable
) {
9670 this.highlighted
= !!state
;
9671 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
9672 this.updateThemeClasses();
9678 * Set pressed state.
9680 * @param {boolean} [state=false] Press option
9683 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
9684 if ( this.constructor.static.pressable
) {
9685 this.pressed
= !!state
;
9686 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
9687 this.updateThemeClasses();
9693 * Make the option's highlight flash.
9695 * While flashing, the visual style of the pressed state is removed if present.
9697 * @return {jQuery.Promise} Promise resolved when flashing is done
9699 OO
.ui
.OptionWidget
.prototype.flash = function () {
9701 $element
= this.$element
,
9702 deferred
= $.Deferred();
9704 if ( !this.isDisabled() && this.constructor.static.pressable
) {
9705 $element
.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
9706 setTimeout( function () {
9707 // Restore original classes
9709 .toggleClass( 'oo-ui-optionWidget-highlighted', widget
.highlighted
)
9710 .toggleClass( 'oo-ui-optionWidget-pressed', widget
.pressed
);
9712 setTimeout( function () {
9719 return deferred
.promise();
9725 * @return {Mixed} Option data
9727 OO
.ui
.OptionWidget
.prototype.getData = function () {
9732 * Option widget with an option icon and indicator.
9734 * Use together with OO.ui.SelectWidget.
9737 * @extends OO.ui.OptionWidget
9738 * @mixins OO.ui.IconElement
9739 * @mixins OO.ui.IndicatorElement
9742 * @param {Mixed} data Option data
9743 * @param {Object} [config] Configuration options
9745 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( data
, config
) {
9746 // Parent constructor
9747 OO
.ui
.DecoratedOptionWidget
.super.call( this, data
, config
);
9749 // Mixin constructors
9750 OO
.ui
.IconElement
.call( this, config
);
9751 OO
.ui
.IndicatorElement
.call( this, config
);
9755 .addClass( 'oo-ui-decoratedOptionWidget' )
9756 .prepend( this.$icon
)
9757 .append( this.$indicator
);
9762 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
9763 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.IconElement
);
9764 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.IndicatorElement
);
9767 * Option widget that looks like a button.
9769 * Use together with OO.ui.ButtonSelectWidget.
9772 * @extends OO.ui.DecoratedOptionWidget
9773 * @mixins OO.ui.ButtonElement
9776 * @param {Mixed} data Option data
9777 * @param {Object} [config] Configuration options
9779 OO
.ui
.ButtonOptionWidget
= function OoUiButtonOptionWidget( data
, config
) {
9780 // Parent constructor
9781 OO
.ui
.ButtonOptionWidget
.super.call( this, data
, config
);
9783 // Mixin constructors
9784 OO
.ui
.ButtonElement
.call( this, config
);
9787 this.$element
.addClass( 'oo-ui-buttonOptionWidget' );
9788 this.$button
.append( this.$element
.contents() );
9789 this.$element
.append( this.$button
);
9794 OO
.inheritClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
9795 OO
.mixinClass( OO
.ui
.ButtonOptionWidget
, OO
.ui
.ButtonElement
);
9797 /* Static Properties */
9799 // Allow button mouse down events to pass through so they can be handled by the parent select widget
9800 OO
.ui
.ButtonOptionWidget
.static.cancelButtonMouseDownEvents
= false;
9807 OO
.ui
.ButtonOptionWidget
.prototype.setSelected = function ( state
) {
9808 OO
.ui
.ButtonOptionWidget
.super.prototype.setSelected
.call( this, state
);
9810 if ( this.constructor.static.selectable
) {
9811 this.setActive( state
);
9818 * Item of an OO.ui.MenuWidget.
9821 * @extends OO.ui.DecoratedOptionWidget
9824 * @param {Mixed} data Item data
9825 * @param {Object} [config] Configuration options
9827 OO
.ui
.MenuItemWidget
= function OoUiMenuItemWidget( data
, config
) {
9828 // Configuration initialization
9829 config
= $.extend( { icon
: 'check' }, config
);
9831 // Parent constructor
9832 OO
.ui
.MenuItemWidget
.super.call( this, data
, config
);
9836 .attr( 'role', 'menuitem' )
9837 .addClass( 'oo-ui-menuItemWidget' );
9842 OO
.inheritClass( OO
.ui
.MenuItemWidget
, OO
.ui
.DecoratedOptionWidget
);
9845 * Section to group one or more items in a OO.ui.MenuWidget.
9848 * @extends OO.ui.DecoratedOptionWidget
9851 * @param {Mixed} data Item data
9852 * @param {Object} [config] Configuration options
9854 OO
.ui
.MenuSectionItemWidget
= function OoUiMenuSectionItemWidget( data
, config
) {
9855 // Parent constructor
9856 OO
.ui
.MenuSectionItemWidget
.super.call( this, data
, config
);
9859 this.$element
.addClass( 'oo-ui-menuSectionItemWidget' );
9864 OO
.inheritClass( OO
.ui
.MenuSectionItemWidget
, OO
.ui
.DecoratedOptionWidget
);
9866 /* Static Properties */
9868 OO
.ui
.MenuSectionItemWidget
.static.selectable
= false;
9870 OO
.ui
.MenuSectionItemWidget
.static.highlightable
= false;
9873 * Items for an OO.ui.OutlineWidget.
9876 * @extends OO.ui.DecoratedOptionWidget
9879 * @param {Mixed} data Item data
9880 * @param {Object} [config] Configuration options
9881 * @cfg {number} [level] Indentation level
9882 * @cfg {boolean} [movable] Allow modification from outline controls
9884 OO
.ui
.OutlineItemWidget
= function OoUiOutlineItemWidget( data
, config
) {
9885 // Config intialization
9886 config
= config
|| {};
9888 // Parent constructor
9889 OO
.ui
.OutlineItemWidget
.super.call( this, data
, config
);
9893 this.movable
= !!config
.movable
;
9894 this.removable
= !!config
.removable
;
9897 this.$element
.addClass( 'oo-ui-outlineItemWidget' );
9898 this.setLevel( config
.level
);
9903 OO
.inheritClass( OO
.ui
.OutlineItemWidget
, OO
.ui
.DecoratedOptionWidget
);
9905 /* Static Properties */
9907 OO
.ui
.OutlineItemWidget
.static.highlightable
= false;
9909 OO
.ui
.OutlineItemWidget
.static.scrollIntoViewOnSelect
= true;
9911 OO
.ui
.OutlineItemWidget
.static.levelClass
= 'oo-ui-outlineItemWidget-level-';
9913 OO
.ui
.OutlineItemWidget
.static.levels
= 3;
9918 * Check if item is movable.
9920 * Movablilty is used by outline controls.
9922 * @return {boolean} Item is movable
9924 OO
.ui
.OutlineItemWidget
.prototype.isMovable = function () {
9925 return this.movable
;
9929 * Check if item is removable.
9931 * Removablilty is used by outline controls.
9933 * @return {boolean} Item is removable
9935 OO
.ui
.OutlineItemWidget
.prototype.isRemovable = function () {
9936 return this.removable
;
9940 * Get indentation level.
9942 * @return {number} Indentation level
9944 OO
.ui
.OutlineItemWidget
.prototype.getLevel = function () {
9951 * Movablilty is used by outline controls.
9953 * @param {boolean} movable Item is movable
9956 OO
.ui
.OutlineItemWidget
.prototype.setMovable = function ( movable
) {
9957 this.movable
= !!movable
;
9958 this.updateThemeClasses();
9965 * Removablilty is used by outline controls.
9967 * @param {boolean} movable Item is removable
9970 OO
.ui
.OutlineItemWidget
.prototype.setRemovable = function ( removable
) {
9971 this.removable
= !!removable
;
9972 this.updateThemeClasses();
9977 * Set indentation level.
9979 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
9982 OO
.ui
.OutlineItemWidget
.prototype.setLevel = function ( level
) {
9983 var levels
= this.constructor.static.levels
,
9984 levelClass
= this.constructor.static.levelClass
,
9987 this.level
= level
? Math
.max( 0, Math
.min( levels
- 1, level
) ) : 0;
9989 if ( this.level
=== i
) {
9990 this.$element
.addClass( levelClass
+ i
);
9992 this.$element
.removeClass( levelClass
+ i
);
9995 this.updateThemeClasses();
10001 * Container for content that is overlaid and positioned absolutely.
10004 * @extends OO.ui.Widget
10005 * @mixins OO.ui.LabelElement
10008 * @param {Object} [config] Configuration options
10009 * @cfg {number} [width=320] Width of popup in pixels
10010 * @cfg {number} [height] Height of popup, omit to use automatic height
10011 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
10012 * @cfg {string} [align='center'] Alignment of popup to origin
10013 * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
10014 * @cfg {jQuery} [$content] Content to append to the popup's body
10015 * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
10016 * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
10017 * @cfg {boolean} [head] Show label and close button at the top
10018 * @cfg {boolean} [padded] Add padding to the body
10020 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
10021 // Config intialization
10022 config
= config
|| {};
10024 // Parent constructor
10025 OO
.ui
.PopupWidget
.super.call( this, config
);
10027 // Mixin constructors
10028 OO
.ui
.LabelElement
.call( this, config
);
10029 OO
.ui
.ClippableElement
.call( this, config
);
10032 this.visible
= false;
10033 this.$popup
= this.$( '<div>' );
10034 this.$head
= this.$( '<div>' );
10035 this.$body
= this.$( '<div>' );
10036 this.$anchor
= this.$( '<div>' );
10037 this.$container
= config
.$container
; // If undefined, will be computed lazily in updateDimensions()
10038 this.autoClose
= !!config
.autoClose
;
10039 this.$autoCloseIgnore
= config
.$autoCloseIgnore
;
10040 this.transitionTimeout
= null;
10041 this.anchor
= null;
10042 this.width
= config
.width
!== undefined ? config
.width
: 320;
10043 this.height
= config
.height
!== undefined ? config
.height
: null;
10044 this.align
= config
.align
|| 'center';
10045 this.closeButton
= new OO
.ui
.ButtonWidget( { $: this.$, framed
: false, icon
: 'close' } );
10046 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
10049 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
10052 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
10053 this.$body
.addClass( 'oo-ui-popupWidget-body' );
10054 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
10056 .addClass( 'oo-ui-popupWidget-head' )
10057 .append( this.$label
, this.closeButton
.$element
);
10058 if ( !config
.head
) {
10062 .addClass( 'oo-ui-popupWidget-popup' )
10063 .append( this.$head
, this.$body
);
10066 .addClass( 'oo-ui-popupWidget' )
10067 .append( this.$popup
, this.$anchor
);
10068 // Move content, which was added to #$element by OO.ui.Widget, to the body
10069 if ( config
.$content
instanceof jQuery
) {
10070 this.$body
.append( config
.$content
);
10072 if ( config
.padded
) {
10073 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
10075 this.setClippableElement( this.$body
);
10080 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
10081 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.LabelElement
);
10082 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.ClippableElement
);
10097 * Handles mouse down events.
10099 * @param {jQuery.Event} e Mouse down event
10101 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
10103 this.isVisible() &&
10104 !$.contains( this.$element
[0], e
.target
) &&
10105 ( !this.$autoCloseIgnore
|| !this.$autoCloseIgnore
.has( e
.target
).length
)
10107 this.toggle( false );
10112 * Bind mouse down listener.
10114 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
10115 // Capture clicks outside popup
10116 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
10120 * Handles close button click events.
10122 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
10123 if ( this.isVisible() ) {
10124 this.toggle( false );
10129 * Unbind mouse down listener.
10131 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
10132 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
10136 * Set whether to show a anchor.
10138 * @param {boolean} [show] Show anchor, omit to toggle
10140 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
10141 show
= show
=== undefined ? !this.anchored
: !!show
;
10143 if ( this.anchored
!== show
) {
10145 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
10147 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
10149 this.anchored
= show
;
10154 * Check if showing a anchor.
10156 * @return {boolean} anchor is visible
10158 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
10159 return this.anchor
;
10165 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
10166 show
= show
=== undefined ? !this.isVisible() : !!show
;
10168 var change
= show
!== this.isVisible();
10171 OO
.ui
.PopupWidget
.super.prototype.toggle
.call( this, show
);
10175 if ( this.autoClose
) {
10176 this.bindMouseDownListener();
10178 this.updateDimensions();
10179 this.toggleClipping( true );
10181 this.toggleClipping( false );
10182 if ( this.autoClose
) {
10183 this.unbindMouseDownListener();
10192 * Set the size of the popup.
10194 * Changing the size may also change the popup's position depending on the alignment.
10196 * @param {number} width Width
10197 * @param {number} height Height
10198 * @param {boolean} [transition=false] Use a smooth transition
10201 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
10202 this.width
= width
;
10203 this.height
= height
!== undefined ? height
: null;
10204 if ( this.isVisible() ) {
10205 this.updateDimensions( transition
);
10210 * Update the size and position.
10212 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
10213 * be called automatically.
10215 * @param {boolean} [transition=false] Use a smooth transition
10218 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
10219 var popupOffset
, originOffset
, containerLeft
, containerWidth
, containerRight
,
10220 popupLeft
, popupRight
, overlapLeft
, overlapRight
, anchorWidth
,
10224 if ( !this.$container
) {
10225 // Lazy-initialize $container if not specified in constructor
10226 this.$container
= this.$( this.getClosestScrollableElementContainer() );
10229 // Set height and width before measuring things, since it might cause our measurements
10230 // to change (e.g. due to scrollbars appearing or disappearing)
10233 height
: this.height
!== null ? this.height
: 'auto'
10236 // Compute initial popupOffset based on alignment
10237 popupOffset
= this.width
* ( { left
: 0, center
: -0.5, right
: -1 } )[this.align
];
10239 // Figure out if this will cause the popup to go beyond the edge of the container
10240 originOffset
= Math
.round( this.$element
.offset().left
);
10241 containerLeft
= Math
.round( this.$container
.offset().left
);
10242 containerWidth
= this.$container
.innerWidth();
10243 containerRight
= containerLeft
+ containerWidth
;
10244 popupLeft
= popupOffset
- padding
;
10245 popupRight
= popupOffset
+ padding
+ this.width
+ padding
;
10246 overlapLeft
= ( originOffset
+ popupLeft
) - containerLeft
;
10247 overlapRight
= containerRight
- ( originOffset
+ popupRight
);
10249 // Adjust offset to make the popup not go beyond the edge, if needed
10250 if ( overlapRight
< 0 ) {
10251 popupOffset
+= overlapRight
;
10252 } else if ( overlapLeft
< 0 ) {
10253 popupOffset
-= overlapLeft
;
10256 // Adjust offset to avoid anchor being rendered too close to the edge
10257 anchorWidth
= this.$anchor
.width();
10258 if ( this.align
=== 'right' ) {
10259 popupOffset
+= anchorWidth
;
10260 } else if ( this.align
=== 'left' ) {
10261 popupOffset
-= anchorWidth
;
10264 // Prevent transition from being interrupted
10265 clearTimeout( this.transitionTimeout
);
10266 if ( transition
) {
10267 // Enable transition
10268 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
10271 // Position body relative to anchor
10272 this.$popup
.css( 'margin-left', popupOffset
);
10274 if ( transition
) {
10275 // Prevent transitioning after transition is complete
10276 this.transitionTimeout
= setTimeout( function () {
10277 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
10280 // Prevent transitioning immediately
10281 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
10284 // Reevaluate clipping state since we've relocated and resized the popup
10293 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
10294 * Results are cleared and populated each time the query is changed.
10297 * @extends OO.ui.Widget
10300 * @param {Object} [config] Configuration options
10301 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
10302 * @cfg {string} [value] Initial query value
10304 OO
.ui
.SearchWidget
= function OoUiSearchWidget( config
) {
10305 // Configuration intialization
10306 config
= config
|| {};
10308 // Parent constructor
10309 OO
.ui
.SearchWidget
.super.call( this, config
);
10312 this.query
= new OO
.ui
.TextInputWidget( {
10315 placeholder
: config
.placeholder
,
10316 value
: config
.value
10318 this.results
= new OO
.ui
.SelectWidget( { $: this.$ } );
10319 this.$query
= this.$( '<div>' );
10320 this.$results
= this.$( '<div>' );
10323 this.query
.connect( this, {
10324 change
: 'onQueryChange',
10325 enter
: 'onQueryEnter'
10327 this.results
.connect( this, {
10328 highlight
: 'onResultsHighlight',
10329 select
: 'onResultsSelect'
10331 this.query
.$input
.on( 'keydown', this.onQueryKeydown
.bind( this ) );
10335 .addClass( 'oo-ui-searchWidget-query' )
10336 .append( this.query
.$element
);
10338 .addClass( 'oo-ui-searchWidget-results' )
10339 .append( this.results
.$element
);
10341 .addClass( 'oo-ui-searchWidget' )
10342 .append( this.$results
, this.$query
);
10347 OO
.inheritClass( OO
.ui
.SearchWidget
, OO
.ui
.Widget
);
10353 * @param {Object|null} item Item data or null if no item is highlighted
10358 * @param {Object|null} item Item data or null if no item is selected
10364 * Handle query key down events.
10366 * @param {jQuery.Event} e Key down event
10368 OO
.ui
.SearchWidget
.prototype.onQueryKeydown = function ( e
) {
10369 var highlightedItem
, nextItem
,
10370 dir
= e
.which
=== OO
.ui
.Keys
.DOWN
? 1 : ( e
.which
=== OO
.ui
.Keys
.UP
? -1 : 0 );
10373 highlightedItem
= this.results
.getHighlightedItem();
10374 if ( !highlightedItem
) {
10375 highlightedItem
= this.results
.getSelectedItem();
10377 nextItem
= this.results
.getRelativeSelectableItem( highlightedItem
, dir
);
10378 this.results
.highlightItem( nextItem
);
10379 nextItem
.scrollElementIntoView();
10384 * Handle select widget select events.
10386 * Clears existing results. Subclasses should repopulate items according to new query.
10388 * @param {string} value New value
10390 OO
.ui
.SearchWidget
.prototype.onQueryChange = function () {
10392 this.results
.clearItems();
10396 * Handle select widget enter key events.
10398 * Selects highlighted item.
10400 * @param {string} value New value
10402 OO
.ui
.SearchWidget
.prototype.onQueryEnter = function () {
10404 this.results
.selectItem( this.results
.getHighlightedItem() );
10408 * Handle select widget highlight events.
10410 * @param {OO.ui.OptionWidget} item Highlighted item
10413 OO
.ui
.SearchWidget
.prototype.onResultsHighlight = function ( item
) {
10414 this.emit( 'highlight', item
? item
.getData() : null );
10418 * Handle select widget select events.
10420 * @param {OO.ui.OptionWidget} item Selected item
10423 OO
.ui
.SearchWidget
.prototype.onResultsSelect = function ( item
) {
10424 this.emit( 'select', item
? item
.getData() : null );
10428 * Get the query input.
10430 * @return {OO.ui.TextInputWidget} Query input
10432 OO
.ui
.SearchWidget
.prototype.getQuery = function () {
10437 * Get the results list.
10439 * @return {OO.ui.SelectWidget} Select list
10441 OO
.ui
.SearchWidget
.prototype.getResults = function () {
10442 return this.results
;
10446 * Generic selection of options.
10448 * Items can contain any rendering, and are uniquely identified by a hash of their data. Any widget
10449 * that provides options, from which the user must choose one, should be built on this class.
10451 * Use together with OO.ui.OptionWidget.
10454 * @extends OO.ui.Widget
10455 * @mixins OO.ui.GroupElement
10458 * @param {Object} [config] Configuration options
10459 * @cfg {OO.ui.OptionWidget[]} [items] Options to add
10461 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
10462 // Config intialization
10463 config
= config
|| {};
10465 // Parent constructor
10466 OO
.ui
.SelectWidget
.super.call( this, config
);
10468 // Mixin constructors
10469 OO
.ui
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
10472 this.pressed
= false;
10473 this.selecting
= null;
10475 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
10476 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
10479 this.$element
.on( {
10480 mousedown
: this.onMouseDown
.bind( this ),
10481 mouseover
: this.onMouseOver
.bind( this ),
10482 mouseleave
: this.onMouseLeave
.bind( this )
10486 this.$element
.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
10487 if ( $.isArray( config
.items
) ) {
10488 this.addItems( config
.items
);
10494 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
10496 // Need to mixin base class as well
10497 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.GroupElement
);
10498 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.GroupWidget
);
10504 * @param {OO.ui.OptionWidget|null} item Highlighted item
10509 * @param {OO.ui.OptionWidget|null} item Pressed item
10514 * @param {OO.ui.OptionWidget|null} item Selected item
10519 * @param {OO.ui.OptionWidget|null} item Chosen item
10524 * @param {OO.ui.OptionWidget[]} items Added items
10525 * @param {number} index Index items were added at
10530 * @param {OO.ui.OptionWidget[]} items Removed items
10536 * Handle mouse down events.
10539 * @param {jQuery.Event} e Mouse down event
10541 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
10544 if ( !this.isDisabled() && e
.which
=== 1 ) {
10545 this.togglePressed( true );
10546 item
= this.getTargetItem( e
);
10547 if ( item
&& item
.isSelectable() ) {
10548 this.pressItem( item
);
10549 this.selecting
= item
;
10550 this.getElementDocument().addEventListener(
10552 this.onMouseUpHandler
,
10555 this.getElementDocument().addEventListener(
10557 this.onMouseMoveHandler
,
10566 * Handle mouse up events.
10569 * @param {jQuery.Event} e Mouse up event
10571 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
10574 this.togglePressed( false );
10575 if ( !this.selecting
) {
10576 item
= this.getTargetItem( e
);
10577 if ( item
&& item
.isSelectable() ) {
10578 this.selecting
= item
;
10581 if ( !this.isDisabled() && e
.which
=== 1 && this.selecting
) {
10582 this.pressItem( null );
10583 this.chooseItem( this.selecting
);
10584 this.selecting
= null;
10587 this.getElementDocument().removeEventListener(
10589 this.onMouseUpHandler
,
10592 this.getElementDocument().removeEventListener(
10594 this.onMouseMoveHandler
,
10602 * Handle mouse move events.
10605 * @param {jQuery.Event} e Mouse move event
10607 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
10610 if ( !this.isDisabled() && this.pressed
) {
10611 item
= this.getTargetItem( e
);
10612 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
10613 this.pressItem( item
);
10614 this.selecting
= item
;
10621 * Handle mouse over events.
10624 * @param {jQuery.Event} e Mouse over event
10626 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
10629 if ( !this.isDisabled() ) {
10630 item
= this.getTargetItem( e
);
10631 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
10637 * Handle mouse leave events.
10640 * @param {jQuery.Event} e Mouse over event
10642 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
10643 if ( !this.isDisabled() ) {
10644 this.highlightItem( null );
10650 * Get the closest item to a jQuery.Event.
10653 * @param {jQuery.Event} e
10654 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
10656 OO
.ui
.SelectWidget
.prototype.getTargetItem = function ( e
) {
10657 var $item
= this.$( e
.target
).closest( '.oo-ui-optionWidget' );
10658 if ( $item
.length
) {
10659 return $item
.data( 'oo-ui-optionWidget' );
10665 * Get selected item.
10667 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
10669 OO
.ui
.SelectWidget
.prototype.getSelectedItem = function () {
10672 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
10673 if ( this.items
[i
].isSelected() ) {
10674 return this.items
[i
];
10681 * Get highlighted item.
10683 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
10685 OO
.ui
.SelectWidget
.prototype.getHighlightedItem = function () {
10688 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
10689 if ( this.items
[i
].isHighlighted() ) {
10690 return this.items
[i
];
10697 * Get an existing item with equivilant data.
10699 * @param {Object} data Item data to search for
10700 * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
10702 OO
.ui
.SelectWidget
.prototype.getItemFromData = function ( data
) {
10703 var hash
= OO
.getHash( data
);
10705 if ( hash
in this.hashes
) {
10706 return this.hashes
[hash
];
10713 * Toggle pressed state.
10715 * @param {boolean} pressed An option is being pressed
10717 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
10718 if ( pressed
=== undefined ) {
10719 pressed
= !this.pressed
;
10721 if ( pressed
!== this.pressed
) {
10723 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
10724 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
10725 this.pressed
= pressed
;
10730 * Highlight an item.
10732 * Highlighting is mutually exclusive.
10734 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
10738 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
10739 var i
, len
, highlighted
,
10742 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
10743 highlighted
= this.items
[i
] === item
;
10744 if ( this.items
[i
].isHighlighted() !== highlighted
) {
10745 this.items
[i
].setHighlighted( highlighted
);
10750 this.emit( 'highlight', item
);
10759 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
10763 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
10764 var i
, len
, selected
,
10767 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
10768 selected
= this.items
[i
] === item
;
10769 if ( this.items
[i
].isSelected() !== selected
) {
10770 this.items
[i
].setSelected( selected
);
10775 this.emit( 'select', item
);
10784 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
10788 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
10789 var i
, len
, pressed
,
10792 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
10793 pressed
= this.items
[i
] === item
;
10794 if ( this.items
[i
].isPressed() !== pressed
) {
10795 this.items
[i
].setPressed( pressed
);
10800 this.emit( 'press', item
);
10809 * Identical to #selectItem, but may vary in subclasses that want to take additional action when
10810 * an item is selected using the keyboard or mouse.
10812 * @param {OO.ui.OptionWidget} item Item to choose
10816 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
10817 this.selectItem( item
);
10818 this.emit( 'choose', item
);
10824 * Get an item relative to another one.
10826 * @param {OO.ui.OptionWidget} item Item to start at
10827 * @param {number} direction Direction to move in
10828 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
10830 OO
.ui
.SelectWidget
.prototype.getRelativeSelectableItem = function ( item
, direction
) {
10831 var inc
= direction
> 0 ? 1 : -1,
10832 len
= this.items
.length
,
10833 index
= item
instanceof OO
.ui
.OptionWidget
?
10834 $.inArray( item
, this.items
) : ( inc
> 0 ? -1 : 0 ),
10835 stopAt
= Math
.max( Math
.min( index
, len
- 1 ), 0 ),
10837 // Default to 0 instead of -1, if nothing is selected let's start at the beginning
10838 Math
.max( index
, -1 ) :
10839 // Default to n-1 instead of -1, if nothing is selected let's start at the end
10840 Math
.min( index
, len
);
10842 while ( len
!== 0 ) {
10843 i
= ( i
+ inc
+ len
) % len
;
10844 item
= this.items
[i
];
10845 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
10848 // Stop iterating when we've looped all the way around
10849 if ( i
=== stopAt
) {
10857 * Get the next selectable item.
10859 * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
10861 OO
.ui
.SelectWidget
.prototype.getFirstSelectableItem = function () {
10864 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
10865 item
= this.items
[i
];
10866 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() ) {
10877 * When items are added with the same values as existing items, the existing items will be
10878 * automatically removed before the new items are added.
10880 * @param {OO.ui.OptionWidget[]} items Items to add
10881 * @param {number} [index] Index to insert items after
10885 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
10886 var i
, len
, item
, hash
,
10889 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
10891 hash
= OO
.getHash( item
.getData() );
10892 if ( hash
in this.hashes
) {
10893 // Remove item with same value
10894 remove
.push( this.hashes
[hash
] );
10896 this.hashes
[hash
] = item
;
10898 if ( remove
.length
) {
10899 this.removeItems( remove
);
10903 OO
.ui
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
10905 // Always provide an index, even if it was omitted
10906 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
10914 * Items will be detached, not removed, so they can be used later.
10916 * @param {OO.ui.OptionWidget[]} items Items to remove
10920 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
10921 var i
, len
, item
, hash
;
10923 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
10925 hash
= OO
.getHash( item
.getData() );
10926 if ( hash
in this.hashes
) {
10927 // Remove existing item
10928 delete this.hashes
[hash
];
10930 if ( item
.isSelected() ) {
10931 this.selectItem( null );
10936 OO
.ui
.GroupWidget
.prototype.removeItems
.call( this, items
);
10938 this.emit( 'remove', items
);
10946 * Items will be detached, not removed, so they can be used later.
10951 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
10952 var items
= this.items
.slice();
10957 OO
.ui
.GroupWidget
.prototype.clearItems
.call( this );
10958 this.selectItem( null );
10960 this.emit( 'remove', items
);
10966 * Select widget containing button options.
10968 * Use together with OO.ui.ButtonOptionWidget.
10971 * @extends OO.ui.SelectWidget
10974 * @param {Object} [config] Configuration options
10976 OO
.ui
.ButtonSelectWidget
= function OoUiButtonSelectWidget( config
) {
10977 // Parent constructor
10978 OO
.ui
.ButtonSelectWidget
.super.call( this, config
);
10981 this.$element
.addClass( 'oo-ui-buttonSelectWidget' );
10986 OO
.inheritClass( OO
.ui
.ButtonSelectWidget
, OO
.ui
.SelectWidget
);
10989 * Overlaid menu of options.
10991 * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
10994 * Use together with OO.ui.MenuItemWidget.
10997 * @extends OO.ui.SelectWidget
10998 * @mixins OO.ui.ClippableElement
11001 * @param {Object} [config] Configuration options
11002 * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
11003 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
11004 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
11006 OO
.ui
.MenuWidget
= function OoUiMenuWidget( config
) {
11007 // Config intialization
11008 config
= config
|| {};
11010 // Parent constructor
11011 OO
.ui
.MenuWidget
.super.call( this, config
);
11013 // Mixin constructors
11014 OO
.ui
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
11017 this.flashing
= false;
11018 this.visible
= false;
11019 this.newItems
= null;
11020 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
11021 this.$input
= config
.input
? config
.input
.$input
: null;
11022 this.$widget
= config
.widget
? config
.widget
.$element
: null;
11023 this.$previousFocus
= null;
11024 this.isolated
= !config
.input
;
11025 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
11026 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
11031 .attr( 'role', 'menu' )
11032 .addClass( 'oo-ui-menuWidget' );
11037 OO
.inheritClass( OO
.ui
.MenuWidget
, OO
.ui
.SelectWidget
);
11038 OO
.mixinClass( OO
.ui
.MenuWidget
, OO
.ui
.ClippableElement
);
11043 * Handles document mouse down events.
11045 * @param {jQuery.Event} e Key down event
11047 OO
.ui
.MenuWidget
.prototype.onDocumentMouseDown = function ( e
) {
11048 if ( !$.contains( this.$element
[0], e
.target
) && ( !this.$widget
|| !$.contains( this.$widget
[0], e
.target
) ) ) {
11049 this.toggle( false );
11054 * Handles key down events.
11056 * @param {jQuery.Event} e Key down event
11058 OO
.ui
.MenuWidget
.prototype.onKeyDown = function ( e
) {
11061 highlightItem
= this.getHighlightedItem();
11063 if ( !this.isDisabled() && this.isVisible() ) {
11064 if ( !highlightItem
) {
11065 highlightItem
= this.getSelectedItem();
11067 switch ( e
.keyCode
) {
11068 case OO
.ui
.Keys
.ENTER
:
11069 this.chooseItem( highlightItem
);
11072 case OO
.ui
.Keys
.UP
:
11073 nextItem
= this.getRelativeSelectableItem( highlightItem
, -1 );
11076 case OO
.ui
.Keys
.DOWN
:
11077 nextItem
= this.getRelativeSelectableItem( highlightItem
, 1 );
11080 case OO
.ui
.Keys
.ESCAPE
:
11081 if ( highlightItem
) {
11082 highlightItem
.setHighlighted( false );
11084 this.toggle( false );
11090 this.highlightItem( nextItem
);
11091 nextItem
.scrollElementIntoView();
11095 e
.preventDefault();
11096 e
.stopPropagation();
11103 * Bind key down listener.
11105 OO
.ui
.MenuWidget
.prototype.bindKeyDownListener = function () {
11106 if ( this.$input
) {
11107 this.$input
.on( 'keydown', this.onKeyDownHandler
);
11109 // Capture menu navigation keys
11110 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
11115 * Unbind key down listener.
11117 OO
.ui
.MenuWidget
.prototype.unbindKeyDownListener = function () {
11118 if ( this.$input
) {
11119 this.$input
.off( 'keydown' );
11121 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
11128 * This will close the menu when done, unlike selectItem which only changes selection.
11130 * @param {OO.ui.OptionWidget} item Item to choose
11133 OO
.ui
.MenuWidget
.prototype.chooseItem = function ( item
) {
11137 OO
.ui
.MenuWidget
.super.prototype.chooseItem
.call( this, item
);
11139 if ( item
&& !this.flashing
) {
11140 this.flashing
= true;
11141 item
.flash().done( function () {
11142 widget
.toggle( false );
11143 widget
.flashing
= false;
11146 this.toggle( false );
11155 OO
.ui
.MenuWidget
.prototype.addItems = function ( items
, index
) {
11159 OO
.ui
.MenuWidget
.super.prototype.addItems
.call( this, items
, index
);
11162 if ( !this.newItems
) {
11163 this.newItems
= [];
11166 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
11168 if ( this.isVisible() ) {
11169 // Defer fitting label until item has been attached
11172 this.newItems
.push( item
);
11176 // Reevaluate clipping
11185 OO
.ui
.MenuWidget
.prototype.removeItems = function ( items
) {
11187 OO
.ui
.MenuWidget
.super.prototype.removeItems
.call( this, items
);
11189 // Reevaluate clipping
11198 OO
.ui
.MenuWidget
.prototype.clearItems = function () {
11200 OO
.ui
.MenuWidget
.super.prototype.clearItems
.call( this );
11202 // Reevaluate clipping
11211 OO
.ui
.MenuWidget
.prototype.toggle = function ( visible
) {
11212 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
11215 change
= visible
!== this.isVisible(),
11216 elementDoc
= this.getElementDocument(),
11217 widgetDoc
= this.$widget
? this.$widget
[0].ownerDocument
: null;
11220 OO
.ui
.MenuWidget
.super.prototype.toggle
.call( this, visible
);
11224 this.bindKeyDownListener();
11226 // Change focus to enable keyboard navigation
11227 if ( this.isolated
&& this.$input
&& !this.$input
.is( ':focus' ) ) {
11228 this.$previousFocus
= this.$( ':focus' );
11229 this.$input
[0].focus();
11231 if ( this.newItems
&& this.newItems
.length
) {
11232 for ( i
= 0, len
= this.newItems
.length
; i
< len
; i
++ ) {
11233 this.newItems
[i
].fitLabel();
11235 this.newItems
= null;
11237 this.toggleClipping( true );
11240 if ( this.autoHide
) {
11241 elementDoc
.addEventListener(
11242 'mousedown', this.onDocumentMouseDownHandler
, true
11244 // Support $widget being in a different document
11245 if ( widgetDoc
&& widgetDoc
!== elementDoc
) {
11246 widgetDoc
.addEventListener(
11247 'mousedown', this.onDocumentMouseDownHandler
, true
11252 this.unbindKeyDownListener();
11253 if ( this.isolated
&& this.$previousFocus
) {
11254 this.$previousFocus
[0].focus();
11255 this.$previousFocus
= null;
11257 elementDoc
.removeEventListener(
11258 'mousedown', this.onDocumentMouseDownHandler
, true
11260 // Support $widget being in a different document
11261 if ( widgetDoc
&& widgetDoc
!== elementDoc
) {
11262 widgetDoc
.removeEventListener(
11263 'mousedown', this.onDocumentMouseDownHandler
, true
11266 this.toggleClipping( false );
11274 * Menu for a text input widget.
11276 * This menu is specially designed to be positioned beneath the text input widget. Even if the input
11277 * is in a different frame, the menu's position is automatically calculated and maintained when the
11278 * menu is toggled or the window is resized.
11281 * @extends OO.ui.MenuWidget
11284 * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
11285 * @param {Object} [config] Configuration options
11286 * @cfg {jQuery} [$container=input.$element] Element to render menu under
11288 OO
.ui
.TextInputMenuWidget
= function OoUiTextInputMenuWidget( input
, config
) {
11289 // Parent constructor
11290 OO
.ui
.TextInputMenuWidget
.super.call( this, config
);
11293 this.input
= input
;
11294 this.$container
= config
.$container
|| this.input
.$element
;
11295 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
11298 this.$element
.addClass( 'oo-ui-textInputMenuWidget' );
11303 OO
.inheritClass( OO
.ui
.TextInputMenuWidget
, OO
.ui
.MenuWidget
);
11308 * Handle window resize event.
11310 * @param {jQuery.Event} e Window resize event
11312 OO
.ui
.TextInputMenuWidget
.prototype.onWindowResize = function () {
11319 OO
.ui
.TextInputMenuWidget
.prototype.toggle = function ( visible
) {
11320 visible
= visible
=== undefined ? !this.isVisible() : !!visible
;
11322 var change
= visible
!== this.isVisible();
11324 if ( change
&& visible
) {
11325 // Make sure the width is set before the parent method runs.
11326 // After this we have to call this.position(); again to actually
11327 // position ourselves correctly.
11332 OO
.ui
.TextInputMenuWidget
.super.prototype.toggle
.call( this, visible
);
11335 if ( this.isVisible() ) {
11337 this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler
);
11339 this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler
);
11347 * Position the menu.
11351 OO
.ui
.TextInputMenuWidget
.prototype.position = function () {
11352 var $container
= this.$container
,
11353 pos
= OO
.ui
.Element
.getRelativePosition( $container
, this.$element
.offsetParent() );
11355 // Position under input
11356 pos
.top
+= $container
.height();
11357 this.$element
.css( pos
);
11360 this.setIdealSize( $container
.width() );
11361 // We updated the position, so re-evaluate the clipping state
11368 * Structured list of items.
11370 * Use with OO.ui.OutlineItemWidget.
11373 * @extends OO.ui.SelectWidget
11376 * @param {Object} [config] Configuration options
11378 OO
.ui
.OutlineWidget
= function OoUiOutlineWidget( config
) {
11379 // Config intialization
11380 config
= config
|| {};
11382 // Parent constructor
11383 OO
.ui
.OutlineWidget
.super.call( this, config
);
11386 this.$element
.addClass( 'oo-ui-outlineWidget' );
11391 OO
.inheritClass( OO
.ui
.OutlineWidget
, OO
.ui
.SelectWidget
);
11394 * Switch that slides on and off.
11397 * @extends OO.ui.Widget
11398 * @mixins OO.ui.ToggleWidget
11401 * @param {Object} [config] Configuration options
11402 * @cfg {boolean} [value=false] Initial value
11404 OO
.ui
.ToggleSwitchWidget
= function OoUiToggleSwitchWidget( config
) {
11405 // Parent constructor
11406 OO
.ui
.ToggleSwitchWidget
.super.call( this, config
);
11408 // Mixin constructors
11409 OO
.ui
.ToggleWidget
.call( this, config
);
11412 this.dragging
= false;
11413 this.dragStart
= null;
11414 this.sliding
= false;
11415 this.$glow
= this.$( '<span>' );
11416 this.$grip
= this.$( '<span>' );
11419 this.$element
.on( 'click', this.onClick
.bind( this ) );
11422 this.$glow
.addClass( 'oo-ui-toggleSwitchWidget-glow' );
11423 this.$grip
.addClass( 'oo-ui-toggleSwitchWidget-grip' );
11425 .addClass( 'oo-ui-toggleSwitchWidget' )
11426 .append( this.$glow
, this.$grip
);
11431 OO
.inheritClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.Widget
);
11432 OO
.mixinClass( OO
.ui
.ToggleSwitchWidget
, OO
.ui
.ToggleWidget
);
11437 * Handle mouse down events.
11439 * @param {jQuery.Event} e Mouse down event
11441 OO
.ui
.ToggleSwitchWidget
.prototype.onClick = function ( e
) {
11442 if ( !this.isDisabled() && e
.which
=== 1 ) {
11443 this.setValue( !this.value
);