diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2015-12-17 09:15:42 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2015-12-17 09:44:51 +0100 |
commit | a1789ddde42033f1b05cc4929491214ee6e79383 (patch) | |
tree | 63615735c4ddffaaabf2428946bb26f90899f7bf /resources/src/mediawiki/mediawiki.js | |
parent | 9e06a62f265e3a2aaabecc598d4bc617e06fa32d (diff) |
Update to MediaWiki 1.26.0
Diffstat (limited to 'resources/src/mediawiki/mediawiki.js')
-rw-r--r-- | resources/src/mediawiki/mediawiki.js | 923 |
1 files changed, 516 insertions, 407 deletions
diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js index ee57c21f..9436dbf2 100644 --- a/resources/src/mediawiki/mediawiki.js +++ b/resources/src/mediawiki/mediawiki.js @@ -7,6 +7,8 @@ * @alternateClassName mediaWiki * @singleton */ +/*jshint latedef:false */ +/*global sha1 */ ( function ( $ ) { 'use strict'; @@ -14,6 +16,7 @@ hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice, trackCallbacks = $.Callbacks( 'memory' ), + trackHandlers = [], trackQueue = []; /** @@ -66,7 +69,7 @@ if ( $.isPlainObject( selection ) ) { for ( s in selection ) { - setGlobalMapValue( this, s, selection[s] ); + setGlobalMapValue( this, s, selection[ s ] ); } return true; } @@ -93,13 +96,13 @@ * @param {Mixed} value */ function setGlobalMapValue( map, key, value ) { - map.values[key] = value; + map.values[ key ] = value; mw.log.deprecate( - window, - key, - value, - // Deprecation notice for mw.config globals (T58550, T72470) - map === mw.config && 'Use mw.config instead.' + window, + key, + value, + // Deprecation notice for mw.config globals (T58550, T72470) + map === mw.config && 'Use mw.config instead.' ); } @@ -126,7 +129,7 @@ selection = slice.call( selection ); results = {}; for ( i = 0; i < selection.length; i++ ) { - results[selection[i]] = this.get( selection[i], fallback ); + results[ selection[ i ] ] = this.get( selection[ i ], fallback ); } return results; } @@ -135,7 +138,7 @@ if ( !hasOwn.call( this.values, selection ) ) { return fallback; } - return this.values[selection]; + return this.values[ selection ]; } if ( selection === undefined ) { @@ -158,12 +161,12 @@ if ( $.isPlainObject( selection ) ) { for ( s in selection ) { - this.values[s] = selection[s]; + this.values[ s ] = selection[ s ]; } return true; } if ( typeof selection === 'string' && arguments.length > 1 ) { - this.values[selection] = value; + this.values[ selection ] = value; return true; } return false; @@ -180,7 +183,7 @@ if ( $.isArray( selection ) ) { for ( s = 0; s < selection.length; s++ ) { - if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { + if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) { return false; } } @@ -282,7 +285,7 @@ params: function ( parameters ) { var i; for ( i = 0; i < parameters.length; i += 1 ) { - this.parameters.push( parameters[i] ); + this.parameters.push( parameters[ i ] ); } return this; }, @@ -420,7 +423,7 @@ var parameters = slice.call( arguments, 1 ); return formatString.replace( /\$(\d+)/g, function ( str, match ) { var index = parseInt( match, 10 ) - 1; - return parameters[index] !== undefined ? parameters[index] : '$' + match; + return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match; } ); }, @@ -461,8 +464,7 @@ */ trackSubscribe: function ( topic, callback ) { var seen = 0; - - trackCallbacks.add( function ( trackQueue ) { + function handler( trackQueue ) { var event; for ( ; seen < trackQueue.length; seen++ ) { event = trackQueue[ seen ]; @@ -470,6 +472,26 @@ callback.call( event, event.topic, event.data ); } } + } + + trackHandlers.push( [ handler, callback ] ); + + trackCallbacks.add( handler ); + }, + + /** + * Stop handling events for a particular handler + * + * @param {Function} callback + */ + trackUnsubscribe: function ( callback ) { + trackHandlers = $.grep( trackHandlers, function ( fns ) { + if ( fns[ 1 ] === callback ) { + trackCallbacks.remove( fns[ 0 ] ); + // Ensure the tuple is removed to avoid holding on to closures + return false; + } + return true; } ); }, @@ -560,6 +582,7 @@ /** * Dummy placeholder for {@link mw.log} + * * @method */ log: ( function () { @@ -574,7 +597,6 @@ /** * Write a message the console's warning channel. - * Also logs a stacktrace for easier debugging. * Actions not supported by the browser console are silently ignored. * * @param {string...} msg Messages to output to console @@ -583,9 +605,22 @@ var console = window.console; if ( console && console.warn && console.warn.apply ) { console.warn.apply( console, arguments ); - if ( console.trace ) { - console.trace(); - } + } + }; + + /** + * Write a message the console's error channel. + * + * Most browsers provide a stacktrace by default if the argument + * is a caught Error object. + * + * @since 1.26 + * @param {Error|string...} msg Messages to output to console + */ + log.error = function () { + var console = window.console; + if ( console && console.error && console.error.apply ) { + console.error.apply( console, arguments ); } }; @@ -599,7 +634,7 @@ * @param {string} [msg] Optional text to include in the deprecation message */ log.deprecate = !Object.defineProperty ? function ( obj, key, val ) { - obj[key] = val; + obj[ key ] = val; } : function ( obj, key, val, msg ) { msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' ); // Support: IE8 @@ -621,7 +656,7 @@ } ); } catch ( err ) { // Fallback to creating a copy of the value to the object. - obj[key] = val; + obj[ key ] = val; } }; @@ -678,28 +713,54 @@ /** * Mapping of registered modules. * - * See #implement for exact details on support for script, style and messages. + * See #implement and #execute for exact details on support for script, style and messages. * * Format: * * { * 'moduleName': { - * // From startup mdoule - * 'version': ############## (unix timestamp) + * // From mw.loader.register() + * 'version': '########' (hash) * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example', (or) null + * + * // Set from execute() or mw.loader.state() * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing' * - * // Added during implementation + * // Optionally added at run-time by mw.loader.implement() * 'skipped': true - * 'script': ... - * 'style': ... - * 'messages': { 'key': 'value' } + * 'script': closure, array of urls, or string + * 'style': { ... } (see #execute) + * 'messages': { 'key': 'value', ... } * } * } * + * State machine: + * + * - `registered`: + * The module is known to the system but not yet requested. + * Meta data is registered via mw.loader#register. Calls to that method are + * generated server-side by the startup module. + * - `loading`: + * The module is requested through mw.loader (either directly or as dependency of + * another module). The client will be fetching module contents from the server. + * The contents are then stashed in the registry via mw.loader#implement. + * - `loaded`: + * The module has been requested from the server and stashed via mw.loader#implement. + * If the module has no more dependencies in-fight, the module will be executed + * right away. Otherwise execution is deferred, controlled via #handlePending. + * - `executing`: + * The module is being executed. + * - `ready`: + * The module has been successfully executed. + * - `error`: + * The module (or one of its dependencies) produced an error during execution. + * - `missing`: + * The module was registered client-side and requested, but the server denied knowledge + * of the module's existence. + * * @property * @private */ @@ -720,7 +781,25 @@ // List of modules to be loaded queue = [], - // List of callback functions waiting for modules to be ready to be called + /** + * List of callback jobs waiting for modules to be ready. + * + * Jobs are created by #request() and run by #handlePending(). + * + * Typically when a job is created for a module, the job's dependencies contain + * both the module being requested and all its recursive dependencies. + * + * Format: + * + * { + * 'dependencies': [ module names ], + * 'ready': Function callback + * 'error': Function callback + * } + * + * @property {Object[]} jobs + * @private + */ jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! @@ -760,7 +839,7 @@ if ( nextnode ) { $( nextnode ).before( s ); } else { - document.getElementsByTagName( 'head' )[0].appendChild( s ); + document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); } if ( s.styleSheet ) { // Support: IE6-10 @@ -784,7 +863,15 @@ * @param {Function} [callback] */ function addEmbeddedCSS( cssText, callback ) { - var $style, styleEl; + var $style, styleEl, newCssText; + + function fireCallbacks() { + var oldCallbacks = cssCallbacks; + // Reset cssCallbacks variable so it's not polluted by any calls to + // addEmbeddedCSS() from one of the callbacks (T105973) + cssCallbacks = $.Callbacks(); + oldCallbacks.fire().empty(); + } if ( callback ) { cssCallbacks.add( callback ); @@ -837,149 +924,61 @@ // Verify that the element before the marker actually is a // <style> tag and one that came from ResourceLoader // (not some other style tag or even a `<meta>` or `<script>`). - if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { + if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) ) { // There's already a dynamic <style> tag present and // we are able to append more to it. styleEl = $style.get( 0 ); // Support: IE6-10 if ( styleEl.styleSheet ) { try { - styleEl.styleSheet.cssText += cssText; + // Support: IE9 + // We can't do styleSheet.cssText += cssText, since IE9 mangles this property on + // write, dropping @media queries from the CSS text. If we read it and used its + // value, we would accidentally apply @media-specific styles to all media. (T108727) + if ( document.documentMode === 9 ) { + newCssText = $style.data( 'ResourceLoaderDynamicStyleTag' ) + cssText; + styleEl.styleSheet.cssText = newCssText; + $style.data( 'ResourceLoaderDynamicStyleTag', newCssText ); + } else { + styleEl.styleSheet.cssText += cssText; + } } catch ( e ) { mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } ); } } else { styleEl.appendChild( document.createTextNode( cssText ) ); } - cssCallbacks.fire().empty(); + fireCallbacks(); return; } } - $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); - - cssCallbacks.fire().empty(); - } - - /** - * Zero-pad three numbers. - * - * @private - * @param {number} a - * @param {number} b - * @param {number} c - * @return {string} - */ - function pad( a, b, c ) { - return ( - ( a < 10 ? '0' : '' ) + a + - ( b < 10 ? '0' : '' ) + b + - ( c < 10 ? '0' : '' ) + c - ); - } - - /** - * Convert UNIX timestamp to ISO8601 format. - * - * @private - * @param {number} timestamp UNIX timestamp - */ - function formatVersionNumber( timestamp ) { - var d = new Date(); - d.setTime( timestamp * 1000 ); - return [ - pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), - 'T', - pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), - 'Z' - ].join( '' ); - } - - /** - * Resolve dependencies and detect circular references. - * - * @private - * @param {string} module Name of the top-level module whose dependencies shall be - * resolved and sorted. - * @param {Array} resolved Returns a topological sort of the given module and its - * dependencies, such that later modules depend on earlier modules. The array - * contains the module names. If the array contains already some module names, - * this function appends its result to the pre-existing array. - * @param {Object} [unresolved] Hash used to track the current dependency - * chain; used to report loops in the dependency graph. - * @throws {Error} If any unregistered module or a dependency loop is encountered - */ - function sortDependencies( module, resolved, unresolved ) { - var n, deps, len, skip; - - if ( !hasOwn.call( registry, module ) ) { - throw new Error( 'Unknown dependency: ' + module ); - } - - if ( registry[module].skip !== null ) { - /*jshint evil:true */ - skip = new Function( registry[module].skip ); - registry[module].skip = null; - if ( skip() ) { - registry[module].skipped = true; - registry[module].dependencies = []; - registry[module].state = 'ready'; - handlePending( module ); - return; - } - } + $style = $( newStyleTag( cssText, getMarker() ) ); - // Resolves dynamic loader function and replaces it with its own results - if ( $.isFunction( registry[module].dependencies ) ) { - registry[module].dependencies = registry[module].dependencies(); - // Ensures the module's dependencies are always in an array - if ( typeof registry[module].dependencies !== 'object' ) { - registry[module].dependencies = [registry[module].dependencies]; - } - } - if ( $.inArray( module, resolved ) !== -1 ) { - // Module already resolved; nothing to do - return; - } - // Create unresolved if not passed in - if ( !unresolved ) { - unresolved = {}; + if ( document.documentMode === 9 ) { + // Support: IE9 + // Preserve original CSS text because IE9 mangles it on write + $style.data( 'ResourceLoaderDynamicStyleTag', cssText ); + } else { + $style.data( 'ResourceLoaderDynamicStyleTag', true ); } - // Tracks down dependencies - deps = registry[module].dependencies; - len = deps.length; - for ( n = 0; n < len; n += 1 ) { - if ( $.inArray( deps[n], resolved ) === -1 ) { - if ( unresolved[deps[n]] ) { - throw new Error( - 'Circular reference detected: ' + module + - ' -> ' + deps[n] - ); - } - // Add to unresolved - unresolved[module] = true; - sortDependencies( deps[n], resolved, unresolved ); - delete unresolved[module]; - } - } - resolved[resolved.length] = module; + fireCallbacks(); } /** - * Get a list of module names that a module depends on in their proper dependency - * order. - * - * @private - * @param {string[]} module Array of string module names - * @return {Array} List of dependencies, including 'module'. + * @since 1.26 + * @param {Array} modules List of module names + * @return {string} Hash of concatenated version hashes. */ - function resolve( modules ) { - var resolved = []; - $.each( modules, function ( idx, module ) { - sortDependencies( module, resolved ); + function getCombinedVersion( modules ) { + var hashes = $.map( modules, function ( module ) { + return registry[ module ].version; } ); - return resolved; + // Trim for consistency with server-side ResourceLoader::makeHash. It also helps + // save precious space in the limited query string. Otherwise modules are more + // likely to require multiple HTTP requests. + return sha1( hashes.join( '' ) ).slice( 0, 12 ); } /** @@ -993,7 +992,7 @@ function allReady( modules ) { var i; for ( i = 0; i < modules.length; i++ ) { - if ( mw.loader.getState( modules[i] ) !== 'ready' ) { + if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) { return false; } } @@ -1011,7 +1010,7 @@ function anyFailed( modules ) { var i, state; for ( i = 0; i < modules.length; i++ ) { - state = mw.loader.getState( modules[i] ); + state = mw.loader.getState( modules[ i ] ); if ( state === 'error' || state === 'missing' ) { return true; } @@ -1033,16 +1032,16 @@ function handlePending( module ) { var j, job, hasErrors, m, stateChange; - if ( registry[module].state === 'error' || registry[module].state === 'missing' ) { + if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) { // If the current module failed, mark all dependent modules also as failed. // Iterate until steady-state to propagate the error state upwards in the // dependency tree. do { stateChange = false; for ( m in registry ) { - if ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) { - if ( anyFailed( registry[m].dependencies ) ) { - registry[m].state = 'error'; + if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) { + if ( anyFailed( registry[ m ].dependencies ) ) { + registry[ m ].state = 'error'; stateChange = true; } } @@ -1052,16 +1051,16 @@ // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module. for ( j = 0; j < jobs.length; j += 1 ) { - hasErrors = anyFailed( jobs[j].dependencies ); - if ( hasErrors || allReady( jobs[j].dependencies ) ) { + hasErrors = anyFailed( jobs[ j ].dependencies ); + if ( hasErrors || allReady( jobs[ j ].dependencies ) ) { // All dependencies satisfied, or some have errors - job = jobs[j]; + job = jobs[ j ]; jobs.splice( j, 1 ); j -= 1; try { if ( hasErrors ) { if ( $.isFunction( job.error ) ) { - job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] ); + job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] ); } } else { if ( $.isFunction( job.ready ) ) { @@ -1076,12 +1075,12 @@ } } - if ( registry[module].state === 'ready' ) { + if ( registry[ module ].state === 'ready' ) { // The current module became 'ready'. Set it in the module store, and recursively execute all // dependent modules that are loaded and now have all dependencies satisfied. - mw.loader.store.set( module, registry[module] ); + mw.loader.store.set( module, registry[ module ] ); for ( m in registry ) { - if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) { + if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) { execute( m ); } } @@ -1089,39 +1088,130 @@ } /** - * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, - * depending on whether document-ready has occurred yet and whether we are in async mode. + * Resolve dependencies and detect circular references. * * @private - * @param {string} src URL to script, will be used as the src attribute in the script tag - * @param {Function} [callback] Callback which will be run when the script is done - * @param {boolean} [async=false] Whether to load modules asynchronously. - * Ignored (and defaulted to `true`) if the document-ready event has already occurred. + * @param {string} module Name of the top-level module whose dependencies shall be + * resolved and sorted. + * @param {Array} resolved Returns a topological sort of the given module and its + * dependencies, such that later modules depend on earlier modules. The array + * contains the module names. If the array contains already some module names, + * this function appends its result to the pre-existing array. + * @param {Object} [unresolved] Hash used to track the current dependency + * chain; used to report loops in the dependency graph. + * @throws {Error} If any unregistered module or a dependency loop is encountered */ - function addScript( src, callback, async ) { - // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895) - if ( $.isReady || async ) { - $.ajax( { - url: src, - dataType: 'script', - // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use - // XHR for a same domain request instead of <script>, which changes the request - // headers (potentially missing a cache hit), and reduces caching in general - // since browsers cache XHR much less (if at all). And XHR means we retreive - // text, so we'd need to $.globalEval, which then messes up line numbers. - crossDomain: true, - cache: true, - async: true - } ).always( callback ); - } else { + function sortDependencies( module, resolved, unresolved ) { + var n, deps, len, skip; + + if ( !hasOwn.call( registry, module ) ) { + throw new Error( 'Unknown dependency: ' + module ); + } + + if ( registry[ module ].skip !== null ) { /*jshint evil:true */ - document.write( mw.html.element( 'script', { 'src': src }, '' ) ); - if ( callback ) { - // Document.write is synchronous, so this is called when it's done. - // FIXME: That's a lie. doc.write isn't actually synchronous. - callback(); + skip = new Function( registry[ module ].skip ); + registry[ module ].skip = null; + if ( skip() ) { + registry[ module ].skipped = true; + registry[ module ].dependencies = []; + registry[ module ].state = 'ready'; + handlePending( module ); + return; + } + } + + // Resolves dynamic loader function and replaces it with its own results + if ( $.isFunction( registry[ module ].dependencies ) ) { + registry[ module ].dependencies = registry[ module ].dependencies(); + // Ensures the module's dependencies are always in an array + if ( typeof registry[ module ].dependencies !== 'object' ) { + registry[ module ].dependencies = [ registry[ module ].dependencies ]; + } + } + if ( $.inArray( module, resolved ) !== -1 ) { + // Module already resolved; nothing to do + return; + } + // Create unresolved if not passed in + if ( !unresolved ) { + unresolved = {}; + } + // Tracks down dependencies + deps = registry[ module ].dependencies; + len = deps.length; + for ( n = 0; n < len; n += 1 ) { + if ( $.inArray( deps[ n ], resolved ) === -1 ) { + if ( unresolved[ deps[ n ] ] ) { + throw new Error( + 'Circular reference detected: ' + module + + ' -> ' + deps[ n ] + ); + } + + // Add to unresolved + unresolved[ module ] = true; + sortDependencies( deps[ n ], resolved, unresolved ); + delete unresolved[ module ]; } } + resolved[ resolved.length ] = module; + } + + /** + * Get a list of module names that a module depends on in their proper dependency + * order. + * + * @private + * @param {string[]} module Array of string module names + * @return {Array} List of dependencies, including 'module'. + */ + function resolve( modules ) { + var resolved = []; + $.each( modules, function ( idx, module ) { + sortDependencies( module, resolved ); + } ); + return resolved; + } + + /** + * Load and execute a script with callback. + * + * @private + * @param {string} src URL to script, will be used as the src attribute in the script tag + * @return {jQuery.Promise} + */ + function addScript( src ) { + return $.ajax( { + url: src, + dataType: 'script', + // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use + // XHR for a same domain request instead of <script>, which changes the request + // headers (potentially missing a cache hit), and reduces caching in general + // since browsers cache XHR much less (if at all). And XHR means we retreive + // text, so we'd need to $.globalEval, which then messes up line numbers. + crossDomain: true, + cache: true + } ); + } + + /** + * Utility function for execute() + * + * @ignore + */ + function addLink( media, url ) { + var el = document.createElement( 'link' ); + // Support: IE + // Insert in document *before* setting href + getMarker().before( el ); + el.rel = 'stylesheet'; + if ( media && media !== 'all' ) { + el.media = media; + } + // If you end up here from an IE exception "SCRIPT: Invalid property value.", + // see #addEmbeddedCSS, bug 31676, and bug 47277 for details. + el.href = url; } /** @@ -1131,47 +1221,30 @@ * @param {string} module Module name to execute */ function execute( module ) { - var key, value, media, i, urls, cssHandle, checkCssHandles, + var key, value, media, i, urls, cssHandle, checkCssHandles, runScript, cssHandlesRegistered = false; if ( !hasOwn.call( registry, module ) ) { throw new Error( 'Module has not been registered yet: ' + module ); - } else if ( registry[module].state === 'registered' ) { - throw new Error( 'Module has not been requested from the server yet: ' + module ); - } else if ( registry[module].state === 'loading' ) { - throw new Error( 'Module has not completed loading yet: ' + module ); - } else if ( registry[module].state === 'ready' ) { - throw new Error( 'Module has already been executed: ' + module ); } - - /** - * Define loop-function here for efficiency - * and to avoid re-using badly scoped variables. - * @ignore - */ - function addLink( media, url ) { - var el = document.createElement( 'link' ); - // Support: IE - // Insert in document *before* setting href - getMarker().before( el ); - el.rel = 'stylesheet'; - if ( media && media !== 'all' ) { - el.media = media; - } - // If you end up here from an IE exception "SCRIPT: Invalid property value.", - // see #addEmbeddedCSS, bug 31676, and bug 47277 for details. - el.href = url; + if ( registry[ module ].state !== 'loaded' ) { + throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module ); } - function runScript() { - var script, markModuleReady, nestedAddScript; + registry[ module ].state = 'executing'; + + runScript = function () { + var script, markModuleReady, nestedAddScript, legacyWait, + // Expand to include dependencies since we have to exclude both legacy modules + // and their dependencies from the legacyWait (to prevent a circular dependency). + legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) ); try { - script = registry[module].script; + script = registry[ module ].script; markModuleReady = function () { - registry[module].state = 'ready'; + registry[ module ].state = 'ready'; handlePending( module ); }; - nestedAddScript = function ( arr, callback, async, i ) { + nestedAddScript = function ( arr, callback, i ) { // Recursively call addScript() in its own callback // for each element of arr. if ( i >= arr.length ) { @@ -1180,85 +1253,94 @@ return; } - addScript( arr[i], function () { - nestedAddScript( arr, callback, async, i + 1 ); - }, async ); + addScript( arr[ i ] ).always( function () { + nestedAddScript( arr, callback, i + 1 ); + } ); }; - if ( $.isArray( script ) ) { - nestedAddScript( script, markModuleReady, registry[module].async, 0 ); - } else if ( $.isFunction( script ) ) { - registry[module].state = 'ready'; - // Pass jQuery twice so that the signature of the closure which wraps - // the script can bind both '$' and 'jQuery'. - script( $, $ ); - handlePending( module ); - } + legacyWait = ( $.inArray( module, legacyModules ) !== -1 ) + ? $.Deferred().resolve() + : mw.loader.using( legacyModules ); + + legacyWait.always( function () { + if ( $.isArray( script ) ) { + nestedAddScript( script, markModuleReady, 0 ); + } else if ( $.isFunction( script ) ) { + // Pass jQuery twice so that the signature of the closure which wraps + // the script can bind both '$' and 'jQuery'. + script( $, $ ); + markModuleReady(); + } else if ( typeof script === 'string' ) { + // Site and user modules are a legacy scripts that run in the global scope. + // This is transported as a string instead of a function to avoid needing + // to use string manipulation to undo the function wrapper. + if ( module === 'user' ) { + // Implicit dependency on the site module. Not real dependency because + // it should run after 'site' regardless of whether it succeeds or fails. + mw.loader.using( 'site' ).always( function () { + $.globalEval( script ); + markModuleReady(); + } ); + } else { + $.globalEval( script ); + markModuleReady(); + } + } else { + // Module without script + markModuleReady(); + } + } ); } catch ( e ) { // This needs to NOT use mw.log because these errors are common in production mode // and not in debug mode, such as when a symbol that should be global isn't exported - registry[module].state = 'error'; + registry[ module ].state = 'error'; mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } ); handlePending( module ); } - } - - // This used to be inside runScript, but since that is now fired asychronously - // (after CSS is loaded) we need to set it here right away. It is crucial that - // when execute() is called this is set synchronously, otherwise modules will get - // executed multiple times as the registry will state that it isn't loading yet. - registry[module].state = 'loading'; + }; // Add localizations to message system - if ( $.isPlainObject( registry[module].messages ) ) { - mw.messages.set( registry[module].messages ); + if ( registry[ module ].messages ) { + mw.messages.set( registry[ module ].messages ); } // Initialise templates - if ( registry[module].templates ) { - mw.templates.set( module, registry[module].templates ); + if ( registry[ module ].templates ) { + mw.templates.set( module, registry[ module ].templates ); } - if ( $.isReady || registry[module].async ) { - // Make sure we don't run the scripts until all (potentially asynchronous) - // stylesheet insertions have completed. - ( function () { - var pending = 0; - checkCssHandles = function () { - // cssHandlesRegistered ensures we don't take off too soon, e.g. when - // one of the cssHandles is fired while we're still creating more handles. - if ( cssHandlesRegistered && pending === 0 && runScript ) { - runScript(); - runScript = undefined; // Revoke + // Make sure we don't run the scripts until all stylesheet insertions have completed. + ( function () { + var pending = 0; + checkCssHandles = function () { + // cssHandlesRegistered ensures we don't take off too soon, e.g. when + // one of the cssHandles is fired while we're still creating more handles. + if ( cssHandlesRegistered && pending === 0 && runScript ) { + runScript(); + runScript = undefined; // Revoke + } + }; + cssHandle = function () { + var check = checkCssHandles; + pending++; + return function () { + if ( check ) { + pending--; + check(); + check = undefined; // Revoke } }; - cssHandle = function () { - var check = checkCssHandles; - pending++; - return function () { - if ( check ) { - pending--; - check(); - check = undefined; // Revoke - } - }; - }; - }() ); - } else { - // We are in blocking mode, and so we can't afford to wait for CSS - cssHandle = function () {}; - // Run immediately - checkCssHandles = runScript; - } + }; + }() ); // Process styles (see also mw.loader.implement) // * back-compat: { <media>: css } // * back-compat: { <media>: [url, ..] } // * { "css": [css, ..] } // * { "url": { <media>: [url, ..] } } - if ( $.isPlainObject( registry[module].style ) ) { - for ( key in registry[module].style ) { - value = registry[module].style[key]; + if ( registry[ module ].style ) { + for ( key in registry[ module ].style ) { + value = registry[ module ].style[ key ]; media = undefined; if ( key !== 'url' && key !== 'css' ) { @@ -1283,10 +1365,10 @@ for ( i = 0; i < value.length; i += 1 ) { if ( key === 'bc-url' ) { // back-compat: { <media>: [url, ..] } - addLink( media, value[i] ); + addLink( media, value[ i ] ); } else if ( key === 'css' ) { // { "css": [css, ..] } - addEmbeddedCSS( value[i], cssHandle() ); + addEmbeddedCSS( value[ i ], cssHandle() ); } } // Not an array, but a regular object @@ -1294,9 +1376,9 @@ } else if ( typeof value === 'object' ) { // { "url": { <media>: [url, ..] } } for ( media in value ) { - urls = value[media]; + urls = value[ media ]; for ( i = 0; i < urls.length; i += 1 ) { - addLink( media, urls[i] ); + addLink( media, urls[ i ] ); } } } @@ -1316,34 +1398,39 @@ * @param {string|string[]} dependencies Module name or array of string module names * @param {Function} [ready] Callback to execute when all dependencies are ready * @param {Function} [error] Callback to execute when any dependency fails - * @param {boolean} [async=false] Whether to load modules asynchronously. - * Ignored (and defaulted to `true`) if the document-ready event has already occurred. */ - function request( dependencies, ready, error, async ) { + function request( dependencies, ready, error ) { // Allow calling by single module name if ( typeof dependencies === 'string' ) { - dependencies = [dependencies]; + dependencies = [ dependencies ]; } // Add ready and error callbacks if they were given if ( ready !== undefined || error !== undefined ) { - jobs[jobs.length] = { + jobs.push( { + // Narrow down the list to modules that are worth waiting for dependencies: $.grep( dependencies, function ( module ) { var state = mw.loader.getState( module ); - return state === 'registered' || state === 'loaded' || state === 'loading'; + return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing'; } ), ready: ready, error: error - }; + } ); } $.each( dependencies, function ( idx, module ) { var state = mw.loader.getState( module ); + // Only queue modules that are still in the initial 'registered' state + // (not ones already loading, ready or error). if ( state === 'registered' && $.inArray( module, queue ) === -1 ) { - queue.push( module ); - if ( async ) { - registry[module].async = true; + // Private modules must be embedded in the page. Don't bother queuing + // these as the server will deny them anyway (T101806). + if ( registry[ module ].group === 'private' ) { + registry[ module ].state = 'error'; + handlePending( module ); + return; } + queue.push( module ); } } ); @@ -1362,7 +1449,7 @@ } a.sort(); for ( key = 0; key < a.length; key += 1 ) { - sorted[a[key]] = o[a[key]]; + sorted[ a[ key ] ] = o[ a[ key ] ]; } return sorted; } @@ -1370,6 +1457,7 @@ /** * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] } * to a query string of the form foo.bar,baz|bar.baz,quux + * * @private */ function buildModulesString( moduleMap ) { @@ -1378,31 +1466,26 @@ for ( prefix in moduleMap ) { p = prefix === '' ? '' : prefix + '.'; - arr.push( p + moduleMap[prefix].join( ',' ) ); + arr.push( p + moduleMap[ prefix ].join( ',' ) ); } return arr.join( '|' ); } /** - * Asynchronously append a script tag to the end of the body - * that invokes load.php + * Load modules from load.php + * * @private * @param {Object} moduleMap Module map, see #buildModulesString * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request * @param {string} sourceLoadScript URL of load.php - * @param {boolean} async Whether to load modules asynchronously. - * Ignored (and defaulted to `true`) if the document-ready event has already occurred. */ - function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { + function doRequest( moduleMap, currReqBase, sourceLoadScript ) { var request = $.extend( { modules: buildModulesString( moduleMap ) }, currReqBase ); request = sortQuery( request ); - // Support: IE6 - // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script - // isn't actually used in IE6, but MediaWiki enforces it in general. - addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async ); + addScript( sourceLoadScript + '?' + $.param( request ) ); } /** @@ -1418,9 +1501,9 @@ */ function resolveIndexedDependencies( modules ) { $.each( modules, function ( idx, module ) { - if ( module[2] ) { - module[2] = $.map( module[2], function ( dep ) { - return typeof dep === 'number' ? modules[dep][0] : dep; + if ( module[ 2 ] ) { + module[ 2 ] = $.map( module[ 2 ], function ( dep ) { + return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep; } ); } } ); @@ -1449,9 +1532,9 @@ */ work: function () { var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup, - source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript, + source, concatSource, origBatch, group, i, modules, sourceLoadScript, currReqBase, currReqBaseLength, moduleMap, l, - lastDotIndex, prefix, suffix, bytesAdded, async; + lastDotIndex, prefix, suffix, bytesAdded; // Build a list of request parameters common to all requests. reqBase = { @@ -1466,12 +1549,12 @@ // Appends a list of modules from the queue to the batch for ( q = 0; q < queue.length; q += 1 ) { // Only request modules which are registered - if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) { + if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) { // Prevent duplicate entries - if ( $.inArray( queue[q], batch ) === -1 ) { - batch[batch.length] = queue[q]; + if ( $.inArray( queue[ q ], batch ) === -1 ) { + batch[ batch.length ] = queue[ q ]; // Mark registered modules as loading - registry[queue[q]].state = 'loading'; + registry[ queue[ q ] ].state = 'loading'; } } } @@ -1507,7 +1590,7 @@ // the error) instead of all of them. mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } ); origBatch = $.grep( origBatch, function ( module ) { - return registry[module].state === 'loading'; + return registry[ module ].state === 'loading'; } ); batch = batch.concat( origBatch ); } @@ -1527,16 +1610,16 @@ // Split batch by source and by group. for ( b = 0; b < batch.length; b += 1 ) { - bSource = registry[batch[b]].source; - bGroup = registry[batch[b]].group; + bSource = registry[ batch[ b ] ].source; + bGroup = registry[ batch[ b ] ].group; if ( !hasOwn.call( splits, bSource ) ) { - splits[bSource] = {}; + splits[ bSource ] = {}; } - if ( !hasOwn.call( splits[bSource], bGroup ) ) { - splits[bSource][bGroup] = []; + if ( !hasOwn.call( splits[ bSource ], bGroup ) ) { + splits[ bSource ][ bGroup ] = []; } - bSourceGroup = splits[bSource][bGroup]; - bSourceGroup[bSourceGroup.length] = batch[b]; + bSourceGroup = splits[ bSource ][ bGroup ]; + bSourceGroup[ bSourceGroup.length ] = batch[ b ]; } // Clear the batch - this MUST happen before we append any @@ -1548,29 +1631,22 @@ for ( source in splits ) { - sourceLoadScript = sources[source]; + sourceLoadScript = sources[ source ]; - for ( group in splits[source] ) { + for ( group in splits[ source ] ) { // Cache access to currently selected list of // modules for this group from this source. - modules = splits[source][group]; + modules = splits[ source ][ group ]; - // Calculate the highest timestamp - maxVersion = 0; - for ( g = 0; g < modules.length; g += 1 ) { - if ( registry[modules[g]].version > maxVersion ) { - maxVersion = registry[modules[g]].version; - } - } - - currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase ); + currReqBase = $.extend( { + version: getCombinedVersion( modules ) + }, reqBase ); // For user modules append a user name to the request. if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) { currReqBase.user = mw.config.get( 'wgUserName' ); } currReqBaseLength = $.param( currReqBase ).length; - async = true; // We may need to split up the request to honor the query string length limit, // so build it piece by piece. l = currReqBaseLength + 9; // '&modules='.length == 9 @@ -1579,42 +1655,35 @@ for ( i = 0; i < modules.length; i += 1 ) { // Determine how many bytes this module would add to the query string - lastDotIndex = modules[i].lastIndexOf( '.' ); + lastDotIndex = modules[ i ].lastIndexOf( '.' ); // If lastDotIndex is -1, substr() returns an empty string - prefix = modules[i].substr( 0, lastDotIndex ); - suffix = modules[i].slice( lastDotIndex + 1 ); + prefix = modules[ i ].substr( 0, lastDotIndex ); + suffix = modules[ i ].slice( lastDotIndex + 1 ); bytesAdded = hasOwn.call( moduleMap, prefix ) ? suffix.length + 3 // '%2C'.length == 3 - : modules[i].length + 3; // '%7C'.length == 3 + : modules[ i ].length + 3; // '%7C'.length == 3 // If the request would become too long, create a new one, // but don't create empty requests if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) { // This request would become too long, create a new one // and fire off the old one - doRequest( moduleMap, currReqBase, sourceLoadScript, async ); + doRequest( moduleMap, currReqBase, sourceLoadScript ); moduleMap = {}; - async = true; l = currReqBaseLength + 9; mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } ); } if ( !hasOwn.call( moduleMap, prefix ) ) { - moduleMap[prefix] = []; - } - moduleMap[prefix].push( suffix ); - if ( !registry[modules[i]].async ) { - // If this module is blocking, make the entire request blocking - // This is slightly suboptimal, but in practice mixing of blocking - // and async modules will only occur in debug mode. - async = false; + moduleMap[ prefix ] = []; } + moduleMap[ prefix ].push( suffix ); l += bytesAdded; } // If there's anything left in moduleMap, request that too if ( !$.isEmptyObject( moduleMap ) ) { - doRequest( moduleMap, currReqBase, sourceLoadScript, async ); + doRequest( moduleMap, currReqBase, sourceLoadScript ); } } } @@ -1637,7 +1706,7 @@ // Allow multiple additions if ( typeof id === 'object' ) { for ( source in id ) { - mw.loader.addSource( source, id[source] ); + mw.loader.addSource( source, id[ source ] ); } return true; } @@ -1650,14 +1719,15 @@ loadUrl = loadUrl.loadScript; } - sources[id] = loadUrl; + sources[ id ] = loadUrl; return true; }, /** - * Register a module, letting the system know about it and its - * properties. Startup modules contain calls to this function. + * Register a module, letting the system know about it and its properties. + * + * The startup modules contain calls to this method. * * When using multiple module registration by passing an array, dependencies that * are specified as references to modules within the array will be resolved before @@ -1665,7 +1735,8 @@ * * @param {string|Array} module Module name or array of arrays, each containing * a list of arguments compatible with this method - * @param {number} version Module version number as a timestamp (falls backs to 0) + * @param {string|number} version Module version hash (falls backs to empty string) + * Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier. * @param {string|Array|Function} dependencies One string or array of strings of module * names on which this module depends, or a function that returns that array. * @param {string} [group=null] Group which the module is in @@ -1679,11 +1750,11 @@ resolveIndexedDependencies( module ); for ( i = 0, len = module.length; i < len; i++ ) { // module is an array of module names - if ( typeof module[i] === 'string' ) { - mw.loader.register( module[i] ); + if ( typeof module[ i ] === 'string' ) { + mw.loader.register( module[ i ] ); // module is an array of arrays - } else if ( typeof module[i] === 'object' ) { - mw.loader.register.apply( mw.loader, module[i] ); + } else if ( typeof module[ i ] === 'object' ) { + mw.loader.register.apply( mw.loader, module[ i ] ); } } return; @@ -1696,8 +1767,8 @@ throw new Error( 'module already registered: ' + module ); } // List the module as registered - registry[module] = { - version: version !== undefined ? parseInt( version, 10 ) : 0, + registry[ module ] = { + version: version !== undefined ? String( version ) : '', dependencies: [], group: typeof group === 'string' ? group : null, source: typeof source === 'string' ? source : 'local', @@ -1706,11 +1777,11 @@ }; if ( typeof dependencies === 'string' ) { // Allow dependencies to be given as a single module name - registry[module].dependencies = [ dependencies ]; + registry[ module ].dependencies = [ dependencies ]; } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) { // Allow dependencies to be given as an array of module names // or a function which returns an array - registry[module].dependencies = dependencies; + registry[ module ].dependencies = dependencies; } }, @@ -1738,22 +1809,22 @@ * The reason css strings are not concatenated anymore is bug 31676. We now check * whether it's safe to extend the stylesheet. * - * @param {Object} [msgs] List of key/value pairs to be added to mw#messages. + * @param {Object} [messages] List of key/value pairs to be added to mw#messages. * @param {Object} [templates] List of key/value pairs to be added to mw#templates. */ - implement: function ( module, script, style, msgs, templates ) { + implement: function ( module, script, style, messages, templates ) { // Validate input if ( typeof module !== 'string' ) { throw new Error( 'module must be of type string, not ' + typeof module ); } - if ( script && !$.isFunction( script ) && !$.isArray( script ) ) { - throw new Error( 'script must be of type function or array, not ' + typeof script ); + if ( script && !$.isFunction( script ) && !$.isArray( script ) && typeof script !== 'string' ) { + throw new Error( 'script must be of type function, array, or script; not ' + typeof script ); } if ( style && !$.isPlainObject( style ) ) { throw new Error( 'style must be of type object, not ' + typeof style ); } - if ( msgs && !$.isPlainObject( msgs ) ) { - throw new Error( 'msgs must be of type object, not a ' + typeof msgs ); + if ( messages && !$.isPlainObject( messages ) ) { + throw new Error( 'messages must be of type object, not a ' + typeof messages ); } if ( templates && !$.isPlainObject( templates ) ) { throw new Error( 'templates must be of type object, not a ' + typeof templates ); @@ -1763,18 +1834,18 @@ mw.loader.register( module ); } // Check for duplicate implementation - if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) { + if ( hasOwn.call( registry, module ) && registry[ module ].script !== undefined ) { throw new Error( 'module already implemented: ' + module ); } // Attach components - registry[module].script = script || []; - registry[module].style = style || {}; - registry[module].messages = msgs || {}; - registry[module].templates = templates || {}; + registry[ module ].script = script || null; + registry[ module ].style = style || null; + registry[ module ].messages = messages || null; + registry[ module ].templates = templates || null; // The module may already have been marked as erroneous - if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) { - registry[module].state = 'loaded'; - if ( allReady( registry[module].dependencies ) ) { + if ( $.inArray( registry[ module ].state, [ 'error', 'missing' ] ) === -1 ) { + registry[ module ].state = 'loaded'; + if ( allReady( registry[ module ].dependencies ) ) { execute( module ); } } @@ -1841,24 +1912,18 @@ * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an * external script or style; acceptable values are "text/css" and * "text/javascript"; if no type is provided, text/javascript is assumed. - * @param {boolean} [async] Whether to load modules asynchronously. - * Ignored (and defaulted to `true`) if the document-ready event has already occurred. - * Defaults to `true` if loading a URL, `false` otherwise. */ - load: function ( modules, type, async ) { + load: function ( modules, type ) { var filtered, l; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { throw new Error( 'modules must be a string or an array, not a ' + typeof modules ); } - // Allow calling with an external url or single dependency as a string + // Allow calling with a url or single dependency as a string if ( typeof modules === 'string' ) { - if ( /^(https?:)?\/\//.test( modules ) ) { - if ( async === undefined ) { - // Assume async for bug 34542 - async = true; - } + // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js" + if ( /^(https?:)?\/?\//.test( modules ) ) { if ( type === 'text/css' ) { // Support: IE 7-8 // Use properties instead of attributes as IE throws security @@ -1871,7 +1936,7 @@ return; } if ( type === 'text/javascript' || type === undefined ) { - addScript( modules, null, async ); + addScript( modules ); return; } // Unknown type @@ -1901,7 +1966,7 @@ return; } // Since some modules are not yet ready, queue up a request. - request( filtered, undefined, undefined, async ); + request( filtered, undefined, undefined ); }, /** @@ -1915,21 +1980,21 @@ if ( typeof module === 'object' ) { for ( m in module ) { - mw.loader.state( m, module[m] ); + mw.loader.state( m, module[ m ] ); } return; } if ( !hasOwn.call( registry, module ) ) { mw.loader.register( module ); } - if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1 - && registry[module].state !== state ) { + if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1 + && registry[ module ].state !== state ) { // Make sure pending modules depending on this one get executed if their // dependencies are now fulfilled! - registry[module].state = state; + registry[ module ].state = state; handlePending( module ); } else { - registry[module].state = state; + registry[ module ].state = state; } }, @@ -1941,10 +2006,10 @@ * in the registry. */ getVersion: function ( module ) { - if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) { + if ( !hasOwn.call( registry, module ) || registry[ module ].version === undefined ) { return null; } - return formatVersionNumber( registry[module].version ); + return registry[ module ].version; }, /** @@ -1955,10 +2020,10 @@ * in the registry. */ getState: function ( module ) { - if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) { + if ( !hasOwn.call( registry, module ) || registry[ module ].state === undefined ) { return null; } - return registry[module].state; + return registry[ module ].state; }, /** @@ -1997,6 +2062,10 @@ // Whether the store is in use on this page. enabled: null, + // Modules whose string representation exceeds 100 kB are ineligible + // for storage due to bug T66721. + MODULE_SIZE_MAX: 100000, + // The contents of the store, mapping '[module name]@[version]' keys // to module implementations. items: {}, @@ -2006,6 +2075,7 @@ /** * Construct a JSON-serializable object representing the content of the store. + * * @return {Object} Module store contents. */ toJSON: function () { @@ -2024,6 +2094,7 @@ /** * Get a key on which to vary the module cache. + * * @return {string} String of concatenated vary conditions. */ getVary: function () { @@ -2042,7 +2113,7 @@ */ getModuleKey: function ( module ) { return hasOwn.call( registry, module ) ? - ( module + '@' + registry[module].version ) : null; + ( module + '@' + registry[ module ].version ) : null; }, /** @@ -2114,7 +2185,7 @@ key = mw.loader.store.getModuleKey( module ); if ( key in mw.loader.store.items ) { mw.loader.store.stats.hits++; - return mw.loader.store.items[key]; + return mw.loader.store.items[ key ]; } mw.loader.store.stats.misses++; return false; @@ -2127,7 +2198,7 @@ * @param {Object} descriptor The module's descriptor as set in the registry */ set: function ( module, descriptor ) { - var args, key; + var args, key, src; if ( !mw.loader.store.enabled ) { return false; @@ -2141,7 +2212,7 @@ // Module failed to load descriptor.state !== 'ready' || // Unversioned, private, or site-/user-specific - ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) || + ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) || // Partial descriptor $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages, descriptor.templates ] ) !== -1 @@ -2162,8 +2233,8 @@ ]; // Attempted workaround for a possible Opera bug (bug T59567). // This regex should never match under sane conditions. - if ( /^\s*\(/.test( args[1] ) ) { - args[1] = 'function' + args[1]; + if ( /^\s*\(/.test( args[ 1 ] ) ) { + args[ 1 ] = 'function' + args[ 1 ]; mw.track( 'resourceloader.assert', { source: 'bug-T59567' } ); } } catch ( e ) { @@ -2171,7 +2242,11 @@ return; } - mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');'; + src = 'mw.loader.implement(' + args.join( ',' ) + ');'; + if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) { + return false; + } + mw.loader.store.items[ key ] = src; mw.loader.store.update(); }, @@ -2190,7 +2265,10 @@ module = key.slice( 0, key.indexOf( '@' ) ); if ( mw.loader.store.getModuleKey( module ) !== key ) { mw.loader.store.stats.expired++; - delete mw.loader.store.items[key]; + delete mw.loader.store.items[ key ]; + } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) { + // This value predates the enforcement of a size limit on cached modules. + delete mw.loader.store.items[ key ]; } } }, @@ -2319,7 +2397,7 @@ var v, attrName, s = '<' + name; for ( attrName in attrs ) { - v = attrs[attrName]; + v = attrs[ attrName ]; // Convert name=true, to name=name if ( v === true ) { v = attrName; @@ -2366,6 +2444,7 @@ /** * Wrapper object for raw HTML passed to mw.html.element(). + * * @class mw.html.Raw */ Raw: function ( value ) { @@ -2374,6 +2453,7 @@ /** * Wrapper object for CDATA element contents passed to mw.html.element() + * * @class mw.html.Cdata */ Cdata: function ( value ) { @@ -2388,6 +2468,9 @@ tokens: new Map() }, + // OOUI widgets specific to MediaWiki + widgets: {}, + /** * Registry and firing of events. * @@ -2440,12 +2523,13 @@ */ return function ( name ) { var list = hasOwn.call( lists, name ) ? - lists[name] : - lists[name] = $.Callbacks( 'memory' ); + lists[ name ] : + lists[ name ] = $.Callbacks( 'memory' ); return { /** * Register a hook handler + * * @param {Function...} handler Function to bind. * @chainable */ @@ -2453,6 +2537,7 @@ /** * Unregister a hook handler + * * @param {Function...} handler Function to unbind. * @chainable */ @@ -2460,6 +2545,7 @@ /** * Run a hook. + * * @param {Mixed...} data * @chainable */ @@ -2514,10 +2600,33 @@ } } - // subscribe to error streams + // Subscribe to error streams mw.trackSubscribe( 'resourceloader.exception', log ); mw.trackSubscribe( 'resourceloader.assert', log ); + /** + * Fired when all modules associated with the page have finished loading. + * + * @event resourceloader_loadEnd + * @member mw.hook + */ + $( function () { + var loading = $.grep( mw.loader.getModuleNames(), function ( module ) { + return mw.loader.getState( module ) === 'loading'; + } ); + // In order to use jQuery.when (which stops early if one of the promises got rejected) + // cast any loading failures into successes. We only need a callback, not the module. + loading = $.map( loading, function ( module ) { + return mw.loader.using( module ).then( null, function () { + return $.Deferred().resolve(); + } ); + } ); + $.when.apply( $, loading ).then( function () { + mwPerformance.mark( 'mwLoadEnd' ); + mw.hook( 'resourceloader.loadEnd' ).fire(); + } ); + } ); + // Attach to window and globally alias window.mw = window.mediaWiki = mw; }( jQuery ) ); |