| | | 'use strict';
|
---|
| | |
|
---|
| | | const slice = Array.prototype.slice;
|
---|
| | |
|
---|
| | | // Apply site-level data
|
---|
| | | mw.config.set( require( './config.json' ) );
|
---|
| | |
|
---|
| | | require( './log.js' );
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * @class mw.Message
|
---|
| | | * @classdesc Describes a translateable text or HTML string. Similar to the Message class in MediaWiki PHP.
|
---|
| | | *
|
---|
| | | * @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',
|
---|
| | | * 'so-unusual': 'You will find: $1'
|
---|
| | | * } );
|
---|
| | | *
|
---|
| | | * obj = mw.message( 'hello' );
|
---|
| | | * mw.log( obj.text() );
|
---|
| | | * // Hello world
|
---|
| | | *
|
---|
| | | * obj = mw.message( 'hello-user', 'John Doe' );
|
---|
| | | * mw.log( obj.text() );
|
---|
| | | * // Hello, John Doe!
|
---|
| | | *
|
---|
| | | * obj = mw.message( '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.msg shortcut, always in "text' format.
|
---|
| | | * str = mw.msg( 'hello-user', 'John Doe' );
|
---|
| | | * mw.log( str );
|
---|
| | | * // Hello, John Doe!
|
---|
| | | *
|
---|
| | | * // Different formats
|
---|
| | | * obj = mw.message( 'so-unusual', 'Time "after" <time>' );
|
---|
| | | *
|
---|
| | | * mw.log( obj.text() );
|
---|
| | | * // You will find: Time "after" <time>
|
---|
| | | *
|
---|
| | | * mw.log( obj.escaped() );
|
---|
| | | * // You will find: Time "after" <time>
|
---|
| | | *
|
---|
| | | * @constructor
|
---|
| | | * @description Object constructor for messages. The constructor is not publicly accessible;
|
---|
| | | * use {@link mw.message} instead.
|
---|
| | | * @param {mw.Map} map Message store
|
---|
| | | * @param {string} key
|
---|
| | | * @param {Array} [parameters]
|
---|
| | | */
|
---|
| | | function Message( map, key, parameters ) {
|
---|
| | | this.map = map;
|
---|
| | | this.key = key;
|
---|
| | | this.parameters = parameters || [];
|
---|
| | | }
|
---|
| | |
|
---|
| | | Message.prototype = /** @lends mw.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.
|
---|
| | | * For internal use by mediawiki.jqueryMsg only
|
---|
| | | *
|
---|
| | | * @private
|
---|
| | | * @param {string} format
|
---|
| | | * @return {string} Parsed message
|
---|
| | | */
|
---|
| | | parser: function ( format ) {
|
---|
| | | let text = this.map.get( this.key );
|
---|
| | |
|
---|
| | | // Apply qqx formatting.
|
---|
| | | //
|
---|
| | | // - Keep this synchronised with LanguageQqx/MessageCache in PHP.
|
---|
| | | // - Keep this synchronised with mw.jqueryMsg.Parser#getAst.
|
---|
| | | //
|
---|
| | | // Unlike LanguageQqx in PHP, this doesn't replace unconditionally.
|
---|
| | | // It replaces non-existent messages, and messages that were exported by
|
---|
| | | // load.php as "(key)" in qqx formatting. Some extensions export other data
|
---|
| | | // via their message blob (T222944).
|
---|
| | | if (
|
---|
| | | mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
|
---|
| | | ( !text || text === '(' + this.key + ')' )
|
---|
| | | ) {
|
---|
| | | text = '(' + this.key + '$*)';
|
---|
| | | }
|
---|
| | | text = mw.format( text, ...this.parameters );
|
---|
| | | if ( format === 'parse' ) {
|
---|
| | | // We don't know how to parse anything, so escape it all
|
---|
| | | text = mw.html.escape( text );
|
---|
| | | }
|
---|
| | | return text;
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Add (does not replace) parameters for `$N` placeholder values.
|
---|
| | | *
|
---|
| | | * @param {Array} parameters
|
---|
| | | * @return {mw.Message}
|
---|
| | | * @chainable
|
---|
| | | */
|
---|
| | | params: function ( parameters ) {
|
---|
| | | this.parameters.push( ...parameters );
|
---|
| | | return this;
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Convert message object to a string using the "text"-format .
|
---|
| | | *
|
---|
| | | * This exists for implicit string type casting only.
|
---|
| | | * Do not call this directly. Use mw.Message#text() instead, one of the
|
---|
| | | * other format methods.
|
---|
| | | *
|
---|
| | | * @private
|
---|
| | | * @param {string} [format="text"] Internal parameter. Uses "text" if called
|
---|
| | | * implicitly through string casting.
|
---|
| | | * @return {string} Message in the given format, or `⧼key⧽` if the key
|
---|
| | | * does not exist.
|
---|
| | | */
|
---|
| | | toString: function ( format ) {
|
---|
| | | if ( !this.exists() ) {
|
---|
| | | // Make sure qqx works for non-existent messages, see parser() above.
|
---|
| | | if ( mw.config.get( 'wgUserLanguage' ) !== 'qqx' ) {
|
---|
| | | // 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 ( !format ) {
|
---|
| | | format = 'text';
|
---|
| | | }
|
---|
| | |
|
---|
| | | if ( format === 'plain' || format === 'text' || format === 'parse' ) {
|
---|
| | | return this.parser( format );
|
---|
| | | }
|
---|
| | |
|
---|
| | | // Format: 'escaped' (including for any invalid format, default to safe escape)
|
---|
| | | return mw.html.escape( this.parser( 'escaped' ) );
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Parse message as wikitext and return HTML.
|
---|
| | | *
|
---|
| | | * If jqueryMsg is loaded, this transforms text and parses a subset of supported wikitext
|
---|
| | | * into HTML. Without jqueryMsg, it is equivalent to {@link mw.Message#escaped}.
|
---|
| | | *
|
---|
| | | * @return {string} String form of parsed message
|
---|
| | | */
|
---|
| | | parse: function () {
|
---|
| | | return this.toString( 'parse' );
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Return message plainly.
|
---|
| | | *
|
---|
| | | * This substitutes parameters, but otherwise does not transform the
|
---|
| | | * message content.
|
---|
| | | *
|
---|
| | | * @return {string} String form of plain message
|
---|
| | | */
|
---|
| | | plain: function () {
|
---|
| | | return this.toString( 'plain' );
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Format message with text transformations applied.
|
---|
| | | *
|
---|
| | | * If jqueryMsg is loaded, `{{`-transformation is done for supported
|
---|
| | | * magic words such as `{{plural:}}`, `{{gender:}}`, and `{{int:}}`.
|
---|
| | | * Without jqueryMsg, it is equivalent to {@link mw.Message#plain}.
|
---|
| | | *
|
---|
| | | * @return {string} String form of text message
|
---|
| | | */
|
---|
| | | text: function () {
|
---|
| | | return this.toString( 'text' );
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Format message and return as escaped text in HTML.
|
---|
| | | *
|
---|
| | | * This is equivalent to the #text format, which is then HTML-escaped.
|
---|
| | | *
|
---|
| | | * @return {string} String form of html escaped message
|
---|
| | | */
|
---|
| | | escaped: function () {
|
---|
| | | return this.toString( 'escaped' );
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Check if a message exists. Equivalent to {@link mw.Map.exists}.
|
---|
| | | *
|
---|
| | | * @return {boolean}
|
---|
| | | */
|
---|
| | | exists: function () {
|
---|
| | | return this.map.exists( this.key );
|
---|
| | | }
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * @class mw
|
---|
| | | * @singleton
|
---|
| | | * @borrows mediawiki.inspect.runReports as inspect
|
---|
| | | */
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Empty object for third-party libraries, for cases where you don't
|
---|
| | | * want to add a new global, or the global is bad and needs containment
|
---|
| | | * or wrapping.
|
---|
| | | *
|
---|
| | | * @type {Object}
|
---|
| | | */
|
---|
| | | mw.libs = {};
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * OOUI widgets specific to MediaWiki.
|
---|
| | | * Initially empty. To expand the amount of available widgets the `mediawiki.widget` module can be loaded.
|
---|
| | | *
|
---|
| | | * @namespace mw.widgets
|
---|
| | | * @example
|
---|
| | | * mw.loader.using('mediawiki.widget').then(() => {
|
---|
| | | * OO.ui.getWindowManager().addWindows( [ new mw.widget.AbandonEditDialog() ] );
|
---|
| | | * });
|
---|
| | | */
|
---|
| | | mw.widgets = {};
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Generates a ResourceLoader report using the
|
---|
| | | * {@link mediawiki.inspect.js.html|mediawiki.inspect module}.
|
---|
| | | *
|
---|
| | | * @ignore
|
---|
| | | */
|
---|
| | | mw.inspect = function ( ...reports ) {
|
---|
| | | // Lazy-load
|
---|
| | | mw.loader.using( 'mediawiki.inspect', () => {
|
---|
| | | mw.inspect.runReports( ...reports );
|
---|
| | | } );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Replace `$*` with a list of parameters for `uselang=qqx` support.
|
---|
| | | *
|
---|
| | | * @private
|
---|
| | | * @since 1.33
|
---|
| | | * @param {string} formatString Format string
|
---|
| | | * @param {Array} parameters Values for $N replacements
|
---|
| | | * @return {string} Transformed format string
|
---|
| | | */
|
---|
| | | mw.internalDoTransformFormatForQqx = function ( formatString, parameters ) {
|
---|
| | | if ( formatString.indexOf( '$*' ) !== -1 ) {
|
---|
| | | let replacement = '';
|
---|
| | | if ( parameters.length ) {
|
---|
| | | replacement = ': ' + parameters.map( ( _, i ) => '$' + ( i + 1 ) ).join( ', ' );
|
---|
| | | }
|
---|
| | | return formatString.replace( '$*', replacement );
|
---|
| | | }
|
---|
| | | return formatString;
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Encode page titles in a way that matches `wfUrlencode` in PHP.
|
---|
| | | *
|
---|
| | | * @see mw.util#wikiUrlencode
|
---|
| | | * @private
|
---|
| | | * @param {string} str
|
---|
| | | * @return {string}
|
---|
| | | */
|
---|
| | | mw.internalWikiUrlencode = function ( str ) {
|
---|
| | | return encodeURIComponent( String( str ) )
|
---|
| | | .replace( /'/g, '%27' )
|
---|
| | | .replace( /%20/g, '_' )
|
---|
| | | .replace( /%3B/g, ';' )
|
---|
| | | .replace( /%40/g, '@' )
|
---|
| | | .replace( /%24/g, '$' )
|
---|
| | | .replace( /%2C/g, ',' )
|
---|
| | | .replace( /%2F/g, '/' )
|
---|
| | | .replace( /%3A/g, ':' );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Format a string. Replace $1, $2 ... $N with positional arguments.
|
---|
| | | *
|
---|
| | | * Used by {@link mw.Message#parse}.
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @since 1.25
|
---|
| | | * @param {string} formatString Format string
|
---|
| | | * @param {...Mixed} parameters Values for $N replacements
|
---|
| | | * @return {string} Formatted string
|
---|
| | | */
|
---|
| | | mw.format = function ( formatString, ...parameters ) {
|
---|
| | | formatString = mw.internalDoTransformFormatForQqx( formatString, parameters );
|
---|
| | | return formatString.replace( /\$(\d+)/g, ( str, match ) => {
|
---|
| | | const index = parseInt( match, 10 ) - 1;
|
---|
| | | return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
|
---|
| | | } );
|
---|
| | | };
|
---|
| | |
|
---|
| | | // Expose Message constructor
|
---|
| | | mw.Message = Message;
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Get a message object.
|
---|
| | | *
|
---|
| | | * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @see {@link mw.Message}
|
---|
| | | * @param {string} key Key of message to get
|
---|
| | | * @param {...Mixed} parameters Values for $N replacements
|
---|
| | | * @return {mw.Message}
|
---|
| | | */
|
---|
| | | mw.message = function ( key ) {
|
---|
| | | const 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()`.
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @see {@link mw.Message}
|
---|
| | | * @param {string} key Key of message to get
|
---|
| | | * @param {...any} parameters Values for $N replacements
|
---|
| | | * @return {string}
|
---|
| | | */
|
---|
| | | mw.msg = function ( key, ...parameters ) {
|
---|
| | | // Shortcut must process text transformations by default
|
---|
| | | // if mediawiki.jqueryMsg is loaded. (T46459)
|
---|
| | | // eslint-disable-next-line mediawiki/msg-doc
|
---|
| | | return mw.message( key, ...parameters ).text();
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Convenience method for loading and accessing the
|
---|
| | | * {@link mw.notification.notify|mw.notification module}.
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
|
---|
| | | * @param {Object} [options] See mw.notification#defaults for the defaults.
|
---|
| | | * @return {jQuery.Promise}
|
---|
| | | */
|
---|
| | | mw.notify = function ( message, options ) {
|
---|
| | | // Lazy load
|
---|
| | | return mw.loader.using( 'mediawiki.notification' ).then( () => mw.notification.notify( message, options ) );
|
---|
| | | };
|
---|
| | |
|
---|
| | | const trackCallbacks = $.Callbacks( 'memory' );
|
---|
| | | let trackHandlers = [];
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * 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 subscription, including buffered events that fired before the handler
|
---|
| | | * was subscribed.
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @param {string} topic Topic name
|
---|
| | | * @param {Object|number|string} [data] Data describing the event.
|
---|
| | | */
|
---|
| | | mw.track = function ( topic, data ) {
|
---|
| | | mw.trackQueue.push( { topic: topic, data: data } );
|
---|
| | | trackCallbacks.fire( mw.trackQueue );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Register a handler for subset of analytic events, specified by topic.
|
---|
| | | *
|
---|
| | | * Handlers will be called once for each tracked event, including for any buffered events that
|
---|
| | | * fired before the handler was subscribed. The callback is passed a `topic` string, and optional
|
---|
| | | * `data` event object. The `this` value for the callback is a plain object with `topic` and
|
---|
| | | * `data` properties set to those same values.
|
---|
| | | *
|
---|
| | | * @example
|
---|
| | | * // To monitor all topics for debugging
|
---|
| | | * mw.trackSubscribe( '', console.log );
|
---|
| | | *
|
---|
| | | * @example
|
---|
| | | * // To subscribe to any of `foo.*`, e.g. both `foo.bar` and `foo.quux`
|
---|
| | | * mw.trackSubscribe( 'foo.', console.log );
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @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]
|
---|
| | | */
|
---|
| | | mw.trackSubscribe = function ( topic, callback ) {
|
---|
| | | let seen = 0;
|
---|
| | | function handler( trackQueue ) {
|
---|
| | | for ( ; seen < trackQueue.length; seen++ ) {
|
---|
| | | const 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.
|
---|
| | | *
|
---|
| | | * @memberof mw
|
---|
| | | * @param {Function} callback
|
---|
| | | */
|
---|
| | | mw.trackUnsubscribe = function ( callback ) {
|
---|
| | | trackHandlers = trackHandlers.filter( ( fns ) => {
|
---|
| | | if ( fns[ 1 ] === callback ) {
|
---|
| | | trackCallbacks.remove( fns[ 0 ] );
|
---|
| | | // Ensure the tuple is removed to avoid holding on to closures
|
---|
| | | return false;
|
---|
| | | }
|
---|
| | | return true;
|
---|
| | | } );
|
---|
| | | };
|
---|
| | |
|
---|
| | | // Notify subscribers of any mw.trackQueue.push() calls
|
---|
| | | // from the startup module before mw.track() is defined.
|
---|
| | | trackCallbacks.fire( mw.trackQueue );
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * @namespace Hooks
|
---|
| | | * @description 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.
|
---|
| | | * See {@link Hook}.
|
---|
| | | *
|
---|
| | | * 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 {@link 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 {@link mw.hook} for that matter).
|
---|
| | | *
|
---|
| | | * ```
|
---|
| | | * var h = mw.hook( 'bar.ready' );
|
---|
| | | * new mw.Foo( .. ).fetch( { callback: h.fire } );
|
---|
| | | * ```
|
---|
| | | *
|
---|
| | | * The function signature for hooks can be considered [stable](https://www.mediawiki.org/wiki/Special:MyLanguage/Stable_interface_policy/Frontend).
|
---|
| | | * See available global events below.
|
---|
| | | */
|
---|
| | |
|
---|
| | | const hooks = Object.create( null );
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Create an instance of {@link Hook}.
|
---|
| | | *
|
---|
| | | * @example
|
---|
| | | * const hook = mw.hook( 'name' );
|
---|
| | | * hook.add( () => alert( 'Hook was fired' ) );
|
---|
| | | * hook.fire();
|
---|
| | | *
|
---|
| | | * @param {string} name Name of hook.
|
---|
| | | * @return {Hook}
|
---|
| | | */
|
---|
| | | mw.hook = function ( name ) {
|
---|
| | | return hooks[ name ] || ( hooks[ name ] = ( function () {
|
---|
| | | let memory;
|
---|
| | | const fns = [];
|
---|
| | | function rethrow( e ) {
|
---|
| | | setTimeout( () => {
|
---|
| | | throw e;
|
---|
| | | } );
|
---|
| | | }
|
---|
| | | /**
|
---|
| | | * @class Hook
|
---|
| | | * @classdesc An instance of a hook, created via [mw.hook method]{@link mw.hook}.
|
---|
| | | * @global
|
---|
| | | * @hideconstructor
|
---|
| | | */
|
---|
| | | return {
|
---|
| | | /**
|
---|
| | | * Register a hook handler.
|
---|
| | | *
|
---|
| | | * @param {...Function} handler Function to bind.
|
---|
| | | * @memberof Hook
|
---|
| | | * @return {Hook}
|
---|
| | | */
|
---|
| | | add: function () {
|
---|
| | | for ( let i = 0; i < arguments.length; i++ ) {
|
---|
| | | fns.push( arguments[ i ] );
|
---|
| | | if ( memory ) {
|
---|
| | | try {
|
---|
| | | arguments[ i ].apply( null, memory );
|
---|
| | | } catch ( e ) {
|
---|
| | | rethrow( e );
|
---|
| | | }
|
---|
| | | }
|
---|
| | | }
|
---|
| | | return this;
|
---|
| | | },
|
---|
| | | /**
|
---|
| | | * Unregister a hook handler.
|
---|
| | | *
|
---|
| | | * @param {...Function} handler Function to unbind.
|
---|
| | | * @memberof Hook
|
---|
| | | * @return {Hook}
|
---|
| | | */
|
---|
| | | remove: function () {
|
---|
| | | for ( let i = 0; i < arguments.length; i++ ) {
|
---|
| | | let j;
|
---|
| | | while ( ( j = fns.indexOf( arguments[ i ] ) ) !== -1 ) {
|
---|
| | | fns.splice( j, 1 );
|
---|
| | | }
|
---|
| | | }
|
---|
| | | return this;
|
---|
| | | },
|
---|
| | | /**
|
---|
| | | * Call hook handlers with data.
|
---|
| | | *
|
---|
| | | * @memberof Hook
|
---|
| | | * @param {...any} data
|
---|
| | | * @return {Hook}
|
---|
| | | * @chainable
|
---|
| | | */
|
---|
| | | fire: function () {
|
---|
| | | for ( let i = 0; i < fns.length; i++ ) {
|
---|
| | | try {
|
---|
| | | fns[ i ].apply( null, arguments );
|
---|
| | | } catch ( e ) {
|
---|
| | | rethrow( e );
|
---|
| | | }
|
---|
| | | }
|
---|
| | | memory = slice.call( arguments );
|
---|
| | | return this;
|
---|
| | | }
|
---|
| | | };
|
---|
| | | }() ) );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * 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>
|
---|
| | | *
|
---|
| | | * @namespace mw.html
|
---|
| | | */
|
---|
| | |
|
---|
| | | function escapeCallback( s ) {
|
---|
| | | switch ( s ) {
|
---|
| | | case '\'':
|
---|
| | | return ''';
|
---|
| | | case '"':
|
---|
| | | return '"';
|
---|
| | | case '<':
|
---|
| | | return '<';
|
---|
| | | case '>':
|
---|
| | | return '>';
|
---|
| | | case '&':
|
---|
| | | return '&';
|
---|
| | | }
|
---|
| | | }
|
---|
| | | mw.html = {
|
---|
| | | /**
|
---|
| | | * Escape a string for HTML.
|
---|
| | | *
|
---|
| | | * Converts special characters to HTML entities.
|
---|
| | | *
|
---|
| | | * @example
|
---|
| | | * mw.html.escape( '< > \' & "' );
|
---|
| | | * // Returns < > ' & "
|
---|
| | | *
|
---|
| | | * @param {string} s The string to escape
|
---|
| | | * @return {string} HTML
|
---|
| | | */
|
---|
| | | escape: function ( s ) {
|
---|
| | | return s.replace( /['"<>&]/g, escapeCallback );
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Create an HTML element string, with safe escaping.
|
---|
| | | *
|
---|
| | | * @param {string} name The tag name.
|
---|
| | | * @param {Object} [attrs] An object with members mapping element names to values
|
---|
| | | * @param {string|mw.html.Raw|null} [contents=null] The contents of the element.
|
---|
| | | *
|
---|
| | | * - string: Text to be escaped.
|
---|
| | | * - null: The element is treated as void with short closing form, e.g. `<br/>`.
|
---|
| | | * - this.Raw: The raw value is directly included.
|
---|
| | | * @return {string} HTML
|
---|
| | | */
|
---|
| | | element: function ( name, attrs, contents ) {
|
---|
| | | let s = '<' + name;
|
---|
| | |
|
---|
| | | if ( attrs ) {
|
---|
| | | for ( const attrName in attrs ) {
|
---|
| | | let v = attrs[ attrName ];
|
---|
| | | // Convert name=true, to name=name
|
---|
| | | if ( v === true ) {
|
---|
| | | v = attrName;
|
---|
| | | // Skip name=false
|
---|
| | | } else if ( v === false ) {
|
---|
| | | continue;
|
---|
| | | }
|
---|
| | | s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
|
---|
| | | }
|
---|
| | | }
|
---|
| | | if ( contents === undefined || contents === null ) {
|
---|
| | | // Self close tag
|
---|
| | | s += '/>';
|
---|
| | | return s;
|
---|
| | | }
|
---|
| | | // Regular open tag
|
---|
| | | s += '>';
|
---|
| | | if ( typeof contents === 'string' ) {
|
---|
| | | // Escaped
|
---|
| | | s += this.escape( contents );
|
---|
| | | } else if ( typeof contents === 'number' || typeof contents === 'boolean' ) {
|
---|
| | | // Convert to string
|
---|
| | | s += String( contents );
|
---|
| | | } else if ( contents instanceof this.Raw ) {
|
---|
| | | // Raw HTML inclusion
|
---|
| | | s += contents.value;
|
---|
| | | } else {
|
---|
| | | throw new Error( 'Invalid content type' );
|
---|
| | | }
|
---|
| | | s += '</' + name + '>';
|
---|
| | | return s;
|
---|
| | | },
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * @classdesc Wrapper object for raw HTML. Can be used with {@link mw.html.element}.
|
---|
| | | * @class mw.html.Raw
|
---|
| | | * @param {string} value
|
---|
| | | * @example
|
---|
| | | * const raw = new mw.html.Raw( 'Text' );
|
---|
| | | * mw.html.element( 'div', { class: 'html' }, raw );
|
---|
| | | */
|
---|
| | | Raw: function ( value ) {
|
---|
| | | this.value = value;
|
---|
| | | }
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Schedule a function to run once the page is ready (DOM loaded).
|
---|
| | | *
|
---|
| | | * @since 1.5.8
|
---|
| | | * @memberof window
|
---|
| | | * @param {Function} fn
|
---|
| | | */
|
---|
| | | window.addOnloadHook = function ( fn ) {
|
---|
| | | $( () => {
|
---|
| | | fn();
|
---|
| | | } );
|
---|
| | | };
|
---|
| | |
|
---|
| | | const loadedScripts = {};
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Import a script using an absolute URI.
|
---|
| | | *
|
---|
| | | * @since 1.12.2
|
---|
| | | * @memberof window
|
---|
| | | * @param {string} url
|
---|
| | | * @return {HTMLElement|null} Script tag, or null if it was already imported before
|
---|
| | | */
|
---|
| | | window.importScriptURI = function ( url ) {
|
---|
| | | if ( loadedScripts[ url ] ) {
|
---|
| | | return null;
|
---|
| | | }
|
---|
| | | loadedScripts[ url ] = true;
|
---|
| | | return mw.loader.addScriptTag( url );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Import a local JS content page, for use by user scripts and site-wide scripts.
|
---|
| | | *
|
---|
| | | * Note that if the same title is imported multiple times, it will only
|
---|
| | | * be loaded and executed once.
|
---|
| | | *
|
---|
| | | * @since 1.12.2
|
---|
| | | * @memberof window
|
---|
| | | * @param {string} title
|
---|
| | | * @return {HTMLElement|null} Script tag, or null if it was already imported before
|
---|
| | | */
|
---|
| | | window.importScript = function ( title ) {
|
---|
| | | return window.importScriptURI(
|
---|
| | | mw.config.get( 'wgScript' ) + '?title=' + mw.internalWikiUrlencode( title ) +
|
---|
| | | '&action=raw&ctype=text/javascript'
|
---|
| | | );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Import a local CSS content page, for use by user scripts and site-wide scripts.
|
---|
| | | *
|
---|
| | | * @since 1.12.2
|
---|
| | | * @memberof window
|
---|
| | | * @param {string} title
|
---|
| | | * @return {HTMLElement} Link tag
|
---|
| | | */
|
---|
| | | window.importStylesheet = function ( title ) {
|
---|
| | | return mw.loader.addLinkTag(
|
---|
| | | mw.config.get( 'wgScript' ) + '?title=' + mw.internalWikiUrlencode( title ) +
|
---|
| | | '&action=raw&ctype=text/css'
|
---|
| | | );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Import a stylesheet using an absolute URI.
|
---|
| | | *
|
---|
| | | * @since 1.12.2
|
---|
| | | * @memberof window
|
---|
| | | * @param {string} url
|
---|
| | | * @param {string} media
|
---|
| | | * @return {HTMLElement} Link tag
|
---|
| | | */
|
---|
| | | window.importStylesheetURI = function ( url, media ) {
|
---|
| | | return mw.loader.addLinkTag( url, media );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Get the names of all registered ResourceLoader modules.
|
---|
| | | *
|
---|
| | | * @memberof mw.loader
|
---|
| | | * @return {string[]}
|
---|
| | | */
|
---|
| | | mw.loader.getModuleNames = function () {
|
---|
| | | return Object.keys( mw.loader.moduleRegistry );
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Execute a function after one or more modules are ready.
|
---|
| | | *
|
---|
| | | * Use this method if you need to dynamically control which modules are loaded
|
---|
| | | * and/or when they loaded (instead of declaring them as dependencies directly
|
---|
| | | * on your module.)
|
---|
| | | *
|
---|
| | | * This uses the same loader as for regular module dependencies. This means
|
---|
| | | * ResourceLoader will not re-download or re-execute a module for the second
|
---|
| | | * time if something else already needed it. And the same browser HTTP cache,
|
---|
| | | * and localStorage are checked before considering to fetch from the network.
|
---|
| | | * And any on-going requests from other dependencies or using() calls are also
|
---|
| | | * automatically re-used.
|
---|
| | | *
|
---|
| | | * Example of inline dependency on OOjs:
|
---|
| | | * ```
|
---|
| | | * mw.loader.using( 'oojs', function () {
|
---|
| | | * OO.compare( [ 1 ], [ 1 ] );
|
---|
| | | * } );
|
---|
| | | * ```
|
---|
| | | *
|
---|
| | | * Example of inline dependency obtained via `require()`:
|
---|
| | | * ```
|
---|
| | | * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
|
---|
| | | * var util = require( 'mediawiki.util' );
|
---|
| | | * } );
|
---|
| | | * ```
|
---|
| | | *
|
---|
| | | * Since MediaWiki 1.23 this returns a promise.
|
---|
| | | *
|
---|
| | | * Since MediaWiki 1.28 the promise is resolved with a `require` function.
|
---|
| | | *
|
---|
| | | * @memberof mw.loader
|
---|
| | | * @param {string|Array} dependencies Module name or array of modules names the
|
---|
| | | * callback depends on to be ready before executing
|
---|
| | | * @param {Function} [ready] Callback to execute when all dependencies are ready
|
---|
| | | * @param {Function} [error] Callback to execute if one or more dependencies failed
|
---|
| | | * @return {jQuery.Promise} With a `require` function
|
---|
| | | */
|
---|
| | | mw.loader.using = function ( dependencies, ready, error ) {
|
---|
| | | const deferred = $.Deferred();
|
---|
| | |
|
---|
| | | // Allow calling with a single dependency as a string
|
---|
| | | if ( !Array.isArray( dependencies ) ) {
|
---|
| | | dependencies = [ dependencies ];
|
---|
| | | }
|
---|
| | |
|
---|
| | | if ( ready ) {
|
---|
| | | deferred.done( ready );
|
---|
| | | }
|
---|
| | | if ( error ) {
|
---|
| | | deferred.fail( error );
|
---|
| | | }
|
---|
| | |
|
---|
| | | try {
|
---|
| | | // Resolve entire dependency map
|
---|
| | | dependencies = mw.loader.resolve( dependencies );
|
---|
| | | } catch ( e ) {
|
---|
| | | return deferred.reject( e ).promise();
|
---|
| | | }
|
---|
| | |
|
---|
| | | mw.loader.enqueue(
|
---|
| | | dependencies,
|
---|
| | | () => {
|
---|
| | | deferred.resolve( mw.loader.require );
|
---|
| | | },
|
---|
| | | deferred.reject
|
---|
| | | );
|
---|
| | |
|
---|
| | | return deferred.promise();
|
---|
| | | };
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Load a script by URL.
|
---|
| | | *
|
---|
| | | * @example
|
---|
| | | * mw.loader.getScript(
|
---|
| | | * 'https://example.org/x-1.0.0.js'
|
---|
| | | * )
|
---|
| | | * .then( function () {
|
---|
| | | * // Script succeeded. You can use X now.
|
---|
| | | * }, function ( e ) {
|
---|
| | | * // Script failed. X is not avaiable
|
---|
| | | * mw.log.error( e.message ); // => "Failed to load script"
|
---|
| | | * } );
|
---|
| | | * } );
|
---|
| | | *
|
---|
| | | * @memberof mw.loader
|
---|
| | | * @param {string} url Script URL
|
---|
| | | * @return {jQuery.Promise} Resolved when the script is loaded
|
---|
| | | */
|
---|
| | | mw.loader.getScript = function ( url ) {
|
---|
| | | return $.ajax( url, { dataType: 'script', cache: true } )
|
---|
| | | .catch( () => {
|
---|
| | | throw new Error( 'Failed to load script' );
|
---|
| | | } );
|
---|
| | | };
|
---|
| | |
|
---|
| | | // Skeleton user object, extended by the 'mediawiki.user' module.
|
---|
| | | /**
|
---|
| | | * @namespace mw.user
|
---|
| | | * @ignore
|
---|
| | | */
|
---|
| | | mw.user = {
|
---|
| | | /**
|
---|
| | | * Map of user preferences and their values.
|
---|
| | | *
|
---|
| | | * @type {mw.Map}
|
---|
| | | */
|
---|
| | | options: new mw.Map(),
|
---|
| | | /**
|
---|
| | | * Map of retrieved user tokens.
|
---|
| | | *
|
---|
| | | * @type {mw.Map}
|
---|
| | | */
|
---|
| | | tokens: new mw.Map()
|
---|
| | | };
|
---|
| | |
|
---|
| | | mw.user.options.set( require( './user.json' ) );
|
---|
| | |
|
---|
| | | // Process callbacks for modern browsers (Grade A) that require modules.
|
---|
| | | const queue = window.RLQ;
|
---|
| | | // Replace temporary RLQ implementation from startup.js with the
|
---|
| | | // final implementation that also processes callbacks that can
|
---|
| | | // require modules. It must also support late arrivals of
|
---|
| | | // plain callbacks. (T208093)
|
---|
| | | window.RLQ = {
|
---|
| | | push: function ( entry ) {
|
---|
| | | if ( typeof entry === 'function' ) {
|
---|
| | | entry();
|
---|
| | | } else {
|
---|
| | | mw.loader.using( entry[ 0 ], entry[ 1 ] );
|
---|
| | | }
|
---|
| | | }
|
---|
| | | };
|
---|
| | | while ( queue[ 0 ] ) {
|
---|
| | | window.RLQ.push( queue.shift() );
|
---|
| | | }
|
---|
| | |
|
---|
| | | /**
|
---|
| | | * Replace document.write/writeln with basic html parsing that appends
|
---|
| | | * to the `<body>` to avoid blanking pages. Added JavaScript will not run.
|
---|
| | | *
|
---|
| | | * @ignore
|
---|
| | | * @deprecated since 1.26
|
---|
| | | */
|
---|
| | | [ 'write', 'writeln' ].forEach( ( func ) => {
|
---|
| | | mw.log.deprecate( document, func, function () {
|
---|
| | | $( document.body ).append( $.parseHTML( slice.call( arguments ).join( '' ) ) );
|
---|
| | | }, 'Use jQuery or mw.loader.load instead.', 'document.' + func );
|
---|
| | | } );
|
---|
| | |
|
---|
| | | // Load other files in the package
|
---|
| | | require( './errorLogger.js' );
|
---|