/** * Base library for MediaWiki. * * @class mw * @alternateClassName mediaWiki * @singleton */ var mw = ( function ( $, undefined ) { 'use strict'; /* Private Members */ var hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice; /** * Log a message to window.console, if possible. Useful to force logging of some * errors that are otherwise hard to detect (I.e., this logs also in production mode). * Gets console references in each invocation, so that delayed debugging tools work * fine. No need for optimization here, which would only result in losing logs. * * @private * @method log_ * @param {string} msg text for the log entry. * @param {Error} [e] */ function log( msg, e ) { var console = window.console; if ( console && console.log ) { console.log( msg ); // If we have an exception object, log it through .error() to trigger // proper stacktraces in browsers that support it. There are no (known) // browsers that don't support .error(), that do support .log() and // have useful exception handling through .log(). if ( e && console.error ) { console.error( String( e ), e ); } } } /* Object constructors */ /** * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. * * @example * * var addies, wanted, results; * * // Create your address book * addies = new mw.Map(); * * // This data could be coming from an external source (eg. API/AJAX) * addies.set( { * 'John Doe' : '10 Wall Street, New York, USA', * 'Jane Jackson' : '21 Oxford St, London, UK', * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL' * } ); * * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson']; * * // You can detect missing keys first * if ( !addies.exists( wanted ) ) { * // One or more are missing (in this case: "George Johnson") * mw.log( 'One or more names were not found in your address book' ); * } * * // Or just let it give you what it can * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' ); * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK" * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US" * * @class mw.Map * * @constructor * @param {boolean} [global=false] Whether to store the values in the global window * object or a exclusively in the object property 'values'. */ function Map( global ) { this.values = global === true ? window : {}; return this; } Map.prototype = { /** * Get the value of one or multiple a keys. * * If called with no arguments, all values will be returned. * * @param {string|Array} selection String key or array of keys to get values for. * @param {Mixed} [fallback] Value to use in case key(s) do not exist. * @return mixed If selection was a string returns the value or null, * If selection was an array, returns an object of key/values (value is null if not found), * If selection was not passed or invalid, will return the 'values' object member (be careful as * objects are always passed by reference in JavaScript!). * @return {string|Object|null} Values as a string or object, null if invalid/inexistant. */ get: function ( selection, fallback ) { var results, i; // If we only do this in the `return` block, it'll fail for the // call to get() from the mutli-selection block. fallback = arguments.length > 1 ? fallback : null; if ( $.isArray( selection ) ) { selection = slice.call( selection ); results = {}; for ( i = 0; i < selection.length; i++ ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { if ( !hasOwn.call( this.values, selection ) ) { return fallback; } return this.values[selection]; } if ( selection === undefined ) { return this.values; } // invalid selection key return null; }, /** * Sets one or multiple key/value pairs. * * @param {string|Object} selection String key to set value for, or object mapping keys to values. * @param {Mixed} [value] Value to set (optional, only in use when key is a string) * @return {Boolean} This returns true on success, false on failure. */ set: function ( selection, value ) { var s; if ( $.isPlainObject( selection ) ) { for ( s in selection ) { this.values[s] = selection[s]; } return true; } if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[selection] = value; return true; } return false; }, /** * Checks if one or multiple keys exist. * * @param {Mixed} selection String key or array of keys to check * @return {boolean} Existence of key(s) */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { for ( s = 0; s < selection.length; s++ ) { if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { return false; } } return true; } return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** * Object constructor for messages. * * Similar to the Message class in MediaWiki PHP. * * Format defaults to 'text'. * * @class mw.Message * * @constructor * @param {mw.Map} map Message storage * @param {string} key * @param {Array} [parameters] */ function Message( map, key, parameters ) { this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); return this; } Message.prototype = { /** * Simple message parser, does $N replacement and nothing else. * * This may be overridden to provide a more complex message parser. * * The primary override is in mediawiki.jqueryMsg. * * This function will not be called for nonexistent messages. */ parser: function () { var parameters = this.parameters; return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) { var index = parseInt( match, 10 ) - 1; return parameters[index] !== undefined ? parameters[index] : '$' + match; } ); }, /** * Appends (does not replace) parameters for replacement to the .parameters property. * * @param {Array} parameters * @chainable */ params: function ( parameters ) { var i; for ( i = 0; i < parameters.length; i += 1 ) { this.parameters.push( parameters[i] ); } return this; }, /** * Converts message object to it's string form based on the state of format. * * @return {string} Message as a string in the current form or `` if key does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use as text if key does not exist if ( this.format === 'escaped' || this.format === 'parse' ) { // format 'escaped' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } if ( this.format === 'escaped' ) { text = this.parser(); text = mw.html.escape( text ); } return text; }, /** * Changes format to 'parse' and converts message to string * * If jqueryMsg is loaded, this parses the message text from wikitext * (where supported) to HTML * * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ parse: function () { this.format = 'parse'; return this.toString(); }, /** * Changes format to 'plain' and converts message to string * * This substitutes parameters, but otherwise does not change the * message text. * * @return {string} String form of plain message */ plain: function () { this.format = 'plain'; return this.toString(); }, /** * Changes format to 'text' and converts message to string * * If jqueryMsg is loaded, {{-transformation is done where supported * (such as {{plural:}}, {{gender:}}, {{int:}}). * * Otherwise, it is equivalent to plain. */ text: function () { this.format = 'text'; return this.toString(); }, /** * Changes the format to 'escaped' and converts message to string * * This is equivalent to using the 'text' format (see text method), then * HTML-escaping the output. * * @return {string} String form of html escaped message */ escaped: function () { this.format = 'escaped'; return this.toString(); }, /** * Checks if message exists * * @see mw.Map#exists * @return {boolean} */ exists: function () { return this.map.exists( this.key ); } }; /** * @class mw */ return { /* Public Members */ /** * Dummy placeholder for {@link mw.log} * @method */ log: ( function () { var log = function () {}; log.warn = function () {}; log.deprecate = function ( obj, key, val ) { obj[key] = val; }; return log; }() ), // Make the Map constructor publicly available. Map: Map, // Make the Message constructor publicly available. Message: Message, /** * Map of configuration values * * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config) * on MediaWiki.org. * * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the * global window object. * * @property {mw.Map} config */ // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`. config: null, /** * Empty object that plugins can be installed in. * @property */ libs: {}, /** * Access container for deprecated functionality that can be moved from * from their legacy location and attached to this object (e.g. a global * function that is deprecated and as stop-gap can be exposed through here). * * This was reserved for future use but never ended up being used. * * @deprecated since 1.22: Let deprecated identifiers keep their original name * and use mw.log#deprecate to create an access container for tracking. * @property */ legacy: {}, /** * Localization system * @property {mw.Map} */ messages: new Map(), /* Public Methods */ /** * Get a message object. * * Similar to wfMessage() in MediaWiki PHP. * * @param {string} key Key of message to get * @param {Mixed...} parameters Parameters for the $N replacements in messages. * @return {mw.Message} */ message: function ( key ) { // Variadic arguments var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** * Get a message string using 'text' format. * * Similar to wfMsg() in MediaWiki PHP. * * @see mw.Message * @param {string} key Key of message to get * @param {Mixed...} parameters Parameters for the $N replacements in messages. * @return {string} */ msg: function () { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Client-side module loader which integrates with the MediaWiki ResourceLoader * @class mw.loader * @singleton */ loader: ( function () { /* Private Members */ /** * Mapping of registered modules * * The jquery module is pre-registered, because it must have already * been provided for this object to have been built, and in debug mode * jquery would have been provided through a unique loader request, * making it impossible to hold back registration of jquery until after * mediawiki. * * For exact details on support for script, style and messages, look at * mw.loader.implement. * * Format: * { * 'moduleName': { * 'version': ############## (unix timestamp), * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null, * 'source': 'local', 'someforeignwiki', (or) null * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing' * 'script': ..., * 'style': ..., * 'messages': { 'key': 'value' }, * } * } * * @property * @private */ var registry = {}, // // Mapping of sources, keyed by source-id, values are objects. // Format: // { // 'sourceId': { // 'loadScript': 'http://foo.bar/w/load.php' // } // } // sources = {}, // List of modules which will be loaded as when ready batch = [], // List of modules to be loaded queue = [], // List of callback functions waiting for modules to be ready to be called jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! $marker = null, // Buffer for addEmbeddedCSS. cssBuffer = '', // Callbacks for addEmbeddedCSS. cssCallbacks = $.Callbacks(); /* Private methods */ function getMarker() { // Cached ? if ( $marker ) { return $marker; } $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); if ( $marker.length ) { return $marker; } mw.log( 'getMarker> No found, inserting dynamically.' ); $marker = $( '' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); return $marker; } /** * Create a new style tag and add it to the DOM. * * @private * @param {string} text CSS text * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be * inserted before. Otherwise it will be appended to ``. * @return {HTMLElement} Reference to the created `