/*! * OOjs UI v0.12.12 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2015 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * * Date: 2015-10-13T20:38:18Z */ ( 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 }; /** * @property {Number} */ OO.ui.elementId = 0; /** * Generate a unique ID for element * * @return {String} [id] */ OO.ui.generateElementId = function () { OO.ui.elementId += 1; return 'oojsui-' + OO.ui.elementId; }; /** * Check if an element is focusable. * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14 * * @param {jQuery} element Element to test * @return {boolean} */ 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.filters.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, 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 * @param {number} wait * @param {boolean} immediate * @return {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 ); } clearTimeout( timeout ); timeout = setTimeout( later, wait ); }; }; /** * Proxy for `node.addEventListener( eventName, handler, true )`, if the browser supports it. * Otherwise falls back to non-capturing event listeners. * * @param {HTMLElement} node * @param {string} eventName * @param {Function} handler */ OO.ui.addCaptureEventListener = function ( node, eventName, handler ) { if ( node.addEventListener ) { node.addEventListener( eventName, handler, true ); } else { node.attachEvent( 'on' + eventName, handler ); } }; /** * Proxy for `node.removeEventListener( eventName, handler, true )`, if the browser supports it. * Otherwise falls back to non-capturing event listeners. * * @param {HTMLElement} node * @param {string} eventName * @param {Function} handler */ OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) { if ( node.addEventListener ) { node.removeEventListener( eventName, handler, true ); } else { node.detachEvent( 'on' + eventName, handler ); } }; /** * 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 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. * * 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. * * 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. * * @abstract * @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 ) { var protocol, // Keep in sync with php/Tag.php whitelist = [ '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.indexOf( ':' ) === -1 ) { // No protocol, safe return true; } protocol = url.split( ':', 1 )[ 0 ] + ':'; if ( !protocol.match( /^([A-za-z0-9\+\.\-])+:/ ) ) { // Not a valid protocol, safe return true; } // Safe if in the whitelist return whitelist.indexOf( protocol ) !== -1; }; } )(); /*! * 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 = {}; /** * PendingElement is a mixin that is used to create elements that notify users that something is happening * and that they should wait before proceeding. The pending state is visually represented with a pending * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input * field of a {@link OO.ui.TextInputWidget text input widget}. * * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used * in process dialogs. * * @example * function MessageDialog( config ) { * MessageDialog.parent.call( this, config ); * } * OO.inheritClass( MessageDialog, OO.ui.MessageDialog ); * * MessageDialog.static.actions = [ * { action: 'save', label: 'Done', flags: 'primary' }, * { label: 'Cancel', flags: 'safe' } * ]; * * MessageDialog.prototype.initialize = function () { * MessageDialog.parent.prototype.initialize.apply( this, arguments ); * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } ); * this.content.$element.append( '

Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.

' ); * this.$body.append( this.content.$element ); * }; * MessageDialog.prototype.getBodyHeight = function () { * return 100; * } * MessageDialog.prototype.getActionProcess = function ( action ) { * var dialog = this; * if ( action === 'save' ) { * dialog.getActions().get({actions: 'save'})[0].pushPending(); * return new OO.ui.Process() * .next( 1000 ) * .next( function () { * dialog.getActions().get({actions: 'save'})[0].popPending(); * } ); * } * return MessageDialog.parent.prototype.getActionProcess.call( this, action ); * }; * * var windowManager = new OO.ui.WindowManager(); * $( 'body' ).append( windowManager.$element ); * * var dialog = new MessageDialog(); * windowManager.addWindows( [ dialog ] ); * windowManager.openWindow( dialog ); * * @abstract * @class * * @constructor * @param {Object} [config] Configuration options * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element */ OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) { // Configuration initialization config = config || {}; // Properties this.pending = 0; this.$pending = null; // Initialisation this.setPendingElement( config.$pending || this.$element ); }; /* Setup */ OO.initClass( OO.ui.mixin.PendingElement ); /* Methods */ /** * Set the pending element (and clean up any existing one). * * @param {jQuery} $pending The element to set to pending. */ OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) { if ( this.$pending ) { this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); } this.$pending = $pending; if ( this.pending > 0 ) { this.$pending.addClass( 'oo-ui-pendingElement-pending' ); } }; /** * Check if an element is pending. * * @return {boolean} Element is pending */ OO.ui.mixin.PendingElement.prototype.isPending = function () { return !!this.pending; }; /** * Increase the pending counter. The pending state will remain active until the counter is zero * (i.e., the number of calls to #pushPending and #popPending is the same). * * @chainable */ OO.ui.mixin.PendingElement.prototype.pushPending = function () { if ( this.pending === 0 ) { this.$pending.addClass( 'oo-ui-pendingElement-pending' ); this.updateThemeClasses(); } this.pending++; return this; }; /** * Decrease the pending counter. The pending state will remain active until the counter is zero * (i.e., the number of calls to #pushPending and #popPending is the same). * * @chainable */ OO.ui.mixin.PendingElement.prototype.popPending = function () { if ( this.pending === 1 ) { this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); this.updateThemeClasses(); } this.pending = Math.max( 0, this.pending - 1 ); return this; }; /** * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them. * Actions can be made available for specific contexts (modes) and circumstances * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}. * * ActionSets contain two types of actions: * * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property. * - Other: Other actions include all non-special visible actions. * * Please see the [OOjs UI documentation on MediaWiki][1] for more information. * * @example * // Example: An action set used in a process dialog * function MyProcessDialog( config ) { * MyProcessDialog.parent.call( this, config ); * } * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog ); * MyProcessDialog.static.title = 'An action set in a process dialog'; * // An action set that uses modes ('edit' and 'help' mode, in this example). * MyProcessDialog.static.actions = [ * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] }, * { action: 'help', modes: 'edit', label: 'Help' }, * { modes: 'edit', label: 'Cancel', flags: 'safe' }, * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' } * ]; * * MyProcessDialog.prototype.initialize = function () { * MyProcessDialog.parent.prototype.initialize.apply( this, arguments ); * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } ); * this.panel1.$element.append( '

This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.

' ); * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } ); * this.panel2.$element.append( '

This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.

' ); * this.stackLayout = new OO.ui.StackLayout( { * items: [ this.panel1, this.panel2 ] * } ); * this.$body.append( this.stackLayout.$element ); * }; * MyProcessDialog.prototype.getSetupProcess = function ( data ) { * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data ) * .next( function () { * this.actions.setMode( 'edit' ); * }, this ); * }; * MyProcessDialog.prototype.getActionProcess = function ( action ) { * if ( action === 'help' ) { * this.actions.setMode( 'help' ); * this.stackLayout.setItem( this.panel2 ); * } else if ( action === 'back' ) { * this.actions.setMode( 'edit' ); * this.stackLayout.setItem( this.panel1 ); * } else if ( action === 'continue' ) { * var dialog = this; * return new OO.ui.Process( function () { * dialog.close(); * } ); * } * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action ); * }; * MyProcessDialog.prototype.getBodyHeight = function () { * return this.panel1.$element.outerHeight( true ); * }; * var windowManager = new OO.ui.WindowManager(); * $( 'body' ).append( windowManager.$element ); * var dialog = new MyProcessDialog( { * size: 'medium' * } ); * windowManager.addWindows( [ dialog ] ); * windowManager.openWindow( dialog ); * * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets * * @abstract * @class * @mixins OO.EventEmitter * * @constructor * @param {Object} [config] Configuration options */ OO.ui.ActionSet = function OoUiActionSet( config ) { // Configuration initialization config = config || {}; // Mixin constructors OO.EventEmitter.call( this ); // Properties this.list = []; this.categories = { actions: 'getAction', flags: 'getFlags', modes: 'getModes' }; this.categorized = {}; this.special = {}; this.others = []; this.organized = false; this.changing = false; this.changed = false; }; /* Setup */ OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter ); /* Static Properties */ /** * Symbolic name of the flags used to identify special actions. Special actions are displayed in the * header of a {@link OO.ui.ProcessDialog process dialog}. * See the [OOjs UI documentation on MediaWiki][2] for more information and examples. * * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs * * @abstract * @static * @inheritable * @property {string} */ OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ]; /* Events */ /** * @event click * * A 'click' event is emitted when an action is clicked. * * @param {OO.ui.ActionWidget} action Action that was clicked */ /** * @event resize * * A 'resize' event is emitted when an action widget is resized. * * @param {OO.ui.ActionWidget} action Action that was resized */ /** * @event add * * An 'add' event is emitted when actions are {@link #method-add added} to the action set. * * @param {OO.ui.ActionWidget[]} added Actions added */ /** * @event remove * * A 'remove' event is emitted when actions are {@link #method-remove removed} * or {@link #clear cleared}. * * @param {OO.ui.ActionWidget[]} added Actions removed */ /** * @event change * * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared}, * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed. * */ /* Methods */ /** * Handle action change events. * * @private * @fires change */ OO.ui.ActionSet.prototype.onActionChange = function () { this.organized = false; if ( this.changing ) { this.changed = true; } else { this.emit( 'change' ); } }; /** * Check if an action is one of the special actions. * * @param {OO.ui.ActionWidget} action Action to check * @return {boolean} Action is special */ OO.ui.ActionSet.prototype.isSpecial = function ( action ) { var flag; for ( flag in this.special ) { if ( action === this.special[ flag ] ) { return true; } } return false; }; /** * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’, * or ‘disabled’. * * @param {Object} [filters] Filters to use, omit to get all actions * @param {string|string[]} [filters.actions] Actions that action widgets must have * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe') * @param {string|string[]} [filters.modes] Modes that action widgets must have * @param {boolean} [filters.visible] Action widgets must be visible * @param {boolean} [filters.disabled] Action widgets must be disabled * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria */ OO.ui.ActionSet.prototype.get = function ( filters ) { var i, len, list, category, actions, index, match, matches; if ( filters ) { this.organize(); // Collect category candidates matches = []; for ( category in this.categorized ) { list = filters[ category ]; if ( list ) { if ( !Array.isArray( list ) ) { list = [ list ]; } for ( i = 0, len = list.length; i < len; i++ ) { actions = this.categorized[ category ][ list[ i ] ]; if ( Array.isArray( actions ) ) { matches.push.apply( matches, actions ); } } } } // Remove by boolean filters for ( i = 0, len = matches.length; i < len; i++ ) { match = matches[ i ]; if ( ( filters.visible !== undefined && match.isVisible() !== filters.visible ) || ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled ) ) { matches.splice( i, 1 ); len--; i--; } } // Remove duplicates for ( i = 0, len = matches.length; i < len; i++ ) { match = matches[ i ]; index = matches.lastIndexOf( match ); while ( index !== i ) { matches.splice( index, 1 ); len--; index = matches.lastIndexOf( match ); } } return matches; } return this.list.slice(); }; /** * Get 'special' actions. * * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'. * Special flags can be configured in subclasses by changing the static #specialFlags property. * * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets. */ OO.ui.ActionSet.prototype.getSpecial = function () { this.organize(); return $.extend( {}, this.special ); }; /** * Get 'other' actions. * * Other actions include all non-special visible action widgets. * * @return {OO.ui.ActionWidget[]} 'Other' action widgets */ OO.ui.ActionSet.prototype.getOthers = function () { this.organize(); return this.others.slice(); }; /** * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured * to be available in the specified mode will be made visible. All other actions will be hidden. * * @param {string} mode The mode. Only actions configured to be available in the specified * mode will be made visible. * @chainable * @fires toggle * @fires change */ OO.ui.ActionSet.prototype.setMode = function ( mode ) { var i, len, action; this.changing = true; for ( i = 0, len = this.list.length; i < len; i++ ) { action = this.list[ i ]; action.toggle( action.hasMode( mode ) ); } this.organized = false; this.changing = false; this.emit( 'change' ); return this; }; /** * Set the abilities of the specified actions. * * Action widgets that are configured with the specified actions will be enabled * or disabled based on the boolean values specified in the `actions` * parameter. * * @param {Object.} actions A list keyed by action name with boolean * values that indicate whether or not the action should be enabled. * @chainable */ OO.ui.ActionSet.prototype.setAbilities = function ( actions ) { var i, len, action, item; for ( i = 0, len = this.list.length; i < len; i++ ) { item = this.list[ i ]; action = item.getAction(); if ( actions[ action ] !== undefined ) { item.setDisabled( !actions[ action ] ); } } return this; }; /** * Executes a function once per action. * * When making changes to multiple actions, use this method instead of iterating over the actions * manually to defer emitting a #change event until after all actions have been changed. * * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get * @param {Function} callback Callback to run for each action; callback is invoked with three * arguments: the action, the action's index, the list of actions being iterated over * @chainable */ OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) { this.changed = false; this.changing = true; this.get( filter ).forEach( callback ); this.changing = false; if ( this.changed ) { this.emit( 'change' ); } return this; }; /** * Add action widgets to the action set. * * @param {OO.ui.ActionWidget[]} actions Action widgets to add * @chainable * @fires add * @fires change */ OO.ui.ActionSet.prototype.add = function ( actions ) { var i, len, action; this.changing = true; for ( i = 0, len = actions.length; i < len; i++ ) { action = actions[ i ]; action.connect( this, { click: [ 'emit', 'click', action ], resize: [ 'emit', 'resize', action ], toggle: [ 'onActionChange' ] } ); this.list.push( action ); } this.organized = false; this.emit( 'add', actions ); this.changing = false; this.emit( 'change' ); return this; }; /** * Remove action widgets from the set. * * To remove all actions, you may wish to use the #clear method instead. * * @param {OO.ui.ActionWidget[]} actions Action widgets to remove * @chainable * @fires remove * @fires change */ OO.ui.ActionSet.prototype.remove = function ( actions ) { var i, len, index, action; this.changing = true; for ( i = 0, len = actions.length; i < len; i++ ) { action = actions[ i ]; index = this.list.indexOf( action ); if ( index !== -1 ) { action.disconnect( this ); this.list.splice( index, 1 ); } } this.organized = false; this.emit( 'remove', actions ); this.changing = false; this.emit( 'change' ); return this; }; /** * Remove all action widets from the set. * * To remove only specified actions, use the {@link #method-remove remove} method instead. * * @chainable * @fires remove * @fires change */ OO.ui.ActionSet.prototype.clear = function () { var i, len, action, removed = this.list.slice(); this.changing = true; for ( i = 0, len = this.list.length; i < len; i++ ) { action = this.list[ i ]; action.disconnect( this ); } this.list = []; this.organized = false; this.emit( 'remove', removed ); this.changing = false; this.emit( 'change' ); return this; }; /** * Organize actions. * * This is called whenever organized information is requested. It will only reorganize the actions * if something has changed since the last time it ran. * * @private * @chainable */ OO.ui.ActionSet.prototype.organize = function () { var i, iLen, j, jLen, flag, action, category, list, item, special, specialFlags = this.constructor.static.specialFlags; if ( !this.organized ) { this.categorized = {}; this.special = {}; this.others = []; for ( i = 0, iLen = this.list.length; i < iLen; i++ ) { action = this.list[ i ]; if ( action.isVisible() ) { // Populate categories for ( category in this.categories ) { if ( !this.categorized[ category ] ) { this.categorized[ category ] = {}; } list = action[ this.categories[ category ] ](); if ( !Array.isArray( list ) ) { list = [ list ]; } for ( j = 0, jLen = list.length; j < jLen; j++ ) { item = list[ j ]; if ( !this.categorized[ category ][ item ] ) { this.categorized[ category ][ item ] = []; } this.categorized[ category ][ item ].push( action ); } } // Populate special/others special = false; for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) { flag = specialFlags[ j ]; if ( !this.special[ flag ] && action.hasFlag( flag ) ) { this.special[ flag ] = action; special = true; break; } } if ( !special ) { this.others.push( action ); } } } this.organized = true; } return this; }; /** * 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 ) { // Configuration initialization config = config || {}; // Properties this.$ = $; this.visible = true; this.data = config.data; this.$element = config.$element || $( document.createElement( this.getTagName() ) ); this.elementGroup = null; this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses ); // Initialization if ( Array.isArray( config.classes ) ) { this.$element.addClass( config.classes.join( ' ' ) ); } if ( config.id ) { this.$element.attr( 'id', 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, data, cls, parts, parent, obj, top, state; if ( typeof idOrNode === 'string' ) { id = idOrNode; $elem = $( document.getElementById( id ) ); } else { $elem = $( idOrNode ); id = $elem.attr( 'id' ); } if ( !$elem.length ) { throw new Error( 'Widget not found: ' + id ); } data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused; if ( data ) { // cached! if ( data === true ) { throw new Error( 'Circular dependency! ' + id ); } return data; } data = $elem.attr( 'data-ooui' ); if ( !data ) { throw new Error( 'No infusion data found: ' + id ); } try { data = $.parseJSON( 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 ) { // The PHP output might be old and not including the "OO.ui" prefix // TODO: Remove this back-compat after next major release cls = OO.getProp.apply( OO, [ OO.ui ].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 data = OO.copy( data, null, function deserialize( value ) { if ( OO.isPlainObject( value ) ) { if ( value.tag ) { return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise ); } if ( value.html ) { return new OO.ui.HtmlSnippet( value.html ); } } } ); // jscs:disable requireCapitalizedConstructors obj = new cls( data ); // rebuild widget // pick up dynamic state, like focus, value of form inputs, scroll position, etc. state = obj.gatherPreInfuseState( $elem ); // now replace old DOM with this new DOM. if ( top ) { $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 (T105828). // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design. $elem[ 0 ].oouiInfused = obj; top.resolve(); } obj.$element.data( 'ooui-infused', obj ); // 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; }; /** * 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 === 9 && 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 ); // Support: IE 8 // Standard Document.defaultView is IE9+ return doc.parentWindow || 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 === 9; 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, // Support: IE 8 // Standard Document.defaultView is IE9+ win = doc.parentWindow || doc.defaultView, style = win && win.getComputedStyle ? win.getComputedStyle( el, null ) : // Support: IE 8 // Standard getComputedStyle() is IE9+ el.currentStyle, $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, // Support: IE 8 // Standard Document.defaultView is IE9+ win = doc.parentWindow || 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 scrollable object parent * * documentElement can't be used to get or set the scrollTop * property on Blink. Changing and testing its 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 scrollable parent for * @return {HTMLElement} Scrollable parent */ 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; if ( 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 window * will be returned. * * @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, // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091 props = [ 'overflow-x', 'overflow-y' ], $parent = $( el ).parent(); if ( dimension === 'x' || dimension === 'y' ) { props = [ 'overflow-' + dimension ]; } while ( $parent.length ) { if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) { return $parent[ 0 ]; } i = props.length; while ( i-- ) { val = $parent.css( props[ i ] ); if ( val === 'auto' || val === 'scroll' ) { return $parent[ 0 ]; } } $parent = $parent.parent(); } return this.getDocument( el ).body; }; /** * Scroll element into view. * * @static * @param {HTMLElement} el Element to scroll into view * @param {Object} [config] Configuration options * @param {string} [config.duration] 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 {Function} [config.complete] Function to call when scrolling completes */ OO.ui.Element.static.scrollIntoView = function ( el, config ) { var rel, anim, callback, sc, $sc, eld, scd, $win; // Configuration initialization config = config || {}; anim = {}; callback = typeof config.complete === 'function' && config.complete; sc = this.getClosestScrollableContainer( el, config.direction ); $sc = $( sc ); eld = this.getDimensions( el ); scd = this.getDimensions( sc ); $win = $( this.getWindow( el ) ); // Compute the distances between the edges of el and the edges of the scroll viewport if ( $sc.is( 'html, body' ) ) { // If the scrollable container is the root, this is easy rel = { top: eld.rect.top, bottom: $win.innerHeight() - eld.rect.bottom, left: eld.rect.left, right: $win.innerWidth() - eld.rect.right }; } else { // Otherwise, we have to subtract el's coordinates from sc's coordinates rel = { top: eld.rect.top - ( scd.rect.top + scd.borders.top ), bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom, left: eld.rect.left - ( scd.rect.left + scd.borders.left ), right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right }; } if ( !config.direction || config.direction === 'y' ) { if ( rel.top < 0 ) { anim.scrollTop = scd.scroll.top + rel.top; } else if ( rel.top > 0 && rel.bottom < 0 ) { anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom ); } } if ( !config.direction || config.direction === 'x' ) { if ( rel.left < 0 ) { anim.scrollLeft = scd.scroll.left + rel.left; } else if ( rel.left > 0 && rel.right < 0 ) { anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right ); } } if ( !$.isEmptyObject( anim ) ) { $sc.stop( true ).animate( anim, config.duration || 'fast' ); if ( callback ) { $sc.queue( function ( next ) { callback(); next(); } ); } } else { if ( callback ) { callback(); } } }; /** * 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} Element data * @chainable */ OO.ui.Element.prototype.setData = function ( data ) { this.data = data; return this; }; /** * 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 () { this.debouncedUpdateThemeClassesHandler(); }; /** * @private * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to * make them synchronous. */ OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () { OO.ui.theme.updateElementClasses( 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. */ 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 */ OO.ui.Element.prototype.scrollElementIntoView = function ( config ) { return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config ); }; /** * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node * (and its children) that represent an Element of the same type and configuration as the current * one, 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 this widget's #$element is inserted into * DOM to replace `node`. * * @protected * @param {HTMLElement} node * @return {Object} */ OO.ui.Element.prototype.gatherPreInfuseState = function () { return {}; }; /** * 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 () { }; /** * 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 ); /* Static Properties */ /** * Whether this widget will behave reasonably when wrapped in a HTML `