X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/resources/lib/oojs-ui/oojs-ui-core.js diff --git a/resources/lib/oojs-ui/oojs-ui-core.js b/resources/lib/oojs-ui/oojs-ui-core.js new file mode 100644 index 00000000..c92ab4dd --- /dev/null +++ b/resources/lib/oojs-ui/oojs-ui-core.js @@ -0,0 +1,11511 @@ +/*! + * OOjs UI v0.23.0 + * https://www.mediawiki.org/wiki/OOjs_UI + * + * Copyright 2011–2017 OOjs UI Team and other contributors. + * Released under the MIT license + * http://oojs.mit-license.org + * + * Date: 2017-09-05T21:23:58Z + */ +( function ( OO ) { + +'use strict'; + +/** + * Namespace for all classes, static methods and static properties. + * + * @class + * @singleton + */ +OO.ui = {}; + +OO.ui.bind = $.proxy; + +/** + * @property {Object} + */ +OO.ui.Keys = { + UNDEFINED: 0, + BACKSPACE: 8, + DELETE: 46, + LEFT: 37, + RIGHT: 39, + UP: 38, + DOWN: 40, + ENTER: 13, + END: 35, + HOME: 36, + TAB: 9, + PAGEUP: 33, + PAGEDOWN: 34, + ESCAPE: 27, + SHIFT: 16, + SPACE: 32 +}; + +/** + * Constants for MouseEvent.which + * + * @property {Object} + */ +OO.ui.MouseButtons = { + LEFT: 1, + MIDDLE: 2, + RIGHT: 3 +}; + +/** + * @property {number} + * @private + */ +OO.ui.elementId = 0; + +/** + * Generate a unique ID for element + * + * @return {string} ID + */ +OO.ui.generateElementId = function () { + OO.ui.elementId++; + return 'oojsui-' + OO.ui.elementId; +}; + +/** + * Check if an element is focusable. + * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14 + * + * @param {jQuery} $element Element to test + * @return {boolean} Element is focusable + */ +OO.ui.isFocusableElement = function ( $element ) { + var nodeName, + element = $element[ 0 ]; + + // Anything disabled is not focusable + if ( element.disabled ) { + return false; + } + + // Check if the element is visible + if ( !( + // This is quicker than calling $element.is( ':visible' ) + $.expr.pseudos.visible( element ) && + // Check that all parents are visible + !$element.parents().addBack().filter( function () { + return $.css( this, 'visibility' ) === 'hidden'; + } ).length + ) ) { + return false; + } + + // Check if the element is ContentEditable, which is the string 'true' + if ( element.contentEditable === 'true' ) { + return true; + } + + // Anything with a non-negative numeric tabIndex is focusable. + // Use .prop to avoid browser bugs + if ( $element.prop( 'tabIndex' ) >= 0 ) { + return true; + } + + // Some element types are naturally focusable + // (indexOf is much faster than regex in Chrome and about the + // same in FF: https://jsperf.com/regex-vs-indexof-array2) + nodeName = element.nodeName.toLowerCase(); + if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) { + return true; + } + + // Links and areas are focusable if they have an href + if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) { + return true; + } + + return false; +}; + +/** + * Find a focusable child + * + * @param {jQuery} $container Container to search in + * @param {boolean} [backwards] Search backwards + * @return {jQuery} Focusable child, or an empty jQuery object if none found + */ +OO.ui.findFocusable = function ( $container, backwards ) { + var $focusable = $( [] ), + // $focusableCandidates is a superset of things that + // could get matched by isFocusableElement + $focusableCandidates = $container + .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' ); + + if ( backwards ) { + $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates ); + } + + $focusableCandidates.each( function () { + var $this = $( this ); + if ( OO.ui.isFocusableElement( $this ) ) { + $focusable = $this; + return false; + } + } ); + return $focusable; +}; + +/** + * Get the user's language and any fallback languages. + * + * These language codes are used to localize user interface elements in the user's language. + * + * In environments that provide a localization system, this function should be overridden to + * return the user's language(s). The default implementation returns English (en) only. + * + * @return {string[]} Language codes, in descending order of priority + */ +OO.ui.getUserLanguages = function () { + return [ 'en' ]; +}; + +/** + * Get a value in an object keyed by language code. + * + * @param {Object.} obj Object keyed by language code + * @param {string|null} [lang] Language code, if omitted or null defaults to any user language + * @param {string} [fallback] Fallback code, used if no matching language can be found + * @return {Mixed} Local value + */ +OO.ui.getLocalValue = function ( obj, lang, fallback ) { + var i, len, langs; + + // Requested language + if ( obj[ lang ] ) { + return obj[ lang ]; + } + // Known user language + langs = OO.ui.getUserLanguages(); + for ( i = 0, len = langs.length; i < len; i++ ) { + lang = langs[ i ]; + if ( obj[ lang ] ) { + return obj[ lang ]; + } + } + // Fallback language + if ( obj[ fallback ] ) { + return obj[ fallback ]; + } + // First existing language + for ( lang in obj ) { + return obj[ lang ]; + } + + return undefined; +}; + +/** + * Check if a node is contained within another node + * + * Similar to jQuery#contains except a list of containers can be supplied + * and a boolean argument allows you to include the container in the match list + * + * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in + * @param {HTMLElement} contained Node to find + * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants + * @return {boolean} The node is in the list of target nodes + */ +OO.ui.contains = function ( containers, contained, matchContainers ) { + var i; + if ( !Array.isArray( containers ) ) { + containers = [ containers ]; + } + for ( i = containers.length - 1; i >= 0; i-- ) { + if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) { + return true; + } + } + return false; +}; + +/** + * Return a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. + * + * Ported from: http://underscorejs.org/underscore.js + * + * @param {Function} func Function to debounce + * @param {number} [wait=0] Wait period in milliseconds + * @param {boolean} [immediate] Trigger on leading edge + * @return {Function} Debounced function + */ +OO.ui.debounce = function ( func, wait, immediate ) { + var timeout; + return function () { + var context = this, + args = arguments, + later = function () { + timeout = null; + if ( !immediate ) { + func.apply( context, args ); + } + }; + if ( immediate && !timeout ) { + func.apply( context, args ); + } + if ( !timeout || wait ) { + clearTimeout( timeout ); + timeout = setTimeout( later, wait ); + } + }; +}; + +/** + * Puts a console warning with provided message. + * + * @param {string} message Message + */ +OO.ui.warnDeprecation = function ( message ) { + if ( OO.getProp( window, 'console', 'warn' ) !== undefined ) { + // eslint-disable-next-line no-console + console.warn( message ); + } +}; + +/** + * Returns a function, that, when invoked, will only be triggered at most once + * during a given window of time. If called again during that window, it will + * wait until the window ends and then trigger itself again. + * + * As it's not knowable to the caller whether the function will actually run + * when the wrapper is called, return values from the function are entirely + * discarded. + * + * @param {Function} func Function to throttle + * @param {number} wait Throttle window length, in milliseconds + * @return {Function} Throttled function + */ +OO.ui.throttle = function ( func, wait ) { + var context, args, timeout, + previous = 0, + run = function () { + timeout = null; + previous = OO.ui.now(); + func.apply( context, args ); + }; + return function () { + // Check how long it's been since the last time the function was + // called, and whether it's more or less than the requested throttle + // period. If it's less, run the function immediately. If it's more, + // set a timeout for the remaining time -- but don't replace an + // existing timeout, since that'd indefinitely prolong the wait. + var remaining = wait - ( OO.ui.now() - previous ); + context = this; + args = arguments; + if ( remaining <= 0 ) { + // Note: unless wait was ridiculously large, this means we'll + // automatically run the first time the function was called in a + // given period. (If you provide a wait period larger than the + // current Unix timestamp, you *deserve* unexpected behavior.) + clearTimeout( timeout ); + run(); + } else if ( !timeout ) { + timeout = setTimeout( run, remaining ); + } + }; +}; + +/** + * A (possibly faster) way to get the current timestamp as an integer + * + * @return {number} Current timestamp, in milliseconds since the Unix epoch + */ +OO.ui.now = Date.now || function () { + return new Date().getTime(); +}; + +/** + * Reconstitute a JavaScript object corresponding to a widget created by + * the PHP implementation. + * + * This is an alias for `OO.ui.Element.static.infuse()`. + * + * @param {string|HTMLElement|jQuery} idOrNode + * A DOM id (if a string) or node for the widget to infuse. + * @return {OO.ui.Element} + * The `OO.ui.Element` corresponding to this (infusable) document node. + */ +OO.ui.infuse = function ( idOrNode ) { + return OO.ui.Element.static.infuse( idOrNode ); +}; + +( function () { + /** + * Message store for the default implementation of OO.ui.msg + * + * Environments that provide a localization system should not use this, but should override + * OO.ui.msg altogether. + * + * @private + */ + var messages = { + // Tool tip for a button that moves items in a list down one place + 'ooui-outline-control-move-down': 'Move item down', + // Tool tip for a button that moves items in a list up one place + 'ooui-outline-control-move-up': 'Move item up', + // Tool tip for a button that removes items from a list + 'ooui-outline-control-remove': 'Remove item', + // Label for the toolbar group that contains a list of all other available tools + 'ooui-toolbar-more': 'More', + // Label for the fake tool that expands the full list of tools in a toolbar group + 'ooui-toolgroup-expand': 'More', + // Label for the fake tool that collapses the full list of tools in a toolbar group + 'ooui-toolgroup-collapse': 'Fewer', + // Default label for the tooltip for the button that removes a tag item + 'ooui-item-remove': 'Remove', + // Default label for the accept button of a confirmation dialog + 'ooui-dialog-message-accept': 'OK', + // Default label for the reject button of a confirmation dialog + 'ooui-dialog-message-reject': 'Cancel', + // Title for process dialog error description + 'ooui-dialog-process-error': 'Something went wrong', + // Label for process dialog dismiss error button, visible when describing errors + 'ooui-dialog-process-dismiss': 'Dismiss', + // Label for process dialog retry action button, visible when describing only recoverable errors + 'ooui-dialog-process-retry': 'Try again', + // Label for process dialog retry action button, visible when describing only warnings + 'ooui-dialog-process-continue': 'Continue', + // Label for the file selection widget's select file button + 'ooui-selectfile-button-select': 'Select a file', + // Label for the file selection widget if file selection is not supported + 'ooui-selectfile-not-supported': 'File selection is not supported', + // Label for the file selection widget when no file is currently selected + 'ooui-selectfile-placeholder': 'No file is selected', + // Label for the file selection widget's drop target + 'ooui-selectfile-dragdrop-placeholder': 'Drop file here' + }; + + /** + * Get a localized message. + * + * After the message key, message parameters may optionally be passed. In the default implementation, + * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc. + * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as + * they support unnamed, ordered message parameters. + * + * In environments that provide a localization system, this function should be overridden to + * return the message translated in the user's language. The default implementation always returns + * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) + * follows. + * + * @example + * var i, iLen, button, + * messagePath = 'oojs-ui/dist/i18n/', + * languages = [ $.i18n().locale, 'ur', 'en' ], + * languageMap = {}; + * + * for ( i = 0, iLen = languages.length; i < iLen; i++ ) { + * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json'; + * } + * + * $.i18n().load( languageMap ).done( function() { + * // Replace the built-in `msg` only once we've loaded the internationalization. + * // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as + * // you put off creating any widgets until this promise is complete, no English + * // will be displayed. + * OO.ui.msg = $.i18n; + * + * // A button displaying "OK" in the default locale + * button = new OO.ui.ButtonWidget( { + * label: OO.ui.msg( 'ooui-dialog-message-accept' ), + * icon: 'check' + * } ); + * $( 'body' ).append( button.$element ); + * + * // A button displaying "OK" in Urdu + * $.i18n().locale = 'ur'; + * button = new OO.ui.ButtonWidget( { + * label: OO.ui.msg( 'ooui-dialog-message-accept' ), + * icon: 'check' + * } ); + * $( 'body' ).append( button.$element ); + * } ); + * + * @param {string} key Message key + * @param {...Mixed} [params] Message parameters + * @return {string} Translated message with parameters substituted + */ + OO.ui.msg = function ( key ) { + var message = messages[ key ], + params = Array.prototype.slice.call( arguments, 1 ); + if ( typeof message === 'string' ) { + // Perform $1 substitution + message = message.replace( /\$(\d+)/g, function ( unused, n ) { + var i = parseInt( n, 10 ); + return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n; + } ); + } else { + // Return placeholder if message not found + message = '[' + key + ']'; + } + return message; + }; +}() ); + +/** + * Package a message and arguments for deferred resolution. + * + * Use this when you are statically specifying a message and the message may not yet be present. + * + * @param {string} key Message key + * @param {...Mixed} [params] Message parameters + * @return {Function} Function that returns the resolved message when executed + */ +OO.ui.deferMsg = function () { + var args = arguments; + return function () { + return OO.ui.msg.apply( OO.ui, args ); + }; +}; + +/** + * Resolve a message. + * + * If the message is a function it will be executed, otherwise it will pass through directly. + * + * @param {Function|string} msg Deferred message, or message text + * @return {string} Resolved message + */ +OO.ui.resolveMsg = function ( msg ) { + if ( $.isFunction( msg ) ) { + return msg(); + } + return msg; +}; + +/** + * @param {string} url + * @return {boolean} + */ +OO.ui.isSafeUrl = function ( url ) { + // Keep this function in sync with php/Tag.php + var i, protocolWhitelist; + + function stringStartsWith( haystack, needle ) { + return haystack.substr( 0, needle.length ) === needle; + } + + protocolWhitelist = [ + 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs', + 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh', + 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp' + ]; + + if ( url === '' ) { + return true; + } + + for ( i = 0; i < protocolWhitelist.length; i++ ) { + if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) { + return true; + } + } + + // This matches '//' too + if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) { + return true; + } + if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) { + return true; + } + + return false; +}; + +/** + * Check if the user has a 'mobile' device. + * + * For our purposes this means the user is primarily using an + * on-screen keyboard, touch input instead of a mouse and may + * have a physically small display. + * + * It is left up to implementors to decide how to compute this + * so the default implementation always returns false. + * + * @return {boolean} Use is on a mobile device + */ +OO.ui.isMobile = function () { + return false; +}; + +/*! + * Mixin namespace. + */ + +/** + * Namespace for OOjs UI mixins. + * + * Mixins are named according to the type of object they are intended to + * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be + * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget + * is intended to be mixed in to an instance of OO.ui.Widget. + * + * @class + * @singleton + */ +OO.ui.mixin = {}; + +/** + * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything + * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events + * connected to them and can't be interacted with. + * + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added + * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2] + * for an example. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample + * @cfg {string} [id] The HTML id attribute used in the rendered tag. + * @cfg {string} [text] Text to insert + * @cfg {Array} [content] An array of content elements to append (after #text). + * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML. + * Instances of OO.ui.Element will have their $element appended. + * @cfg {jQuery} [$content] Content elements to append (after #text). + * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName. + * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object). + * Data can also be specified with the #setData method. + */ +OO.ui.Element = function OoUiElement( config ) { + this.initialConfig = config; + // Configuration initialization + config = config || {}; + + // Properties + this.$ = $; + this.elementId = null; + this.visible = true; + this.data = config.data; + this.$element = config.$element || + $( document.createElement( this.getTagName() ) ); + this.elementGroup = null; + + // Initialization + if ( Array.isArray( config.classes ) ) { + this.$element.addClass( config.classes.join( ' ' ) ); + } + if ( config.id ) { + this.setElementId( config.id ); + } + if ( config.text ) { + this.$element.text( config.text ); + } + if ( config.content ) { + // The `content` property treats plain strings as text; use an + // HtmlSnippet to append HTML content. `OO.ui.Element`s get their + // appropriate $element appended. + this.$element.append( config.content.map( function ( v ) { + if ( typeof v === 'string' ) { + // Escape string so it is properly represented in HTML. + return document.createTextNode( v ); + } else if ( v instanceof OO.ui.HtmlSnippet ) { + // Bypass escaping. + return v.toString(); + } else if ( v instanceof OO.ui.Element ) { + return v.$element; + } + return v; + } ) ); + } + if ( config.$content ) { + // The `$content` property treats plain strings as HTML. + this.$element.append( config.$content ); + } +}; + +/* Setup */ + +OO.initClass( OO.ui.Element ); + +/* Static Properties */ + +/** + * The name of the HTML tag used by the element. + * + * The static value may be ignored if the #getTagName method is overridden. + * + * @static + * @inheritable + * @property {string} + */ +OO.ui.Element.static.tagName = 'div'; + +/* Static Methods */ + +/** + * Reconstitute a JavaScript object corresponding to a widget created + * by the PHP implementation. + * + * @param {string|HTMLElement|jQuery} idOrNode + * A DOM id (if a string) or node for the widget to infuse. + * @return {OO.ui.Element} + * The `OO.ui.Element` corresponding to this (infusable) document node. + * For `Tag` objects emitted on the HTML side (used occasionally for content) + * the value returned is a newly-created Element wrapping around the existing + * DOM node. + */ +OO.ui.Element.static.infuse = function ( idOrNode ) { + var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false ); + // Verify that the type matches up. + // FIXME: uncomment after T89721 is fixed, see T90929. + /* + if ( !( obj instanceof this['class'] ) ) { + throw new Error( 'Infusion type mismatch!' ); + } + */ + return obj; +}; + +/** + * Implementation helper for `infuse`; skips the type check and has an + * extra property so that only the top-level invocation touches the DOM. + * + * @private + * @param {string|HTMLElement|jQuery} idOrNode + * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved + * when the top-level widget of this infusion is inserted into DOM, + * replacing the original node; or false for top-level invocation. + * @return {OO.ui.Element} + */ +OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) { + // look for a cached result of a previous infusion. + var id, $elem, error, data, cls, parts, parent, obj, top, state, infusedChildren; + if ( typeof idOrNode === 'string' ) { + id = idOrNode; + $elem = $( document.getElementById( id ) ); + } else { + $elem = $( idOrNode ); + id = $elem.attr( 'id' ); + } + if ( !$elem.length ) { + if ( typeof idOrNode === 'string' ) { + error = 'Widget not found: ' + idOrNode; + } else if ( idOrNode && idOrNode.selector ) { + error = 'Widget not found: ' + idOrNode.selector; + } else { + error = 'Widget not found'; + } + throw new Error( error ); + } + if ( $elem[ 0 ].oouiInfused ) { + $elem = $elem[ 0 ].oouiInfused; + } + data = $elem.data( 'ooui-infused' ); + if ( data ) { + // cached! + if ( data === true ) { + throw new Error( 'Circular dependency! ' + id ); + } + if ( domPromise ) { + // pick up dynamic state, like focus, value of form inputs, scroll position, etc. + state = data.constructor.static.gatherPreInfuseState( $elem, data ); + // restore dynamic state after the new element is re-inserted into DOM under infused parent + domPromise.done( data.restorePreInfuseState.bind( data, state ) ); + infusedChildren = $elem.data( 'ooui-infused-children' ); + if ( infusedChildren && infusedChildren.length ) { + infusedChildren.forEach( function ( data ) { + var state = data.constructor.static.gatherPreInfuseState( $elem, data ); + domPromise.done( data.restorePreInfuseState.bind( data, state ) ); + } ); + } + } + return data; + } + data = $elem.attr( 'data-ooui' ); + if ( !data ) { + throw new Error( 'No infusion data found: ' + id ); + } + try { + data = JSON.parse( data ); + } catch ( _ ) { + data = null; + } + if ( !( data && data._ ) ) { + throw new Error( 'No valid infusion data found: ' + id ); + } + if ( data._ === 'Tag' ) { + // Special case: this is a raw Tag; wrap existing node, don't rebuild. + return new OO.ui.Element( { $element: $elem } ); + } + parts = data._.split( '.' ); + cls = OO.getProp.apply( OO, [ window ].concat( parts ) ); + if ( cls === undefined ) { + throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ ); + } + + // Verify that we're creating an OO.ui.Element instance + parent = cls.parent; + + while ( parent !== undefined ) { + if ( parent === OO.ui.Element ) { + // Safe + break; + } + + parent = parent.parent; + } + + if ( parent !== OO.ui.Element ) { + throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ ); + } + + if ( domPromise === false ) { + top = $.Deferred(); + domPromise = top.promise(); + } + $elem.data( 'ooui-infused', true ); // prevent loops + data.id = id; // implicit + infusedChildren = []; + data = OO.copy( data, null, function deserialize( value ) { + var infused; + if ( OO.isPlainObject( value ) ) { + if ( value.tag ) { + infused = OO.ui.Element.static.unsafeInfuse( value.tag, domPromise ); + infusedChildren.push( infused ); + // Flatten the structure + infusedChildren.push.apply( infusedChildren, infused.$element.data( 'ooui-infused-children' ) || [] ); + infused.$element.removeData( 'ooui-infused-children' ); + return infused; + } + if ( value.html !== undefined ) { + return new OO.ui.HtmlSnippet( value.html ); + } + } + } ); + // allow widgets to reuse parts of the DOM + data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data ); + // pick up dynamic state, like focus, value of form inputs, scroll position, etc. + state = cls.static.gatherPreInfuseState( $elem[ 0 ], data ); + // rebuild widget + // eslint-disable-next-line new-cap + obj = new cls( data ); + // now replace old DOM with this new DOM. + if ( top ) { + // An efficient constructor might be able to reuse the entire DOM tree of the original element, + // so only mutate the DOM if we need to. + if ( $elem[ 0 ] !== obj.$element[ 0 ] ) { + $elem.replaceWith( obj.$element ); + // This element is now gone from the DOM, but if anyone is holding a reference to it, + // let's allow them to OO.ui.infuse() it and do what they expect, see T105828. + // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design. + $elem[ 0 ].oouiInfused = obj.$element; + } + top.resolve(); + } + obj.$element.data( 'ooui-infused', obj ); + obj.$element.data( 'ooui-infused-children', infusedChildren ); + // set the 'data-ooui' attribute so we can identify infused widgets + obj.$element.attr( 'data-ooui', '' ); + // restore dynamic state after the new element is inserted into DOM + domPromise.done( obj.restorePreInfuseState.bind( obj, state ) ); + return obj; +}; + +/** + * Pick out parts of `node`'s DOM to be reused when infusing a widget. + * + * This method **must not** make any changes to the DOM, only find interesting pieces and add them + * to `config` (which should then be returned). Actual DOM juggling should then be done by the + * constructor, which will be given the enhanced config. + * + * @protected + * @param {HTMLElement} node + * @param {Object} config + * @return {Object} + */ +OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) { + return config; +}; + +/** + * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node + * (and its children) that represent an Element of the same class and the given configuration, + * generated by the PHP implementation. + * + * This method is called just before `node` is detached from the DOM. The return value of this + * function will be passed to #restorePreInfuseState after the newly created widget's #$element + * is inserted into DOM to replace `node`. + * + * @protected + * @param {HTMLElement} node + * @param {Object} config + * @return {Object} + */ +OO.ui.Element.static.gatherPreInfuseState = function () { + return {}; +}; + +/** + * Get a jQuery function within a specific document. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to + * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is + * not in an iframe + * @return {Function} Bound jQuery function + */ +OO.ui.Element.static.getJQuery = function ( context, $iframe ) { + function wrapper( selector ) { + return $( selector, wrapper.context ); + } + + wrapper.context = this.getDocument( context ); + + if ( $iframe ) { + wrapper.$iframe = $iframe; + } + + return wrapper; +}; + +/** + * Get the document of an element. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for + * @return {HTMLDocument|null} Document object + */ +OO.ui.Element.static.getDocument = function ( obj ) { + // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable + return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) || + // Empty jQuery selections might have a context + obj.context || + // HTMLElement + obj.ownerDocument || + // Window + obj.document || + // HTMLDocument + ( obj.nodeType === Node.DOCUMENT_NODE && obj ) || + null; +}; + +/** + * Get the window of an element or document. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for + * @return {Window} Window object + */ +OO.ui.Element.static.getWindow = function ( obj ) { + var doc = this.getDocument( obj ); + return doc.defaultView; +}; + +/** + * Get the direction of an element or document. + * + * @static + * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for + * @return {string} Text direction, either 'ltr' or 'rtl' + */ +OO.ui.Element.static.getDir = function ( obj ) { + var isDoc, isWin; + + if ( obj instanceof jQuery ) { + obj = obj[ 0 ]; + } + isDoc = obj.nodeType === Node.DOCUMENT_NODE; + isWin = obj.document !== undefined; + if ( isDoc || isWin ) { + if ( isWin ) { + obj = obj.document; + } + obj = obj.body; + } + return $( obj ).css( 'direction' ); +}; + +/** + * Get the offset between two frames. + * + * TODO: Make this function not use recursion. + * + * @static + * @param {Window} from Window of the child frame + * @param {Window} [to=window] Window of the parent frame + * @param {Object} [offset] Offset to start with, used internally + * @return {Object} Offset object, containing left and top properties + */ +OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) { + var i, len, frames, frame, rect; + + if ( !to ) { + to = window; + } + if ( !offset ) { + offset = { top: 0, left: 0 }; + } + if ( from.parent === from ) { + return offset; + } + + // Get iframe element + frames = from.parent.document.getElementsByTagName( 'iframe' ); + for ( i = 0, len = frames.length; i < len; i++ ) { + if ( frames[ i ].contentWindow === from ) { + frame = frames[ i ]; + break; + } + } + + // Recursively accumulate offset values + if ( frame ) { + rect = frame.getBoundingClientRect(); + offset.left += rect.left; + offset.top += rect.top; + if ( from !== to ) { + this.getFrameOffset( from.parent, offset ); + } + } + return offset; +}; + +/** + * Get the offset between two elements. + * + * The two elements may be in a different frame, but in that case the frame $element is in must + * be contained in the frame $anchor is in. + * + * @static + * @param {jQuery} $element Element whose position to get + * @param {jQuery} $anchor Element to get $element's position relative to + * @return {Object} Translated position coordinates, containing top and left properties + */ +OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) { + var iframe, iframePos, + pos = $element.offset(), + anchorPos = $anchor.offset(), + elementDocument = this.getDocument( $element ), + anchorDocument = this.getDocument( $anchor ); + + // If $element isn't in the same document as $anchor, traverse up + while ( elementDocument !== anchorDocument ) { + iframe = elementDocument.defaultView.frameElement; + if ( !iframe ) { + throw new Error( '$element frame is not contained in $anchor frame' ); + } + iframePos = $( iframe ).offset(); + pos.left += iframePos.left; + pos.top += iframePos.top; + elementDocument = iframe.ownerDocument; + } + pos.left -= anchorPos.left; + pos.top -= anchorPos.top; + return pos; +}; + +/** + * Get element border sizes. + * + * @static + * @param {HTMLElement} el Element to measure + * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties + */ +OO.ui.Element.static.getBorders = function ( el ) { + var doc = el.ownerDocument, + win = doc.defaultView, + style = win.getComputedStyle( el, null ), + $el = $( el ), + top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0, + left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0, + bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0, + right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0; + + return { + top: top, + left: left, + bottom: bottom, + right: right + }; +}; + +/** + * Get dimensions of an element or window. + * + * @static + * @param {HTMLElement|Window} el Element to measure + * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties + */ +OO.ui.Element.static.getDimensions = function ( el ) { + var $el, $win, + doc = el.ownerDocument || el.document, + win = doc.defaultView; + + if ( win === el || el === doc.documentElement ) { + $win = $( win ); + return { + borders: { top: 0, left: 0, bottom: 0, right: 0 }, + scroll: { + top: $win.scrollTop(), + left: $win.scrollLeft() + }, + scrollbar: { right: 0, bottom: 0 }, + rect: { + top: 0, + left: 0, + bottom: $win.innerHeight(), + right: $win.innerWidth() + } + }; + } else { + $el = $( el ); + return { + borders: this.getBorders( el ), + scroll: { + top: $el.scrollTop(), + left: $el.scrollLeft() + }, + scrollbar: { + right: $el.innerWidth() - el.clientWidth, + bottom: $el.innerHeight() - el.clientHeight + }, + rect: el.getBoundingClientRect() + }; + } +}; + +/** + * Get the number of pixels that an element's content is scrolled to the left. + * + * Adapted from . + * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License. + * + * This function smooths out browser inconsistencies (nicely described in the README at + * ) and produces a result consistent + * with Firefox's 'scrollLeft', which seems the sanest. + * + * @static + * @method + * @param {HTMLElement|Window} el Element to measure + * @return {number} Scroll position from the left. + * If the element's direction is LTR, this is a positive number between `0` (initial scroll position) + * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position). + * If the element's direction is RTL, this is a negative number between `0` (initial scroll position) + * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position). + */ +OO.ui.Element.static.getScrollLeft = ( function () { + var rtlScrollType = null; + + function test() { + var $definer = $( '
A
' ), + definer = $definer[ 0 ]; + + $definer.appendTo( 'body' ); + if ( definer.scrollLeft > 0 ) { + // Safari, Chrome + rtlScrollType = 'default'; + } else { + definer.scrollLeft = 1; + if ( definer.scrollLeft === 0 ) { + // Firefox, old Opera + rtlScrollType = 'negative'; + } else { + // Internet Explorer, Edge + rtlScrollType = 'reverse'; + } + } + $definer.remove(); + } + + return function getScrollLeft( el ) { + var isRoot = el.window === el || + el === el.ownerDocument.body || + el === el.ownerDocument.documentElement, + scrollLeft = isRoot ? $( window ).scrollLeft() : el.scrollLeft, + // All browsers use the correct scroll type ('negative') on the root, so don't + // do any fixups when looking at the root element + direction = isRoot ? 'ltr' : $( el ).css( 'direction' ); + + if ( direction === 'rtl' ) { + if ( rtlScrollType === null ) { + test(); + } + if ( rtlScrollType === 'reverse' ) { + scrollLeft = -scrollLeft; + } else if ( rtlScrollType === 'default' ) { + scrollLeft = scrollLeft - el.scrollWidth + el.clientWidth; + } + } + + return scrollLeft; + }; +}() ); + +/** + * Get the root scrollable element of given element's document. + * + * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set + * the scrollTop property; instead we have to use `document.body`. Changing and testing the value + * lets us use 'body' or 'documentElement' based on what is working. + * + * https://code.google.com/p/chromium/issues/detail?id=303131 + * + * @static + * @param {HTMLElement} el Element to find root scrollable parent for + * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement` + * depending on browser + */ +OO.ui.Element.static.getRootScrollableElement = function ( el ) { + var scrollTop, body; + + if ( OO.ui.scrollableElement === undefined ) { + body = el.ownerDocument.body; + scrollTop = body.scrollTop; + body.scrollTop = 1; + + // In some browsers (observed in Chrome 56 on Linux Mint 18.1), + // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76 + if ( Math.round( body.scrollTop ) === 1 ) { + body.scrollTop = scrollTop; + OO.ui.scrollableElement = 'body'; + } else { + OO.ui.scrollableElement = 'documentElement'; + } + } + + return el.ownerDocument[ OO.ui.scrollableElement ]; +}; + +/** + * Get closest scrollable container. + * + * Traverses up until either a scrollable element or the root is reached, in which case the root + * scrollable element will be returned (see #getRootScrollableElement). + * + * @static + * @param {HTMLElement} el Element to find scrollable container for + * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either + * @return {HTMLElement} Closest scrollable container + */ +OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) { + var i, val, + // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and + // 'overflow-y' have different values, so we need to check the separate properties. + props = [ 'overflow-x', 'overflow-y' ], + $parent = $( el ).parent(); + + if ( dimension === 'x' || dimension === 'y' ) { + props = [ 'overflow-' + dimension ]; + } + + // Special case for the document root (which doesn't really have any scrollable container, since + // it is the ultimate scrollable container, but this is probably saner than null or exception) + if ( $( el ).is( 'html, body' ) ) { + return this.getRootScrollableElement( el ); + } + + while ( $parent.length ) { + if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) { + return $parent[ 0 ]; + } + i = props.length; + while ( i-- ) { + val = $parent.css( props[ i ] ); + // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be + // scrolled in that direction, but they can actually be scrolled programatically. The user can + // unintentionally perform a scroll in such case even if the application doesn't scroll + // programatically, e.g. when jumping to an anchor, or when using built-in find functionality. + // This could cause funny issues... + if ( val === 'auto' || val === 'scroll' ) { + return $parent[ 0 ]; + } + } + $parent = $parent.parent(); + } + // The element is unattached... return something mostly sane + return this.getRootScrollableElement( el ); +}; + +/** + * Scroll element into view. + * + * @static + * @param {HTMLElement} el Element to scroll into view + * @param {Object} [config] Configuration options + * @param {string} [config.duration='fast'] jQuery animation duration value + * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit + * to scroll in both directions + * @return {jQuery.Promise} Promise which resolves when the scroll is complete + */ +OO.ui.Element.static.scrollIntoView = function ( el, config ) { + var position, animations, container, $container, elementDimensions, containerDimensions, $window, + deferred = $.Deferred(); + + // Configuration initialization + config = config || {}; + + animations = {}; + container = this.getClosestScrollableContainer( el, config.direction ); + $container = $( container ); + elementDimensions = this.getDimensions( el ); + containerDimensions = this.getDimensions( container ); + $window = $( this.getWindow( el ) ); + + // Compute the element's position relative to the container + if ( $container.is( 'html, body' ) ) { + // If the scrollable container is the root, this is easy + position = { + top: elementDimensions.rect.top, + bottom: $window.innerHeight() - elementDimensions.rect.bottom, + left: elementDimensions.rect.left, + right: $window.innerWidth() - elementDimensions.rect.right + }; + } else { + // Otherwise, we have to subtract el's coordinates from container's coordinates + position = { + top: elementDimensions.rect.top - ( containerDimensions.rect.top + containerDimensions.borders.top ), + bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom - containerDimensions.scrollbar.bottom - elementDimensions.rect.bottom, + left: elementDimensions.rect.left - ( containerDimensions.rect.left + containerDimensions.borders.left ), + right: containerDimensions.rect.right - containerDimensions.borders.right - containerDimensions.scrollbar.right - elementDimensions.rect.right + }; + } + + if ( !config.direction || config.direction === 'y' ) { + if ( position.top < 0 ) { + animations.scrollTop = containerDimensions.scroll.top + position.top; + } else if ( position.top > 0 && position.bottom < 0 ) { + animations.scrollTop = containerDimensions.scroll.top + Math.min( position.top, -position.bottom ); + } + } + if ( !config.direction || config.direction === 'x' ) { + if ( position.left < 0 ) { + animations.scrollLeft = containerDimensions.scroll.left + position.left; + } else if ( position.left > 0 && position.right < 0 ) { + animations.scrollLeft = containerDimensions.scroll.left + Math.min( position.left, -position.right ); + } + } + if ( !$.isEmptyObject( animations ) ) { + $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration ); + $container.queue( function ( next ) { + deferred.resolve(); + next(); + } ); + } else { + deferred.resolve(); + } + return deferred.promise(); +}; + +/** + * Force the browser to reconsider whether it really needs to render scrollbars inside the element + * and reserve space for them, because it probably doesn't. + * + * Workaround primarily for , but also + * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need + * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow, + * and then reattach (or show) them back. + * + * @static + * @param {HTMLElement} el Element to reconsider the scrollbars on + */ +OO.ui.Element.static.reconsiderScrollbars = function ( el ) { + var i, len, scrollLeft, scrollTop, nodes = []; + // Save scroll position + scrollLeft = el.scrollLeft; + scrollTop = el.scrollTop; + // Detach all children + while ( el.firstChild ) { + nodes.push( el.firstChild ); + el.removeChild( el.firstChild ); + } + // Force reflow + void el.offsetHeight; + // Reattach all children + for ( i = 0, len = nodes.length; i < len; i++ ) { + el.appendChild( nodes[ i ] ); + } + // Restore scroll position (no-op if scrollbars disappeared) + el.scrollLeft = scrollLeft; + el.scrollTop = scrollTop; +}; + +/* Methods */ + +/** + * Toggle visibility of an element. + * + * @param {boolean} [show] Make element visible, omit to toggle visibility + * @fires visible + * @chainable + */ +OO.ui.Element.prototype.toggle = function ( show ) { + show = show === undefined ? !this.visible : !!show; + + if ( show !== this.isVisible() ) { + this.visible = show; + this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible ); + this.emit( 'toggle', show ); + } + + return this; +}; + +/** + * Check if element is visible. + * + * @return {boolean} element is visible + */ +OO.ui.Element.prototype.isVisible = function () { + return this.visible; +}; + +/** + * Get element data. + * + * @return {Mixed} Element data + */ +OO.ui.Element.prototype.getData = function () { + return this.data; +}; + +/** + * Set element data. + * + * @param {Mixed} data Element data + * @chainable + */ +OO.ui.Element.prototype.setData = function ( data ) { + this.data = data; + return this; +}; + +/** + * Set the element has an 'id' attribute. + * + * @param {string} id + * @chainable + */ +OO.ui.Element.prototype.setElementId = function ( id ) { + this.elementId = id; + this.$element.attr( 'id', id ); + return this; +}; + +/** + * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing, + * and return its value. + * + * @return {string} + */ +OO.ui.Element.prototype.getElementId = function () { + if ( this.elementId === null ) { + this.setElementId( OO.ui.generateElementId() ); + } + return this.elementId; +}; + +/** + * Check if element supports one or more methods. + * + * @param {string|string[]} methods Method or list of methods to check + * @return {boolean} All methods are supported + */ +OO.ui.Element.prototype.supports = function ( methods ) { + var i, len, + support = 0; + + methods = Array.isArray( methods ) ? methods : [ methods ]; + for ( i = 0, len = methods.length; i < len; i++ ) { + if ( $.isFunction( this[ methods[ i ] ] ) ) { + support++; + } + } + + return methods.length === support; +}; + +/** + * Update the theme-provided classes. + * + * @localdoc This is called in element mixins and widget classes any time state changes. + * Updating is debounced, minimizing overhead of changing multiple attributes and + * guaranteeing that theme updates do not occur within an element's constructor + */ +OO.ui.Element.prototype.updateThemeClasses = function () { + OO.ui.theme.queueUpdateElementClasses( this ); +}; + +/** + * Get the HTML tag name. + * + * Override this method to base the result on instance information. + * + * @return {string} HTML tag name + */ +OO.ui.Element.prototype.getTagName = function () { + return this.constructor.static.tagName; +}; + +/** + * Check if the element is attached to the DOM + * + * @return {boolean} The element is attached to the DOM + */ +OO.ui.Element.prototype.isElementAttached = function () { + return $.contains( this.getElementDocument(), this.$element[ 0 ] ); +}; + +/** + * Get the DOM document. + * + * @return {HTMLDocument} Document object + */ +OO.ui.Element.prototype.getElementDocument = function () { + // Don't cache this in other ways either because subclasses could can change this.$element + return OO.ui.Element.static.getDocument( this.$element ); +}; + +/** + * Get the DOM window. + * + * @return {Window} Window object + */ +OO.ui.Element.prototype.getElementWindow = function () { + return OO.ui.Element.static.getWindow( this.$element ); +}; + +/** + * Get closest scrollable container. + * + * @return {HTMLElement} Closest scrollable container + */ +OO.ui.Element.prototype.getClosestScrollableElementContainer = function () { + return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] ); +}; + +/** + * Get group element is in. + * + * @return {OO.ui.mixin.GroupElement|null} Group element, null if none + */ +OO.ui.Element.prototype.getElementGroup = function () { + return this.elementGroup; +}; + +/** + * Set group element is in. + * + * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none + * @chainable + */ +OO.ui.Element.prototype.setElementGroup = function ( group ) { + this.elementGroup = group; + return this; +}; + +/** + * Scroll element into view. + * + * @param {Object} [config] Configuration options + * @return {jQuery.Promise} Promise which resolves when the scroll is complete + */ +OO.ui.Element.prototype.scrollElementIntoView = function ( config ) { + if ( + !this.isElementAttached() || + !this.isVisible() || + ( this.getElementGroup() && !this.getElementGroup().isVisible() ) + ) { + return $.Deferred().resolve(); + } + return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config ); +}; + +/** + * Restore the pre-infusion dynamic state for this widget. + * + * This method is called after #$element has been inserted into DOM. The parameter is the return + * value of #gatherPreInfuseState. + * + * @protected + * @param {Object} state + */ +OO.ui.Element.prototype.restorePreInfuseState = function () { +}; + +/** + * Wraps an HTML snippet for use with configuration values which default + * to strings. This bypasses the default html-escaping done to string + * values. + * + * @class + * + * @constructor + * @param {string} [content] HTML content + */ +OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) { + // Properties + this.content = content; +}; + +/* Setup */ + +OO.initClass( OO.ui.HtmlSnippet ); + +/* Methods */ + +/** + * Render into HTML. + * + * @return {string} Unchanged HTML snippet. + */ +OO.ui.HtmlSnippet.prototype.toString = function () { + return this.content; +}; + +/** + * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way + * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined. + * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout}, + * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout}, + * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples. + * + * @abstract + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.Layout = function OoUiLayout( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.Layout.parent.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Initialization + this.$element.addClass( 'oo-ui-layout' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.Layout, OO.ui.Element ); +OO.mixinClass( OO.ui.Layout, OO.EventEmitter ); + +/** + * Widgets are compositions of one or more OOjs UI elements that users can both view + * and interact with. All widgets can be configured and modified via a standard API, + * and their state can change dynamically according to a model. + * + * @abstract + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their + * appearance reflects this state. + */ +OO.ui.Widget = function OoUiWidget( config ) { + // Initialize config + config = $.extend( { disabled: false }, config ); + + // Parent constructor + OO.ui.Widget.parent.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.disabled = null; + this.wasDisabled = null; + + // Initialization + this.$element.addClass( 'oo-ui-widget' ); + this.setDisabled( !!config.disabled ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.Widget, OO.ui.Element ); +OO.mixinClass( OO.ui.Widget, OO.EventEmitter ); + +/* Events */ + +/** + * @event disable + * + * A 'disable' event is emitted when the disabled state of the widget changes + * (i.e. on disable **and** enable). + * + * @param {boolean} disabled Widget is disabled + */ + +/** + * @event toggle + * + * A 'toggle' event is emitted when the visibility of the widget changes. + * + * @param {boolean} visible Widget is visible + */ + +/* Methods */ + +/** + * Check if the widget is disabled. + * + * @return {boolean} Widget is disabled + */ +OO.ui.Widget.prototype.isDisabled = function () { + return this.disabled; +}; + +/** + * Set the 'disabled' state of the widget. + * + * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state. + * + * @param {boolean} disabled Disable widget + * @chainable + */ +OO.ui.Widget.prototype.setDisabled = function ( disabled ) { + var isDisabled; + + this.disabled = !!disabled; + isDisabled = this.isDisabled(); + if ( isDisabled !== this.wasDisabled ) { + this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled ); + this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled ); + this.$element.attr( 'aria-disabled', isDisabled.toString() ); + this.emit( 'disable', isDisabled ); + this.updateThemeClasses(); + } + this.wasDisabled = isDisabled; + + return this; +}; + +/** + * Update the disabled state, in case of changes in parent widget. + * + * @chainable + */ +OO.ui.Widget.prototype.updateDisabled = function () { + this.setDisabled( this.disabled ); + return this; +}; + +/** + * Get an ID of a labelable node which is part of this widget, if any, to be used for `