resourceloader: Async all the way
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.js
index 2c88e93..3c5a5f5 100644 (file)
                        function addEmbeddedCSS( cssText, callback ) {
                                var $style, styleEl;
 
                        function addEmbeddedCSS( cssText, callback ) {
                                var $style, styleEl;
 
+                               function fireCallbacks() {
+                                       var oldCallbacks = cssCallbacks;
+                                       // Reset cssCallbacks variable so it's not polluted by any calls to
+                                       // addEmbeddedCSS() from one of the callbacks (T105973)
+                                       cssCallbacks = $.Callbacks();
+                                       oldCallbacks.fire().empty();
+                               }
+
                                if ( callback ) {
                                        cssCallbacks.add( callback );
                                }
                                if ( callback ) {
                                        cssCallbacks.add( callback );
                                }
                                                } else {
                                                        styleEl.appendChild( document.createTextNode( cssText ) );
                                                }
                                                } else {
                                                        styleEl.appendChild( document.createTextNode( cssText ) );
                                                }
-                                               cssCallbacks.fire().empty();
+                                               fireCallbacks();
                                                return;
                                        }
                                }
 
                                $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
 
                                                return;
                                        }
                                }
 
                                $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
 
-                               cssCallbacks.fire().empty();
+                               fireCallbacks();
                        }
 
                        /**
                        }
 
                        /**
                        }
 
                        /**
                        }
 
                        /**
-                        * 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.
+                        * Load and execute a script with callback.
                         *
                         * @private
                         * @param {string} src URL to script, will be used as the src attribute in the script tag
                         * @param {Function} [callback] 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
-                        * @param {boolean} [async=false] Whether to load modules asynchronously.
-                        *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
                         */
                         */
-                       function addScript( src, callback, async ) {
-                               // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
-                               if ( $.isReady || async ) {
-                                       $.ajax( {
-                                               url: src,
-                                               dataType: 'script',
-                                               // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
-                                               // XHR for a same domain request instead of <script>, which changes the request
-                                               // headers (potentially missing a cache hit), and reduces caching in general
-                                               // since browsers cache XHR much less (if at all). And XHR means we retreive
-                                               // text, so we'd need to $.globalEval, which then messes up line numbers.
-                                               crossDomain: true,
-                                               cache: true,
-                                               async: true
-                                       } ).always( callback );
-                               } else {
-                                       /*jshint evil:true */
-                                       document.write( mw.html.element( 'script', { 'src': src }, '' ) );
-                                       if ( callback ) {
-                                               // Document.write is synchronous, so this is called when it's done.
-                                               // FIXME: That's a lie. doc.write isn't actually synchronous.
-                                               callback();
-                                       }
-                               }
+                       function addScript( src, callback ) {
+                               $.ajax( {
+                                       url: src,
+                                       dataType: 'script',
+                                       // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
+                                       // XHR for a same domain request instead of <script>, which changes the request
+                                       // headers (potentially missing a cache hit), and reduces caching in general
+                                       // since browsers cache XHR much less (if at all). And XHR means we retreive
+                                       // text, so we'd need to $.globalEval, which then messes up line numbers.
+                                       crossDomain: true,
+                                       cache: true
+                               } ).always( callback );
                        }
 
                        /**
                        }
 
                        /**
 
                                if ( !hasOwn.call( registry, module ) ) {
                                        throw new Error( 'Module has not been registered yet: ' + module );
 
                                if ( !hasOwn.call( registry, module ) ) {
                                        throw new Error( 'Module has not been registered yet: ' + module );
-                               } else if ( registry[module].state === 'registered' ) {
+                               }
+                               if ( registry[module].state === 'registered' ) {
                                        throw new Error( 'Module has not been requested from the server yet: ' + module );
                                        throw new Error( 'Module has not been requested from the server yet: ' + module );
-                               } else if ( registry[module].state === 'loading' ) {
+                               }
+                               if ( registry[module].state === 'loading' ) {
                                        throw new Error( 'Module has not completed loading yet: ' + module );
                                        throw new Error( 'Module has not completed loading yet: ' + module );
-                               } else if ( registry[module].state === 'ready' ) {
+                               }
+                               if ( registry[module].state === 'ready' ) {
                                        throw new Error( 'Module has already been executed: ' + module );
                                }
 
                                        throw new Error( 'Module has already been executed: ' + module );
                                }
 
                                                        registry[module].state = 'ready';
                                                        handlePending( module );
                                                };
                                                        registry[module].state = 'ready';
                                                        handlePending( module );
                                                };
-                                               nestedAddScript = function ( arr, callback, async, i ) {
+                                               nestedAddScript = function ( arr, callback, i ) {
                                                        // Recursively call addScript() in its own callback
                                                        // for each element of arr.
                                                        if ( i >= arr.length ) {
                                                        // Recursively call addScript() in its own callback
                                                        // for each element of arr.
                                                        if ( i >= arr.length ) {
                                                        }
 
                                                        addScript( arr[i], function () {
                                                        }
 
                                                        addScript( arr[i], function () {
-                                                               nestedAddScript( arr, callback, async, i + 1 );
-                                                       }, async );
+                                                               nestedAddScript( arr, callback, i + 1 );
+                                                       } );
                                                };
 
                                                if ( $.isArray( script ) ) {
                                                };
 
                                                if ( $.isArray( script ) ) {
-                                                       nestedAddScript( script, markModuleReady, registry[module].async, 0 );
+                                                       nestedAddScript( script, markModuleReady, 0 );
                                                } else if ( $.isFunction( script ) ) {
                                                } else if ( $.isFunction( script ) ) {
-                                                       registry[module].state = 'ready';
                                                        // Pass jQuery twice so that the signature of the closure which wraps
                                                        // the script can bind both '$' and 'jQuery'.
                                                        // Pass jQuery twice so that the signature of the closure which wraps
                                                        // the script can bind both '$' and 'jQuery'.
+                                                       registry[module].state = 'ready';
                                                        script( $, $ );
                                                        handlePending( module );
                                                        script( $, $ );
                                                        handlePending( module );
+                                               } else if ( typeof script === 'string' ) {
+                                                       // Site and user modules are a legacy scripts that run in the global scope.
+                                                       // This is transported as a string instead of a function to avoid needing
+                                                       // to use string manipulation to undo the function wrapper.
+                                                       if ( module === 'user' ) {
+                                                               // Implicit dependency on the site module. Not real dependency because
+                                                               // it should run after 'site' regardless of whether it succeeds or fails.
+                                                               mw.loader.using( 'site' ).always( function () {
+                                                                       registry[module].state = 'ready';
+                                                                       $.globalEval( script );
+                                                                       handlePending( module );
+                                                               } );
+                                                       } else {
+                                                               registry[module].state = 'ready';
+                                                               $.globalEval( script );
+                                                               handlePending( module );
+                                                       }
                                                }
                                        } catch ( e ) {
                                                // This needs to NOT use mw.log because these errors are common in production mode
                                                }
                                        } catch ( e ) {
                                                // This needs to NOT use mw.log because these errors are common in production mode
                                        mw.templates.set( module, registry[module].templates );
                                }
 
                                        mw.templates.set( module, registry[module].templates );
                                }
 
-                               if ( $.isReady || registry[module].async ) {
-                                       // Make sure we don't run the scripts until all (potentially asynchronous)
-                                       // stylesheet insertions have completed.
-                                       ( function () {
-                                               var pending = 0;
-                                               checkCssHandles = function () {
-                                                       // cssHandlesRegistered ensures we don't take off too soon, e.g. when
-                                                       // one of the cssHandles is fired while we're still creating more handles.
-                                                       if ( cssHandlesRegistered && pending === 0 && runScript ) {
-                                                               runScript();
-                                                               runScript = undefined; // Revoke
+                               // Make sure we don't run the scripts until all stylesheet insertions have completed.
+                               ( function () {
+                                       var pending = 0;
+                                       checkCssHandles = function () {
+                                               // cssHandlesRegistered ensures we don't take off too soon, e.g. when
+                                               // one of the cssHandles is fired while we're still creating more handles.
+                                               if ( cssHandlesRegistered && pending === 0 && runScript ) {
+                                                       runScript();
+                                                       runScript = undefined; // Revoke
+                                               }
+                                       };
+                                       cssHandle = function () {
+                                               var check = checkCssHandles;
+                                               pending++;
+                                               return function () {
+                                                       if ( check ) {
+                                                               pending--;
+                                                               check();
+                                                               check = undefined; // Revoke
                                                        }
                                                };
                                                        }
                                                };
-                                               cssHandle = function () {
-                                                       var check = checkCssHandles;
-                                                       pending++;
-                                                       return function () {
-                                                               if ( check ) {
-                                                                       pending--;
-                                                                       check();
-                                                                       check = undefined; // Revoke
-                                                               }
-                                                       };
-                                               };
-                                       }() );
-                               } else {
-                                       // We are in blocking mode, and so we can't afford to wait for CSS
-                                       cssHandle = function () {};
-                                       // Run immediately
-                                       checkCssHandles = runScript;
-                               }
+                                       };
+                               }() );
 
                                // Process styles (see also mw.loader.implement)
                                // * back-compat: { <media>: css }
 
                                // Process styles (see also mw.loader.implement)
                                // * back-compat: { <media>: css }
                         * @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 {string|string[]} dependencies Module name or array of string module names
                         * @param {Function} [ready] Callback to execute when all dependencies are ready
                         * @param {Function} [error] Callback to execute when any dependency fails
-                        * @param {boolean} [async=false] Whether to load modules asynchronously.
-                        *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
                         */
                         */
-                       function request( dependencies, ready, error, async ) {
+                       function request( dependencies, ready, error ) {
                                // Allow calling by single module name
                                if ( typeof dependencies === 'string' ) {
                                        dependencies = [dependencies];
                                // Allow calling by single module name
                                if ( typeof dependencies === 'string' ) {
                                        dependencies = [dependencies];
                                                        return;
                                                }
                                                queue.push( module );
                                                        return;
                                                }
                                                queue.push( module );
-                                               if ( async ) {
-                                                       registry[module].async = true;
-                                               }
                                        }
                                } );
 
                                        }
                                } );
 
                        }
 
                        /**
                        }
 
                        /**
-                        * Asynchronously append a script tag to the end of the body
-                        * that invokes load.php
+                        * Load modules from load.php
                         * @private
                         * @param {Object} moduleMap Module map, see #buildModulesString
                         * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
                         * @param {string} sourceLoadScript URL of load.php
                         * @private
                         * @param {Object} moduleMap Module map, see #buildModulesString
                         * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
                         * @param {string} sourceLoadScript URL of load.php
-                        * @param {boolean} async Whether to load modules asynchronously.
-                        *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
                         */
                         */
-                       function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
+                       function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
                                var request = $.extend(
                                        { modules: buildModulesString( moduleMap ) },
                                        currReqBase
                                );
                                request = sortQuery( request );
                                // Support: IE6
                                var request = $.extend(
                                        { modules: buildModulesString( moduleMap ) },
                                        currReqBase
                                );
                                request = sortQuery( request );
                                // Support: IE6
-                               // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
-                               // isn't actually used in IE6, but MediaWiki enforces it in general.
-                               addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
+                               // Append &* to satisfy load.php's WebRequest::checkUrlExtension test.
+                               // This script isn't actually used in IE6, but MediaWiki enforces it in general.
+                               addScript( sourceLoadScript + '?' + $.param( request ) + '&*' );
                        }
 
                        /**
                        }
 
                        /**
                                        var     reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
                                                source, concatSource, origBatch, group, i, modules, sourceLoadScript,
                                                currReqBase, currReqBaseLength, moduleMap, l,
                                        var     reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
                                                source, concatSource, origBatch, group, i, modules, sourceLoadScript,
                                                currReqBase, currReqBaseLength, moduleMap, l,
-                                               lastDotIndex, prefix, suffix, bytesAdded, async;
+                                               lastDotIndex, prefix, suffix, bytesAdded;
 
                                        // Build a list of request parameters common to all requests.
                                        reqBase = {
 
                                        // Build a list of request parameters common to all requests.
                                        reqBase = {
                                                                currReqBase.user = mw.config.get( 'wgUserName' );
                                                        }
                                                        currReqBaseLength = $.param( currReqBase ).length;
                                                                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
                                                        // 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
                                                                if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
                                                                        // This request would become too long, create a new one
                                                                        // and fire off the old one
                                                                if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
                                                                        // This request would become too long, create a new one
                                                                        // and fire off the old one
-                                                                       doRequest( moduleMap, currReqBase, sourceLoadScript, async );
+                                                                       doRequest( moduleMap, currReqBase, sourceLoadScript );
                                                                        moduleMap = {};
                                                                        moduleMap = {};
-                                                                       async = true;
                                                                        l = currReqBaseLength + 9;
                                                                        mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                                }
                                                                        l = currReqBaseLength + 9;
                                                                        mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                                }
                                                                        moduleMap[prefix] = [];
                                                                }
                                                                moduleMap[prefix].push( suffix );
                                                                        moduleMap[prefix] = [];
                                                                }
                                                                moduleMap[prefix].push( suffix );
-                                                               if ( !registry[modules[i]].async ) {
-                                                                       // If this module is blocking, make the entire request blocking
-                                                                       // This is slightly suboptimal, but in practice mixing of blocking
-                                                                       // and async modules will only occur in debug mode.
-                                                                       async = false;
-                                                               }
                                                                l += bytesAdded;
                                                        }
                                                        // If there's anything left in moduleMap, request that too
                                                        if ( !$.isEmptyObject( moduleMap ) ) {
                                                                l += bytesAdded;
                                                        }
                                                        // If there's anything left in moduleMap, request that too
                                                        if ( !$.isEmptyObject( moduleMap ) ) {
-                                                               doRequest( moduleMap, currReqBase, sourceLoadScript, async );
+                                                               doRequest( moduleMap, currReqBase, sourceLoadScript );
                                                        }
                                                }
                                        }
                                                        }
                                                }
                                        }
                                        if ( typeof module !== 'string' ) {
                                                throw new Error( 'module must be of type string, not ' + typeof module );
                                        }
                                        if ( typeof module !== 'string' ) {
                                                throw new Error( 'module must be of type string, not ' + typeof module );
                                        }
-                                       if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
-                                               throw new Error( 'script must be of type function or array, not ' + typeof script );
+                                       if ( script && !$.isFunction( script ) && !$.isArray( script ) && typeof script !== 'string' ) {
+                                               throw new Error( 'script must be of type function, array, or script; not ' + typeof script );
                                        }
                                        if ( style && !$.isPlainObject( style ) ) {
                                                throw new Error( 'style must be of type object, not ' + typeof style );
                                        }
                                        if ( style && !$.isPlainObject( style ) ) {
                                                throw new Error( 'style must be of type object, not ' + typeof style );
                                 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
                                 *  external script or style; acceptable values are "text/css" and
                                 *  "text/javascript"; if no type is provided, text/javascript is assumed.
                                 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
                                 *  external script or style; acceptable values are "text/css" and
                                 *  "text/javascript"; if no type is provided, text/javascript is assumed.
-                                * @param {boolean} [async] Whether to load modules asynchronously.
-                                *  Ignored (and defaulted to `true`) if the document-ready event has already occurred.
-                                *  Defaults to `true` if loading a URL, `false` otherwise.
                                 */
                                 */
-                               load: function ( modules, type, async ) {
+                               load: function ( modules, type ) {
                                        var filtered, l;
 
                                        // Validate input
                                        var filtered, l;
 
                                        // Validate input
                                        // Allow calling with an external url or single dependency as a string
                                        if ( typeof modules === 'string' ) {
                                                if ( /^(https?:)?\/\//.test( modules ) ) {
                                        // Allow calling with an external url or single dependency as a string
                                        if ( typeof modules === 'string' ) {
                                                if ( /^(https?:)?\/\//.test( modules ) ) {
-                                                       if ( async === undefined ) {
-                                                               // Assume async for bug 34542
-                                                               async = true;
-                                                       }
                                                        if ( type === 'text/css' ) {
                                                                // Support: IE 7-8
                                                                // Use properties instead of attributes as IE throws security
                                                        if ( type === 'text/css' ) {
                                                                // Support: IE 7-8
                                                                // Use properties instead of attributes as IE throws security
                                                                return;
                                                        }
                                                        if ( type === 'text/javascript' || type === undefined ) {
                                                                return;
                                                        }
                                                        if ( type === 'text/javascript' || type === undefined ) {
-                                                               addScript( modules, null, async );
+                                                               addScript( modules );
                                                                return;
                                                        }
                                                        // Unknown type
                                                                return;
                                                        }
                                                        // Unknown type
                                                return;
                                        }
                                        // Since some modules are not yet ready, queue up a request.
                                                return;
                                        }
                                        // Since some modules are not yet ready, queue up a request.
-                                       request( filtered, undefined, undefined, async );
+                                       request( filtered, undefined, undefined );
                                },
 
                                /**
                                },
 
                                /**
                                                        // Module failed to load
                                                        descriptor.state !== 'ready' ||
                                                        // Unversioned, private, or site-/user-specific
                                                        // Module failed to load
                                                        descriptor.state !== 'ready' ||
                                                        // Unversioned, private, or site-/user-specific
-                                                       ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
+                                                       ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
                                                        // Partial descriptor
                                                        $.inArray( undefined, [ descriptor.script, descriptor.style,
                                                                        descriptor.messages, descriptor.templates ] ) !== -1
                                                        // Partial descriptor
                                                        $.inArray( undefined, [ descriptor.script, descriptor.style,
                                                                        descriptor.messages, descriptor.templates ] ) !== -1