/**
* Dummy placeholder for {@link mw.log}
+ *
* @method
*/
log: ( function () {
* 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` (1):
+ * - `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.
* 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.
- * - `loading` (2):
+ * - `executing`:
* The module is being executed.
- * TODO: Create a new state instead of re-using the "loading" state.
* - `ready`:
* The module has been successfully executed.
* - `error`:
// 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!
*
* @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
+ * @return {jQuery.Promise}
*/
- function addScript( src, callback ) {
- $.ajax( {
+ function addScript( src ) {
+ return $.ajax( {
url: src,
dataType: 'script',
// Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
// text, so we'd need to $.globalEval, which then messes up line numbers.
crossDomain: true,
cache: true
- } ).always( callback );
+ } );
+ }
+
+ /**
+ * 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;
}
/**
if ( !hasOwn.call( registry, module ) ) {
throw new Error( 'Module has not been registered yet: ' + module );
}
- if ( registry[ module ].state === 'registered' ) {
- throw new Error( 'Module has not been requested from the server yet: ' + module );
- }
- if ( registry[ module ].state === 'loading' ) {
- throw new Error( 'Module has not completed loading yet: ' + module );
- }
- if ( registry[ module ].state === 'ready' ) {
- throw new Error( 'Module has already been executed: ' + module );
+ if ( registry[ module ].state !== 'loaded' ) {
+ throw new Error( 'Module in state "' + registry[ module ].state + '" may not be 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;
- }
+ registry[ module ].state = 'executing';
runScript = function () {
var script, markModuleReady, nestedAddScript, legacyWait,
return;
}
- addScript( arr[ i ], function () {
+ addScript( arr[ i ] ).always( function () {
nestedAddScript( arr, callback, i + 1 );
} );
};
$.globalEval( script );
markModuleReady();
}
+ } else {
+ // Module without script
+ markModuleReady();
}
} );
} catch ( e ) {
}
};
- // 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 ( registry[ module ].messages ) {
mw.messages.set( registry[ module ].messages );
// 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 ) {
/**
* 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 ) {
/**
* 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
throw new Error( 'module already implemented: ' + module );
}
// Attach components
- registry[ module ].script = script || [];
- registry[ module ].style = style || {};
- registry[ module ].messages = messages || {};
- 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 ( 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 ) ) {
+ // "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
/**
* Construct a JSON-serializable object representing the content of the store.
+ *
* @return {Object} Module store contents.
*/
toJSON: function () {
/**
* Get a key on which to vary the module cache.
+ *
* @return {string} String of concatenated vary conditions.
*/
getVary: function () {
/**
* Wrapper object for raw HTML passed to mw.html.element().
+ *
* @class mw.html.Raw
*/
Raw: function ( value ) {
/**
* Wrapper object for CDATA element contents passed to mw.html.element()
+ *
* @class mw.html.Cdata
*/
Cdata: function ( value ) {
return {
/**
* Register a hook handler
+ *
* @param {Function...} handler Function to bind.
* @chainable
*/
/**
* Unregister a hook handler
+ *
* @param {Function...} handler Function to unbind.
* @chainable
*/
/**
* Run a hook.
+ *
* @param {Mixed...} data
* @chainable
*/
}
}
- // 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 ) );