diff options
Diffstat (limited to 'resources/mediawiki/mediawiki.js')
-rw-r--r-- | resources/mediawiki/mediawiki.js | 751 |
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; } |