/*! * OOUI v0.44.0 * https://www.mediawiki.org/wiki/OOUI * * Copyright 2011–2022 OOUI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * * Date: 2022-05-17T17:50:55Z */ ( 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 'ooui-' + 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=false] 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 // eslint-disable-next-line no-unreachable-loop 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 = Date.now() - wait, run = function () { timeout = null; previous = Date.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 = Math.max( wait - ( Date.now() - previous ), 0 ); context = this; args = arguments; if ( !timeout ) { // If time is up, do setTimeout( run, 0 ) so the function // always runs asynchronously, just like Promise#then . timeout = setTimeout( run, remaining ); } }; }; /** * 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} node A single node for the widget to infuse. * String must be a selector (deprecated). * @param {Object} [config] Configuration options * @return {OO.ui.Element} * The `OO.ui.Element` corresponding to this (infusable) document node. */ OO.ui.infuse = function ( node, config ) { if ( typeof node === 'string' ) { // Deprecate passing a selector, which was accidentally introduced in Ibf95b0dee. // @since 0.41.0 OO.ui.warnDeprecation( 'Passing a selector to infuse is deprecated. Use an HTMLElement or jQuery collection instead.' ); } return OO.ui.Element.static.infuse( node, config ); }; /** * 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. * // OOUI 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' * } ); * $( document.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' * } ); * $( document.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 ) { // `OO.ui.msg.messages` is defined in code generated during the build process var messages = OO.ui.msg.messages, 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|Mixed} msg * @return {string|Mixed} Resolved message when there was something to resolve, pass through * otherwise */ OO.ui.resolveMsg = function ( msg ) { if ( typeof msg === 'function' ) { 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, protocolAllowList; function stringStartsWith( haystack, needle ) { return haystack.slice( 0, needle.length ) === needle; } protocolAllowList = [ '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 < protocolAllowList.length; i++ ) { if ( stringStartsWith( url, protocolAllowList[ 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} User is on a mobile device */ OO.ui.isMobile = function () { return false; }; /** * Get the additional spacing that should be taken into account when displaying elements that are * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid * such menus overlapping any fixed headers/toolbars/navigation used by the site. * * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing * the extra spacing from that edge of viewport (in pixels) */ OO.ui.getViewportSpacing = function () { return { top: 0, right: 0, bottom: 0, left: 0 }; }; /** * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`. * See . * * @return {jQuery} Default overlay node */ OO.ui.getDefaultOverlay = function () { if ( !OO.ui.$defaultOverlay ) { OO.ui.$defaultOverlay = $( '
' ).addClass( 'oo-ui-defaultOverlay' ); $( document.body ).append( OO.ui.$defaultOverlay ); } return OO.ui.$defaultOverlay; }; /** * 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 */ OO.ui.msg.messages = { "ooui-outline-control-move-down": "Move item down", "ooui-outline-control-move-up": "Move item up", "ooui-outline-control-remove": "Remove item", "ooui-toolbar-more": "More", "ooui-toolgroup-expand": "More", "ooui-toolgroup-collapse": "Fewer", "ooui-item-remove": "Remove", "ooui-dialog-message-accept": "OK", "ooui-dialog-message-reject": "Cancel", "ooui-dialog-process-error": "Something went wrong", "ooui-dialog-process-dismiss": "Dismiss", "ooui-dialog-process-retry": "Try again", "ooui-dialog-process-continue": "Continue", "ooui-combobox-button-label": "Toggle options", "ooui-selectfile-button-select": "Select a file", "ooui-selectfile-button-select-multiple": "Select files", "ooui-selectfile-not-supported": "File selection is not supported", "ooui-selectfile-placeholder": "No file is selected", "ooui-selectfile-dragdrop-placeholder": "Drop file here", "ooui-selectfile-dragdrop-placeholder-multiple": "Drop files here", "ooui-popup-widget-close-button-aria-label": "Close", "ooui-field-help": "Help" }; /*! * Mixin namespace. */ /** * Namespace for OOUI 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 = {}; // getDocument( element ) is preferrable to window.document /* global document:off */ /** * 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 * [OOUI documentation on MediaWiki][2] for an example. * [2]: https://www.mediawiki.org/wiki/OOUI/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 ) { var doc; if ( OO.ui.isDemo ) { this.initialConfig = config; } // Configuration initialization config = config || {}; // Properties this.elementId = null; this.visible = true; this.data = config.data; this.$element = config.$element || $( window.document.createElement( this.getTagName() ) ); this.elementGroup = null; // Initialization doc = OO.ui.Element.static.getDocument( this.$element ); if ( Array.isArray( config.classes ) ) { this.$element.addClass( // Remove empty strings to work around jQuery bug // https://github.com/jquery/jquery/issues/4998 config.classes.filter( function ( val ) { return val; } ) ); } 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. // Don't create empty text nodes for empty strings. return v ? doc.createTextNode( v ) : undefined; } 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 {HTMLElement|jQuery} node * A single node for the widget to infuse. * @param {Object} [config] Configuration options * @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 ( node, config ) { var obj = OO.ui.Element.static.unsafeInfuse( node, config, 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 {HTMLElement|jQuery} elem * @param {Object} [config] Configuration options * @param {jQuery.Promise} [domPromise] A promise that will be resolved * when the top-level widget of this infusion is inserted into DOM, * replacing the original element; only used internally. * @return {OO.ui.Element} */ OO.ui.Element.static.unsafeInfuse = function ( elem, config, domPromise ) { // look for a cached result of a previous infusion. var data, cls, parts, obj, top, state, infusedChildren, doc, id, $elem = $( elem ); if ( $elem.length > 1 ) { throw new Error( 'Collection contains more than one element' ); } if ( !$elem.length ) { throw new Error( 'Widget not found' ); } if ( $elem[ 0 ].$oouiInfused ) { $elem = $elem[ 0 ].$oouiInfused; } id = $elem.attr( 'id' ); doc = this.getDocument( $elem ); 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 ( childData ) { var childState = childData.constructor.static.gatherPreInfuseState( $elem, childData ); domPromise.done( childData.restorePreInfuseState.bind( childData, childState ) ); } ); } } 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( $.extend( {}, config, { $element: $elem } ) ); } parts = data._.split( '.' ); cls = OO.getProp.apply( OO, [ window ].concat( parts ) ); if ( !( cls && ( cls === OO.ui.Element || cls.prototype instanceof OO.ui.Element ) ) ) { throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ ); } if ( !domPromise ) { 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 && doc.getElementById( value.tag ) ) { infused = OO.ui.Element.static.unsafeInfuse( doc.getElementById( value.tag ), config, 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( $.extend( {}, config, data ) ); // If anyone is holding a reference to the old DOM element, // 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; // 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 ); } top.resolve(); } obj.$element .data( { 'ooui-infused': obj, 'ooui-infused-children': infusedChildren } ) // set the 'data-ooui' attribute so we can identify infused widgets .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 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 ) { // HTMLElement return obj.ownerDocument || // Window obj.document || // HTMLDocument ( obj.nodeType === Node.DOCUMENT_NODE && obj ) || // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable ( obj[ 0 ] && obj[ 0 ].ownerDocument ) || // Empty jQuery selections might have a context obj.context || 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 $ ) { 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 = this.getDocument( iframe ); } 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 = this.getDocument( el ), 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 = this.getDocument( el ), 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: OO.ui.Element.static.getScrollLeft( win ) }, 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: OO.ui.Element.static.getScrollLeft( el ) }, scrollbar: { right: $el.innerWidth() - el.clientWidth, bottom: $el.innerHeight() - el.clientHeight }, rect: el.getBoundingClientRect() }; } }; ( function () { var rtlScrollType = null; // Adapted from . // Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License. function rtlScrollTypeTest() { var $definer = $( '
' ).attr( { dir: 'rtl', style: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;' } ).text( 'ABCD' ), 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(); } function isRoot( el ) { return el === el.window || el === el.ownerDocument.body || el === el.ownerDocument.documentElement; } /** * Convert native `scrollLeft` value to a value consistent between browsers. See #getScrollLeft. * * @param {number} nativeOffset Native `scrollLeft` value * @param {HTMLElement|Window} el Element from which the value was obtained * @return {number} */ OO.ui.Element.static.computeNormalizedScrollLeft = function ( nativeOffset, el ) { // All browsers use the correct scroll type ('negative') on the root, so don't // do any fixups when looking at the root element var direction = isRoot( el ) ? 'ltr' : $( el ).css( 'direction' ); if ( direction === 'rtl' ) { if ( rtlScrollType === null ) { rtlScrollTypeTest(); } if ( rtlScrollType === 'reverse' ) { return -nativeOffset; } else if ( rtlScrollType === 'default' ) { return nativeOffset - el.scrollWidth + el.clientWidth; } } return nativeOffset; }; /** * Convert our normalized `scrollLeft` value to a value for current browser. See #getScrollLeft. * * @param {number} normalizedOffset Normalized `scrollLeft` value * @param {HTMLElement|Window} el Element on which the value will be set * @return {number} */ OO.ui.Element.static.computeNativeScrollLeft = function ( normalizedOffset, el ) { // All browsers use the correct scroll type ('negative') on the root, so don't // do any fixups when looking at the root element var direction = isRoot( el ) ? 'ltr' : $( el ).css( 'direction' ); if ( direction === 'rtl' ) { if ( rtlScrollType === null ) { rtlScrollTypeTest(); } if ( rtlScrollType === 'reverse' ) { return -normalizedOffset; } else if ( rtlScrollType === 'default' ) { return normalizedOffset + el.scrollWidth - el.clientWidth; } } return normalizedOffset; }; /** * Get the number of pixels that an element's content is scrolled to the left. * * This function smooths out browser inconsistencies (nicely described in the README at * ) and produces a result consistent * with Firefox's 'scrollLeft', which seems the most sensible. * * (Firefox's scrollLeft handling is nice because it increases from left to right, consistently * with `getBoundingClientRect().left` and related APIs; because initial value is zero, so * resetting it is easy; because adapting a hardcoded scroll position to a symmetrical RTL * interface requires just negating it, rather than involving `clientWidth` and `scrollWidth`; * and because if you mess up and don't adapt your code to RTL, it will scroll to the beginning * rather than somewhere randomly in the middle but not where you wanted.) * * @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 ( el ) { var scrollLeft = isRoot( el ) ? $( window ).scrollLeft() : el.scrollLeft; scrollLeft = OO.ui.Element.static.computeNormalizedScrollLeft( scrollLeft, el ); return scrollLeft; }; /** * Set the number of pixels that an element's content is scrolled to the left. * * See #getScrollLeft. * * @static * @method * @param {HTMLElement|Window} el Element to scroll (and to use in calculations) * @param {number} scrollLeft Scroll position from the left. * If the element's direction is LTR, this must be 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 must be a negative number between * `0` (initial scroll position) and `-el.scrollWidth + el.clientWidth` * (furthest possible scroll position). */ OO.ui.Element.static.setScrollLeft = function ( el, scrollLeft ) { scrollLeft = OO.ui.Element.static.computeNativeScrollLeft( scrollLeft, el ); if ( isRoot( el ) ) { $( window ).scrollLeft( scrollLeft ); } else { el.scrollLeft = scrollLeft; } }; }() ); /** * Get the root scrollable element of given element's document. * * Support: Chrome <= 60 * On older versions of Blink, `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 {HTMLBodyElement|HTMLHtmlElement} Scrollable parent, `` or `` */ OO.ui.Element.static.getRootScrollableElement = function ( el ) { var scrollTop, body, doc = this.getDocument( el ); if ( OO.ui.scrollableElement === undefined ) { body = doc.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 doc[ 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, doc = this.getDocument( el ), rootScrollableElement = this.getRootScrollableElement( el ), // 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 ( el === doc.documentElement ) { return rootScrollableElement; } if ( dimension === 'x' || dimension === 'y' ) { props = [ 'overflow-' + dimension ]; } // The parent of is the document, so check we haven't traversed that far while ( $parent.length && $parent[ 0 ] !== doc ) { if ( $parent[ 0 ] === rootScrollableElement ) { 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' ) { if ( $parent[ 0 ] === doc.body ) { // If overflow is set on , return the rootScrollableElement // ( or ) as may not be scrollable. return rootScrollableElement; } else { return $parent[ 0 ]; } } } $parent = $parent.parent(); } // The element is unattached… return something moderately sensible. return rootScrollableElement; }; /** * Scroll element into view. * * @static * @param {HTMLElement|Object} elOrPosition Element to scroll into view * @param {Object} [config] Configuration options * @param {string} [config.animate=true] Animate to the new scroll offset. * @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 * @param {Object} [config.padding] Additional padding on the container to scroll past. * Object containing any of 'top', 'bottom', 'left', or 'right' as numbers. * @param {Object} [config.scrollContainer] Scroll container. Defaults to * getClosestScrollableContainer of the element. * @return {jQuery.Promise} Promise which resolves when the scroll is complete */ OO.ui.Element.static.scrollIntoView = function ( elOrPosition, config ) { var position, animations, container, $container, elementPosition, containerDimensions, $window, padding, animate, method, deferred = $.Deferred(); // Configuration initialization config = config || {}; padding = $.extend( { top: 0, bottom: 0, left: 0, right: 0 }, config.padding ); animate = config.animate !== false; animations = {}; elementPosition = elOrPosition instanceof HTMLElement ? this.getDimensions( elOrPosition ).rect : elOrPosition; container = config.scrollContainer || ( elOrPosition instanceof HTMLElement ? this.getClosestScrollableContainer( elOrPosition, config.direction ) : // No scrollContainer or element, use global document this.getClosestScrollableContainer( window.document.body ) ); $container = $( container ); containerDimensions = this.getDimensions( container ); $window = $( this.getWindow( container ) ); // 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: elementPosition.top, bottom: $window.innerHeight() - elementPosition.bottom, left: elementPosition.left, right: $window.innerWidth() - elementPosition.right }; } else { // Otherwise, we have to subtract el's coordinates from container's coordinates position = { top: elementPosition.top - ( containerDimensions.rect.top + containerDimensions.borders.top ), bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom - containerDimensions.scrollbar.bottom - elementPosition.bottom, left: elementPosition.left - ( containerDimensions.rect.left + containerDimensions.borders.left ), right: containerDimensions.rect.right - containerDimensions.borders.right - containerDimensions.scrollbar.right - elementPosition.right }; } if ( !config.direction || config.direction === 'y' ) { if ( position.top < padding.top ) { animations.scrollTop = containerDimensions.scroll.top + position.top - padding.top; } else if ( position.bottom < padding.bottom ) { animations.scrollTop = containerDimensions.scroll.top + // Scroll the bottom into view, but not at the expense // of scrolling the top out of view Math.min( position.top - padding.top, -position.bottom + padding.bottom ); } } if ( !config.direction || config.direction === 'x' ) { if ( position.left < padding.left ) { animations.scrollLeft = containerDimensions.scroll.left + position.left - padding.left; } else if ( position.right < padding.right ) { animations.scrollLeft = containerDimensions.scroll.left + // Scroll the right into view, but not at the expense // of scrolling the left out of view Math.min( position.left - padding.left, -position.right + padding.right ); } if ( animations.scrollLeft !== undefined ) { animations.scrollLeft = OO.ui.Element.static.computeNativeScrollLeft( animations.scrollLeft, container ); } } if ( !$.isEmptyObject( animations ) ) { if ( animate ) { // eslint-disable-next-line no-jquery/no-animate $container.stop( true ).animate( animations, { duration: config.duration === undefined ? 'fast' : config.duration, always: deferred.resolve } ); } else { $container.stop( true ); for ( method in animations ) { $container[ method ]( animations[ method ] ); } deferred.resolve(); } } 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 // eslint-disable-next-line no-void 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 * @return {OO.ui.Element} The element, for chaining */ 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 * @return {OO.ui.Element} The element, for chaining */ OO.ui.Element.prototype.setData = function ( data ) { this.data = data; return this; }; /** * Set the element has an 'id' attribute. * * @param {string} id * @chainable * @return {OO.ui.Element} The element, for chaining */ 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 ) { if ( !Array.isArray( methods ) ) { return typeof this[ methods ] === 'function'; } var element = this; return methods.every( function ( method ) { return typeof element[ method ] === 'function'; } ); }; /** * 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 * @return {OO.ui.Element} The element, for chaining */ 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.super.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 ); /* Methods */ /** * Reset scroll offsets * * @chainable * @return {OO.ui.Layout} The layout, for chaining */ OO.ui.Layout.prototype.resetScroll = function () { this.$element[ 0 ].scrollTop = 0; OO.ui.Element.static.setScrollLeft( this.$element[ 0 ], 0 ); return this; }; /** * Widgets are compositions of one or more OOUI 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 ) { // Parent constructor OO.ui.Widget.super.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 && 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 * @return {OO.ui.Widget} The widget, for chaining */ OO.ui.Widget.prototype.setDisabled = function ( disabled ) { this.disabled = !!disabled; var 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 ? 'true' : null ); this.emit( 'disable', isDisabled ); this.updateThemeClasses(); this.wasDisabled = isDisabled; } return this; }; /** * Update the disabled state, in case of changes in parent widget. * * @chainable * @return {OO.ui.Widget} The widget, for chaining */ 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 `