summaryrefslogtreecommitdiff
path: root/resources/src/mediawiki/mediawiki.js
diff options
context:
space:
mode:
Diffstat (limited to 'resources/src/mediawiki/mediawiki.js')
-rw-r--r--resources/src/mediawiki/mediawiki.js923
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 ) );