Added direct file loading functionality to debug mode for both scripts and styles...
authorTrevor Parscal <tparscal@users.mediawiki.org>
Sat, 14 May 2011 12:15:58 +0000 (12:15 +0000)
committerTrevor Parscal <tparscal@users.mediawiki.org>
Sat, 14 May 2011 12:15:58 +0000 (12:15 +0000)
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
resources/mediawiki/mediawiki.js

index e149141..6054791 100644 (file)
@@ -471,14 +471,15 @@ class ResourceLoader {
                foreach ( $modules as $name => $module ) {
                        wfProfileIn( __METHOD__ . '-' . $name );
                        try {
-                               // Scripts
                                $scripts = '';
                                if ( $context->shouldIncludeScripts() ) {
-                                       // bug 27054: Append semicolon to prevent weird bugs
-                                       // caused by files not terminating their statements right
-                                       $scripts .= $module->getScript( $context ) . ";\n";
+                                       $scripts = $module->getScript( $context );
+                                       if ( is_string( $scripts ) ) {
+                                               // bug 27054: Append semicolon to prevent weird bugs
+                                               // caused by files not terminating their statements right
+                                               $scripts .= ";\n";
+                                       }
                                }
-
                                // Styles
                                $styles = array();
                                if ( $context->shouldIncludeStyles() ) {
@@ -491,7 +492,11 @@ class ResourceLoader {
                                // Append output
                                switch ( $context->getOnly() ) {
                                        case 'scripts':
-                                               $out .= $scripts;
+                                               if ( is_string( $scripts ) ) {
+                                                       $out .= $scripts;
+                                               } else if ( is_array( $scripts ) ) {
+                                                       $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() );
+                                               }
                                                break;
                                        case 'styles':
                                                $out .= self::makeCombinedStyles( $styles );
@@ -504,7 +509,9 @@ class ResourceLoader {
                                                // (unless in debug mode)
                                                if ( !$context->getDebug() ) {
                                                        foreach ( $styles as $media => $style ) {
-                                                               $styles[$media] = $this->filter( 'minify-css', $style );
+                                                               if ( is_string( $style ) ) {
+                                                                       $styles[$media] = $this->filter( 'minify-css', $style );
+                                                               }
                                                        }
                                                }
                                                $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
@@ -556,22 +563,24 @@ class ResourceLoader {
         * given properties.
         *
         * @param $name Module name
-        * @param $scripts Array: List of JavaScript code snippets to be executed after the 
-        *     module is loaded
-        * @param $styles Array: List of CSS strings keyed by media type
+        * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
+        * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to
+        * CSS files keyed by media type
         * @param $messages Mixed: List of messages associated with this module. May either be an 
         *     associative array mapping message key to value, or a JSON-encoded message blob containing
         *     the same data, wrapped in an XmlJsCode object.
         */
        public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
-               if ( is_array( $scripts ) ) {
-                       $scripts = implode( $scripts, "\n" );
+               if ( is_string( $scripts ) ) {
+                       $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" );
+               } else if ( !is_array( $scripts ) ) {
+                       throw MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
                }
                return Xml::encodeJsCall( 
                        'mw.loader.implement', 
                        array(
                                $name,
-                               new XmlJsCode( "function( $ ) {{$scripts}}" ),
+                               $scripts,
                                (object)$styles,
                                (object)$messages
                        ) );
index 03e7bc9..813b789 100644 (file)
@@ -215,21 +215,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * @return String: JavaScript code for $context
         */
        public function getScript( ResourceLoaderContext $context ) {
-               $files = array_merge(
-                       $this->scripts,
-                       self::tryForKey( $this->languageScripts, $context->getLanguage() ),
-                       self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
-               );
-               if ( $context->getDebug() ) {
-                       $files = array_merge( $files, $this->debugScripts );
-                       if ( $this->debugRaw ) {
-                               $script = '';
-                               foreach ( $files as $file ) {
-                                       $path = $this->getRemotePath( $file );
-                                       $script .= "\n\t" . Xml::encodeJsCall( 'mw.loader.load', array( $path ) );
-                               }
-                               return $script;
+               $files = $this->getScriptFiles( $context );
+               if ( $context->getDebug() && $this->debugRaw ) {
+                       $urls = array();
+                       foreach ( $this->getScriptFiles( $context ) as $file ) {
+                               $urls[] = $this->getRemotePath( $file );
                        }
+                       return $urls;
                }
                return $this->readScriptFiles( $files );
        }
@@ -253,19 +245,19 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * @return String: CSS code for $context
         */
        public function getStyles( ResourceLoaderContext $context ) {
-               // Merge general styles and skin specific styles, retaining media type collation
-               $styles = $this->readStyleFiles( $this->styles, $this->getFlip( $context ) );
-               $skinStyles = $this->readStyleFiles( 
-                       self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
+               $styles = $this->readStyleFiles(
+                       $this->getStyleFiles( $context ),
                        $this->getFlip( $context )
                );
-               
-               foreach ( $skinStyles as $media => $style ) {
-                       if ( isset( $styles[$media] ) ) {
-                               $styles[$media] .= $style;
-                       } else {
-                               $styles[$media] = $style;
+               if ( !$context->getOnly() && $context->getDebug() && $this->debugRaw ) {
+                       $urls = array();
+                       foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
+                               $urls[$mediaType] = array();
+                               foreach ( $list as $file ) {
+                                       $urls[$mediaType][] = $this->getRemotePath( $file );
+                               }
                        }
+                       return $urls;
                }
                // Collect referenced files
                $this->localFileRefs = array_unique( $this->localFileRefs );
@@ -381,7 +373,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                return $this->modifiedTime[$context->getHash()];
        }
 
-       /* Protected Members */
+       /* Protected Methods */
 
        protected function getLocalPath( $path ) {
                return "{$this->localBasePath}/$path";
@@ -442,6 +434,39 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                return array();
        }
 
+       /**
+        * Gets a list of file paths for all scripts in this module, in order of propper execution.
+        * 
+        * @param $context ResourceLoaderContext: Context
+        * @return Array: List of file paths
+        */
+       protected function getScriptFiles( ResourceLoaderContext $context ) {
+               $files = array_merge(
+                       $this->scripts,
+                       self::tryForKey( $this->languageScripts, $context->getLanguage() ),
+                       self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
+               );
+               if ( $context->getDebug() ) {
+                       $files = array_merge( $files, $this->debugScripts );
+               }
+               return $files;
+       }
+
+       /**
+        * Gets a list of file paths for all styles in this module, in order of propper inclusion.
+        * 
+        * @param $context ResourceLoaderContext: Context
+        * @return Array: List of file paths
+        */
+       protected function getStyleFiles( ResourceLoaderContext $context ) {
+               return array_merge_recursive(
+                       self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
+                       self::collateFilePathListByOption(
+                               self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all'
+                       )
+               );
+       }
+
        /**
         * Gets the contents of a list of JavaScript files.
         * 
@@ -467,7 +492,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
        /**
         * Gets the contents of a list of CSS files.
         * 
-        * @param $styles Array: List of file paths to styles to read, remap and concetenate
+        * @param $styles Array: List of media type/list of file paths pairs, to read, remap and
+        * concetenate
         * @return Array: List of concatenated and remapped CSS data from $styles, 
         *     keyed by media type
         */
@@ -475,7 +501,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                if ( empty( $styles ) ) {
                        return array();
                }
-               $styles = self::collateFilePathListByOption( $styles, 'media', 'all' );
                foreach ( $styles as $media => $files ) {
                        $uniqueFiles = array_unique( $files );
                        $styles[$media] = implode(
index be55324..9fff95a 100644 (file)
@@ -733,7 +733,7 @@ window.mediaWiki = new ( function( $ ) {
                 *
                 * @param module string module name to execute
                 */
-               function execute( module ) {
+               function execute( module, callback ) {
                        var _fn = 'mw.loader::execute> ';
                        if ( typeof registry[module] === 'undefined' ) {
                                throw new Error( 'Module has not been registered yet: ' + module );
@@ -744,30 +744,74 @@ window.mediaWiki = new ( function( $ ) {
                        } else if ( registry[module].state === 'ready' ) {
                                throw new Error( 'Module has already been loaded: ' + module );
                        }
-                       // Add style sheet to document
-                       if ( typeof registry[module].style === 'string' && registry[module].style.length ) {
-                               $marker.before( mw.html.element( 'style',
-                                               { type: 'text/css' },
-                                               new mw.html.Cdata( registry[module].style )
-                                       ) );
-                       } else if ( typeof registry[module].style === 'object'
-                               && !( $.isArray( registry[module].style ) ) )
-                       {
+                       // Add styles
+                       if ( $.isPlainObject( registry[module].style ) ) {
                                for ( var media in registry[module].style ) {
-                                       $marker.before( mw.html.element( 'style',
-                                               { type: 'text/css', media: media },
-                                               new mw.html.Cdata( registry[module].style[media] )
-                                       ) );
+                                       var style = registry[module].style[media];
+                                       if ( $.isArray( style ) ) {
+                                               for ( var i = 0; i < style.length; i++ ) {
+                                                       $marker.before( mw.html.element( 'link', {
+                                                               'type': 'text/css',
+                                                               'rel': 'stylesheet',
+                                                               'href': style[i]
+                                                       } ) );
+                                               }
+                                       } else if ( typeof style === 'string' ) {
+                                               $marker.before( mw.html.element(
+                                                       'style',
+                                                       { 'type': 'text/css', 'media': media },
+                                                       new mw.html.Cdata( style )
+                                               ) );
+                                       }
                                }
                        }
                        // Add localizations to message system
-                       if ( typeof registry[module].messages === 'object' ) {
+                       if ( $.isPlainObject( registry[module].messages ) ) {
                                mw.messages.set( registry[module].messages );
                        }
                        // Execute script
                        try {
-                               registry[module].script( jQuery );
-                               registry[module].state = 'ready';
+                               var script = registry[module].script;
+                               if ( $.isArray( script ) ) {
+                                       var done = 0;
+                                       for ( var i = 0; i < script.length; i++ ) {
+                                               registry[module].state = 'loading';
+                                               addScript( script[i], function() {
+                                                       if ( ++done == script.length ) {
+                                                               registry[module].state = 'ready';
+                                                               handlePending();
+                                                               if ( $.isFunction( callback ) ) {
+                                                                       callback();
+                                                               }
+                                                       }
+                                               } );
+                                       }
+                               } else if ( $.isFunction( script ) ) {
+                                       script( jQuery );
+                                       registry[module].state = 'ready';
+                                       handlePending();
+                                       if ( $.isFunction( callback ) ) {
+                                               callback();
+                                       }
+                               }
+                       } 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( _fn + 'Exception thrown by ' + module + ': ' + e.message );
+                                       console.log( e );
+                               }
+                               registry[module].state = 'error';
+                       }
+               }
+
+               /**
+                * Automatically executes jobs and modules which are pending with satistifed dependencies.
+                * 
+                * This is used when dependencies are satisfied, such as when a module is executed.
+                */
+               function handlePending() {
+                       try {
                                // Run jobs who's dependencies have just been met
                                for ( var j = 0; j < jobs.length; j++ ) {
                                        if ( compare(
@@ -793,13 +837,6 @@ window.mediaWiki = new ( function( $ ) {
                                        }
                                }
                        } 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( _fn + 'Exception thrown by ' + module + ': ' + e.message );
-                                       console.log( e );
-                               }
-                               registry[module].state = 'error';
                                // Run error callbacks of jobs affected by this condition
                                for ( var j = 0; j < jobs.length; j++ ) {
                                        if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) {
@@ -883,8 +920,11 @@ window.mediaWiki = new ( function( $ ) {
                /**
                 * Adds a script tag to the body, either using document.write or low-level DOM manipulation,
                 * depending on whether document-ready has occured yet.
+                * 
+                * @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 ) {
+               function addScript( src, callback ) {
                        if ( ready ) {
                                // jQuery's getScript method is NOT better than doing this the old-fassioned way
                                // because jQuery will eval the script's code, and errors will not have sane
@@ -892,11 +932,18 @@ window.mediaWiki = new ( function( $ ) {
                                var script = document.createElement( 'script' );
                                script.setAttribute( 'src', src );
                                script.setAttribute( 'type', 'text/javascript' );
+                               if ( $.isFunction( callback ) ) {
+                                       script.onload = script.onreadystatechange = callback;
+                               }
                                document.body.appendChild( script );
                        } else {
                                document.write( mw.html.element(
                                        'script', { 'type': 'text/javascript', 'src': src }, ''
                                ) );
+                               if ( $.isFunction( callback ) ) {
+                                       // Document.write is synchronous, so this is called when it's done
+                                       callback();
+                               }
                        }
                }
 
@@ -1050,27 +1097,36 @@ window.mediaWiki = new ( function( $ ) {
                 * Implements a module, giving the system a course of action to take
                 * upon loading. Results of a request for one or more modules contain
                 * calls to this function.
+                * 
+                * 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
+                * as the href attribute when adding a link element to the head
+                * @param msgs Object: List of key/value pairs to be passed through mw.messages.set
                 */
-               this.implement = function( module, script, style, localization ) {
-                       // Automatically register module
-                       if ( typeof registry[module] === 'undefined' ) {
-                               mw.loader.register( module );
-                       }
+               this.implement = function( module, script, style, msgs ) {
                        // Validate input
-                       if ( !$.isFunction( script ) ) {
-                               throw new Error( 'script must be a function, not a ' + typeof script );
+                       if ( typeof module !== 'string' ) {
+                               throw new Error( 'module must be a string, not a ' + typeof module );
                        }
-                       if ( typeof style !== 'undefined'
-                               && typeof style !== 'string'
-                               && typeof style !== 'object' )
-                       {
-                               throw new Error( 'style must be a string or object, not a ' + typeof style );
+                       if ( !$.isFunction( script ) && !$.isArray( script ) ) {
+                               throw new Error( 'script must be a function or an array, not a ' + typeof script );
                        }
-                       if ( typeof localization !== 'undefined'
-                               && typeof localization !== 'object' )
-                       {
-                               throw new Error( 'localization must be an object, not a ' + typeof localization );
+                       if ( !$.isPlainObject( style ) ) {
+                               throw new Error( 'style must be a object or a string, not a ' + typeof style );
+                       }
+                       if ( !$.isPlainObject( msgs ) ) {
+                               throw new Error( 'msgs must be an object, not a ' + typeof msgs );
+                       }
+                       // Automatically register module
+                       if ( typeof registry[module] === 'undefined' ) {
+                               mw.loader.register( module );
                        }
+                       // Check for duplicate implementation
                        if ( typeof registry[module] !== 'undefined'
                                && typeof registry[module].script !== 'undefined' )
                        {
@@ -1080,14 +1136,8 @@ window.mediaWiki = new ( function( $ ) {
                        registry[module].state = 'loaded';
                        // Attach components
                        registry[module].script = script;
-                       if ( typeof style === 'string'
-                               || typeof style === 'object' && !( style instanceof Array ) )
-                       {
-                               registry[module].style = style;
-                       }
-                       if ( typeof localization === 'object' ) {
-                               registry[module].messages = localization;
-                       }
+                       registry[module].style = style;
+                       registry[module].messages = msgs;
                        // Execute or queue callback
                        if ( compare(
                                filter( ['ready'], registry[module].dependencies ),