From: Trevor Parscal Date: Sat, 14 May 2011 12:15:58 +0000 (+0000) Subject: Added direct file loading functionality to debug mode for both scripts and styles... X-Git-Tag: 1.31.0-rc.0~30213 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=e6bee6f043aac8a1e48e6302b32fc6712a180d79;p=lhc%2Fweb%2Fwiklou.git Added direct file loading functionality to debug mode for both scripts and styles, with callbacks for module state changes (changing to ready) and executing of jobs and modules awaiting dependency resolutions. These changes also provide a way to used mw.loader.implement with arrays of URLs for the scripts and styles arguments, which will make it possible to implement modules using user scripts. This probably solves bug #27023 - tests to verify that will be coming soon. --- diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index e1491416ae..6054791f0b 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -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 ) ); diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 03e7bc90c7..813b7897a3 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -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( diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index be553242be..9fff95a5bd 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -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 ),