diff options
Diffstat (limited to 'resources/mediawiki/mediawiki.js')
-rw-r--r-- | resources/mediawiki/mediawiki.js | 1083 |
1 files changed, 685 insertions, 398 deletions
diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 19112aed..80223e5d 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -1,26 +1,81 @@ -/* - * Core MediaWiki JavaScript Library +/** + * Base library for MediaWiki. + * + * @class mw + * @alternateClassName mediaWiki + * @singleton */ -/*global mw:true */ + var mw = ( function ( $, undefined ) { - "use strict"; + '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 + * @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 */ /** - * Map - * * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. * - * @param global boolean Whether to store the values in the global window + * @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'. - * @return Map */ function Map( global ) { this.values = global === true ? window : {}; @@ -33,32 +88,32 @@ var mw = ( function ( $, undefined ) { * * If called with no arguments, all values will be returned. * - * @param selection mixed String key or array of keys to get values for. - * @param fallback mixed Value to use in case key(s) do not exist (optional). + * @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 Values as a string or object, null if invalid/inexistant. + * @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 += 1 ) { + for ( i = 0; i < selection.length; i++ ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { - if ( this.values[selection] === undefined ) { - if ( fallback !== undefined ) { - return fallback; - } - return null; + if ( !hasOwn.call( this.values, selection ) ) { + return fallback; } return this.values[selection]; } @@ -74,8 +129,8 @@ var mw = ( function ( $, undefined ) { /** * Sets one or multiple key/value pairs. * - * @param selection {mixed} String key or array of keys to set values for. - * @param value {mixed} Value to set (optional, only in use when key is a string) + * @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 ) { @@ -87,7 +142,7 @@ var mw = ( function ( $, undefined ) { } return true; } - if ( typeof selection === 'string' && value !== undefined ) { + if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[selection] = value; return true; } @@ -97,37 +152,40 @@ var mw = ( function ( $, undefined ) { /** * Checks if one or multiple keys exist. * - * @param selection {mixed} String key or array of keys to check - * @return {Boolean} Existence of key(s) + * @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 += 1 ) { - if ( this.values[selection[s]] === undefined ) { + for ( s = 0; s < selection.length; s++ ) { + if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { return false; } } return true; } - return this.values[selection] !== undefined; + return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** - * Message + * Object constructor for messages. + * + * Similar to the Message class in MediaWiki PHP. + * + * Format defaults to 'text'. * - * Object constructor for messages, - * similar to the Message class in MediaWiki PHP. + * @class mw.Message * - * @param map Map Instance of mw.Map - * @param key String - * @param parameters Array - * @return Message + * @constructor + * @param {mw.Map} map Message storage + * @param {string} key + * @param {Array} [parameters] */ function Message( map, key, parameters ) { - this.format = 'plain'; + this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); @@ -137,8 +195,11 @@ var mw = ( function ( $, undefined ) { 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 () { @@ -152,8 +213,8 @@ var mw = ( function ( $, undefined ) { /** * Appends (does not replace) parameters for replacement to the .parameters property. * - * @param parameters Array - * @return Message + * @param {Array} parameters + * @chainable */ params: function ( parameters ) { var i; @@ -166,25 +227,21 @@ var mw = ( function ( $, undefined ) { /** * 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 <key> if key does not exist. + * @return {string} Message as a string in the current form or `<key>` if key does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use <key> as text if key does not exist - if ( this.format !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped + 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' ) { - // @todo FIXME: Although not applicable to core Message, - // Plugins like jQueryMsg should be able to distinguish - // between 'plain' (only variable replacement and plural/gender) - // and actually parsing wikitext to HTML. + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } @@ -193,15 +250,16 @@ var mw = ( function ( $, undefined ) { text = mw.html.escape( text ); } - if ( this.format === 'parse' ) { - text = this.parser(); - } - return text; }, /** - * Changes format to parse and converts message to string + * 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 */ @@ -211,7 +269,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to plain and converts message to string + * 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 */ @@ -221,7 +282,23 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the format to html escaped and converts message to string + * 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 */ @@ -233,7 +310,8 @@ var mw = ( function ( $, undefined ) { /** * Checks if message exists * - * @return {string} String form of parsed message + * @see mw.Map#exists + * @return {boolean} */ exists: function () { return this.map.exists( this.key ); @@ -244,82 +322,98 @@ var mw = ( function ( $, undefined ) { /* Public Members */ /** - * Dummy function which in debug mode can be replaced with a function that - * emulates console.log in console-less environments. + * Dummy placeholder for {@link mw.log} + * @method */ - log: function () { }, + log: ( function () { + var log = function () {}; + log.warn = function () {}; + log.deprecate = function ( obj, key, val ) { + obj[key] = val; + }; + return log; + }() ), - /** - * @var constructor Make the Map constructor publicly available. - */ + // Make the Map constructor publicly available. Map: Map, - /** - * @var constructor Make the Message constructor publicly available. - */ + // Make the Message constructor publicly available. Message: Message, /** - * List of configuration values + * 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. * - * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). - * If $wgLegacyJavaScriptGlobals is true, this Map will have its values - * in the global window object. + * 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, /** - * @var object - * * Empty object that plugins can be installed in. + * @property */ libs: {}, - /* Extension points */ - + /** + * 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 */ /** - * Gets a message object, similar to wfMessage() + * Get a message object. + * + * Similar to wfMessage() in MediaWiki PHP. * - * @param key string Key of message to get - * @param parameter_1 mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return Message + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {mw.Message} */ - message: function ( key, parameter_1 /* [, parameter_2] */ ) { - var parameters; - // Support variadic arguments - if ( parameter_1 !== undefined ) { - parameters = slice.call( arguments ); - parameters.shift(); - } else { - parameters = []; - } + message: function ( key ) { + // Variadic arguments + var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** - * Gets a message string, similar to wfMessage() + * Get a message string using 'text' format. * - * @param key string Key of message to get - * @param parameters mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return String. + * 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 ( /* key, parameter_1, parameter_2, .. */ ) { + 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 () { @@ -338,29 +432,32 @@ var mw = ( function ( $, undefined ) { * 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', 'loading', 'loaded', 'ready', 'error' or 'missing' - * 'script': ..., - * 'style': ..., - * 'messages': { 'key': 'value' }, - * } - * } + * { + * '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' - * } - * } - */ + // + // 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 = [], @@ -369,7 +466,11 @@ var mw = ( function ( $, undefined ) { // 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; + $marker = null, + // Buffer for addEmbeddedCSS. + cssBuffer = '', + // Callbacks for addEmbeddedCSS. + cssCallbacks = $.Callbacks(); /* Private methods */ @@ -392,12 +493,13 @@ var mw = ( function ( $, undefined ) { /** * Create a new style tag and add it to the DOM. * - * @param text String: CSS text - * @param nextnode mixed: [optional] An Element or jQuery object for an element where - * the style tag should be inserted before. Otherwise appended to the <head>. - * @return HTMLStyleElement + * @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 `<head>`. + * @return {HTMLElement} Reference to the created `<style>` element. */ - function addStyleTag( text, nextnode ) { + function newStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); // Insert into document before setting cssText (bug 33305) if ( nextnode ) { @@ -429,76 +531,112 @@ var mw = ( function ( $, undefined ) { } /** - * Checks if certain cssText is safe to append to - * a stylesheet. + * Checks whether it is safe to add this css to a stylesheet. * - * Right now it only makes sure that cssText containing @import - * rules will end up in a new stylesheet (as those only work when - * placed at the start of a stylesheet; bug 35562). - * This could later be extended to take care of other bugs, such as - * the IE cssRules limit - not the same as the IE styleSheets limit). + * @private + * @param {string} cssText + * @return {boolean} False if a new one must be created. */ - function canExpandStylesheetWith( $style, cssText ) { + function canExpandStylesheetWith( cssText ) { + // Makes sure that cssText containing `@import` + // rules will end up in a new stylesheet (as those only work when + // placed at the start of a stylesheet; bug 35562). return cssText.indexOf( '@import' ) === -1; } - function addEmbeddedCSS( cssText ) { + /** + * Add a bit of CSS text to the current browser page. + * + * The CSS will be appended to an existing ResourceLoader-created `<style>` tag + * or create a new one based on whether the given `cssText` is safe for extension. + * + * @param {string} [cssText=cssBuffer] If called without cssText, + * the internal buffer will be inserted instead. + * @param {Function} [callback] + */ + function addEmbeddedCSS( cssText, callback ) { var $style, styleEl; - $style = getMarker().prev(); - // Re-use <style> tags if possible, this to try to stay - // under the IE stylesheet limit (bug 31676). - // Also verify that the the element before Marker actually is one - // that came from ResourceLoader, and not a style tag that some - // other script inserted before our marker, or, more importantly, - // it may not be a style tag at all (could be <meta> or <script>). - if ( - $style.data( 'ResourceLoaderDynamicStyleTag' ) === true && - canExpandStylesheetWith( $style, cssText ) - ) { - // There's already a dynamic <style> tag present and - // canExpandStylesheetWith() gave a green light to append more to it. - styleEl = $style.get( 0 ); - if ( styleEl.styleSheet ) { - try { - styleEl.styleSheet.cssText += cssText; // IE - } catch ( e ) { - log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e ); - } - } else { - styleEl.appendChild( document.createTextNode( String( cssText ) ) ); + + if ( callback ) { + cssCallbacks.add( callback ); + } + + // Yield once before inserting the <style> tag. There are likely + // more calls coming up which we can combine this way. + // Appending a stylesheet and waiting for the browser to repaint + // is fairly expensive, this reduces it (bug 45810) + if ( cssText ) { + // Be careful not to extend the buffer with css that needs a new stylesheet + if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) { + // Linebreak for somewhat distinguishable sections + // (the rl-cachekey comment separating each) + cssBuffer += '\n' + cssText; + // TODO: Use requestAnimationFrame in the future which will + // perform even better by not injecting styles while the browser + // is paiting. + setTimeout( function () { + // Can't pass addEmbeddedCSS to setTimeout directly because Firefox + // (below version 13) has the non-standard behaviour of passing a + // numerical "lateness" value as first argument to this callback + // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/ + addEmbeddedCSS(); + } ); + return; } + + // This is a delayed call and we got a buffer still + } else if ( cssBuffer ) { + cssText = cssBuffer; + cssBuffer = ''; } else { - $( addStyleTag( cssText, getMarker() ) ) - .data( 'ResourceLoaderDynamicStyleTag', true ); + // This is a delayed call, but buffer is already cleared by + // another delayed call. + return; } - } - function compare( a, b ) { - var i; - if ( a.length !== b.length ) { - return false; - } - for ( i = 0; i < b.length; i += 1 ) { - if ( $.isArray( a[i] ) ) { - if ( !compare( a[i], b[i] ) ) { - return false; + // By default, always create a new <style>. Appending text + // to a <style> tag means the contents have to be re-parsed (bug 45810). + // Except, of course, in IE below 9, in there we default to + // re-using and appending to a <style> tag due to the + // IE stylesheet limit (bug 31676). + if ( 'documentMode' in document && document.documentMode <= 9 ) { + + $style = getMarker().prev(); + // Verify that the the element before Marker actually is a + // <style> tag and one that came from ResourceLoader + // (not some other style tag or even a `<meta>` or `<script>`). + if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { + // There's already a dynamic <style> tag present and + // canExpandStylesheetWith() gave a green light to append more to it. + styleEl = $style.get( 0 ); + if ( styleEl.styleSheet ) { + try { + styleEl.styleSheet.cssText += cssText; // IE + } catch ( e ) { + log( 'addEmbeddedCSS fail', e ); + } + } else { + styleEl.appendChild( document.createTextNode( String( cssText ) ) ); } - } - if ( a[i] !== b[i] ) { - return false; + cssCallbacks.fire().empty(); + return; } } - return true; + + $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); + + cssCallbacks.fire().empty(); } /** * Generates an ISO8601 "basic" string from a UNIX timestamp + * @private */ function formatVersionNumber( timestamp ) { - var pad = function ( a, b, c ) { - return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); - }, - d = new Date(); + var d = new Date(); + function pad( a, b, c ) { + return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); + } d.setTime( timestamp * 1000 ); return [ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', @@ -509,15 +647,16 @@ var mw = ( function ( $, undefined ) { /** * Resolves dependencies and detects circular references. * - * @param module String Name of the top-level module whose dependencies shall be + * @private + * @param {string} module Name of the top-level module whose dependencies shall be * resolved and sorted. - * @param resolved Array Returns a topological sort of the given module and its + * @param {Array} resolved Returns a topological sort of the given module and its * dependencies, such that later modules depend on earlier modules. The array * contains the module names. If the array contains already some module names, * this function appends its result to the pre-existing array. - * @param unresolved Object [optional] Hash used to track the current dependency + * @param {Object} [unresolved] Hash used to track the current dependency * chain; used to report loops in the dependency graph. - * @throws Error if any unregistered module or a dependency loop is encountered + * @throws {Error} If any unregistered module or a dependency loop is encountered */ function sortDependencies( module, resolved, unresolved ) { var n, deps, len; @@ -566,9 +705,10 @@ var mw = ( function ( $, undefined ) { * Gets a list of module names that a module depends on in their proper dependency * order. * - * @param module string module name or array of string module names - * @return list of dependencies, including 'module'. - * @throws Error if circular reference is detected + * @private + * @param {string} module Module name or array of string module names + * @return {Array} list of dependencies, including 'module'. + * @throws {Error} If circular reference is detected */ function resolve( module ) { var m, resolved; @@ -597,10 +737,11 @@ var mw = ( function ( $, undefined ) { * One can also filter for 'unregistered', which will return the * modules names that don't have a registry entry. * - * @param states string or array of strings of module states to filter by - * @param modules array list of module names to filter (optional, by default the entire + * @private + * @param {string|string[]} states Module states to filter by + * @param {Array} [modules] List of module names to filter (optional, by default the entire * registry is used) - * @return array list of filtered module names + * @return {Array} List of filtered module names */ function filter( states, modules ) { var list, module, s, m; @@ -642,44 +783,22 @@ var mw = ( function ( $, undefined ) { * Determine whether all dependencies are in state 'ready', which means we may * execute the module or job now. * - * @param dependencies Array dependencies (module names) to be checked. - * - * @return Boolean true if all dependencies are in state 'ready', false otherwise + * @private + * @param {Array} dependencies Dependencies (module names) to be checked. + * @return {boolean} True if all dependencies are in state 'ready', false otherwise */ function allReady( dependencies ) { return filter( 'ready', dependencies ).length === dependencies.length; } /** - * 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. - * - * @param msg String text for the log entry. - * @param e Error [optional] to also log. - */ - 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( e ); - } - } - } - - /** * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs * and modules that depend upon this module. if the given module failed, propagate the 'error' * state up the dependency tree; otherwise, execute all jobs/modules that now have all their * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any. * - * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'. + * @private + * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'. */ function handlePending( module ) { var j, job, hasErrors, m, stateChange; @@ -712,22 +831,18 @@ var mw = ( function ( $, undefined ) { j -= 1; try { if ( hasErrors ) { - throw new Error ("Module " + module + " failed."); + if ( $.isFunction( job.error ) ) { + job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] ); + } } else { if ( $.isFunction( job.ready ) ) { job.ready(); } } } catch ( e ) { - if ( $.isFunction( job.error ) ) { - try { - job.error( e, [module] ); - } catch ( ex ) { - // A user-defined operation raised an exception. Swallow to protect - // our state machine! - log( 'Exception thrown by job.error()', ex ); - } - } + // A user-defined callback raised an exception. + // Swallow it to protect our state machine! + log( 'Exception thrown by job.error', e ); } } } @@ -747,27 +862,32 @@ var mw = ( function ( $, undefined ) { * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, * depending on whether document-ready has occurred yet and whether we are in async mode. * - * @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 + * @private + * @param {string} src URL to script, will be used as the src attribute in the script tag + * @param {Function} [callback] Callback which will be run when the script is done */ function addScript( src, callback, async ) { /*jshint evil:true */ - var script, head, - done = false; + var script, head, done; // Using isReady directly instead of storing it locally from // a $.fn.ready callback (bug 31895). if ( $.isReady || async ) { - // jQuery's getScript method is NOT better than doing this the old-fashioned way - // because jQuery will eval the script's code, and errors will not have sane - // line numbers. + // Can't use jQuery.getScript because that only uses <script> for cross-domain, + // it uses XHR and eval for same-domain scripts, which we don't want because it + // messes up line numbers. + // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js) + + // IE-safe way of getting the <head>. document.head isn't supported + // in old IE, and doesn't work when in the <head>. + done = false; + head = document.getElementsByTagName( 'head' )[0] || document.body; + script = document.createElement( 'script' ); - script.setAttribute( 'src', src ); - script.setAttribute( 'type', 'text/javascript' ); + script.async = true; + script.src = src; if ( $.isFunction( callback ) ) { - // Attach handlers for all browsers (based on jQuery.ajax) script.onload = script.onreadystatechange = function () { - if ( !done && ( @@ -775,24 +895,20 @@ var mw = ( function ( $, undefined ) { || /loaded|complete/.test( script.readyState ) ) ) { - done = true; - callback(); + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; - // Handle memory leak in IE. This seems to fail in - // IE7 sometimes (Permission Denied error when - // accessing script.parentNode) so wrap it in - // a try catch. - try { - script.onload = script.onreadystatechange = null; - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = undefined; - } catch ( e ) { } + // Detach the element from the document + if ( script.parentNode ) { + script.parentNode.removeChild( script ); + } + + // Dereference the element from javascript + script = undefined; + + callback(); } }; } @@ -800,20 +916,17 @@ var mw = ( function ( $, undefined ) { if ( window.opera ) { // Appending to the <head> blocks rendering completely in Opera, // so append to the <body> after document ready. This means the - // scripts only start loading after the document has been rendered, + // scripts only start loading after the document has been rendered, // but so be it. Opera users don't deserve faster web pages if their - // browser makes it impossible - $( function () { document.body.appendChild( script ); } ); + // browser makes it impossible. + $( function () { + document.body.appendChild( script ); + } ); } else { - // IE-safe way of getting the <head> . document.documentElement.head doesn't - // work in scripts that run in the <head> - head = document.getElementsByTagName( 'head' )[0]; - ( document.body || head ).appendChild( script ); + head.appendChild( script ); } } else { - document.write( mw.html.element( - 'script', { 'type': 'text/javascript', 'src': src }, '' - ) ); + document.write( mw.html.element( 'script', { 'src': src }, '' ) ); if ( $.isFunction( callback ) ) { // Document.write is synchronous, so this is called when it's done // FIXME: that's a lie. doc.write isn't actually synchronous @@ -825,10 +938,12 @@ var mw = ( function ( $, undefined ) { /** * Executes a loaded module, making it ready to use * - * @param module string module name to execute + * @private + * @param {string} module Module name to execute */ function execute( module ) { - var key, value, media, i, urls, script, markModuleReady, nestedAddScript; + var key, value, media, i, urls, cssHandle, checkCssHandles, + cssHandlesRegistered = false; if ( registry[module] === undefined ) { throw new Error( 'Module has not been registered yet: ' + module ); @@ -837,12 +952,13 @@ var mw = ( function ( $, undefined ) { } else if ( registry[module].state === 'loading' ) { throw new Error( 'Module has not completed loading yet: ' + module ); } else if ( registry[module].state === 'ready' ) { - throw new Error( 'Module has already been loaded: ' + module ); + throw new Error( 'Module has already been executed: ' + module ); } /** * Define loop-function here for efficiency * and to avoid re-using badly scoped variables. + * @ignore */ function addLink( media, url ) { var el = document.createElement( 'link' ); @@ -854,6 +970,87 @@ var mw = ( function ( $, undefined ) { el.href = url; } + function runScript() { + var script, markModuleReady, nestedAddScript; + try { + script = registry[module].script; + markModuleReady = function () { + registry[module].state = 'ready'; + handlePending( module ); + }; + nestedAddScript = function ( arr, callback, async, i ) { + // Recursively call addScript() in its own callback + // for each element of arr. + if ( i >= arr.length ) { + // We're at the end of the array + callback(); + return; + } + + addScript( arr[i], function () { + nestedAddScript( arr, callback, async, i + 1 ); + }, async ); + }; + + if ( $.isArray( script ) ) { + nestedAddScript( script, markModuleReady, registry[module].async, 0 ); + } else if ( $.isFunction( script ) ) { + registry[module].state = 'ready'; + script( $ ); + handlePending( module ); + } + } 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 + log( 'Exception thrown by ' + module, e ); + registry[module].state = 'error'; + handlePending( module ); + } + } + + // This used to be inside runScript, but since that is now fired asychronously + // (after CSS is loaded) we need to set it here right away. It is crucial that + // when execute() is called this is set synchronously, otherwise modules will get + // executed multiple times as the registry will state that it isn't loading yet. + registry[module].state = 'loading'; + + // Add localizations to message system + if ( $.isPlainObject( registry[module].messages ) ) { + mw.messages.set( registry[module].messages ); + } + + if ( $.isReady || registry[module].async ) { + // Make sure we don't run the scripts until all (potentially asynchronous) + // stylesheet insertions have completed. + ( function () { + var pending = 0; + checkCssHandles = function () { + // cssHandlesRegistered ensures we don't take off too soon, e.g. when + // one of the cssHandles is fired while we're still creating more handles. + if ( cssHandlesRegistered && pending === 0 && runScript ) { + runScript(); + runScript = undefined; // Revoke + } + }; + cssHandle = function () { + var check = checkCssHandles; + pending++; + return function () { + if (check) { + pending--; + check(); + check = undefined; // Revoke + } + }; + }; + }() ); + } else { + // We are in blocking mode, and so we can't afford to wait for CSS + cssHandle = function () {}; + // Run immediately + checkCssHandles = runScript; + } + // Process styles (see also mw.loader.implement) // * back-compat: { <media>: css } // * back-compat: { <media>: [url, ..] } @@ -872,7 +1069,7 @@ var mw = ( function ( $, undefined ) { // Strings are pre-wrapped in "@media". The media-type was just "" // (because it had to be set to something). // This is one of the reasons why this format is no longer used. - addEmbeddedCSS( value ); + addEmbeddedCSS( value, cssHandle() ); } else { // back-compat: { <media>: [url, ..] } media = key; @@ -889,7 +1086,7 @@ var mw = ( function ( $, undefined ) { addLink( media, value[i] ); } else if ( key === 'css' ) { // { "css": [css, ..] } - addEmbeddedCSS( value[i] ); + addEmbeddedCSS( value[i], cssHandle() ); } } // Not an array, but a regular object @@ -906,61 +1103,24 @@ var mw = ( function ( $, undefined ) { } } - // Add localizations to message system - if ( $.isPlainObject( registry[module].messages ) ) { - mw.messages.set( registry[module].messages ); - } - - // Execute script - try { - script = registry[module].script; - markModuleReady = function () { - registry[module].state = 'ready'; - handlePending( module ); - }; - nestedAddScript = function ( arr, callback, async, i ) { - // Recursively call addScript() in its own callback - // for each element of arr. - if ( i >= arr.length ) { - // We're at the end of the array - callback(); - return; - } - - addScript( arr[i], function () { - nestedAddScript( arr, callback, async, i + 1 ); - }, async ); - }; - - if ( $.isArray( script ) ) { - registry[module].state = 'loading'; - nestedAddScript( script, markModuleReady, registry[module].async, 0 ); - } else if ( $.isFunction( script ) ) { - registry[module].state = 'ready'; - script( $ ); - handlePending( module ); - } - } 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 - log( 'Exception thrown by ' + module + ': ' + e.message, e ); - registry[module].state = 'error'; - handlePending( module ); - } + // Kick off. + cssHandlesRegistered = true; + checkCssHandles(); } /** * Adds a dependencies to the queue with optional callbacks to be run * when the dependencies are ready or fail * - * @param dependencies string module name or array of string module names - * @param ready function callback to execute when all dependencies are ready - * @param error function callback to execute when any dependency fails - * @param async (optional) If true, load modules asynchronously even if - * document ready has not yet occurred + * @private + * @param {string|string[]} dependencies Module name or array of string module names + * @param {Function} [ready] Callback to execute when all dependencies are ready + * @param {Function} [error] Callback to execute when any dependency fails + * @param {boolean} [async] If true, load modules asynchronously even if + * document ready has not yet occurred. */ function request( dependencies, ready, error, async ) { - var regItemDeps, regItemDepLen, n; + var n; // Allow calling by single module name if ( typeof dependencies === 'string' ) { @@ -1012,6 +1172,7 @@ var mw = ( function ( $, undefined ) { /** * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] } * to a query string of the form foo.bar,baz|bar.baz,quux + * @private */ function buildModulesString( moduleMap ) { var arr = [], p, prefix; @@ -1025,14 +1186,15 @@ var mw = ( function ( $, undefined ) { /** * Asynchronously append a script tag to the end of the body * that invokes load.php - * @param moduleMap {Object}: Module map, see buildModulesString() - * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request - * @param sourceLoadScript {String}: URL of load.php - * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred + * @private + * @param {Object} moduleMap Module map, see #buildModulesString + * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request + * @param {string} sourceLoadScript URL of load.php + * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred */ function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { var request = $.extend( - { 'modules': buildModulesString( moduleMap ) }, + { modules: buildModulesString( moduleMap ) }, currReqBase ); request = sortQuery( request ); @@ -1043,10 +1205,24 @@ var mw = ( function ( $, undefined ) { /* Public Methods */ return { - addStyleTag: addStyleTag, + /** + * The module registry is exposed as an aid for debugging and inspecting page + * state; it is not a public interface for modifying the registry. + * + * @see #registry + * @property + * @private + */ + moduleRegistry: registry, /** - * Requests dependencies from server, loading and executing when things when ready. + * @inheritdoc #newStyleTag + * @method + */ + addStyleTag: newStyleTag, + + /** + * Batch-request queued dependencies from the server. */ work: function () { var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup, @@ -1127,9 +1303,9 @@ var mw = ( function ( $, undefined ) { } } - currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase ); + currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase ); // For user modules append a user name to the request. - if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) { + if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) { currReqBase.user = mw.config.get( 'wgUserName' ); } currReqBaseLength = $.param( currReqBase ).length; @@ -1183,10 +1359,10 @@ var mw = ( function ( $, undefined ) { /** * Register a source. * - * @param id {String}: Short lowercase a-Z string representing a source, only used internally. - * @param props {Object}: Object containing only the loadScript property which is a url to - * the load.php location of the source. - * @return {Boolean} + * @param {string} id Short lowercase a-Z string representing a source, only used internally. + * @param {Object} props Object containing only the loadScript property which is a url to + * the load.php location of the source. + * @return {boolean} */ addSource: function ( id, props ) { var source; @@ -1208,15 +1384,15 @@ var mw = ( function ( $, undefined ) { }, /** - * Registers a module, letting the system know about it and its + * Register a module, letting the system know about it and its * properties. Startup modules contain calls to this function. * - * @param module {String}: Module name - * @param version {Number}: Module version number as a timestamp (falls backs to 0) - * @param dependencies {String|Array|Function}: One string or array of strings of module + * @param {string} module Module name + * @param {number} version Module version number as a timestamp (falls backs to 0) + * @param {string|Array|Function} dependencies One string or array of strings of module * names on which this module depends, or a function that returns that array. - * @param group {String}: Group which the module is in (optional, defaults to null) - * @param source {String}: Name of the source. Defaults to local. + * @param {string} [group=null] Group which the module is in + * @param {string} [source='local'] Name of the source */ register: function ( module, version, dependencies, group, source ) { var m; @@ -1242,15 +1418,15 @@ var mw = ( function ( $, undefined ) { } // List the module as registered registry[module] = { - 'version': version !== undefined ? parseInt( version, 10 ) : 0, - 'dependencies': [], - 'group': typeof group === 'string' ? group : null, - 'source': typeof source === 'string' ? source: 'local', - 'state': 'registered' + version: version !== undefined ? parseInt( version, 10 ) : 0, + dependencies: [], + group: typeof group === 'string' ? group : null, + source: typeof source === 'string' ? source: 'local', + state: 'registered' }; if ( typeof dependencies === 'string' ) { // Allow dependencies to be given as a single module name - registry[module].dependencies = [dependencies]; + registry[module].dependencies = [ dependencies ]; } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) { // Allow dependencies to be given as an array of module names // or a function which returns an array @@ -1259,26 +1435,27 @@ var mw = ( function ( $, undefined ) { }, /** - * 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. + * Implement a module given the components that make up the module. + * + * When #load or #using requests one or more modules, the server + * response contain calls to this function. * * All arguments are required. * - * @param {String} module Name of module + * @param {string} module Name of module * @param {Function|Array} script Function with module code or Array of URLs to - * be used as the src attribute of a new <script> tag. + * be used as the src attribute of a new `<script>` tag. * @param {Object} style Should follow one of the following patterns: - * { "css": [css, ..] } - * { "url": { <media>: [url, ..] } } - * And for backwards compatibility (needs to be supported forever due to caching): - * { <media>: css } - * { <media>: [url, ..] } + * { "css": [css, ..] } + * { "url": { <media>: [url, ..] } } + * And for backwards compatibility (needs to be supported forever due to caching): + * { <media>: css } + * { <media>: [url, ..] } * - * The reason css strings are not concatenated anymore is bug 31676. We now check - * whether it's safe to extend the stylesheet (see canExpandStylesheetWith). + * The reason css strings are not concatenated anymore is bug 31676. We now check + * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith). * - * @param {Object} msgs List of key/value pairs to be passed through mw.messages.set + * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}. */ implement: function ( module, script, style, msgs ) { // Validate input @@ -1316,12 +1493,12 @@ var mw = ( function ( $, undefined ) { }, /** - * Executes a function as soon as one or more required modules are ready + * Execute a function as soon as one or more required modules are ready. * - * @param dependencies {String|Array} Module name or array of modules names the callback + * @param {string|Array} dependencies Module name or array of modules names the callback * dependends on to be ready before executing - * @param ready {Function} callback to execute when all dependencies are ready (optional) - * @param error {Function} callback to execute when if dependencies have a errors (optional) + * @param {Function} [ready] callback to execute when all dependencies are ready + * @param {Function} [error] callback to execute when if dependencies have a errors */ using: function ( dependencies, ready, error ) { var tod = typeof dependencies; @@ -1331,7 +1508,7 @@ var mw = ( function ( $, undefined ) { } // Allow calling with a single dependency as a string if ( tod === 'string' ) { - dependencies = [dependencies]; + dependencies = [ dependencies ]; } // Resolve entire dependency map dependencies = resolve( dependencies ); @@ -1353,20 +1530,20 @@ var mw = ( function ( $, undefined ) { }, /** - * Loads an external script or one or more modules for future use + * Load an external script or one or more modules. * - * @param modules {mixed} Either the name of a module, array of modules, + * @param {string|Array} modules Either the name of a module, array of modules, * or a URL of an external script or style - * @param type {String} mime-type to use if calling with a URL of an + * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an * external script or style; acceptable values are "text/css" and * "text/javascript"; if no type is provided, text/javascript is assumed. - * @param async {Boolean} (optional) If true, load modules asynchronously - * even if document ready has not yet occurred. If false (default), - * block before document ready and load async after. If not set, true will - * be assumed if loading a URL, and false will be assumed otherwise. + * @param {boolean} [async] If true, load modules asynchronously + * even if document ready has not yet occurred. If false, block before + * document ready and load async after. If not set, true will be + * assumed if loading a URL, and false will be assumed otherwise. */ load: function ( modules, type, async ) { - var filtered, m, module; + var filtered, m, module, l; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { @@ -1381,11 +1558,13 @@ var mw = ( function ( $, undefined ) { async = true; } if ( type === 'text/css' ) { - $( 'head' ).append( $( '<link>', { - rel: 'stylesheet', - type: 'text/css', - href: modules - } ) ); + // IE7-8 throws security warnings when inserting a <link> tag + // with a protocol-relative URL set though attributes (instead of + // properties) - when on HTTPS. See also bug #. + l = document.createElement( 'link' ); + l.rel = 'stylesheet'; + l.href = modules; + $( 'head' ).append( l ); return; } if ( type === 'text/javascript' || type === undefined ) { @@ -1396,7 +1575,7 @@ var mw = ( function ( $, undefined ) { throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type ); } // Called with single module - modules = [modules]; + modules = [ modules ]; } // Filter out undefined modules, otherwise resolve() will throw @@ -1427,14 +1606,14 @@ var mw = ( function ( $, undefined ) { return; } // Since some modules are not yet ready, queue up a request. - request( filtered, null, null, async ); + request( filtered, undefined, undefined, async ); }, /** - * Changes the state of a module + * Change the state of one or more modules. * - * @param module {String|Object} module name or object of module name/state pairs - * @param state {String} state name + * @param {string|Object} module module name or object of module name/state pairs + * @param {string} state state name */ state: function ( module, state ) { var m; @@ -1448,7 +1627,7 @@ var mw = ( function ( $, undefined ) { if ( registry[module] === undefined ) { mw.loader.register( module ); } - if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1 + if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1 && registry[module].state !== state ) { // Make sure pending modules depending on this one get executed if their // dependencies are now fulfilled! @@ -1460,9 +1639,9 @@ var mw = ( function ( $, undefined ) { }, /** - * Gets the version of a module + * Get the version of a module. * - * @param module string name of module to get version for + * @param {string} module Name of module to get version for */ getVersion: function ( module ) { if ( registry[module] !== undefined && registry[module].version !== undefined ) { @@ -1472,16 +1651,17 @@ var mw = ( function ( $, undefined ) { }, /** - * @deprecated since 1.18 use mw.loader.getVersion() instead + * @inheritdoc #getVersion + * @deprecated since 1.18 use #getVersion instead */ version: function () { return mw.loader.getVersion.apply( mw.loader, arguments ); }, /** - * Gets the state of a module + * Get the state of a module. * - * @param module string name of module to get state for + * @param {string} module name of module to get state for */ getState: function ( module ) { if ( registry[module] !== undefined && registry[module].state !== undefined ) { @@ -1502,19 +1682,52 @@ var mw = ( function ( $, undefined ) { }, /** - * For backwards-compatibility with Squid-cached pages. Loads mw.user + * Load the `mediawiki.user` module. + * + * For backwards-compatibility with cached pages from before 2013 where: + * + * - the `mediawiki.user` module didn't exist yet + * - `mw.user` was still part of mediawiki.js + * - `mw.loader.go` still existed and called after `mw.loader.load()` */ go: function () { mw.loader.load( 'mediawiki.user' ); + }, + + /** + * @inheritdoc mw.inspect#runReports + * @method + */ + inspect: function () { + var args = slice.call( arguments ); + mw.loader.using( 'mediawiki.inspect', function () { + mw.inspect.runReports.apply( mw.inspect, args ); + } ); } + }; }() ), - /** HTML construction helper functions */ + /** + * HTML construction helper functions + * + * @example + * + * var Html, output; + * + * Html = mw.html; + * output = Html.element( 'div', {}, new Html.Raw( + * Html.element( 'img', { src: '<' } ) + * ) ); + * mw.log( output ); // <div><img src="<"/></div> + * + * @class mw.html + * @singleton + */ html: ( function () { function escapeCallback( s ) { switch ( s ) { - case "'": + case '\'': return '''; case '"': return '"'; @@ -1530,46 +1743,24 @@ var mw = ( function ( $, undefined ) { return { /** * Escape a string for HTML. Converts special characters to HTML entities. - * @param s The string to escape + * @param {string} s The string to escape */ escape: function ( s ) { return s.replace( /['"<>&]/g, escapeCallback ); }, /** - * Wrapper object for raw HTML passed to mw.html.element(). - * @constructor - */ - Raw: function ( value ) { - this.value = value; - }, - - /** - * Wrapper object for CDATA element contents passed to mw.html.element() - * @constructor - */ - Cdata: function ( value ) { - this.value = value; - }, - - /** * Create an HTML element string, with safe escaping. * - * @param name The tag name. - * @param attrs An object with members mapping element names to values - * @param contents The contents of the element. May be either: + * @param {string} name The tag name. + * @param {Object} attrs An object with members mapping element names to values + * @param {Mixed} contents The contents of the element. May be either: * - string: The string is escaped. * - null or undefined: The short closing form is used, e.g. <br/>. * - this.Raw: The value attribute is included without escaping. * - this.Cdata: The value attribute is included, and an exception is * thrown if it contains an illegal ETAGO delimiter. * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2 - * - * Example: - * var h = mw.html; - * return h.element( 'div', {}, - * new h.Raw( h.element( 'img', {src: '<'} ) ) ); - * Returns <div><img src="<"/></div> */ element: function ( name, attrs, contents ) { var v, attrName, s = '<' + name; @@ -1618,6 +1809,22 @@ var mw = ( function ( $, undefined ) { } s += '</' + name + '>'; return s; + }, + + /** + * Wrapper object for raw HTML passed to mw.html.element(). + * @class mw.html.Raw + */ + Raw: function ( value ) { + this.value = value; + }, + + /** + * Wrapper object for CDATA element contents passed to mw.html.element() + * @class mw.html.Cdata + */ + Cdata: function ( value ) { + this.value = value; } }; }() ), @@ -1626,7 +1833,87 @@ var mw = ( function ( $, undefined ) { user: { options: new Map(), tokens: new Map() - } + }, + + /** + * Registry and firing of events. + * + * MediaWiki has various interface components that are extended, enhanced + * or manipulated in some other way by extensions, gadgets and even + * in core itself. + * + * This framework helps streamlining the timing of when these other + * code paths fire their plugins (instead of using document-ready, + * which can and should be limited to firing only once). + * + * Features like navigating to other wiki pages, previewing an edit + * and editing itself – without a refresh – can then retrigger these + * hooks accordingly to ensure everything still works as expected. + * + * Example usage: + * + * mw.hook( 'wikipage.content' ).add( fn ).remove( fn ); + * mw.hook( 'wikipage.content' ).fire( $content ); + * + * Handlers can be added and fired for arbitrary event names at any time. The same + * event can be fired multiple times. The last run of an event is memorized + * (similar to `$(document).ready` and `$.Deferred().done`). + * This means if an event is fired, and a handler added afterwards, the added + * function will be fired right away with the last given event data. + * + * Like Deferreds and Promises, the mw.hook object is both detachable and chainable. + * Thus allowing flexible use and optimal maintainability and authority control. + * You can pass around the `add` and/or `fire` method to another piece of code + * without it having to know the event name (or `mw.hook` for that matter). + * + * var h = mw.hook( 'bar.ready' ); + * new mw.Foo( .. ).fetch( { callback: h.fire } ); + * + * Note: Events are documented with an underscore instead of a dot in the event + * name due to jsduck not supporting dots in that position. + * + * @class mw.hook + */ + hook: ( function () { + var lists = {}; + + /** + * Create an instance of mw.hook. + * + * @method hook + * @member mw + * @param {string} name Name of hook. + * @return {mw.hook} + */ + return function ( name ) { + var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) ); + + return { + /** + * Register a hook handler + * @param {Function...} handler Function to bind. + * @chainable + */ + add: list.add, + + /** + * Unregister a hook handler + * @param {Function...} handler Function to unbind. + * @chainable + */ + remove: list.remove, + + /** + * Run a hook. + * @param {Mixed...} data + * @chainable + */ + fire: function () { + return list.fireWith( null, slice.call( arguments ) ); + } + }; + }; + }() ) }; }( jQuery ) ); |