* @singleton
*/
-/* global mwNow */
-/* eslint-disable no-use-before-define */
-
( function ( $ ) {
'use strict';
var mw, StringSet, log,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
- trackCallbacks = $.Callbacks( 'memory' ),
- trackHandlers = [],
trackQueue = [];
/**
}() );
}
+ /**
+ * Alias property to the global object.
+ *
+ * @private
+ * @static
+ * @member mw.Map
+ * @param {mw.Map} map
+ * @param {string} key
+ * @param {Mixed} value
+ */
+ function setGlobalMapValue( map, key, value ) {
+ map.values[ key ] = value;
+ log.deprecate(
+ window,
+ key,
+ value,
+ // Deprecation notice for mw.config globals (T58550, T72470)
+ map === mw.config && 'Use mw.config instead.'
+ );
+ }
+
+ /**
+ * 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 instead of caching the
+ * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
+ *
+ * @private
+ * @param {string} topic Stream name passed by mw.track
+ * @param {Object} data Data passed by mw.track
+ * @param {Error} [data.exception]
+ * @param {string} data.source Error source
+ * @param {string} [data.module] Name of module which caused the error
+ */
+ function logError( topic, data ) {
+ /* eslint-disable no-console */
+ var msg,
+ e = data.exception,
+ source = data.source,
+ module = data.module,
+ console = window.console;
+
+ if ( console && console.log ) {
+ msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
+ if ( module ) {
+ msg += ' in module ' + module;
+ }
+ msg += ( e ? ':' : '.' );
+ console.log( msg );
+
+ // If we have an exception object, log it to the warning channel to trigger
+ // proper stacktraces in browsers that support it.
+ if ( e && console.warn ) {
+ console.warn( e );
+ }
+ }
+ /* eslint-enable no-console */
+ }
+
/**
* Create an object that can be read from or written to via methods that allow
* interaction both with single and multiple properties at once.
}
}
- /**
- * Alias property to the global object.
- *
- * @private
- * @static
- * @param {mw.Map} map
- * @param {string} key
- * @param {Mixed} value
- */
- function setGlobalMapValue( map, key, value ) {
- map.values[ key ] = value;
- log.deprecate(
- window,
- key,
- value,
- // Deprecation notice for mw.config globals (T58550, T72470)
- map === mw.config && 'Use mw.config instead.'
- );
- }
-
Map.prototype = {
constructor: Map,
}
};
- /**
- * Object constructor for messages.
- *
- * Similar to the Message class in MediaWiki PHP.
- *
- * Format defaults to 'text'.
- *
- * @example
- *
- * var obj, str;
- * mw.messages.set( {
- * 'hello': 'Hello world',
- * 'hello-user': 'Hello, $1!',
- * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
- * } );
- *
- * obj = new mw.Message( mw.messages, 'hello' );
- * mw.log( obj.text() );
- * // Hello world
- *
- * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
- * mw.log( obj.text() );
- * // Hello, John Doe!
- *
- * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
- * mw.log( obj.text() );
- * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
- *
- * // Using mw.message shortcut
- * obj = mw.message( 'hello-user', 'John Doe' );
- * mw.log( obj.text() );
- * // Hello, John Doe!
- *
- * // Using mw.msg shortcut
- * str = mw.msg( 'hello-user', 'John Doe' );
- * mw.log( str );
- * // Hello, John Doe!
- *
- * // Different formats
- * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
- *
- * obj.format = 'text';
- * str = obj.toString();
- * // Same as:
- * str = obj.text();
- *
- * mw.log( str );
- * // Hello, John "Wiki" <3 Doe!
- *
- * mw.log( obj.escaped() );
- * // Hello, John "Wiki" <3 Doe!
- *
- * @class mw.Message
- *
- * @constructor
- * @param {mw.Map} map Message store
- * @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 = {
- /**
- * Get parsed contents of the message.
- *
- * The default parser does simple $N replacements and nothing else.
- * This may be overridden to provide a more complex message parser.
- * The primary override is in the mediawiki.jqueryMsg module.
- *
- * This function will not be called for nonexistent messages.
- *
- * @return {string} Parsed message
- */
- parser: function () {
- return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
- },
-
- // eslint-disable-next-line valid-jsdoc
- /**
- * Add (does not replace) parameters for `$N` placeholder values.
- *
- * @param {Array} parameters
- * @chainable
- */
- params: function ( parameters ) {
- var i;
- for ( i = 0; i < parameters.length; i++ ) {
- this.parameters.push( parameters[ i ] );
- }
- return this;
- },
-
- /**
- * Convert message object to its string form based on current format.
- *
- * @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
- // Err on the side of safety, ensure that the output
- // is always html safe in the event the message key is
- // missing, since in that case its highly likely the
- // message key is user-controlled.
- // '⧼' is used instead of '<' to side-step any
- // double-escaping issues.
- // (Keep synchronised with Message::toString() in PHP.)
- return '⧼' + mw.html.escape( 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;
- },
-
- /**
- * Change format to 'parse' and convert 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();
- },
-
- /**
- * Change format to 'plain' and convert 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();
- },
-
- /**
- * Change format to 'text' and convert message to string
- *
- * If jqueryMsg is loaded, {{-transformation is done where supported
- * (such as {{plural:}}, {{gender:}}, {{int:}}).
- *
- * Otherwise, it is equivalent to plain
- *
- * @return {string} String form of text message
- */
- text: function () {
- this.format = 'text';
- return this.toString();
- },
-
- /**
- * Change the format to 'escaped' and convert message to string
- *
- * This is equivalent to using the 'text' format (see #text), then
- * HTML-escaping the output.
- *
- * @return {string} String form of html escaped message
- */
- escaped: function () {
- this.format = 'escaped';
- return this.toString();
- },
-
- /**
- * Check if a message exists
- *
- * @see mw.Map#exists
- * @return {boolean}
- */
- exists: function () {
- return this.map.exists( this.key );
- }
- };
-
defineFallbacks();
/* eslint-disable no-console */
*
* @return {number} Current time
*/
- now: mwNow,
- // mwNow is defined in startup.js
+ now: ( function () {
+ var perf = window.performance,
+ navStart = perf && perf.timing && perf.timing.navigationStart;
+ return navStart && typeof perf.now === 'function' ?
+ function () { return navStart + perf.now(); } :
+ function () { return Date.now(); };
+ }() ),
/**
- * Format a string. Replace $1, $2 ... $N with positional arguments.
- *
- * Used by Message#parser().
+ * List of all analytic events emitted so far.
*
- * @since 1.25
- * @param {string} formatString Format string
- * @param {...Mixed} parameters Values for $N replacements
- * @return {string} Formatted string
+ * @private
+ * @property {Array}
*/
- format: function ( formatString ) {
- var parameters = slice.call( arguments, 1 );
- return formatString.replace( /\$(\d+)/g, function ( str, match ) {
- var index = parseInt( match, 10 ) - 1;
- return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
- } );
- },
+ trackQueue: trackQueue,
- /**
- * Track an analytic event.
- *
- * This method provides a generic means for MediaWiki JavaScript code to capture state
- * information for analysis. Each logged event specifies a string topic name that describes
- * the kind of event that it is. Topic names consist of dot-separated path components,
- * arranged from most general to most specific. Each path component should have a clear and
- * well-defined purpose.
- *
- * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
- * events that match their subcription, including those that fired before the handler was
- * bound.
- *
- * @param {string} topic Topic name
- * @param {Object} [data] Data describing the event, encoded as an object
- */
track: function ( topic, data ) {
trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
- trackCallbacks.fire( trackQueue );
+ // The base module extends this method to fire events here
},
/**
- * Register a handler for subset of analytic events, specified by topic.
- *
- * Handlers will be called once for each tracked event, including any events that fired before the
- * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
- * the exact time at which the event fired, a string 'topic' property naming the event, and a
- * 'data' property which is an object of event-specific data. The event topic and event data are
- * also passed to the callback as the first and second arguments, respectively.
+ * Track an early error event via mw.track and send it to the window console.
*
- * @param {string} topic Handle events whose name starts with this string prefix
- * @param {Function} callback Handler to call for each matching tracked event
- * @param {string} callback.topic
- * @param {Object} [callback.data]
- */
- trackSubscribe: function ( topic, callback ) {
- var seen = 0;
- function handler( trackQueue ) {
- var event;
- for ( ; seen < trackQueue.length; seen++ ) {
- event = trackQueue[ seen ];
- if ( event.topic.indexOf( topic ) === 0 ) {
- callback.call( event, event.topic, event.data );
- }
- }
- }
-
- trackHandlers.push( [ handler, callback ] );
-
- trackCallbacks.add( handler );
- },
-
- /**
- * Stop handling events for a particular handler
- *
- * @param {Function} callback
+ * @private
+ * @param {string} topic Topic name
+ * @param {Object} data Data describing the event, encoded as an object; see mw#logError
*/
- trackUnsubscribe: function ( callback ) {
- trackHandlers = trackHandlers.filter( function ( fns ) {
- if ( fns[ 1 ] === callback ) {
- trackCallbacks.remove( fns[ 0 ] );
- // Ensure the tuple is removed to avoid holding on to closures
- return false;
- }
- return true;
- } );
+ trackError: function ( topic, data ) {
+ mw.track( topic, data );
+ logError( topic, data );
},
// Expose Map constructor
Map: Map,
- // Expose Message constructor
- Message: Message,
-
/**
* Map of configuration values.
*
*/
templates: new Map(),
- /**
- * Get a message object.
- *
- * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
- *
- * @see mw.Message
- * @param {string} key Key of message to get
- * @param {...Mixed} parameters Values for $N replacements
- * @return {mw.Message}
- */
- message: function ( key ) {
- var parameters = slice.call( arguments, 1 );
- return new Message( mw.messages, key, parameters );
- },
-
- /**
- * Get a message string using the (default) 'text' format.
- *
- * Shortcut for `mw.message( key, parameters... ).text()`.
- *
- * @see mw.Message
- * @param {string} key Key of message to get
- * @param {...Mixed} parameters Values for $N replacements
- * @return {string}
- */
- msg: function () {
- return mw.message.apply( mw.message, arguments ).toString();
- },
-
// Expose mw.log
log: log,
} catch ( e ) {
// A user-defined callback raised an exception.
// Swallow it to protect our state machine!
- mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
+ mw.trackError( 'resourceloader.exception', {
+ exception: e,
+ module: module,
+ source: 'load-callback'
+ } );
}
}
}
mw.loader.store.set( module, registry[ module ] );
for ( m in registry ) {
if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
+ // eslint-disable-next-line no-use-before-define
execute( m );
}
}
for ( i = 0; i < deps.length; i++ ) {
if ( resolved.indexOf( deps[ i ] ) === -1 ) {
if ( unresolved.has( deps[ i ] ) ) {
- throw new Error( mw.format(
- 'Circular reference detected: $1 -> $2',
- module,
- deps[ i ]
- ) );
+ throw new Error(
+ 'Circular reference detected: ' + module + ' -> ' + deps[ i ]
+ );
}
unresolved.add( module );
// This module is unknown or has unknown dependencies.
// Undo any incomplete resolutions made and keep going.
resolved = saved;
- mw.track( 'resourceloader.exception', {
+ mw.trackError( 'resourceloader.exception', {
exception: err,
source: 'resolve'
} );
} else if ( typeof script === 'function' ) {
// Pass jQuery twice so that the signature of the closure which wraps
// the script can bind both '$' and 'jQuery'.
- script( $, $, mw.loader.require, registry[ module ].module );
+ script( window.$, window.$, mw.loader.require, registry[ module ].module );
markModuleReady();
} else if ( typeof script === 'string' ) {
// Use mw.track instead of mw.log because these errors are common in production mode
// (e.g. undefined variable), and mw.log is only enabled in debug mode.
registry[ module ].state = 'error';
- mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
+ mw.trackError( 'resourceloader.exception', {
+ exception: e, module:
+ module, source: 'module-execute'
+ } );
handlePending( module );
}
};
}
}
+ /**
+ * @private
+ * @param {Object} params Map of parameter names to values
+ * @return {string}
+ */
+ function makeQueryString( params ) {
+ return Object.keys( params ).map( function ( key ) {
+ return encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] );
+ } ).join( '&' );
+ }
+
/**
* Create network requests for a batch of modules.
*
// combining versions from the module query string in-order. (T188076)
query.version = getCombinedVersion( packed.list );
query = sortQuery( query );
- addScript( sourceLoadScript + '?' + $.param( query ) );
+ addScript( sourceLoadScript + '?' + makeQueryString( query ) );
}
if ( !batch.length ) {
// > '&modules='.length === 9
// > '&version=1234567'.length === 16
// > 9 + 16 = 25
- currReqBaseLength = $.param( currReqBase ).length + 25;
+ currReqBaseLength = makeQueryString( currReqBase ).length + 25;
// We may need to split up the request to honor the query string length limit,
// so build it piece by piece.
// risks and clear everything in this cache.
mw.loader.store.clear();
- mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
+ mw.trackError( 'resourceloader.exception', {
+ exception: err,
+ source: 'store-eval'
+ } );
// Re-add the failed ones that are still pending back to the batch
failed = sourceModules.filter( function ( module ) {
return registry[ module ].state === 'loading';
return registry[ moduleName ].module.exports;
},
- /**
- * @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 );
- } );
- },
-
/**
* On browsers that implement the localStorage API, the module store serves as a
* smart complement to the browser cache. Unlike the browser cache, the module store
return;
}
} catch ( e ) {
- mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
+ mw.trackError( 'resourceloader.exception', {
+ exception: e,
+ source: 'store-localstorage-init'
+ } );
}
if ( raw === undefined ) {
// This regex should never match under sane conditions.
if ( /^\s*\(/.test( args[ 1 ] ) ) {
args[ 1 ] = 'function' + args[ 1 ];
- mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
+ mw.trackError( 'resourceloader.assert', { source: 'bug-T59567' } );
}
} catch ( e ) {
- mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
+ mw.trackError( 'resourceloader.exception', {
+ exception: e,
+ source: 'store-localstorage-json'
+ } );
return false;
}
data = JSON.stringify( mw.loader.store );
localStorage.setItem( key, data );
} catch ( e ) {
- mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
+ mw.trackError( 'resourceloader.exception', {
+ exception: e,
+ source: 'store-localstorage-update'
+ } );
}
hasPendingWrite = false;
*/
remove: list.remove,
- // eslint-disable-next-line valid-jsdoc
/**
* Run a hook.
*
* @param {...Mixed} data
+ * @return {mw.hook}
* @chainable
*/
fire: function () {
// @deprecated since 1.23 Use $ or jQuery instead
mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
- /**
- * 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 instead of caching the
- * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
- *
- * @private
- * @param {string} topic Stream name passed by mw.track
- * @param {Object} data Data passed by mw.track
- * @param {Error} [data.exception]
- * @param {string} data.source Error source
- * @param {string} [data.module] Name of module which caused the error
- */
- function logError( topic, data ) {
- /* eslint-disable no-console */
- var msg,
- e = data.exception,
- source = data.source,
- module = data.module,
- console = window.console;
-
- if ( console && console.log ) {
- msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
- if ( module ) {
- msg += ' in module ' + module;
- }
- msg += ( e ? ':' : '.' );
- console.log( msg );
-
- // If we have an exception object, log it to the warning channel to trigger
- // proper stacktraces in browsers that support it.
- if ( e && console.warn ) {
- console.warn( e );
- }
- }
- /* eslint-enable no-console */
- }
-
- // Subscribe to error streams
- mw.trackSubscribe( 'resourceloader.exception', logError );
- mw.trackSubscribe( 'resourceloader.assert', logError );
-
// Attach to window and globally alias
window.mw = window.mediaWiki = mw;
}( jQuery ) );