diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2013-08-12 09:28:15 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2013-08-12 09:28:15 +0200 |
commit | 08aa4418c30cfc18ccc69a0f0f9cb9e17be6c196 (patch) | |
tree | 577a29fb579188d16003a209ce2a2e9c5b0aa2bd /resources/mediawiki/mediawiki.js | |
parent | cacc939b34e315b85e2d72997811eb6677996cc1 (diff) |
Update to MediaWiki 1.21.1
Diffstat (limited to 'resources/mediawiki/mediawiki.js')
-rw-r--r-- | resources/mediawiki/mediawiki.js | 699 |
1 files changed, 404 insertions, 295 deletions
diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 19112aed..ca987543 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -1,9 +1,9 @@ /* * Core MediaWiki JavaScript Library */ -/*global mw:true */ + var mw = ( function ( $, undefined ) { - "use strict"; + 'use strict'; /* Private Members */ @@ -13,14 +13,13 @@ var mw = ( function ( $, undefined ) { /* Object constructors */ /** - * Map - * * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. + * @class mw.Map * - * @param global boolean Whether to store the values in the global window + * @constructor + * @param {boolean} global Whether to store the values in the global window * object or a exclusively in the object property 'values'. - * @return Map */ function Map( global ) { this.values = global === true ? window : {}; @@ -39,26 +38,26 @@ var mw = ( function ( $, undefined ) { * If selection was an array, returns an object of key/values (value is null if not found), * If selection was not passed or invalid, will return the 'values' object member (be careful as * objects are always passed by reference in JavaScript!). - * @return Values as a string or object, null if invalid/inexistant. + * @return {string|Object|null} Values as a string or object, null if invalid/inexistant. */ get: function ( selection, fallback ) { var results, i; + // If we only do this in the `return` block, it'll fail for the + // call to get() from the mutli-selection block. + fallback = arguments.length > 1 ? fallback : null; if ( $.isArray( selection ) ) { selection = slice.call( selection ); results = {}; - for ( i = 0; i < selection.length; i += 1 ) { + for ( i = 0; i < selection.length; i++ ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { - if ( this.values[selection] === undefined ) { - if ( fallback !== undefined ) { - return fallback; - } - return null; + if ( !hasOwn.call( this.values, selection ) ) { + return fallback; } return this.values[selection]; } @@ -87,7 +86,7 @@ var mw = ( function ( $, undefined ) { } return true; } - if ( typeof selection === 'string' && value !== undefined ) { + if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[selection] = value; return true; } @@ -98,36 +97,35 @@ var mw = ( function ( $, undefined ) { * Checks if one or multiple keys exist. * * @param selection {mixed} String key or array of keys to check - * @return {Boolean} Existence of key(s) + * @return {boolean} Existence of key(s) */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { - for ( s = 0; s < selection.length; s += 1 ) { - if ( this.values[selection[s]] === undefined ) { + for ( s = 0; s < selection.length; s++ ) { + if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { return false; } } return true; } - return this.values[selection] !== undefined; + return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** - * Message - * * Object constructor for messages, * similar to the Message class in MediaWiki PHP. + * @class mw.Message * - * @param map Map Instance of mw.Map - * @param key String - * @param parameters Array - * @return Message + * @constructor + * @param {mw.Map} map Message storage + * @param {string} key + * @param {Array} [parameters] */ function Message( map, key, parameters ) { - this.format = 'plain'; + this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); @@ -136,9 +134,13 @@ var mw = ( function ( $, undefined ) { Message.prototype = { /** - * Simple message parser, does $N replacement and nothing else. + * Simple message parser, does $N replacement, HTML-escaping (only for + * 'escaped' format), and nothing else. + * * This may be overridden to provide a more complex message parser. * + * The primary override is in mediawiki.jqueryMsg. + * * This function will not be called for nonexistent messages. */ parser: function () { @@ -152,8 +154,8 @@ var mw = ( function ( $, undefined ) { /** * Appends (does not replace) parameters for replacement to the .parameters property. * - * @param parameters Array - * @return Message + * @param {Array} parameters + * @chainable */ params: function ( parameters ) { var i; @@ -166,25 +168,21 @@ var mw = ( function ( $, undefined ) { /** * Converts message object to it's string form based on the state of format. * - * @return string Message as a string in the current form or <key> if key does not exist. + * @return {string} Message as a string in the current form or `<key>` if key does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use <key> as text if key does not exist - if ( this.format !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped + if ( this.format === 'escaped' || this.format === 'parse' ) { + // format 'escaped' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } - if ( this.format === 'plain' ) { - // @todo FIXME: Although not applicable to core Message, - // Plugins like jQueryMsg should be able to distinguish - // between 'plain' (only variable replacement and plural/gender) - // and actually parsing wikitext to HTML. + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } @@ -193,15 +191,16 @@ var mw = ( function ( $, undefined ) { text = mw.html.escape( text ); } - if ( this.format === 'parse' ) { - text = this.parser(); - } - return text; }, /** - * Changes format to parse and converts message to string + * Changes format to 'parse' and converts message to string + * + * If jqueryMsg is loaded, this parses the message text from wikitext + * (where supported) to HTML + * + * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ @@ -211,7 +210,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to plain and converts message to string + * Changes format to 'plain' and converts message to string + * + * This substitutes parameters, but otherwise does not change the + * message text. * * @return {string} String form of plain message */ @@ -221,7 +223,23 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the format to html escaped and converts message to string + * Changes format to 'text' and converts message to string + * + * If jqueryMsg is loaded, {{-transformation is done where supported + * (such as {{plural:}}, {{gender:}}, {{int:}}). + * + * Otherwise, it is equivalent to plain. + */ + text: function () { + this.format = 'text'; + return this.toString(); + }, + + /** + * Changes the format to 'escaped' and converts message to string + * + * This is equivalent to using the 'text' format (see text method), then + * HTML-escaping the output. * * @return {string} String form of html escaped message */ @@ -233,13 +251,19 @@ var mw = ( function ( $, undefined ) { /** * Checks if message exists * - * @return {string} String form of parsed message + * @see mw.Map#exists + * @return {boolean} */ exists: function () { return this.map.exists( this.key ); } }; + /** + * @class mw + * @alternateClassName mediaWiki + * @singleton + */ return { /* Public Members */ @@ -249,77 +273,72 @@ var mw = ( function ( $, undefined ) { */ log: function () { }, - /** - * @var constructor Make the Map constructor publicly available. - */ + // Make the Map constructor publicly available. Map: Map, - /** - * @var constructor Make the Message constructor publicly available. - */ + // Make the Message constructor publicly available. Message: Message, /** * List of configuration values * * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). - * If $wgLegacyJavaScriptGlobals is true, this Map will have its values + * If `$wgLegacyJavaScriptGlobals` is true, this Map will have its values * in the global window object. + * @property */ config: null, /** - * @var object - * * Empty object that plugins can be installed in. + * @property */ libs: {}, /* Extension points */ + /** + * @property + */ legacy: {}, /** * Localization system + * @property {mw.Map} */ messages: new Map(), /* Public Methods */ /** - * Gets a message object, similar to wfMessage() + * Gets a message object, similar to wfMessage(). * - * @param key string Key of message to get - * @param parameter_1 mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return Message + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {mw.Message} */ - message: function ( key, parameter_1 /* [, parameter_2] */ ) { - var parameters; - // Support variadic arguments - if ( parameter_1 !== undefined ) { - parameters = slice.call( arguments ); - parameters.shift(); - } else { - parameters = []; - } + message: function ( key ) { + // Variadic arguments + var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** * 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. + * @see mw.Message#toString + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {string} */ - msg: function ( /* key, parameter_1, parameter_2, .. */ ) { + msg: function ( /* key, parameters... */ ) { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Client-side module loader which integrates with the MediaWiki ResourceLoader + * @class mw.loader + * @singleton */ loader: ( function () { @@ -338,29 +357,32 @@ var mw = ( function ( $, undefined ) { * mw.loader.implement. * * Format: - * { - * 'moduleName': { - * 'version': ############## (unix timestamp), - * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} - * 'group': 'somegroup', (or) null, - * 'source': 'local', 'someforeignwiki', (or) null - * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' - * 'script': ..., - * 'style': ..., - * 'messages': { 'key': 'value' }, - * } - * } + * { + * 'moduleName': { + * 'version': ############## (unix timestamp), + * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} + * 'group': 'somegroup', (or) null, + * 'source': 'local', 'someforeignwiki', (or) null + * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing' + * 'script': ..., + * 'style': ..., + * 'messages': { 'key': 'value' }, + * } + * } + * + * @property + * @private */ var registry = {}, - /** - * Mapping of sources, keyed by source-id, values are objects. - * Format: - * { - * 'sourceId': { - * 'loadScript': 'http://foo.bar/w/load.php' - * } - * } - */ + // + // Mapping of sources, keyed by source-id, values are objects. + // Format: + // { + // 'sourceId': { + // 'loadScript': 'http://foo.bar/w/load.php' + // } + // } + // sources = {}, // List of modules which will be loaded as when ready batch = [], @@ -369,7 +391,11 @@ var mw = ( function ( $, undefined ) { // List of callback functions waiting for modules to be ready to be called jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! - $marker = null; + $marker = null, + // Buffer for addEmbeddedCSS. + cssBuffer = '', + // Callbacks for addEmbeddedCSS. + cssCallbacks = $.Callbacks(); /* Private methods */ @@ -392,10 +418,11 @@ var mw = ( function ( $, undefined ) { /** * 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 + * @private + * @param {string} text CSS text + * @param {Mixed} [nextnode] An Element or jQuery object for an element where + * the style tag should be inserted before. Otherwise appended to the `<head>`. + * @return {HTMLElement} Node reference to the created `<style>` tag. */ function addStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); @@ -429,76 +456,107 @@ var mw = ( function ( $, undefined ) { } /** - * 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). + * Checks whether it is safe to add this css to a stylesheet. + * + * @private + * @param {string} cssText + * @return {boolean} False if a new one must be created. */ - function canExpandStylesheetWith( $style, cssText ) { + function canExpandStylesheetWith( cssText ) { + // 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). return cssText.indexOf( '@import' ) === -1; } - function addEmbeddedCSS( cssText ) { + /** + * @param {string} [cssText=cssBuffer] If called without cssText, + * the internal buffer will be inserted instead. + * @param {Function} [callback] + */ + function addEmbeddedCSS( cssText, callback ) { 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 ) ) ); + + if ( callback ) { + cssCallbacks.add( callback ); + } + + // Yield once before inserting the <style> tag. There are likely + // more calls coming up which we can combine this way. + // Appending a stylesheet and waiting for the browser to repaint + // is fairly expensive, this reduces it (bug 45810) + if ( cssText ) { + // Be careful not to extend the buffer with css that needs a new stylesheet + if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) { + // Linebreak for somewhat distinguishable sections + // (the rl-cachekey comment separating each) + cssBuffer += '\n' + cssText; + // TODO: Use requestAnimationFrame in the future which will + // perform even better by not injecting styles while the browser + // is paiting. + setTimeout( function () { + // Can't pass addEmbeddedCSS to setTimeout directly because Firefox + // (below version 13) has the non-standard behaviour of passing a + // numerical "lateness" value as first argument to this callback + // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/ + addEmbeddedCSS(); + } ); + return; } + + // This is a delayed call and we got a buffer still + } else if ( cssBuffer ) { + cssText = cssBuffer; + cssBuffer = ''; } else { - $( addStyleTag( cssText, getMarker() ) ) - .data( 'ResourceLoaderDynamicStyleTag', true ); + // This is a delayed call, but buffer is already cleared by + // another delayed call. + return; } - } - function compare( a, b ) { - var i; - if ( a.length !== b.length ) { - return false; - } - for ( i = 0; i < b.length; i += 1 ) { - if ( $.isArray( a[i] ) ) { - if ( !compare( a[i], b[i] ) ) { - return false; + // By default, always create a new <style>. Appending text + // to a <style> tag means the contents have to be re-parsed (bug 45810). + // Except, of course, in IE below 9, in there we default to + // re-using and appending to a <style> tag due to the + // IE stylesheet limit (bug 31676). + if ( 'documentMode' in document && document.documentMode <= 9 ) { + + $style = getMarker().prev(); + // Verify that the the element before 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 ) { + // 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 ) ) ); } - } - if ( a[i] !== b[i] ) { - return false; + cssCallbacks.fire().empty(); + return; } } - return true; + + $( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); + + cssCallbacks.fire().empty(); } /** * Generates an ISO8601 "basic" string from a UNIX timestamp + * @private */ function formatVersionNumber( timestamp ) { - var pad = function ( a, b, c ) { - return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); - }, - d = new Date(); + var d = new Date(); + function pad( a, b, c ) { + return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); + } d.setTime( timestamp * 1000 ); return [ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', @@ -509,15 +567,16 @@ var mw = ( function ( $, undefined ) { /** * Resolves dependencies and detects circular references. * - * @param module String Name of the top-level module whose dependencies shall be + * @private + * @param {string} module 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 + * @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 unresolved Object [optional] Hash used to track the current dependency + * @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 + * @throws {Error} If any unregistered module or a dependency loop is encountered */ function sortDependencies( module, resolved, unresolved ) { var n, deps, len; @@ -566,9 +625,10 @@ var mw = ( function ( $, undefined ) { * 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 + * @private + * @param {string} module Module name or array of string module names + * @return {Array} list of dependencies, including 'module'. + * @throws {Error} If circular reference is detected */ function resolve( module ) { var m, resolved; @@ -597,10 +657,11 @@ var mw = ( function ( $, undefined ) { * One can also filter for 'unregistered', which will return the * modules names that don't have a registry entry. * - * @param states string or array of strings of module states to filter by - * @param modules array list of module names to filter (optional, by default the entire + * @private + * @param {string|string[]} states Module states to filter by + * @param {Array} modules List of module names to filter (optional, by default the entire * registry is used) - * @return array list of filtered module names + * @return {Array} List of filtered module names */ function filter( states, modules ) { var list, module, s, m; @@ -642,9 +703,9 @@ var mw = ( function ( $, undefined ) { * 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 + * @private + * @param {Array} dependencies 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; @@ -656,8 +717,9 @@ var mw = ( function ( $, undefined ) { * 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. * - * @param msg String text for the log entry. - * @param e Error [optional] to also log. + * @private + * @param {string} msg text for the log entry. + * @param {Error} [e] */ function log( msg, e ) { var console = window.console; @@ -679,7 +741,8 @@ var mw = ( function ( $, undefined ) { * 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'. + * @private + * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'. */ function handlePending( module ) { var j, job, hasErrors, m, stateChange; @@ -712,7 +775,7 @@ var mw = ( function ( $, undefined ) { j -= 1; try { if ( hasErrors ) { - throw new Error ("Module " + module + " failed."); + throw new Error( 'Module ' + module + ' failed.'); } else { if ( $.isFunction( job.ready ) ) { job.ready(); @@ -747,8 +810,9 @@ var mw = ( function ( $, undefined ) { * 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. * - * @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 + * @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 */ function addScript( src, callback, async ) { /*jshint evil:true */ @@ -758,16 +822,20 @@ var mw = ( function ( $, undefined ) { // 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. + // Can't use jQuery.getScript because that only uses <script> for cross-domain, + // it uses XHR and eval for same-domain scripts, which we don't want because it + // messes up line numbers. + // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js) + + // IE-safe way of getting the <head>. document.head isn't supported + // in old IE, and doesn't work when in the <head>. + head = document.getElementsByTagName( 'head' )[0] || document.body; + script = document.createElement( 'script' ); - script.setAttribute( 'src', src ); - script.setAttribute( 'type', 'text/javascript' ); + script.async = true; + script.src = src; if ( $.isFunction( callback ) ) { - // Attach handlers for all browsers (based on jQuery.ajax) script.onload = script.onreadystatechange = function () { - if ( !done && ( @@ -775,24 +843,20 @@ var mw = ( function ( $, undefined ) { || /loaded|complete/.test( script.readyState ) ) ) { - done = true; - callback(); + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; - // Handle memory leak in IE. This seems to fail in - // IE7 sometimes (Permission Denied error when - // accessing script.parentNode) so wrap it in - // a try catch. - try { - script.onload = script.onreadystatechange = null; - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = undefined; - } catch ( e ) { } + // Remove the script + if ( script.parentNode ) { + script.parentNode.removeChild( script ); + } + + // Dereference the script + script = undefined; + + callback(); } }; } @@ -800,20 +864,17 @@ var mw = ( function ( $, undefined ) { 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, + // 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 ); } ); + // browser makes it impossible. + $( 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> - head = document.getElementsByTagName( 'head' )[0]; - ( document.body || head ).appendChild( script ); + head.appendChild( script ); } } else { - document.write( mw.html.element( - 'script', { 'type': 'text/javascript', 'src': src }, '' - ) ); + document.write( mw.html.element( 'script', { 'src': src }, '' ) ); if ( $.isFunction( callback ) ) { // Document.write is synchronous, so this is called when it's done // FIXME: that's a lie. doc.write isn't actually synchronous @@ -825,10 +886,12 @@ var mw = ( function ( $, undefined ) { /** * Executes a loaded module, making it ready to use * - * @param module string module name to execute + * @private + * @param {string} module Module name to execute */ function execute( module ) { - var key, value, media, i, urls, script, markModuleReady, nestedAddScript; + var key, value, media, i, urls, cssHandle, checkCssHandles, + cssHandlesRegistered = false; if ( registry[module] === undefined ) { throw new Error( 'Module has not been registered yet: ' + module ); @@ -837,12 +900,13 @@ var mw = ( function ( $, undefined ) { } 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 loaded: ' + module ); + 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' ); @@ -854,6 +918,80 @@ var mw = ( function ( $, undefined ) { el.href = url; } + function runScript() { + var script, markModuleReady, nestedAddScript; + try { + script = registry[module].script; + markModuleReady = function () { + registry[module].state = 'ready'; + handlePending( module ); + }; + nestedAddScript = function ( arr, callback, async, i ) { + // Recursively call addScript() in its own callback + // for each element of arr. + if ( i >= arr.length ) { + // We're at the end of the array + callback(); + return; + } + + addScript( arr[i], function () { + nestedAddScript( arr, callback, async, i + 1 ); + }, async ); + }; + + if ( $.isArray( script ) ) { + nestedAddScript( script, markModuleReady, registry[module].async, 0 ); + } else if ( $.isFunction( script ) ) { + registry[module].state = 'ready'; + script( $ ); + 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 + log( 'Exception thrown by ' + module + ': ' + e.message, e ); + registry[module].state = 'error'; + 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 ); + } + + // 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 + } + }; + cssHandle = function () { + var check = checkCssHandles; + pending++; + return function () { + if (check) { + pending--; + check(); + check = undefined; // Revoke + } + }; + }; + }() ); + // Process styles (see also mw.loader.implement) // * back-compat: { <media>: css } // * back-compat: { <media>: [url, ..] } @@ -872,7 +1010,7 @@ var mw = ( function ( $, undefined ) { // 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 ); + addEmbeddedCSS( value, cssHandle() ); } else { // back-compat: { <media>: [url, ..] } media = key; @@ -889,7 +1027,7 @@ var mw = ( function ( $, undefined ) { addLink( media, value[i] ); } else if ( key === 'css' ) { // { "css": [css, ..] } - addEmbeddedCSS( value[i] ); + addEmbeddedCSS( value[i], cssHandle() ); } } // Not an array, but a regular object @@ -906,61 +1044,24 @@ var mw = ( function ( $, undefined ) { } } - // 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 () { - registry[module].state = 'ready'; - handlePending( module ); - }; - nestedAddScript = function ( arr, callback, async, i ) { - // Recursively call addScript() in its own callback - // for each element of arr. - if ( i >= arr.length ) { - // We're at the end of the array - callback(); - return; - } - - 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( $ ); - 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 - log( 'Exception thrown by ' + module + ': ' + e.message, e ); - registry[module].state = 'error'; - handlePending( module ); - } + // Kick off. + cssHandlesRegistered = true; + checkCssHandles(); } /** * Adds a dependencies to the queue with optional callbacks to be run * when the dependencies are ready or fail * - * @param dependencies string module name or array of string module names - * @param ready function callback to execute when all dependencies are ready - * @param error function callback to execute when any dependency fails - * @param async (optional) If true, load modules asynchronously even if - * document ready has not yet occurred + * @private + * @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] If true, load modules asynchronously even if + * document ready has not yet occurred. */ function request( dependencies, ready, error, async ) { - var regItemDeps, regItemDepLen, n; + var n; // Allow calling by single module name if ( typeof dependencies === 'string' ) { @@ -1012,6 +1113,7 @@ var mw = ( function ( $, undefined ) { /** * 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 ) { var arr = [], p, prefix; @@ -1025,14 +1127,15 @@ var mw = ( function ( $, undefined ) { /** * Asynchronously append a script tag to the end of the body * that invokes load.php - * @param moduleMap {Object}: Module map, see buildModulesString() - * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request - * @param sourceLoadScript {String}: URL of load.php - * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred + * @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 If true, use an asynchrounous request even if document ready has not yet occurred */ function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { var request = $.extend( - { 'modules': buildModulesString( moduleMap ) }, + { modules: buildModulesString( moduleMap ) }, currReqBase ); request = sortQuery( request ); @@ -1127,9 +1230,9 @@ var mw = ( function ( $, undefined ) { } } - currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase ); + currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase ); // For user modules append a user name to the request. - if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) { + if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) { currReqBase.user = mw.config.get( 'wgUserName' ); } currReqBaseLength = $.param( currReqBase ).length; @@ -1183,10 +1286,10 @@ var mw = ( function ( $, undefined ) { /** * Register a source. * - * @param id {String}: Short lowercase a-Z string representing a source, only used internally. - * @param props {Object}: Object containing only the loadScript property which is a url to - * the load.php location of the source. - * @return {Boolean} + * @param {string} id Short lowercase a-Z string representing a source, only used internally. + * @param {Object} props Object containing only the loadScript property which is a url to + * the load.php location of the source. + * @return {boolean} */ addSource: function ( id, props ) { var source; @@ -1242,15 +1345,15 @@ var mw = ( function ( $, undefined ) { } // List the module as registered registry[module] = { - 'version': version !== undefined ? parseInt( version, 10 ) : 0, - 'dependencies': [], - 'group': typeof group === 'string' ? group : null, - 'source': typeof source === 'string' ? source: 'local', - 'state': 'registered' + version: version !== undefined ? parseInt( version, 10 ) : 0, + dependencies: [], + group: typeof group === 'string' ? group : null, + source: typeof source === 'string' ? source: 'local', + state: 'registered' }; 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 @@ -1265,20 +1368,20 @@ var mw = ( function ( $, undefined ) { * * All arguments are required. * - * @param {String} module Name of module + * @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. + * 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, ..] } + * { "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). + * 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 + * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}. */ implement: function ( module, script, style, msgs ) { // Validate input @@ -1331,7 +1434,7 @@ var mw = ( function ( $, undefined ) { } // Allow calling with a single dependency as a string if ( tod === 'string' ) { - dependencies = [dependencies]; + dependencies = [ dependencies ]; } // Resolve entire dependency map dependencies = resolve( dependencies ); @@ -1366,7 +1469,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, module; + var filtered, m, module, l; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { @@ -1381,11 +1484,13 @@ var mw = ( function ( $, undefined ) { async = true; } if ( type === 'text/css' ) { - $( 'head' ).append( $( '<link>', { - rel: 'stylesheet', - type: 'text/css', - href: modules - } ) ); + // IE7-8 throws security warnings when inserting a <link> tag + // with a protocol-relative URL set though attributes (instead of + // properties) - when on HTTPS. See also bug #. + l = document.createElement( 'link' ); + l.rel = 'stylesheet'; + l.href = modules; + $( 'head' ).append( l ); return; } if ( type === 'text/javascript' || type === undefined ) { @@ -1396,7 +1501,7 @@ var mw = ( function ( $, undefined ) { throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type ); } // Called with single module - modules = [modules]; + modules = [ modules ]; } // Filter out undefined modules, otherwise resolve() will throw @@ -1427,7 +1532,7 @@ var mw = ( function ( $, undefined ) { return; } // Since some modules are not yet ready, queue up a request. - request( filtered, null, null, async ); + request( filtered, undefined, undefined, async ); }, /** @@ -1448,7 +1553,7 @@ var mw = ( function ( $, undefined ) { if ( registry[module] === undefined ) { mw.loader.register( module ); } - if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1 + 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! @@ -1510,11 +1615,15 @@ var mw = ( function ( $, undefined ) { }; }() ), - /** HTML construction helper functions */ + /** + * HTML construction helper functions + * @class mw.html + * @singleton + */ html: ( function () { function escapeCallback( s ) { switch ( s ) { - case "'": + case '\'': return '''; case '"': return '"'; @@ -1530,7 +1639,7 @@ var mw = ( function ( $, undefined ) { return { /** * Escape a string for HTML. Converts special characters to HTML entities. - * @param s The string to escape + * @param {string} s The string to escape */ escape: function ( s ) { return s.replace( /['"<>&]/g, escapeCallback ); @@ -1538,7 +1647,7 @@ var mw = ( function ( $, undefined ) { /** * Wrapper object for raw HTML passed to mw.html.element(). - * @constructor + * @class mw.html.Raw */ Raw: function ( value ) { this.value = value; @@ -1546,7 +1655,7 @@ var mw = ( function ( $, undefined ) { /** * Wrapper object for CDATA element contents passed to mw.html.element() - * @constructor + * @class mw.html.Cdata */ Cdata: function ( value ) { this.value = value; |