JS2 Overview

JS2 is a next-generation framework for MediaWiki, intended to support our increasingly-complex needs to manage and load Javascript.

It contains a script loader that minimizes page loading times by minifying, compressing, and grouping JavaScript files. It provides a unified approach to translation of user messages in JavaScript-based interfaces. It includes JavaScript helpers for dynamically loading JavaScript libraries that provide interfaces as needed rather than the traditional approach of loading all JavaScript in-line. And finally it promotes structuring JavaScript as highly modular reusable components, with clean separation of configuration, invocation binding and interface code.

See also Media Projects Overview, MwEmbed.

Primary motivation

edit

We need a script server to package language text into JavaScript. When a user invokes an interface component, say the add media wizard, we don't want to pull the entire interface code base and all the messages at once. Instead we just want to grab enough to display the current interface interaction. Once the user click on some tab, say the ‘archive.org search’, we then want to import the code to run the archive.org search and the localized messages specific to that interface component. In other words we don't want to package all the message text in the HTML output of the initial page because JavaScript interactions can result in many different interfaces being invoked from that context and we only want to load what is absolutely necessary on initial page display/load.

Another example: say you're on a view page. Then click “edit” on a section and we want to do that edit in-line (without going to another page). We now want to load the toolbar JavaScript all its dependences and localized msg text in a single gziped, minified and indefinitely-client-cached by SVN revision request. (Then we want to grab the actual section wiki-text via the API.)

How to use the ScriptLoader in your extension

edit

JS2 Support Extension

edit

The JS2 Support extension (broken link)includes a ScriptLoader, mwEmbed javascript library and a repository for shared javascript like jquery.ui components.

The js2-work branch has been deprecated.

mwEmbed Stand Alone

edit

You can learn more about mwEmbed and stand alone at kaltura html5 library page

This extension solely supports other extensions, and hence would be a good candidate for inclusion into core. Its built as an extension to enable development to move forward while the core integration is sorted out.

Developing with JS2

edit

To add the base js2 library support simply add the following to your LocalSettings.php:

require_once( "$IP/extensions/JS2Support/JS2Support.php" );

By default, js2 supports groups script and css requests. To disable script grouping in your localSettings.php set:

$wgEnableScriptLoader = false;

If you want a fresh copy of all the scripts ( and to disable minification in when wgEnableScriptLoader = true you can set )

$wgDebugJavaScript = true;

How to enable all js2 extensions

edit

Add the following to your LocalSettings.php:

# Base JS2 Support ( includes mwEmbed )
require_once( "$IP/extensions/JS2Support/JS2Support.php"  );

# Add Media Wizard
require_once( "$IP/extensions/AddMediaWizard/AddMediaWizard.php" ); 

# Upload Wizard
require_once( "$IP/extensions/UploadWizard/UploadWizard.php" );

# TimedMediaHandler, update to Ogg Handler with html5 player, multi-format transcoding & subtitle support. 
require_once( "$IP/extensions/TimedMediaHandler/TimedMediaHandler.php" );

Using JS2 script-grouping with your extension

edit

Once you include the extension you can use the following:

To add a javascript or css to the named paths ( so that the script loader can load it by name )

$wgScriptLoaderNamedPaths[ 'myJavascriptName' ] = 'extensions/myExtension/myJavascriptFile.js';

#Then in your php you can include your with the following line:   
$wgOut->addScriptClass( 'myJavascriptFile' /* scriptloader named path */ , 'page'/* group bucket id */ );

This lets you use the named-path for includes and gives you control over script grouping with the grouping argument. If for example your script is included on a set of special pages then you could create a bucket id string for that set to not mangle the cache of each special page .

If important that user-specific js like user-selected gadgets or user-custom pages are put into the user bucket. This is so in the future cookie based request can be issued for javascript browsers enabling us to send the same "cached" page to all logging users that share a base configuration.

If you would like to keep compatibility with non-js2 extension support you can use the base outputPage addScriptFile method:

$wgOut->addScriptFile( 'extensions/myExtension/myExtension.js' )

This way regardless of if you're running the js2support extension or not script-includes will continue to work. But you won't be able to control the script bucket, and the script-loader will have to guess what group to put it in.

Using JS2 with JS-modules

edit

JS Modules are reusable sets of javascript that can be dynamically configured, loaded and executed in arbitrary application contexts. Javascript modules promote a clean separation of configuration, interface, and application flow. These modules define relative paths for assets and can be hosted inside extensions or optionally used in stand-alone javascript interfaces.

The use of "loaders" enables dynamic packaging of modules with the intent of avoiding multiple requests, for dynamically loaded interfaces.

To add a javascript Module to your extension you must supply the relative path to the module in your main extension .php file:

$wgExtensionJavascriptModules[ 'myJsModule' ] = 'extensions/myExtension/MyJsModule';

The root of your module should contain: loader.js ( "module loader" ) {moduleName}.i18n.php ( "mediaWiki localization format localization file" ) {libraryCode.js files} ( "library code" )

JS Module Components Overview

edit

The three components of a js-module are "module loaders", "module activators" and "library code and assets".

  • "module loaders" define named paths and build the request set of the javascript and css assets

for a given interface "module". The set of requested libraries can driven by site configuration, and user preferences. The requested set of modules can include core libraries like jquery.ui helpers.

  • "module activators" Call the module loaders in a given application context and then

invoke the interface or application.

  • "library code and assets" These consist of library code and assets that is the user interface or application.

These libraries are driven by a given configuration.

Example Flow and File Contents

edit

When an application wants to use a interface component it calls the module loader from its activator. The module loader checks relevant configuration, it then issues a single script-loader request that checks javascript application state for existing satisfied dependencies or native browser support it then retrieves all the needed javascript, css and localized message text for that interface. The returned javascript will be minfied and gziped and cached on the server. The activator gets a callback that the interface library is loaded, and can then use that interface.

Example Loader loader.js:

edit

Loaders should define named class paths, named "module loader" functions and default configuration if appropriate. Style sheet assets should be named mw.style[ {styleName} ] this is so we can register the presence of style sheets in javascript and libraries can dynamically share style sheet components.

/* 
* First we define the named script class paths
* Each class  "mw.myJsModuleUI" defines its class name in the javascript
* Note these named paths should be in "JSON" not javascript 
* The scriptLoader will parse these named paths.
* 
* By putting these in javascript instead of php we can develop the library 
* in "raw" file mode ( by setting wgEnableScriptLoader = false )  
*/
mw.addClassFilePaths( {
	"mw.myJsModuleHandler" : "js/mw.myJSModuleHandler.js", 
	"mw.myJsModuleUI" : "js/mw.myJSModuleUI.js"
	"mw.style.myJsModule" : "css/mw.style.myJsModule.css"		
});

/* 
* Then we setup the module loader that builds a loader request for the given module 
*/
mw.addModuleLoader( 'myJsModule', function( callback ){
	// Set the intial request set: 
	var requestSet = ["mw.myJsModuleHandler", "mw.myJsModuleUI", "mw.style.myJsModule", "$j.ui", "$j.ui.tabs" ];
	
	// Check configuration and add to request set 
	if( mw.getConfig( 'myJsModuleFeatureOrSubModuleEnabled' ){
		// add the given feature or submodule to the requestSet
	}
	
	// If we want to enable other  "extensions" or "modules" to "addHooks" to our module loader we do that here
	$j( mw ).trigger( 'LoaderMyJsModuleAddFeatures', requestSet ); 
	
	// Load the request set and run the ModuleLoader callback. 
	mw.load( requestSet, callback ); 
	
});

Example "Activator" MyModulePage.js

edit
/* 
* MyModulePage.js is included on pages that we want via normal php extension hooks.
* Once we want to use the module we simply call the module loader
*/ 
var myModuleConfig = {
	'configOption1' : mw.getConfig( 'myJsModule.configOption1' )
}
mw.load( 'myJsModule', function(){
	var myModule = new mw.myJsModule( myModuleConfig );
	myModule.drawUI();
	
	// if your interface operates on a DOM element like a jQuery plugin you may 
	// invoke it differently ie: 
	$j('#targetElement').myJsModule( myModuleConfig );	 
}

Example Library Code

edit

The key thing to remember about library code is that it defines a named class ie: a file by name 'mw.Foo.js' would define a object named 'mw.Foo' ie something like:

  
mw.Foo = function( options ){
	this.init( options ); 
}

mw.Foo.prototype = { 
	'init' : function(){ /* constructor */ }
	'drawUI' : function(){ ... }  
}

Library code should operate off of global configuration mw.getConfig( 'configurationOption' ) and or local configuration ( where appropriate ) ie the constructor gets passed an options object that defines named value pairs of configuration or callbacks.

The other key thing to remember about library code is to handle localization in some way. The localization options are presented bellow

Localization

edit

Each javascript module hosts a {moduleName}.i18n.php file that works with the existing mediaWiki translate system. Note when the script-loader populates the msg replacements it uses mediaWiki's wfGetMsg function so any database MediaWiki namespace strings will be included in your javascript messages.

You have three options for packaging messages in your javascript module

1) If your javascript module is simple and your interface is mostly set in a single file you may want to load all your msgs at once. You can get all the msgs in your javascript module's localization file by simply running:

  
mw.includeAllModuleMessages();

Putting this function call at the top of your primary library code javascript file will be replaced with all the msgs in your modules php localization file in the current language. NOTE you won't be able to pull in msgKeys that are hosted outside of your extension ( to do that you must list the keys you want at the top of the javascript file with localization option 2 )

2) If your code includes many sub-modules and you may want fine grain control over what messages are packaged when. You can define the set of msg keys it at the top of any javascript file. For example:

  
mw.addMessageKeys( ['list', 'of', 'msg-keys' ] );

Will be replaced by the script-loader with the localized msgs for every listed key.

3) If you are doing a lot of stand alone testing it can be faster to use a json array at the top of your javascript file.

  
mw.addMessages( { "msg-key" : "English fallback" } );

Your javascript file then becomes your developer reference English messages. A maintenance scripts then copy your English fallbacks into the php localization file for the translate wiki scripts to work with. Running the JS2Support maintenance script "mergeJavascriptMsgs.php" will sync javascript into phpt files. This is the most complicated setup, if your unsure use the first option and call: mw.includeAllModuleMessages(); In your primary library code javascript.

Accessing Msgs in Javascript

edit

The JS2 Localization system works similar to its php counterpart. It includes support for {{PLURAL}} transforms.

To get a message simply issue the call:

$j('#myTarget').append( 
    gM( 'msg-key', 'replacement1', 'replacement2' ) 
);

Tight integration with jQuery means you can pass in jQuery replacements like so:

mw.addMessages( { "my-msg-key": "You should [$1 click here]" } ); 
$j('#target').append( 
    gM( 'my-msg-key', 
        $j('<a />').click( 
            function(){ 
                alert('foo' )
            }
        )
    )
);
/*
* Will result in the text "You should click here" with 'click here' carrying the jquery binding. 
*/

Plurals work the same way as in mediawiki. mwEmbed maintains a copy of every PLURAL transformation ported to javascript and dynamically packages the transformation as part of the localization script-loader request. A test suite of mediawiki vs javascript transformation for all 356 language keys is available. They might not work with fractional numbers, though.

mw.addMessages( { "my-msg-key": "You have replaced {{PLURAL:$1|$1 word|$1 words}}" } ); 
$j('#target').append( 
    gM( 'my-msg-key', 5)
);
/*
* Will result in the text "You have replaced 5 words" 
*/

For more documentation see: mwEmbed/languages/mw.Language.js and mwEmbed/languages/mw.Parser.js

mwEmbed as a Gadget

edit

mwEmbed works as a stand alone package as a gadget. The mwEmbed gadget description list the major parts of the gadget.

Debugging mwEmbed Gadget

edit

By default mwEbed Gadget is optimized for a small package and few round trips to the server which can make it difficult to debug. You have a few options to debug the mwEmbed gadget.

The easiest way to debug mwEmbed is simply add &debug=true to the URL of the page your viewing. This will cause mwEmbed to load all the JavaScript, Css and message file assets individually instead of as a package, so you can easily identify where the application is failing and use the JavaScript console log in the bug report.

Working on mwEmbed-Gadget

edit

1) Like with debugging you will want to disable the gadget and move to a user-script include. But instead of referencing prototype server you should setup your own local server with a check out of mwEmbed Stand alone ie:

mw.loader.load( 'http://localhost/MwEmbedStandAlone/remotes/mediaWiki.js?debug=true&uselang=' + mw.config.get( 'wgUserLanguage' ) );

2) Once your user script file has been update, you should be able to directly modify the scripts in your code editor and run it directly on commons or en wiki with full source path preservation.

Developing a Gadget using mwEmbed

edit

MwEmbed offers many convenient functions for mediaWiki javascript interfaces, and provied a shared repository of jquery.ui and useful jquery plugins.

  1. To use mwEmbed in your gadget you should first include the mwEmbed gadget ( try and use the exact same url as the gadget )
mw.loader.load( 'http://prototype.wikimedia.org/mwe-gadget/mwEmbed/remotes/mediaWiki.js?uselang=' + mw.config.get( 'wgUserLanguage' ) );
  1. Now once mwEmbed is ready, you can start using the mwEmbed functions
mw.ready( function(){
	// code to run using mwEmbed
});

Example Gadget using mwEmbed

edit

To quickly illustrate how mwEmbed streamlines mediaWiki javascript development, lets assume a gadget on commons wants to leave a message on a users en.wikipedia talk page.

mw.loader.load( 'http://prototype.wikimedia.org/mwe-gadget/mwEmbed/remotes/mediaWiki.js?uselang=' + mw.config.get( 'wgUserLanguage' ) );
mw.ready( function(){
	mw.addMessages({ 'mygadget-send-message-to' : "Sending message to $1" });

	$j('#targetSendMessageButton').click( function(){
		// UserName of user to send mesg to
		var targetUserName = 'JoeEditor';

		var enApiUrl = '//en.wikipedia.org/w/api.php';
		var targetTalkPage =  'User_talk:' + targetUserName;
		
		// Normally set from some input field 
		var talkPageMessage = "Thanks for your great image contribution ! ~~~~ ";
		
		// Show a dialog window while the edit is loading
		$myDialog = mw.addLoaderDialog( 
			gM( 'mygadget-send-message-to', targetUserName );
		);
		// Get the editToken
		mw.getToken( enApiUrl, targetTalkPage, function( token ) {			
			if( token === false ){
				$myDialog.html( gM('mygadget-failure-token') );
				return ;
			}
			var editRequest = {
				'action':'edit',
				'title': targetTalkPage,
				'summary': "Message from " + mw.config.get( "wgUserName" ) + " to " + targetUserName,
				'appendtext': "\n\n" + talkPageMessage,
				'token': token
			}
			mw.getJSON( enApiUrl, editRequest, function( result ){
				if(result.edit && result.edit.newrevid){				
					// success replace dialog with new dialog that includes an "ok" button:
					var buttons = { };
					buttons[ gM( 'mwe-ok' ) ] = function() {
						//do stuff now the user clicked "ok"
					};
					mw.addDialog( 
						gM('mygadget-edit-success-title' ),
						gM('mygadget-edit-success-message'),
						buttons
					);
				}else{
					$myDialog.html( gM('mygadget-failure-message') );
				}
			});			
		});
	});
});

See also

edit