summaryrefslogtreecommitdiff
path: root/resources/mediawiki/mediawiki.js
diff options
context:
space:
mode:
Diffstat (limited to 'resources/mediawiki/mediawiki.js')
-rw-r--r--resources/mediawiki/mediawiki.js751
1 files changed, 471 insertions, 280 deletions
diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js
index 121d5399..1a72ed13 100644
--- a/resources/mediawiki/mediawiki.js
+++ b/resources/mediawiki/mediawiki.js
@@ -3,11 +3,13 @@
*/
var mw = ( function ( $, undefined ) {
-"use strict";
+ "use strict";
/* Private Members */
- var hasOwn = Object.prototype.hasOwnProperty;
+ var hasOwn = Object.prototype.hasOwnProperty,
+ slice = Array.prototype.slice;
+
/* Object constructors */
/**
@@ -43,13 +45,15 @@ var mw = ( function ( $, undefined ) {
var results, i;
if ( $.isArray( selection ) ) {
- selection = $.makeArray( selection );
+ selection = slice.call( selection );
results = {};
for ( i = 0; i < selection.length; i += 1 ) {
results[selection[i]] = this.get( selection[i], fallback );
}
return results;
- } else if ( typeof selection === 'string' ) {
+ }
+
+ if ( typeof selection === 'string' ) {
if ( this.values[selection] === undefined ) {
if ( fallback !== undefined ) {
return fallback;
@@ -58,11 +62,13 @@ var mw = ( function ( $, undefined ) {
}
return this.values[selection];
}
+
if ( selection === undefined ) {
return this.values;
- } else {
- return null; // invalid selection key
}
+
+ // invalid selection key
+ return null;
},
/**
@@ -80,7 +86,8 @@ var mw = ( function ( $, undefined ) {
this.values[s] = selection[s];
}
return true;
- } else if ( typeof selection === 'string' && value !== undefined ) {
+ }
+ if ( typeof selection === 'string' && value !== undefined ) {
this.values[selection] = value;
return true;
}
@@ -103,9 +110,8 @@ var mw = ( function ( $, undefined ) {
}
}
return true;
- } else {
- return this.values[selection] !== undefined;
}
+ return this.values[selection] !== undefined;
}
};
@@ -124,7 +130,7 @@ var mw = ( function ( $, undefined ) {
this.format = 'plain';
this.map = map;
this.key = key;
- this.parameters = parameters === undefined ? [] : $.makeArray( parameters );
+ this.parameters = parameters === undefined ? [] : slice.call( parameters );
return this;
}
@@ -132,17 +138,17 @@ var mw = ( function ( $, undefined ) {
/**
* Simple message parser, does $N replacement and nothing else.
* This may be overridden to provide a more complex message parser.
- *
+ *
* This function will not be called for nonexistent messages.
*/
- parser: function() {
+ parser: function () {
var parameters = this.parameters;
return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
var index = parseInt( match, 10 ) - 1;
return parameters[index] !== undefined ? parameters[index] : '$' + match;
} );
},
-
+
/**
* Appends (does not replace) parameters for replacement to the .parameters property.
*
@@ -162,7 +168,7 @@ var mw = ( function ( $, undefined ) {
*
* @return string Message as a string in the current form or <key> if key does not exist.
*/
- toString: function() {
+ toString: function () {
var text;
if ( !this.exists() ) {
@@ -186,7 +192,7 @@ var mw = ( function ( $, undefined ) {
text = this.parser();
text = mw.html.escape( text );
}
-
+
if ( this.format === 'parse' ) {
text = this.parser();
}
@@ -199,7 +205,7 @@ var mw = ( function ( $, undefined ) {
*
* @return {string} String form of parsed message
*/
- parse: function() {
+ parse: function () {
this.format = 'parse';
return this.toString();
},
@@ -209,7 +215,7 @@ var mw = ( function ( $, undefined ) {
*
* @return {string} String form of plain message
*/
- plain: function() {
+ plain: function () {
this.format = 'plain';
return this.toString();
},
@@ -219,7 +225,7 @@ var mw = ( function ( $, undefined ) {
*
* @return {string} String form of html escaped message
*/
- escaped: function() {
+ escaped: function () {
this.format = 'escaped';
return this.toString();
},
@@ -229,7 +235,7 @@ var mw = ( function ( $, undefined ) {
*
* @return {string} String form of parsed message
*/
- exists: function() {
+ exists: function () {
return this.map.exists( this.key );
}
};
@@ -241,8 +247,8 @@ var mw = ( function ( $, undefined ) {
* Dummy function which in debug mode can be replaced with a function that
* emulates console.log in console-less environments.
*/
- log: function() { },
-
+ log: function () { },
+
/**
* @var constructor Make the Map constructor publicly available.
*/
@@ -252,7 +258,7 @@ var mw = ( function ( $, undefined ) {
* @var constructor Make the Message constructor publicly available.
*/
Message: Message,
-
+
/**
* List of configuration values
*
@@ -261,25 +267,25 @@ var mw = ( function ( $, undefined ) {
* in the global window object.
*/
config: null,
-
+
/**
* @var object
*
* Empty object that plugins can be installed in.
*/
libs: {},
-
+
/* Extension points */
-
+
legacy: {},
-
+
/**
* Localization system
*/
messages: new Map(),
-
+
/* Public Methods */
-
+
/**
* Gets a message object, similar to wfMessage()
*
@@ -292,33 +298,33 @@ var mw = ( function ( $, undefined ) {
var parameters;
// Support variadic arguments
if ( parameter_1 !== undefined ) {
- parameters = $.makeArray( arguments );
+ parameters = slice.call( arguments );
parameters.shift();
} else {
parameters = [];
}
return new Message( mw.messages, key, parameters );
},
-
+
/**
- * Gets a message string, similar to wfMsg()
+ * Gets a message string, similar to wfMessage()
*
* @param key string Key of message to get
* @param parameters mixed First argument in a list of variadic arguments,
* each a parameter for $N replacement in messages.
* @return String.
*/
- msg: function ( key, parameters ) {
+ msg: function ( /* key, parameter_1, parameter_2, .. */ ) {
return mw.message.apply( mw.message, arguments ).toString();
},
-
+
/**
* Client-side module loader which integrates with the MediaWiki ResourceLoader
*/
- loader: ( function() {
-
+ loader: ( function () {
+
/* Private Members */
-
+
/**
* Mapping of registered modules
*
@@ -335,7 +341,7 @@ var mw = ( function ( $, undefined ) {
* {
* 'moduleName': {
* 'version': ############## (unix timestamp),
- * 'dependencies': ['required.foo', 'bar.also', ...], (or) function() {}
+ * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
* 'group': 'somegroup', (or) null,
* 'source': 'local', 'someforeignwiki', (or) null
* 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
@@ -345,7 +351,7 @@ var mw = ( function ( $, undefined ) {
* }
* }
*/
- var registry = {},
+ var registry = {},
/**
* Mapping of sources, keyed by source-id, values are objects.
* Format:
@@ -362,68 +368,111 @@ var mw = ( function ( $, undefined ) {
queue = [],
// List of callback functions waiting for modules to be ready to be called
jobs = [],
- // Flag indicating that document ready has occured
- ready = false,
// Selector cache for the marker element. Use getMarker() to get/use the marker!
$marker = null;
-
- /* Cache document ready status */
-
- $(document).ready( function () {
- ready = true;
- } );
-
+
/* Private methods */
-
+
function getMarker() {
// Cached ?
if ( $marker ) {
return $marker;
- } else {
- $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
- if ( $marker.length ) {
- return $marker;
- }
- mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
- $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
+ }
+
+ $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
+ if ( $marker.length ) {
return $marker;
}
+ mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
+ $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
+
+ return $marker;
+ }
+
+ /**
+ * Create a new style tag and add it to the DOM.
+ *
+ * @param text String: CSS text
+ * @param nextnode mixed: [optional] An Element or jQuery object for an element where
+ * the style tag should be inserted before. Otherwise appended to the <head>.
+ * @return HTMLStyleElement
+ */
+ function addStyleTag( text, nextnode ) {
+ var s = document.createElement( 'style' );
+ // Insert into document before setting cssText (bug 33305)
+ if ( nextnode ) {
+ // Must be inserted with native insertBefore, not $.fn.before.
+ // When using jQuery to insert it, like $nextnode.before( s ),
+ // then IE6 will throw "Access is denied" when trying to append
+ // to .cssText later. Some kind of weird security measure.
+ // http://stackoverflow.com/q/12586482/319266
+ // Works: jsfiddle.net/zJzMy/1
+ // Fails: jsfiddle.net/uJTQz
+ // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
+ if ( nextnode.jquery ) {
+ nextnode = nextnode.get( 0 );
+ }
+ nextnode.parentNode.insertBefore( s, nextnode );
+ } else {
+ document.getElementsByTagName( 'head' )[0].appendChild( s );
+ }
+ if ( s.styleSheet ) {
+ // IE
+ s.styleSheet.cssText = text;
+ } else {
+ // Other browsers.
+ // (Safari sometimes borks on non-string values,
+ // play safe by casting to a string, just in case.)
+ s.appendChild( document.createTextNode( String( text ) ) );
+ }
+ return s;
+ }
+
+ /**
+ * Checks if certain cssText is safe to append to
+ * a stylesheet.
+ *
+ * Right now it only makes sure that cssText containing @import
+ * rules will end up in a new stylesheet (as those only work when
+ * placed at the start of a stylesheet; bug 35562).
+ * This could later be extended to take care of other bugs, such as
+ * the IE cssRules limit - not the same as the IE styleSheets limit).
+ */
+ function canExpandStylesheetWith( $style, cssText ) {
+ return cssText.indexOf( '@import' ) === -1;
}
-
- function addInlineCSS( css, media ) {
- var $style = getMarker().prev(),
- $newStyle,
- attrs = { 'type': 'text/css', 'media': media };
- if ( $style.is( 'style' ) && $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
- // There's already a dynamic <style> tag present, append to it
- // This recycling of <style> tags is for bug 31676 (can't have
- // more than 32 <style> tags in IE)
-
- // Also, calling .append() on a <style> tag explodes with a JS error in IE,
- // so if the .append() fails we fall back to building a new <style> tag and
- // replacing the existing one
- try {
- // Do cdata sanitization on the provided CSS, and prepend a double newline
- css = $( mw.html.element( 'style', {}, new mw.html.Cdata( "\n\n" + css ) ) ).html();
- $style.append( css );
- } catch ( e ) {
- // Generate a new tag with the combined CSS
- css = $style.html() + "\n\n" + css;
- $newStyle = $( mw.html.element( 'style', attrs, new mw.html.Cdata( css ) ) )
- .data( 'ResourceLoaderDynamicStyleTag', true );
- // Prevent a flash of unstyled content by inserting the new tag
- // before removing the old one
- $style.after( $newStyle );
- $style.remove();
+
+ function addEmbeddedCSS( cssText ) {
+ var $style, styleEl;
+ $style = getMarker().prev();
+ // Re-use <style> tags if possible, this to try to stay
+ // under the IE stylesheet limit (bug 31676).
+ // Also verify that the the element before Marker actually is one
+ // that came from ResourceLoader, and not a style tag that some
+ // other script inserted before our marker, or, more importantly,
+ // it may not be a style tag at all (could be <meta> or <script>).
+ if (
+ $style.data( 'ResourceLoaderDynamicStyleTag' ) === true &&
+ canExpandStylesheetWith( $style, cssText )
+ ) {
+ // There's already a dynamic <style> tag present and
+ // canExpandStylesheetWith() gave a green light to append more to it.
+ styleEl = $style.get( 0 );
+ if ( styleEl.styleSheet ) {
+ try {
+ styleEl.styleSheet.cssText += cssText; // IE
+ } catch ( e ) {
+ log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
+ }
+ } else {
+ styleEl.appendChild( document.createTextNode( String( cssText ) ) );
}
} else {
- // Create a new <style> tag and insert it
- $style = $( mw.html.element( 'style', attrs, new mw.html.Cdata( css ) ) );
- $style.data( 'ResourceLoaderDynamicStyleTag', true );
- getMarker().before( $style );
+ $( addStyleTag( cssText, getMarker() ) )
+ .data( 'ResourceLoaderDynamicStyleTag', true );
}
}
-
+
function compare( a, b ) {
var i;
if ( a.length !== b.length ) {
@@ -441,7 +490,7 @@ var mw = ( function ( $, undefined ) {
}
return true;
}
-
+
/**
* Generates an ISO8601 "basic" string from a UNIX timestamp
*/
@@ -456,13 +505,23 @@ var mw = ( function ( $, undefined ) {
pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
].join( '' );
}
-
+
/**
- * Recursively resolves dependencies and detects circular references
+ * Resolves dependencies and detects circular references.
+ *
+ * @param module String Name of the top-level module whose dependencies shall be
+ * resolved and sorted.
+ * @param resolved Array 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 unresolved Object [optional] 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 recurse( module, resolved, unresolved ) {
+ function sortDependencies( module, resolved, unresolved ) {
var n, deps, len;
-
+
if ( registry[module] === undefined ) {
throw new Error( 'Unknown dependency: ' + module );
}
@@ -474,12 +533,20 @@ var mw = ( function ( $, undefined ) {
registry[module].dependencies = [registry[module].dependencies];
}
}
+ if ( $.inArray( module, resolved ) !== -1 ) {
+ // Module already resolved; nothing to do.
+ return;
+ }
+ // unresolved is optional, supply it 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 ( $.inArray( deps[n], unresolved ) !== -1 ) {
+ if ( unresolved[deps[n]] ) {
throw new Error(
'Circular reference detected: ' + module +
' -> ' + deps[n]
@@ -487,43 +554,43 @@ var mw = ( function ( $, undefined ) {
}
// Add to unresolved
- unresolved[unresolved.length] = module;
- recurse( deps[n], resolved, unresolved );
- // module is at the end of unresolved
- unresolved.pop();
+ unresolved[module] = true;
+ sortDependencies( deps[n], resolved, unresolved );
+ delete unresolved[module];
}
}
resolved[resolved.length] = module;
}
-
+
/**
- * Gets a list of module names that a module depends on in their proper dependency order
+ * Gets a list of module names that a module depends on in their proper dependency
+ * order.
*
* @param module string module name or array of string module names
* @return list of dependencies, including 'module'.
* @throws Error if circular reference is detected
*/
function resolve( module ) {
- var modules, m, deps, n, resolved;
-
+ var m, resolved;
+
// Allow calling with an array of module names
if ( $.isArray( module ) ) {
- modules = [];
+ resolved = [];
for ( m = 0; m < module.length; m += 1 ) {
- deps = resolve( module[m] );
- for ( n = 0; n < deps.length; n += 1 ) {
- modules[modules.length] = deps[n];
- }
+ sortDependencies( module[m], resolved );
}
- return modules;
- } else if ( typeof module === 'string' ) {
+ return resolved;
+ }
+
+ if ( typeof module === 'string' ) {
resolved = [];
- recurse( module, resolved, [] );
+ sortDependencies( module, resolved );
return resolved;
}
+
throw new Error( 'Invalid module argument: ' + module );
}
-
+
/**
* Narrows a list of module names down to those matching a specific
* state (see comment on top of this scope for a list of valid states).
@@ -537,7 +604,7 @@ var mw = ( function ( $, undefined ) {
*/
function filter( states, modules ) {
var list, module, s, m;
-
+
// Allow states to be given as a string
if ( typeof states === 'string' ) {
states = [states];
@@ -570,66 +637,127 @@ var mw = ( function ( $, undefined ) {
}
return list;
}
-
+
+ /**
+ * Determine whether all dependencies are in state 'ready', which means we may
+ * execute the module or job now.
+ *
+ * @param dependencies Array dependencies (module names) to be checked.
+ *
+ * @return Boolean true if all dependencies are in state 'ready', false otherwise
+ */
+ function allReady( dependencies ) {
+ return filter( 'ready', dependencies ).length === dependencies.length;
+ }
+
/**
- * Automatically executes jobs and modules which are pending with satistifed dependencies.
+ * Log a message to window.console, if possible. Useful to force logging of some
+ * errors that are otherwise hard to detect (I.e., this logs also in production mode).
+ * Gets console references in each invocation, so that delayed debugging tools work
+ * fine. No need for optimization here, which would only result in losing logs.
*
- * This is used when dependencies are satisfied, such as when a module is executed.
+ * @param msg String text for the log entry.
+ * @param e Error [optional] to also log.
+ */
+ function log( msg, e ) {
+ var console = window.console;
+ if ( console && console.log ) {
+ console.log( msg );
+ // If we have an exception object, log it through .error() to trigger
+ // proper stacktraces in browsers that support it. There are no (known)
+ // browsers that don't support .error(), that do support .log() and
+ // have useful exception handling through .log().
+ if ( e && console.error ) {
+ console.error( e );
+ }
+ }
+ }
+
+ /**
+ * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
+ * and modules that depend upon this module. if the given module failed, propagate the 'error'
+ * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
+ * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
+ *
+ * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'.
*/
function handlePending( module ) {
- var j, r;
-
- try {
- // Run jobs whose dependencies have just been met
- for ( j = 0; j < jobs.length; j += 1 ) {
- if ( compare(
- filter( 'ready', jobs[j].dependencies ),
- jobs[j].dependencies ) )
- {
- var callback = jobs[j].ready;
- jobs.splice( j, 1 );
- j -= 1;
- if ( $.isFunction( callback ) ) {
- callback();
+ var j, job, hasErrors, m, stateChange;
+
+ // Modules.
+ if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
+ // 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 ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
+ if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
+ registry[m].state = 'error';
+ stateChange = true;
+ }
}
}
- }
- // Execute modules whose dependencies have just been met
- for ( r in registry ) {
- if ( registry[r].state === 'loaded' ) {
- if ( compare(
- filter( ['ready'], registry[r].dependencies ),
- registry[r].dependencies ) )
- {
- execute( r );
+ } while ( stateChange );
+ }
+
+ // 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 = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
+ if ( hasErrors || allReady( jobs[j].dependencies ) ) {
+ // All dependencies satisfied, or some have errors
+ job = jobs[j];
+ jobs.splice( j, 1 );
+ j -= 1;
+ try {
+ if ( hasErrors ) {
+ throw new Error ("Module " + module + " failed.");
+ } else {
+ if ( $.isFunction( job.ready ) ) {
+ job.ready();
+ }
+ }
+ } catch ( e ) {
+ if ( $.isFunction( job.error ) ) {
+ try {
+ job.error( e, [module] );
+ } catch ( ex ) {
+ // A user-defined operation raised an exception. Swallow to protect
+ // our state machine!
+ log( 'Exception thrown by job.error()', ex );
+ }
}
}
}
- } catch ( e ) {
- // Run error callbacks of jobs affected by this condition
- for ( j = 0; j < jobs.length; j += 1 ) {
- if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) {
- if ( $.isFunction( jobs[j].error ) ) {
- jobs[j].error( e, module );
- }
- jobs.splice( j, 1 );
- j -= 1;
+ }
+
+ if ( registry[module].state === 'ready' ) {
+ // The current module became 'ready'. Recursively execute all dependent modules that are loaded
+ // and now have all dependencies satisfied.
+ for ( m in registry ) {
+ if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
+ execute( m );
}
}
- throw e;
}
}
-
+
/**
* Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
- * depending on whether document-ready has occured yet and whether we are in async mode.
+ * depending on whether document-ready has occurred yet and whether we are in async mode.
*
* @param src String: URL to script, will be used as the src attribute in the script tag
* @param callback Function: Optional callback which will be run when the script is done
*/
function addScript( src, callback, async ) {
- var done = false, script, head;
- if ( ready || async ) {
+ /*jshint evil:true */
+ var script, head,
+ done = false;
+
+ // Using isReady directly instead of storing it locally from
+ // a $.fn.ready callback (bug 31895).
+ if ( $.isReady || async ) {
// jQuery's getScript method is NOT better than doing this the old-fashioned way
// because jQuery will eval the script's code, and errors will not have sane
// line numbers.
@@ -638,8 +766,8 @@ var mw = ( function ( $, undefined ) {
script.setAttribute( 'type', 'text/javascript' );
if ( $.isFunction( callback ) ) {
// Attach handlers for all browsers (based on jQuery.ajax)
- script.onload = script.onreadystatechange = function() {
-
+ script.onload = script.onreadystatechange = function () {
+
if (
!done
&& (
@@ -647,11 +775,11 @@ var mw = ( function ( $, undefined ) {
|| /loaded|complete/.test( script.readyState )
)
) {
-
+
done = true;
-
+
callback();
-
+
// Handle memory leak in IE. This seems to fail in
// IE7 sometimes (Permission Denied error when
// accessing script.parentNode) so wrap it in
@@ -661,21 +789,21 @@ var mw = ( function ( $, undefined ) {
if ( script.parentNode ) {
script.parentNode.removeChild( script );
}
-
+
// Dereference the script
script = undefined;
} catch ( e ) { }
}
};
}
-
+
if ( window.opera ) {
// Appending to the <head> blocks rendering completely in Opera,
// so append to the <body> after document ready. This means the
// scripts only start loading after the document has been rendered,
// but so be it. Opera users don't deserve faster web pages if their
// browser makes it impossible
- $( function() { document.body.appendChild( script ); } );
+ $( function () { document.body.appendChild( script ); } );
} else {
// IE-safe way of getting the <head> . document.documentElement.head doesn't
// work in scripts that run in the <head>
@@ -693,15 +821,15 @@ var mw = ( function ( $, undefined ) {
}
}
}
-
+
/**
* Executes a loaded module, making it ready to use
*
* @param module string module name to execute
*/
- function execute( module, callback ) {
- var style, media, i, script, markModuleReady, nestedAddScript;
-
+ function execute( module ) {
+ var key, value, media, i, urls, script, markModuleReady, nestedAddScript;
+
if ( registry[module] === undefined ) {
throw new Error( 'Module has not been registered yet: ' + module );
} else if ( registry[module].state === 'registered' ) {
@@ -711,38 +839,84 @@ var mw = ( function ( $, undefined ) {
} else if ( registry[module].state === 'ready' ) {
throw new Error( 'Module has already been loaded: ' + module );
}
-
- // Add styles
+
+ /**
+ * Define loop-function here for efficiency
+ * and to avoid re-using badly scoped variables.
+ */
+ function addLink( media, url ) {
+ var el = document.createElement( 'link' );
+ getMarker().before( el ); // IE: Insert in dom before setting href
+ el.rel = 'stylesheet';
+ if ( media && media !== 'all' ) {
+ el.media = media;
+ }
+ el.href = url;
+ }
+
+ // 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 ( media in registry[module].style ) {
- style = registry[module].style[media];
- if ( $.isArray( style ) ) {
- for ( i = 0; i < style.length; i += 1 ) {
- getMarker().before( mw.html.element( 'link', {
- 'type': 'text/css',
- 'media': media,
- 'rel': 'stylesheet',
- 'href': style[i]
- } ) );
+ for ( key in registry[module].style ) {
+ value = registry[module].style[key];
+ media = undefined;
+
+ if ( key !== 'url' && key !== 'css' ) {
+ // Backwards compatibility, key is a media-type
+ if ( typeof value === 'string' ) {
+ // back-compat: { <media>: css }
+ // Ignore 'media' because it isn't supported (nor was it used).
+ // Strings are pre-wrapped in "@media". The media-type was just ""
+ // (because it had to be set to something).
+ // This is one of the reasons why this format is no longer used.
+ addEmbeddedCSS( value );
+ } else {
+ // back-compat: { <media>: [url, ..] }
+ media = key;
+ key = 'bc-url';
+ }
+ }
+
+ // Array of css strings in key 'css',
+ // or back-compat array of urls from media-type
+ if ( $.isArray( value ) ) {
+ for ( i = 0; i < value.length; i += 1 ) {
+ if ( key === 'bc-url' ) {
+ // back-compat: { <media>: [url, ..] }
+ addLink( media, value[i] );
+ } else if ( key === 'css' ) {
+ // { "css": [css, ..] }
+ addEmbeddedCSS( value[i] );
+ }
+ }
+ // Not an array, but a regular object
+ // Array of urls inside media-type key
+ } else if ( typeof value === 'object' ) {
+ // { "url": { <media>: [url, ..] } }
+ for ( media in value ) {
+ urls = value[media];
+ for ( i = 0; i < urls.length; i += 1 ) {
+ addLink( media, urls[i] );
+ }
}
- } else if ( typeof style === 'string' ) {
- addInlineCSS( style, media );
}
}
}
+
// Add localizations to message system
if ( $.isPlainObject( registry[module].messages ) ) {
mw.messages.set( registry[module].messages );
}
+
// Execute script
try {
script = registry[module].script;
- markModuleReady = function() {
+ markModuleReady = function () {
registry[module].state = 'ready';
handlePending( module );
- if ( $.isFunction( callback ) ) {
- callback();
- }
};
nestedAddScript = function ( arr, callback, async, i ) {
// Recursively call addScript() in its own callback
@@ -752,29 +926,29 @@ var mw = ( function ( $, undefined ) {
callback();
return;
}
-
- addScript( arr[i], function() {
+
+ addScript( arr[i], function () {
nestedAddScript( arr, callback, async, i + 1 );
}, async );
};
-
+
if ( $.isArray( script ) ) {
registry[module].state = 'loading';
nestedAddScript( script, markModuleReady, registry[module].async, 0 );
} else if ( $.isFunction( script ) ) {
+ registry[module].state = 'ready';
script( $ );
- markModuleReady();
+ handlePending( module );
}
} 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
- if ( window.console && typeof window.console.log === 'function' ) {
- console.log( 'mw.loader::execute> Exception thrown by ' + module + ': ' + e.message );
- }
+ log( 'Exception thrown by ' + module + ': ' + e.message, e );
registry[module].state = 'error';
+ handlePending( module );
}
}
-
+
/**
* Adds a dependencies to the queue with optional callbacks to be run
* when the dependencies are ready or fail
@@ -787,22 +961,14 @@ var mw = ( function ( $, undefined ) {
*/
function request( dependencies, ready, error, async ) {
var regItemDeps, regItemDepLen, n;
-
+
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
dependencies = [dependencies];
- if ( registry[dependencies[0]] !== undefined ) {
- // Cache repetitively accessed deep level object member
- regItemDeps = registry[dependencies[0]].dependencies;
- // Cache to avoid looped access to length property
- regItemDepLen = regItemDeps.length;
- for ( n = 0; n < regItemDepLen; n += 1 ) {
- dependencies[dependencies.length] = regItemDeps[n];
- }
- }
}
+
// Add ready and error callbacks if they were given
- if ( arguments.length > 1 ) {
+ if ( ready !== undefined || error !== undefined ) {
jobs[jobs.length] = {
'dependencies': filter(
['registered', 'loading', 'loaded'],
@@ -812,6 +978,7 @@ var mw = ( function ( $, undefined ) {
'error': error
};
}
+
// Queue up any dependencies that are registered
dependencies = filter( ['registered'], dependencies );
for ( n = 0; n < dependencies.length; n += 1 ) {
@@ -823,10 +990,11 @@ var mw = ( function ( $, undefined ) {
}
}
}
+
// Work the queue
mw.loader.work();
}
-
+
function sortQuery(o) {
var sorted = {}, key, a = [];
for ( key in o ) {
@@ -840,7 +1008,7 @@ var mw = ( function ( $, undefined ) {
}
return sorted;
}
-
+
/**
* 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
@@ -853,7 +1021,7 @@ var mw = ( function ( $, undefined ) {
}
return arr.join( '|' );
}
-
+
/**
* Asynchronously append a script tag to the end of the body
* that invokes load.php
@@ -872,9 +1040,11 @@ var mw = ( function ( $, undefined ) {
// Append &* to avoid triggering the IE6 extension check
addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
}
-
+
/* Public Methods */
return {
+ addStyleTag: addStyleTag,
+
/**
* Requests dependencies from server, loading and executing when things when ready.
*/
@@ -883,7 +1053,7 @@ var mw = ( function ( $, undefined ) {
source, group, g, i, modules, maxVersion, sourceLoadScript,
currReqBase, currReqBaseLength, moduleMap, l,
lastDotIndex, prefix, suffix, bytesAdded, async;
-
+
// Build a list of request parameters common to all requests.
reqBase = {
skin: mw.config.get( 'skin' ),
@@ -893,7 +1063,7 @@ var mw = ( function ( $, undefined ) {
// Split module batch by source and by group.
splits = {};
maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
-
+
// 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
@@ -910,14 +1080,14 @@ var mw = ( function ( $, undefined ) {
if ( !batch.length ) {
return;
}
-
+
// The queue has been processed into the batch, clear up the queue.
queue = [];
-
+
// Always order modules alphabetically to help reduce cache
// misses for otherwise identical content.
batch.sort();
-
+
// Split batch by source and by group.
for ( b = 0; b < batch.length; b += 1 ) {
bSource = registry[batch[b]].source;
@@ -931,24 +1101,24 @@ var mw = ( function ( $, undefined ) {
bSourceGroup = splits[bSource][bGroup];
bSourceGroup[bSourceGroup.length] = batch[b];
}
-
+
// Clear the batch - this MUST happen before we append any
// script elements to the body or it's possible that a script
// will be locally cached, instantly load, and work the batch
// again, all before we've cleared it causing each request to
// include modules which are already loaded.
batch = [];
-
+
for ( source in splits ) {
-
+
sourceLoadScript = sources[source].loadScript;
-
+
for ( group in splits[source] ) {
-
+
// Cache access to currently selected list of
// modules for this group from this source.
modules = splits[source][group];
-
+
// Calculate the highest timestamp
maxVersion = 0;
for ( g = 0; g < modules.length; g += 1 ) {
@@ -956,16 +1126,20 @@ var mw = ( function ( $, undefined ) {
maxVersion = registry[modules[g]].version;
}
}
-
+
currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, 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
-
+
moduleMap = {}; // { prefix: [ suffixes ] }
-
+
for ( i = 0; i < modules.length; i += 1 ) {
// Determine how many bytes this module would add to the query string
lastDotIndex = modules[i].lastIndexOf( '.' );
@@ -975,7 +1149,7 @@ var mw = ( function ( $, undefined ) {
bytesAdded = moduleMap[prefix] !== undefined
? suffix.length + 3 // '%2C'.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 ) {
@@ -1005,7 +1179,7 @@ var mw = ( function ( $, undefined ) {
}
}
},
-
+
/**
* Register a source.
*
@@ -1023,16 +1197,16 @@ var mw = ( function ( $, undefined ) {
}
return true;
}
-
+
if ( sources[id] !== undefined ) {
throw new Error( 'source already registered: ' + id );
}
-
+
sources[id] = props;
-
+
return true;
},
-
+
/**
* Registers a module, letting the system know about it and its
* properties. Startup modules contain calls to this function.
@@ -1083,7 +1257,7 @@ var mw = ( function ( $, undefined ) {
registry[module].dependencies = dependencies;
}
},
-
+
/**
* Implements a module, giving the system a course of action to take
* upon loading. Results of a request for one or more modules contain
@@ -1091,12 +1265,20 @@ var mw = ( function ( $, undefined ) {
*
* All arguments are required.
*
- * @param module String: Name of module
- * @param script Mixed: Function of module code or String of URL to be used as the src
- * attribute when adding a script element to the body
- * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs
- * keyed by media-type
- * @param msgs Object: List of key/value pairs to be passed through mw.messages.set
+ * @param {String} module Name of module
+ * @param {Function|Array} script Function with module code or Array of URLs to
+ * be used as the src attribute of a new <script> tag.
+ * @param {Object} style Should follow one of the following patterns:
+ * { "css": [css, ..] }
+ * { "url": { <media>: [url, ..] } }
+ * And for backwards compatibility (needs to be supported forever due to caching):
+ * { <media>: css }
+ * { <media>: [url, ..] }
+ *
+ * The reason css strings are not concatenated anymore is bug 31676. We now check
+ * whether it's safe to extend the stylesheet (see canExpandStylesheetWith).
+ *
+ * @param {Object} msgs List of key/value pairs to be passed through mw.messages.set
*/
implement: function ( module, script, style, msgs ) {
// Validate input
@@ -1120,21 +1302,19 @@ var mw = ( function ( $, undefined ) {
if ( registry[module] !== undefined && registry[module].script !== undefined ) {
throw new Error( 'module already implemented: ' + module );
}
- // Mark module as loaded
- registry[module].state = 'loaded';
// Attach components
registry[module].script = script;
registry[module].style = style;
registry[module].messages = msgs;
- // Execute or queue callback
- if ( compare(
- filter( ['ready'], registry[module].dependencies ),
- registry[module].dependencies ) )
- {
- execute( module );
+ // 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 ) ) {
+ execute( module );
+ }
}
},
-
+
/**
* Executes a function as soon as one or more required modules are ready
*
@@ -1155,25 +1335,23 @@ var mw = ( function ( $, undefined ) {
}
// Resolve entire dependency map
dependencies = resolve( dependencies );
- // If all dependencies are met, execute ready immediately
- if ( compare( filter( ['ready'], dependencies ), dependencies ) ) {
+ if ( allReady( dependencies ) ) {
+ // Run ready immediately
if ( $.isFunction( ready ) ) {
ready();
}
- }
- // If any dependencies have errors execute error immediately
- else if ( filter( ['error'], dependencies ).length ) {
+ } else if ( filter( ['error', 'missing'], dependencies ).length ) {
+ // Execute error immediately if any dependencies have errors
if ( $.isFunction( error ) ) {
- error( new Error( 'one or more dependencies have state "error"' ),
+ error( new Error( 'one or more dependencies have state "error" or "missing"' ),
dependencies );
}
- }
- // Since some dependencies are not yet ready, queue up a request
- else {
+ } else {
+ // Not all dependencies are ready: queue up a request
request( dependencies, ready, error );
}
},
-
+
/**
* Loads an external script or one or more modules for future use
*
@@ -1188,7 +1366,7 @@ var mw = ( function ( $, undefined ) {
* be assumed if loading a URL, and false will be assumed otherwise.
*/
load: function ( modules, type, async ) {
- var filtered, m;
+ var filtered, m, module;
// Validate input
if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
@@ -1209,7 +1387,8 @@ var mw = ( function ( $, undefined ) {
href: modules
} ) );
return;
- } else if ( type === 'text/javascript' || type === undefined ) {
+ }
+ if ( type === 'text/javascript' || type === undefined ) {
addScript( modules, null, async );
return;
}
@@ -1226,28 +1405,31 @@ var mw = ( function ( $, undefined ) {
// an array of unrelated modules, whereas the modules passed to
// using() are related and must all be loaded.
for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
- if ( registry[modules[m]] !== undefined ) {
- filtered[filtered.length] = modules[m];
+ module = registry[modules[m]];
+ if ( module !== undefined ) {
+ if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
+ filtered[filtered.length] = modules[m];
+ }
}
}
- // Resolve entire dependency map
- filtered = resolve( filtered );
- // If all modules are ready, nothing dependency be done
- if ( compare( filter( ['ready'], filtered ), filtered ) ) {
+ if ( filtered.length === 0 ) {
return;
}
- // If any modules have errors
- else if ( filter( ['error'], filtered ).length ) {
+ // Resolve entire dependency map
+ filtered = resolve( filtered );
+ // If all modules are ready, nothing to be done
+ if ( allReady( filtered ) ) {
return;
}
- // Since some modules are not yet ready, queue up a request
- else {
- request( filtered, null, null, async );
+ // If any modules have errors: also quit.
+ if ( filter( ['error', 'missing'], filtered ).length ) {
return;
}
+ // Since some modules are not yet ready, queue up a request.
+ request( filtered, null, null, async );
},
-
+
/**
* Changes the state of a module
*
@@ -1256,6 +1438,7 @@ var mw = ( function ( $, undefined ) {
*/
state: function ( module, state ) {
var m;
+
if ( typeof module === 'object' ) {
for ( m in module ) {
mw.loader.state( m, module[m] );
@@ -1265,9 +1448,17 @@ var mw = ( function ( $, undefined ) {
if ( registry[module] === undefined ) {
mw.loader.register( module );
}
- 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;
+ handlePending( module );
+ } else {
+ registry[module].state = state;
+ }
},
-
+
/**
* Gets the version of a module
*
@@ -1279,14 +1470,14 @@ var mw = ( function ( $, undefined ) {
}
return null;
},
-
+
/**
* @deprecated since 1.18 use mw.loader.getVersion() instead
*/
version: function () {
return mw.loader.getVersion.apply( mw.loader, arguments );
},
-
+
/**
* Gets the state of a module
*
@@ -1298,7 +1489,7 @@ var mw = ( function ( $, undefined ) {
}
return null;
},
-
+
/**
* Get names of all registered modules.
*
@@ -1309,7 +1500,7 @@ var mw = ( function ( $, undefined ) {
return key;
} );
},
-
+
/**
* For backwards-compatibility with Squid-cached pages. Loads mw.user
*/
@@ -1318,7 +1509,7 @@ var mw = ( function ( $, undefined ) {
}
};
}() ),
-
+
/** HTML construction helper functions */
html: ( function () {
function escapeCallback( s ) {
@@ -1344,7 +1535,7 @@ var mw = ( function ( $, undefined ) {
escape: function ( s ) {
return s.replace( /['"<>&]/g, escapeCallback );
},
-
+
/**
* Wrapper object for raw HTML passed to mw.html.element().
* @constructor
@@ -1352,7 +1543,7 @@ var mw = ( function ( $, undefined ) {
Raw: function ( value ) {
this.value = value;
},
-
+
/**
* Wrapper object for CDATA element contents passed to mw.html.element()
* @constructor
@@ -1360,7 +1551,7 @@ var mw = ( function ( $, undefined ) {
Cdata: function ( value ) {
this.value = value;
},
-
+
/**
* Create an HTML element string, with safe escaping.
*
@@ -1382,7 +1573,7 @@ var mw = ( function ( $, undefined ) {
*/
element: function ( name, attrs, contents ) {
var v, attrName, s = '<' + name;
-
+
for ( attrName in attrs ) {
v = attrs[attrName];
// Convert name=true, to name=name
@@ -1429,7 +1620,7 @@ var mw = ( function ( $, undefined ) {
return s;
}
};
- })(),
+ }() ),
// Skeleton user object. mediawiki.user.js extends this
user: {
@@ -1437,8 +1628,8 @@ var mw = ( function ( $, undefined ) {
tokens: new Map()
}
};
-
-})( jQuery );
+
+}( jQuery ) );
// Alias $j to jQuery for backwards compatibility
window.$j = jQuery;
@@ -1447,7 +1638,7 @@ window.$j = jQuery;
window.mw = window.mediaWiki = mw;
// Auto-register from pre-loaded startup scripts
-if ( typeof startUp !== 'undefined' && jQuery.isFunction( startUp ) ) {
- startUp();
- startUp = undefined;
+if ( jQuery.isFunction( window.startUp ) ) {
+ window.startUp();
+ window.startUp = undefined;
}