From a1789ddde42033f1b05cc4929491214ee6e79383 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Thu, 17 Dec 2015 09:15:42 +0100 Subject: Update to MediaWiki 1.26.0 --- resources/lib/oojs-ui/oojs-ui.js | 6101 ++++++++++++++++++++++++++++++-------- 1 file changed, 4833 insertions(+), 1268 deletions(-) (limited to 'resources/lib/oojs-ui/oojs-ui.js') diff --git a/resources/lib/oojs-ui/oojs-ui.js b/resources/lib/oojs-ui/oojs-ui.js index 9692d5cf..aeff69e0 100644 --- a/resources/lib/oojs-ui/oojs-ui.js +++ b/resources/lib/oojs-ui/oojs-ui.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.11.3 + * OOjs UI v0.12.12 * https://www.mediawiki.org/wiki/OOjs_UI * - * Copyright 2011–2015 OOjs Team and other contributors. + * Copyright 2011–2015 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2015-05-12T12:15:37Z + * Date: 2015-10-13T20:38:18Z */ ( function ( OO ) { @@ -44,37 +44,102 @@ OO.ui.Keys = { 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} [description] + * @return {boolean} */ OO.ui.isFocusableElement = function ( $element ) { - var node = $element[0], - nodeName = node.nodeName.toLowerCase(), - // Check if the element have tabindex set - isInElementGroup = /^(input|select|textarea|button|object)$/.test( nodeName ), - // Check if the element is a link with href or if it has tabindex - isOtherElement = ( - ( nodeName === 'a' && node.href ) || - !isNaN( $element.attr( 'tabindex' ) ) - ), - // Check if the element is visible - isVisible = ( - // This is quicker than calling $element.is( ':visible' ) - $.expr.filters.visible( node ) && - // Check that all parents are visible - !$element.parents().addBack().filter( function () { - return $.css( this, 'visibility' ) === 'hidden'; - } ).length - ); + var nodeName, + element = $element[ 0 ]; - return ( - ( isInElementGroup ? !node.disabled : isOtherElement ) && - isVisible - ); + // 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; }; /** @@ -182,6 +247,38 @@ OO.ui.debounce = function ( func, wait, immediate ) { }; }; +/** + * 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. @@ -230,7 +327,15 @@ OO.ui.infuse = function ( idOrNode ) { // 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' + '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' }; /** @@ -297,10 +402,102 @@ OO.ui.infuse = function ( idOrNode ) { 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 = {}; + /** - * Element that can be marked as pending. + * 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 @@ -309,7 +506,7 @@ OO.ui.infuse = function ( idOrNode ) { * @param {Object} [config] Configuration options * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element */ -OO.ui.PendingElement = function OoUiPendingElement( config ) { +OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) { // Configuration initialization config = config || {}; @@ -323,7 +520,7 @@ OO.ui.PendingElement = function OoUiPendingElement( config ) { /* Setup */ -OO.initClass( OO.ui.PendingElement ); +OO.initClass( OO.ui.mixin.PendingElement ); /* Methods */ @@ -332,7 +529,7 @@ OO.initClass( OO.ui.PendingElement ); * * @param {jQuery} $pending The element to set to pending. */ -OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) { +OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) { if ( this.$pending ) { this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); } @@ -344,20 +541,21 @@ OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) { }; /** - * Check if input is pending. + * Check if an element is pending. * - * @return {boolean} + * @return {boolean} Element is pending */ -OO.ui.PendingElement.prototype.isPending = function () { +OO.ui.mixin.PendingElement.prototype.isPending = function () { return !!this.pending; }; /** - * Increase the pending stack. + * 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.PendingElement.prototype.pushPending = function () { +OO.ui.mixin.PendingElement.prototype.pushPending = function () { if ( this.pending === 0 ) { this.$pending.addClass( 'oo-ui-pendingElement-pending' ); this.updateThemeClasses(); @@ -368,13 +566,12 @@ OO.ui.PendingElement.prototype.pushPending = function () { }; /** - * Reduce the pending stack. - * - * Clamped at zero. + * 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.PendingElement.prototype.popPending = function () { +OO.ui.mixin.PendingElement.prototype.popPending = function () { if ( this.pending === 1 ) { this.$pending.removeClass( 'oo-ui-pendingElement-pending' ); this.updateThemeClasses(); @@ -399,7 +596,7 @@ OO.ui.PendingElement.prototype.popPending = function () { * @example * // Example: An action set used in a process dialog * function MyProcessDialog( config ) { - * MyProcessDialog.super.call( this, config ); + * MyProcessDialog.parent.call( this, config ); * } * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog ); * MyProcessDialog.static.title = 'An action set in a process dialog'; @@ -412,7 +609,7 @@ OO.ui.PendingElement.prototype.popPending = function () { * ]; * * MyProcessDialog.prototype.initialize = function () { - * MyProcessDialog.super.prototype.initialize.apply( this, arguments ); + * 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 } ); @@ -423,7 +620,7 @@ OO.ui.PendingElement.prototype.popPending = function () { * this.$body.append( this.stackLayout.$element ); * }; * MyProcessDialog.prototype.getSetupProcess = function ( data ) { - * return MyProcessDialog.super.prototype.getSetupProcess.call( this, data ) + * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data ) * .next( function () { * this.actions.setMode( 'edit' ); * }, this ); @@ -441,7 +638,7 @@ OO.ui.PendingElement.prototype.popPending = function () { * dialog.close(); * } ); * } - * return MyProcessDialog.super.prototype.getActionProcess.call( this, action ); + * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action ); * }; * MyProcessDialog.prototype.getBodyHeight = function () { * return this.panel1.$element.outerHeight( true ); @@ -908,7 +1105,8 @@ OO.ui.ActionSet.prototype.organize = function () { * @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} [$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. */ @@ -923,8 +1121,7 @@ OO.ui.Element = function OoUiElement( config ) { this.$element = config.$element || $( document.createElement( this.getTagName() ) ); this.elementGroup = null; - this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this ); - this.updateThemeClassesPending = false; + this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses ); // Initialization if ( Array.isArray( config.classes ) ) { @@ -991,7 +1188,7 @@ OO.ui.Element.static.tagName = 'div'; * DOM node. */ OO.ui.Element.static.infuse = function ( idOrNode ) { - var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, true ); + var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false ); // Verify that the type matches up. // FIXME: uncomment after T89721 is fixed (see T90929) /* @@ -1007,12 +1204,14 @@ OO.ui.Element.static.infuse = function ( idOrNode ) { * extra property so that only the top-level invocation touches the DOM. * @private * @param {string|HTMLElement|jQuery} idOrNode - * @param {boolean} top True only for top-level invocation. + * @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, top ) { +OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) { // look for a cached result of a previous infusion. - var id, $elem, data, cls, obj; + var id, $elem, data, cls, parts, parent, obj, top, state; if ( typeof idOrNode === 'string' ) { id = idOrNode; $elem = $( document.getElementById( id ) ); @@ -1020,7 +1219,10 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) { $elem = $( idOrNode ); id = $elem.attr( 'id' ); } - data = $elem.data( 'ooui-infused' ); + if ( !$elem.length ) { + throw new Error( 'Widget not found: ' + id ); + } + data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused; if ( data ) { // cached! if ( data === true ) { @@ -1028,9 +1230,6 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) { } return data; } - if ( !$elem.length ) { - throw new Error( 'Widget not found: ' + id ); - } data = $elem.attr( 'data-ooui' ); if ( !data ) { throw new Error( 'No infusion data found: ' + id ); @@ -1047,16 +1246,43 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) { // Special case: this is a raw Tag; wrap existing node, don't rebuild. return new OO.ui.Element( { $element: $elem } ); } - cls = OO.ui[data._]; - if ( !cls ) { - throw new Error( 'Unknown widget type: ' + id ); + 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, false ); + return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise ); } if ( value.html ) { return new OO.ui.HtmlSnippet( value.html ); @@ -1065,13 +1291,22 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) { } ); // 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; }; @@ -1128,6 +1363,8 @@ OO.ui.Element.static.getDocument = function ( obj ) { */ 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; }; @@ -1243,9 +1480,13 @@ OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) { */ 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, @@ -1271,6 +1512,8 @@ OO.ui.Element.static.getBorders = function ( el ) { 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 ) { @@ -1351,11 +1594,12 @@ OO.ui.Element.static.getRootScrollableElement = function ( el ) { */ OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) { var i, val, - props = [ 'overflow' ], + // 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.push( 'overflow-' + dimension ); + props = [ 'overflow-' + dimension ]; } while ( $parent.length ) { @@ -1386,16 +1630,18 @@ OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) * @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 || {}; - var rel, 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 ) ); + 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' ) ) { @@ -1458,7 +1704,10 @@ OO.ui.Element.static.scrollIntoView = function ( el, config ) { * @param {HTMLElement} el Element to reconsider the scrollbars on */ OO.ui.Element.static.reconsiderScrollbars = function ( el ) { - var i, len, nodes = []; + 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 ); @@ -1470,6 +1719,9 @@ OO.ui.Element.static.reconsiderScrollbars = function ( el ) { 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 */ @@ -1550,18 +1802,16 @@ OO.ui.Element.prototype.supports = function ( methods ) { * guaranteeing that theme updates do not occur within an element's constructor */ OO.ui.Element.prototype.updateThemeClasses = function () { - if ( !this.updateThemeClassesPending ) { - this.updateThemeClassesPending = true; - setTimeout( this.debouncedUpdateThemeClassesHandler ); - } + 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 ); - this.updateThemeClassesPending = false; }; /** @@ -1612,7 +1862,7 @@ OO.ui.Element.prototype.getClosestScrollableElementContainer = function () { /** * Get group element is in. * - * @return {OO.ui.GroupElement|null} Group element, null if none + * @return {OO.ui.mixin.GroupElement|null} Group element, null if none */ OO.ui.Element.prototype.getElementGroup = function () { return this.elementGroup; @@ -1621,7 +1871,7 @@ OO.ui.Element.prototype.getElementGroup = function () { /** * Set group element is in. * - * @param {OO.ui.GroupElement|null} group Group element, null if none + * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none * @chainable */ OO.ui.Element.prototype.setElementGroup = function ( group ) { @@ -1638,12 +1888,41 @@ 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}, - * and {@link OO.ui.BookletLayout BookletLayout} for more information and examples. + * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples. * * @abstract * @class @@ -1658,7 +1937,7 @@ OO.ui.Layout = function OoUiLayout( config ) { config = config || {}; // Parent constructor - OO.ui.Layout.super.call( this, config ); + OO.ui.Layout.parent.call( this, config ); // Mixin constructors OO.EventEmitter.call( this ); @@ -1692,7 +1971,7 @@ OO.ui.Widget = function OoUiWidget( config ) { config = $.extend( { disabled: false }, config ); // Parent constructor - OO.ui.Widget.super.call( this, config ); + OO.ui.Widget.parent.call( this, config ); // Mixin constructors OO.EventEmitter.call( this ); @@ -1711,12 +1990,26 @@ OO.ui.Widget = function OoUiWidget( config ) { 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 `