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() ) {
// 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 );
// (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,
* 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
) );
* @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 );
}
* @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 );
return $this->modifiedTime[$context->getHash()];
}
- /* Protected Members */
+ /* Protected Methods */
protected function getLocalPath( $path ) {
return "{$this->localBasePath}/$path";
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.
*
/**
* 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
*/
if ( empty( $styles ) ) {
return array();
}
- $styles = self::collateFilePathListByOption( $styles, 'media', 'all' );
foreach ( $styles as $media => $files ) {
$uniqueFiles = array_unique( $files );
$styles[$media] = implode(
*
* @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 );
} 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(
}
}
} 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 ) {
/**
* 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
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();
+ }
}
}
* 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' )
{
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 ),