Index: trunk/phase3/includes/resourceloader/ResourceLoader.php |
— | — | @@ -471,14 +471,15 @@ |
472 | 472 | foreach ( $modules as $name => $module ) { |
473 | 473 | wfProfileIn( __METHOD__ . '-' . $name ); |
474 | 474 | try { |
475 | | - // Scripts |
476 | 475 | $scripts = ''; |
477 | 476 | if ( $context->shouldIncludeScripts() ) { |
478 | | - // bug 27054: Append semicolon to prevent weird bugs |
479 | | - // caused by files not terminating their statements right |
480 | | - $scripts .= $module->getScript( $context ) . ";\n"; |
| 477 | + $scripts = $module->getScript( $context ); |
| 478 | + if ( is_string( $scripts ) ) { |
| 479 | + // bug 27054: Append semicolon to prevent weird bugs |
| 480 | + // caused by files not terminating their statements right |
| 481 | + $scripts .= ";\n"; |
| 482 | + } |
481 | 483 | } |
482 | | - |
483 | 484 | // Styles |
484 | 485 | $styles = array(); |
485 | 486 | if ( $context->shouldIncludeStyles() ) { |
— | — | @@ -491,7 +492,11 @@ |
492 | 493 | // Append output |
493 | 494 | switch ( $context->getOnly() ) { |
494 | 495 | case 'scripts': |
495 | | - $out .= $scripts; |
| 496 | + if ( is_string( $scripts ) ) { |
| 497 | + $out .= $scripts; |
| 498 | + } else if ( is_array( $scripts ) ) { |
| 499 | + $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() ); |
| 500 | + } |
496 | 501 | break; |
497 | 502 | case 'styles': |
498 | 503 | $out .= self::makeCombinedStyles( $styles ); |
— | — | @@ -504,7 +509,9 @@ |
505 | 510 | // (unless in debug mode) |
506 | 511 | if ( !$context->getDebug() ) { |
507 | 512 | foreach ( $styles as $media => $style ) { |
508 | | - $styles[$media] = $this->filter( 'minify-css', $style ); |
| 513 | + if ( is_string( $style ) ) { |
| 514 | + $styles[$media] = $this->filter( 'minify-css', $style ); |
| 515 | + } |
509 | 516 | } |
510 | 517 | } |
511 | 518 | $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, |
— | — | @@ -556,22 +563,24 @@ |
557 | 564 | * given properties. |
558 | 565 | * |
559 | 566 | * @param $name Module name |
560 | | - * @param $scripts Array: List of JavaScript code snippets to be executed after the |
561 | | - * module is loaded |
562 | | - * @param $styles Array: List of CSS strings keyed by media type |
| 567 | + * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code |
| 568 | + * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to |
| 569 | + * CSS files keyed by media type |
563 | 570 | * @param $messages Mixed: List of messages associated with this module. May either be an |
564 | 571 | * associative array mapping message key to value, or a JSON-encoded message blob containing |
565 | 572 | * the same data, wrapped in an XmlJsCode object. |
566 | 573 | */ |
567 | 574 | public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { |
568 | | - if ( is_array( $scripts ) ) { |
569 | | - $scripts = implode( $scripts, "\n" ); |
| 575 | + if ( is_string( $scripts ) ) { |
| 576 | + $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" ); |
| 577 | + } else if ( !is_array( $scripts ) ) { |
| 578 | + throw MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); |
570 | 579 | } |
571 | 580 | return Xml::encodeJsCall( |
572 | 581 | 'mw.loader.implement', |
573 | 582 | array( |
574 | 583 | $name, |
575 | | - new XmlJsCode( "function( $ ) {{$scripts}}" ), |
| 584 | + $scripts, |
576 | 585 | (object)$styles, |
577 | 586 | (object)$messages |
578 | 587 | ) ); |
Index: trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php |
— | — | @@ -215,21 +215,13 @@ |
216 | 216 | * @return String: JavaScript code for $context |
217 | 217 | */ |
218 | 218 | public function getScript( ResourceLoaderContext $context ) { |
219 | | - $files = array_merge( |
220 | | - $this->scripts, |
221 | | - self::tryForKey( $this->languageScripts, $context->getLanguage() ), |
222 | | - self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) |
223 | | - ); |
224 | | - if ( $context->getDebug() ) { |
225 | | - $files = array_merge( $files, $this->debugScripts ); |
226 | | - if ( $this->debugRaw ) { |
227 | | - $script = ''; |
228 | | - foreach ( $files as $file ) { |
229 | | - $path = $this->getRemotePath( $file ); |
230 | | - $script .= "\n\t" . Xml::encodeJsCall( 'mw.loader.load', array( $path ) ); |
231 | | - } |
232 | | - return $script; |
| 219 | + $files = $this->getScriptFiles( $context ); |
| 220 | + if ( $context->getDebug() && $this->debugRaw ) { |
| 221 | + $urls = array(); |
| 222 | + foreach ( $this->getScriptFiles( $context ) as $file ) { |
| 223 | + $urls[] = $this->getRemotePath( $file ); |
233 | 224 | } |
| 225 | + return $urls; |
234 | 226 | } |
235 | 227 | return $this->readScriptFiles( $files ); |
236 | 228 | } |
— | — | @@ -253,19 +245,19 @@ |
254 | 246 | * @return String: CSS code for $context |
255 | 247 | */ |
256 | 248 | public function getStyles( ResourceLoaderContext $context ) { |
257 | | - // Merge general styles and skin specific styles, retaining media type collation |
258 | | - $styles = $this->readStyleFiles( $this->styles, $this->getFlip( $context ) ); |
259 | | - $skinStyles = $this->readStyleFiles( |
260 | | - self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), |
| 249 | + $styles = $this->readStyleFiles( |
| 250 | + $this->getStyleFiles( $context ), |
261 | 251 | $this->getFlip( $context ) |
262 | 252 | ); |
263 | | - |
264 | | - foreach ( $skinStyles as $media => $style ) { |
265 | | - if ( isset( $styles[$media] ) ) { |
266 | | - $styles[$media] .= $style; |
267 | | - } else { |
268 | | - $styles[$media] = $style; |
| 253 | + if ( !$context->getOnly() && $context->getDebug() && $this->debugRaw ) { |
| 254 | + $urls = array(); |
| 255 | + foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { |
| 256 | + $urls[$mediaType] = array(); |
| 257 | + foreach ( $list as $file ) { |
| 258 | + $urls[$mediaType][] = $this->getRemotePath( $file ); |
| 259 | + } |
269 | 260 | } |
| 261 | + return $urls; |
270 | 262 | } |
271 | 263 | // Collect referenced files |
272 | 264 | $this->localFileRefs = array_unique( $this->localFileRefs ); |
— | — | @@ -381,7 +373,7 @@ |
382 | 374 | return $this->modifiedTime[$context->getHash()]; |
383 | 375 | } |
384 | 376 | |
385 | | - /* Protected Members */ |
| 377 | + /* Protected Methods */ |
386 | 378 | |
387 | 379 | protected function getLocalPath( $path ) { |
388 | 380 | return "{$this->localBasePath}/$path"; |
— | — | @@ -443,6 +435,39 @@ |
444 | 436 | } |
445 | 437 | |
446 | 438 | /** |
| 439 | + * Gets a list of file paths for all scripts in this module, in order of propper execution. |
| 440 | + * |
| 441 | + * @param $context ResourceLoaderContext: Context |
| 442 | + * @return Array: List of file paths |
| 443 | + */ |
| 444 | + protected function getScriptFiles( ResourceLoaderContext $context ) { |
| 445 | + $files = array_merge( |
| 446 | + $this->scripts, |
| 447 | + self::tryForKey( $this->languageScripts, $context->getLanguage() ), |
| 448 | + self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) |
| 449 | + ); |
| 450 | + if ( $context->getDebug() ) { |
| 451 | + $files = array_merge( $files, $this->debugScripts ); |
| 452 | + } |
| 453 | + return $files; |
| 454 | + } |
| 455 | + |
| 456 | + /** |
| 457 | + * Gets a list of file paths for all styles in this module, in order of propper inclusion. |
| 458 | + * |
| 459 | + * @param $context ResourceLoaderContext: Context |
| 460 | + * @return Array: List of file paths |
| 461 | + */ |
| 462 | + protected function getStyleFiles( ResourceLoaderContext $context ) { |
| 463 | + return array_merge_recursive( |
| 464 | + self::collateFilePathListByOption( $this->styles, 'media', 'all' ), |
| 465 | + self::collateFilePathListByOption( |
| 466 | + self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' |
| 467 | + ) |
| 468 | + ); |
| 469 | + } |
| 470 | + |
| 471 | + /** |
447 | 472 | * Gets the contents of a list of JavaScript files. |
448 | 473 | * |
449 | 474 | * @param $scripts Array: List of file paths to scripts to read, remap and concetenate |
— | — | @@ -467,7 +492,8 @@ |
468 | 493 | /** |
469 | 494 | * Gets the contents of a list of CSS files. |
470 | 495 | * |
471 | | - * @param $styles Array: List of file paths to styles to read, remap and concetenate |
| 496 | + * @param $styles Array: List of media type/list of file paths pairs, to read, remap and |
| 497 | + * concetenate |
472 | 498 | * @return Array: List of concatenated and remapped CSS data from $styles, |
473 | 499 | * keyed by media type |
474 | 500 | */ |
— | — | @@ -475,7 +501,6 @@ |
476 | 502 | if ( empty( $styles ) ) { |
477 | 503 | return array(); |
478 | 504 | } |
479 | | - $styles = self::collateFilePathListByOption( $styles, 'media', 'all' ); |
480 | 505 | foreach ( $styles as $media => $files ) { |
481 | 506 | $uniqueFiles = array_unique( $files ); |
482 | 507 | $styles[$media] = implode( |
Index: trunk/phase3/resources/mediawiki/mediawiki.js |
— | — | @@ -733,7 +733,7 @@ |
734 | 734 | * |
735 | 735 | * @param module string module name to execute |
736 | 736 | */ |
737 | | - function execute( module ) { |
| 737 | + function execute( module, callback ) { |
738 | 738 | var _fn = 'mw.loader::execute> '; |
739 | 739 | if ( typeof registry[module] === 'undefined' ) { |
740 | 740 | throw new Error( 'Module has not been registered yet: ' + module ); |
— | — | @@ -744,30 +744,74 @@ |
745 | 745 | } else if ( registry[module].state === 'ready' ) { |
746 | 746 | throw new Error( 'Module has already been loaded: ' + module ); |
747 | 747 | } |
748 | | - // Add style sheet to document |
749 | | - if ( typeof registry[module].style === 'string' && registry[module].style.length ) { |
750 | | - $marker.before( mw.html.element( 'style', |
751 | | - { type: 'text/css' }, |
752 | | - new mw.html.Cdata( registry[module].style ) |
753 | | - ) ); |
754 | | - } else if ( typeof registry[module].style === 'object' |
755 | | - && !( $.isArray( registry[module].style ) ) ) |
756 | | - { |
| 748 | + // Add styles |
| 749 | + if ( $.isPlainObject( registry[module].style ) ) { |
757 | 750 | for ( var media in registry[module].style ) { |
758 | | - $marker.before( mw.html.element( 'style', |
759 | | - { type: 'text/css', media: media }, |
760 | | - new mw.html.Cdata( registry[module].style[media] ) |
761 | | - ) ); |
| 751 | + var style = registry[module].style[media]; |
| 752 | + if ( $.isArray( style ) ) { |
| 753 | + for ( var i = 0; i < style.length; i++ ) { |
| 754 | + $marker.before( mw.html.element( 'link', { |
| 755 | + 'type': 'text/css', |
| 756 | + 'rel': 'stylesheet', |
| 757 | + 'href': style[i] |
| 758 | + } ) ); |
| 759 | + } |
| 760 | + } else if ( typeof style === 'string' ) { |
| 761 | + $marker.before( mw.html.element( |
| 762 | + 'style', |
| 763 | + { 'type': 'text/css', 'media': media }, |
| 764 | + new mw.html.Cdata( style ) |
| 765 | + ) ); |
| 766 | + } |
762 | 767 | } |
763 | 768 | } |
764 | 769 | // Add localizations to message system |
765 | | - if ( typeof registry[module].messages === 'object' ) { |
| 770 | + if ( $.isPlainObject( registry[module].messages ) ) { |
766 | 771 | mw.messages.set( registry[module].messages ); |
767 | 772 | } |
768 | 773 | // Execute script |
769 | 774 | try { |
770 | | - registry[module].script( jQuery ); |
771 | | - registry[module].state = 'ready'; |
| 775 | + var script = registry[module].script; |
| 776 | + if ( $.isArray( script ) ) { |
| 777 | + var done = 0; |
| 778 | + for ( var i = 0; i < script.length; i++ ) { |
| 779 | + registry[module].state = 'loading'; |
| 780 | + addScript( script[i], function() { |
| 781 | + if ( ++done == script.length ) { |
| 782 | + registry[module].state = 'ready'; |
| 783 | + handlePending(); |
| 784 | + if ( $.isFunction( callback ) ) { |
| 785 | + callback(); |
| 786 | + } |
| 787 | + } |
| 788 | + } ); |
| 789 | + } |
| 790 | + } else if ( $.isFunction( script ) ) { |
| 791 | + script( jQuery ); |
| 792 | + registry[module].state = 'ready'; |
| 793 | + handlePending(); |
| 794 | + if ( $.isFunction( callback ) ) { |
| 795 | + callback(); |
| 796 | + } |
| 797 | + } |
| 798 | + } catch ( e ) { |
| 799 | + // This needs to NOT use mw.log because these errors are common in production mode |
| 800 | + // and not in debug mode, such as when a symbol that should be global isn't exported |
| 801 | + if ( window.console && typeof window.console.log === 'function' ) { |
| 802 | + console.log( _fn + 'Exception thrown by ' + module + ': ' + e.message ); |
| 803 | + console.log( e ); |
| 804 | + } |
| 805 | + registry[module].state = 'error'; |
| 806 | + } |
| 807 | + } |
| 808 | + |
| 809 | + /** |
| 810 | + * Automatically executes jobs and modules which are pending with satistifed dependencies. |
| 811 | + * |
| 812 | + * This is used when dependencies are satisfied, such as when a module is executed. |
| 813 | + */ |
| 814 | + function handlePending() { |
| 815 | + try { |
772 | 816 | // Run jobs who's dependencies have just been met |
773 | 817 | for ( var j = 0; j < jobs.length; j++ ) { |
774 | 818 | if ( compare( |
— | — | @@ -793,13 +837,6 @@ |
794 | 838 | } |
795 | 839 | } |
796 | 840 | } catch ( e ) { |
797 | | - // This needs to NOT use mw.log because these errors are common in production mode |
798 | | - // and not in debug mode, such as when a symbol that should be global isn't exported |
799 | | - if ( window.console && typeof window.console.log === 'function' ) { |
800 | | - console.log( _fn + 'Exception thrown by ' + module + ': ' + e.message ); |
801 | | - console.log( e ); |
802 | | - } |
803 | | - registry[module].state = 'error'; |
804 | 841 | // Run error callbacks of jobs affected by this condition |
805 | 842 | for ( var j = 0; j < jobs.length; j++ ) { |
806 | 843 | if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) { |
— | — | @@ -883,8 +920,11 @@ |
884 | 921 | /** |
885 | 922 | * Adds a script tag to the body, either using document.write or low-level DOM manipulation, |
886 | 923 | * depending on whether document-ready has occured yet. |
| 924 | + * |
| 925 | + * @param src String: URL to script, will be used as the src attribute in the script tag |
| 926 | + * @param callback Function: Optional callback which will be run when the script is done |
887 | 927 | */ |
888 | | - function addScript( src ) { |
| 928 | + function addScript( src, callback ) { |
889 | 929 | if ( ready ) { |
890 | 930 | // jQuery's getScript method is NOT better than doing this the old-fassioned way |
891 | 931 | // because jQuery will eval the script's code, and errors will not have sane |
— | — | @@ -892,11 +932,18 @@ |
893 | 933 | var script = document.createElement( 'script' ); |
894 | 934 | script.setAttribute( 'src', src ); |
895 | 935 | script.setAttribute( 'type', 'text/javascript' ); |
| 936 | + if ( $.isFunction( callback ) ) { |
| 937 | + script.onload = script.onreadystatechange = callback; |
| 938 | + } |
896 | 939 | document.body.appendChild( script ); |
897 | 940 | } else { |
898 | 941 | document.write( mw.html.element( |
899 | 942 | 'script', { 'type': 'text/javascript', 'src': src }, '' |
900 | 943 | ) ); |
| 944 | + if ( $.isFunction( callback ) ) { |
| 945 | + // Document.write is synchronous, so this is called when it's done |
| 946 | + callback(); |
| 947 | + } |
901 | 948 | } |
902 | 949 | } |
903 | 950 | |
— | — | @@ -1050,27 +1097,36 @@ |
1051 | 1098 | * Implements a module, giving the system a course of action to take |
1052 | 1099 | * upon loading. Results of a request for one or more modules contain |
1053 | 1100 | * calls to this function. |
| 1101 | + * |
| 1102 | + * All arguments are required. |
| 1103 | + * |
| 1104 | + * @param module String: Name of module |
| 1105 | + * @param script Mixed: Function of module code or String of URL to be used as the src |
| 1106 | + * attribute when adding a script element to the body |
| 1107 | + * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs |
| 1108 | + * keyed by media-type |
| 1109 | + * as the href attribute when adding a link element to the head |
| 1110 | + * @param msgs Object: List of key/value pairs to be passed through mw.messages.set |
1054 | 1111 | */ |
1055 | | - this.implement = function( module, script, style, localization ) { |
| 1112 | + this.implement = function( module, script, style, msgs ) { |
| 1113 | + // Validate input |
| 1114 | + if ( typeof module !== 'string' ) { |
| 1115 | + throw new Error( 'module must be a string, not a ' + typeof module ); |
| 1116 | + } |
| 1117 | + if ( !$.isFunction( script ) && !$.isArray( script ) ) { |
| 1118 | + throw new Error( 'script must be a function or an array, not a ' + typeof script ); |
| 1119 | + } |
| 1120 | + if ( !$.isPlainObject( style ) ) { |
| 1121 | + throw new Error( 'style must be a object or a string, not a ' + typeof style ); |
| 1122 | + } |
| 1123 | + if ( !$.isPlainObject( msgs ) ) { |
| 1124 | + throw new Error( 'msgs must be an object, not a ' + typeof msgs ); |
| 1125 | + } |
1056 | 1126 | // Automatically register module |
1057 | 1127 | if ( typeof registry[module] === 'undefined' ) { |
1058 | 1128 | mw.loader.register( module ); |
1059 | 1129 | } |
1060 | | - // Validate input |
1061 | | - if ( !$.isFunction( script ) ) { |
1062 | | - throw new Error( 'script must be a function, not a ' + typeof script ); |
1063 | | - } |
1064 | | - if ( typeof style !== 'undefined' |
1065 | | - && typeof style !== 'string' |
1066 | | - && typeof style !== 'object' ) |
1067 | | - { |
1068 | | - throw new Error( 'style must be a string or object, not a ' + typeof style ); |
1069 | | - } |
1070 | | - if ( typeof localization !== 'undefined' |
1071 | | - && typeof localization !== 'object' ) |
1072 | | - { |
1073 | | - throw new Error( 'localization must be an object, not a ' + typeof localization ); |
1074 | | - } |
| 1130 | + // Check for duplicate implementation |
1075 | 1131 | if ( typeof registry[module] !== 'undefined' |
1076 | 1132 | && typeof registry[module].script !== 'undefined' ) |
1077 | 1133 | { |
— | — | @@ -1080,14 +1136,8 @@ |
1081 | 1137 | registry[module].state = 'loaded'; |
1082 | 1138 | // Attach components |
1083 | 1139 | registry[module].script = script; |
1084 | | - if ( typeof style === 'string' |
1085 | | - || typeof style === 'object' && !( style instanceof Array ) ) |
1086 | | - { |
1087 | | - registry[module].style = style; |
1088 | | - } |
1089 | | - if ( typeof localization === 'object' ) { |
1090 | | - registry[module].messages = localization; |
1091 | | - } |
| 1140 | + registry[module].style = style; |
| 1141 | + registry[module].messages = msgs; |
1092 | 1142 | // Execute or queue callback |
1093 | 1143 | if ( compare( |
1094 | 1144 | filter( ['ready'], registry[module].dependencies ), |