'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.inspect' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.inspect.js',
+ 'scripts' => 'resources/src/mediawiki.inspect.js',
'dependencies' => [
'mediawiki.String',
'mediawiki.RegExp',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.notify' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.notify.js',
+ 'scripts' => 'resources/src/mediawiki.notify.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.notification.convertmessagebox' => [
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.RegExp' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.RegExp.js',
+ 'scripts' => 'resources/src/mediawiki.RegExp.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.String' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.String.js',
+ 'scripts' => 'resources/src/mediawiki.String.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.pager.tablePager' => [
],
],
'mediawiki.storage' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.storage.js',
+ 'scripts' => 'resources/src/mediawiki.storage.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.Title' => [
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.user' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.user.js',
+ 'scripts' => 'resources/src/mediawiki.user.js',
'dependencies' => [
'mediawiki.api',
'mediawiki.api.user',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.userSuggest' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.userSuggest.js',
+ 'scripts' => 'resources/src/mediawiki.userSuggest.js',
'dependencies' => [
'jquery.suggestions',
'mediawiki.api'
],
'mediawiki.util' => [
'class' => ResourceLoaderMediaWikiUtilModule::class,
- 'scripts' => 'resources/src/mediawiki/mediawiki.util.js',
+ 'scripts' => 'resources/src/mediawiki.util.js',
'dependencies' => [
'jquery.accessKeyLabel',
'mediawiki.RegExp',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.viewport' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.viewport.js',
+ 'scripts' => 'resources/src/mediawiki.viewport.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.checkboxtoggle' => [
'styles' => 'resources/src/mediawiki/mediawiki.checkboxtoggle.css',
],
'mediawiki.cookie' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.cookie.js',
+ 'scripts' => 'resources/src/mediawiki.cookie.js',
'dependencies' => 'jquery.cookie',
'targets' => [ 'desktop', 'mobile' ],
],
'dependencies' => 'jquery.textSelection',
],
'mediawiki.experiments' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.experiments.js',
+ 'scripts' => 'resources/src/mediawiki.experiments.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.editfont.styles' => [
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.visibleTimeout' => [
- 'scripts' => 'resources/src/mediawiki/mediawiki.visibleTimeout.js',
+ 'scripts' => 'resources/src/mediawiki.visibleTimeout.js',
'targets' => [ 'desktop', 'mobile' ],
],
--- /dev/null
+( function ( mw ) {
+ /**
+ * @class mw.RegExp
+ */
+ mw.RegExp = {
+ /**
+ * Escape string for safe inclusion in regular expression
+ *
+ * The following characters are escaped:
+ *
+ * \ { } ( ) | . ? * + - ^ $ [ ]
+ *
+ * @since 1.26
+ * @static
+ * @param {string} str String to escape
+ * @return {string} Escaped string
+ */
+ escape: function ( str ) {
+ return str.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' ); // eslint-disable-line no-useless-escape
+ }
+ };
+}( mediaWiki ) );
--- /dev/null
+( function () {
+
+ /**
+ * @class mw.String
+ * @singleton
+ */
+
+ /**
+ * Calculate the byte length of a string (accounting for UTF-8).
+ *
+ * @author Jan Paul Posma, 2011
+ * @author Timo Tijhof, 2012
+ * @author David Chan, 2013
+ *
+ * @param {string} str
+ * @return {number}
+ */
+ function byteLength( str ) {
+ // This basically figures out how many bytes a UTF-16 string (which is what js sees)
+ // will take in UTF-8 by replacing a 2 byte character with 2 *'s, etc, and counting that.
+ // Note, surrogate (\uD800-\uDFFF) characters are counted as 2 bytes, since there's two of them
+ // and the actual character takes 4 bytes in UTF-8 (2*2=4). Might not work perfectly in
+ // edge cases such as illegal sequences, but that should never happen.
+
+ // https://en.wikipedia.org/wiki/UTF-8#Description
+ // The mapping from UTF-16 code units to UTF-8 bytes is as follows:
+ // > Range 0000-007F: codepoints that become 1 byte of UTF-8
+ // > Range 0080-07FF: codepoints that become 2 bytes of UTF-8
+ // > Range 0800-D7FF: codepoints that become 3 bytes of UTF-8
+ // > Range D800-DFFF: Surrogates (each pair becomes 4 bytes of UTF-8)
+ // > Range E000-FFFF: codepoints that become 3 bytes of UTF-8 (continued)
+
+ return str
+ .replace( /[\u0080-\u07FF\uD800-\uDFFF]/g, '**' )
+ .replace( /[\u0800-\uD7FF\uE000-\uFFFF]/g, '***' )
+ .length;
+ }
+
+ /**
+ * Calculate the character length of a string (accounting for UTF-16 surrogates).
+ *
+ * @param {string} str
+ * @return {number}
+ */
+ function codePointLength( str ) {
+ return str
+ // Low surrogate + high surrogate pairs represent one character (codepoint) each
+ .replace( /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '*' )
+ .length;
+ }
+
+ // Like String#charAt, but return the pair of UTF-16 surrogates for characters outside of BMP.
+ function codePointAt( string, offset, backwards ) {
+ // We don't need to check for offsets at the beginning or end of string,
+ // String#slice will simply return a shorter (or empty) substring.
+ var maybePair = backwards ?
+ string.slice( offset - 1, offset + 1 ) :
+ string.slice( offset, offset + 2 );
+ if ( /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( maybePair ) ) {
+ return maybePair;
+ } else {
+ return string.charAt( offset );
+ }
+ }
+
+ function trimLength( safeVal, newVal, length, lengthFn ) {
+ var startMatches, endMatches, matchesLen, inpParts, chopOff, oldChar, newChar,
+ oldVal = safeVal;
+
+ // Run the hook if one was provided, but only on the length
+ // assessment. The value itself is not to be affected by the hook.
+ if ( lengthFn( newVal ) <= length ) {
+ // Limit was not reached, just remember the new value
+ // and let the user continue.
+ return {
+ newVal: newVal,
+ trimmed: false
+ };
+ }
+
+ // Current input is longer than the active limit.
+ // Figure out what was added and limit the addition.
+ startMatches = 0;
+ endMatches = 0;
+
+ // It is important that we keep the search within the range of
+ // the shortest string's length.
+ // Imagine a user adds text that matches the end of the old value
+ // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without
+ // limiting both searches to the shortest length, endMatches would
+ // also be 3.
+ matchesLen = Math.min( newVal.length, oldVal.length );
+
+ // Count same characters from the left, first.
+ // (if "foo" -> "foofoo", assume addition was at the end).
+ while ( startMatches < matchesLen ) {
+ oldChar = codePointAt( oldVal, startMatches, false );
+ newChar = codePointAt( newVal, startMatches, false );
+ if ( oldChar !== newChar ) {
+ break;
+ }
+ startMatches += oldChar.length;
+ }
+
+ while ( endMatches < ( matchesLen - startMatches ) ) {
+ oldChar = codePointAt( oldVal, oldVal.length - 1 - endMatches, true );
+ newChar = codePointAt( newVal, newVal.length - 1 - endMatches, true );
+ if ( oldChar !== newChar ) {
+ break;
+ }
+ endMatches += oldChar.length;
+ }
+
+ inpParts = [
+ // Same start
+ newVal.slice( 0, startMatches ),
+ // Inserted content
+ newVal.slice( startMatches, newVal.length - endMatches ),
+ // Same end
+ newVal.slice( newVal.length - endMatches )
+ ];
+
+ // Chop off characters from the end of the "inserted content" string
+ // until the limit is statisfied.
+ // Make sure to stop when there is nothing to slice (T43450).
+ while ( lengthFn( inpParts.join( '' ) ) > length && inpParts[ 1 ].length > 0 ) {
+ // Do not chop off halves of surrogate pairs
+ chopOff = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( inpParts[ 1 ] ) ? 2 : 1;
+ inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -chopOff );
+ }
+
+ return {
+ newVal: inpParts.join( '' ),
+ // For pathological lengthFn() that always returns a length greater than the limit, we might have
+ // ended up not trimming - check for this case to avoid infinite loops
+ trimmed: newVal !== inpParts.join( '' )
+ };
+ }
+
+ /**
+ * Utility function to trim down a string, based on byteLimit
+ * and given a safe start position. It supports insertion anywhere
+ * in the string, so "foo" to "fobaro" if limit is 4 will result in
+ * "fobo", not "foba". Basically emulating the native maxlength by
+ * reconstructing where the insertion occurred.
+ *
+ * @param {string} safeVal Known value that was previously returned by this
+ * function, if none, pass empty string.
+ * @param {string} newVal New value that may have to be trimmed down.
+ * @param {number} byteLimit Number of bytes the value may be in size.
+ * @param {Function} [filterFn] Function to call on the string before assessing the length.
+ * @return {Object}
+ * @return {string} return.newVal
+ * @return {boolean} return.trimmed
+ */
+ function trimByteLength( safeVal, newVal, byteLimit, filterFn ) {
+ var lengthFn;
+ if ( filterFn ) {
+ lengthFn = function ( val ) {
+ return byteLength( filterFn( val ) );
+ };
+ } else {
+ lengthFn = byteLength;
+ }
+
+ return trimLength( safeVal, newVal, byteLimit, lengthFn );
+ }
+
+ /**
+ * Utility function to trim down a string, based on codePointLimit
+ * and given a safe start position. It supports insertion anywhere
+ * in the string, so "foo" to "fobaro" if limit is 4 will result in
+ * "fobo", not "foba". Basically emulating the native maxlength by
+ * reconstructing where the insertion occurred.
+ *
+ * @param {string} safeVal Known value that was previously returned by this
+ * function, if none, pass empty string.
+ * @param {string} newVal New value that may have to be trimmed down.
+ * @param {number} codePointLimit Number of characters the value may be in size.
+ * @param {Function} [filterFn] Function to call on the string before assessing the length.
+ * @return {Object}
+ * @return {string} return.newVal
+ * @return {boolean} return.trimmed
+ */
+ function trimCodePointLength( safeVal, newVal, codePointLimit, filterFn ) {
+ var lengthFn;
+ if ( filterFn ) {
+ lengthFn = function ( val ) {
+ return codePointLength( filterFn( val ) );
+ };
+ } else {
+ lengthFn = codePointLength;
+ }
+
+ return trimLength( safeVal, newVal, codePointLimit, lengthFn );
+ }
+
+ module.exports = {
+ byteLength: byteLength,
+ codePointLength: codePointLength,
+ trimByteLength: trimByteLength,
+ trimCodePointLength: trimCodePointLength
+ };
+
+}() );
--- /dev/null
+( function ( mw, $ ) {
+ 'use strict';
+
+ /**
+ * Provides an API for getting and setting cookies that is
+ * syntactically and functionally similar to the server-side cookie
+ * API (`WebRequest#getCookie` and `WebResponse#setcookie`).
+ *
+ * @author Sam Smith <samsmith@wikimedia.org>
+ * @author Matthew Flaschen <mflaschen@wikimedia.org>
+ * @author Timo Tijhof <krinklemail@gmail.com>
+ *
+ * @class mw.cookie
+ * @singleton
+ */
+ mw.cookie = {
+
+ /**
+ * Set or delete a cookie.
+ *
+ * While this is natural in JavaScript, contrary to `WebResponse#setcookie` in PHP, the
+ * default values for the `options` properties only apply if that property isn't set
+ * already in your options object (e.g. passing `{ secure: null }` or `{ secure: undefined }`
+ * overrides the default value for `options.secure`).
+ *
+ * @param {string} key
+ * @param {string|null} value Value of cookie. If `value` is `null` then this method will
+ * instead remove a cookie by name of `key`.
+ * @param {Object|Date} [options] Options object, or expiry date
+ * @param {Date|number|null} [options.expires] The expiry date of the cookie, or lifetime in seconds.
+ *
+ * If `options.expires` is null, then a session cookie is set.
+ *
+ * By default cookie expiration is based on `wgCookieExpiration`. Similar to `WebResponse`
+ * in PHP, we set a session cookie if `wgCookieExpiration` is 0. And for non-zero values
+ * it is interpreted as lifetime in seconds.
+ *
+ * @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
+ * @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
+ * @param {string} [options.path=wgCookiePath] The path attribute of the cookie
+ * @param {boolean} [options.secure=false] Whether or not to include the secure attribute.
+ * (Does **not** use the wgCookieSecure configuration variable)
+ */
+ set: function ( key, value, options ) {
+ var config, defaultOptions, date;
+
+ // wgCookieSecure is not used for now, since 'detect' could not work with
+ // ResourceLoaderStartUpModule, as module cache is not fragmented by protocol.
+ config = mw.config.get( [
+ 'wgCookiePrefix',
+ 'wgCookieDomain',
+ 'wgCookiePath',
+ 'wgCookieExpiration'
+ ] );
+
+ defaultOptions = {
+ prefix: config.wgCookiePrefix,
+ domain: config.wgCookieDomain,
+ path: config.wgCookiePath,
+ secure: false
+ };
+
+ // Options argument can also be a shortcut for the expiry
+ // Expiry can be a Date or null
+ if ( $.type( options ) !== 'object' ) {
+ // Also takes care of options = undefined, in which case we also don't need $.extend()
+ defaultOptions.expires = options;
+ options = defaultOptions;
+ } else {
+ options = $.extend( defaultOptions, options );
+ }
+
+ // Default to using wgCookieExpiration (lifetime in seconds).
+ // If wgCookieExpiration is 0, that is considered a special value indicating
+ // all cookies should be session cookies by default.
+ if ( options.expires === undefined && config.wgCookieExpiration !== 0 ) {
+ date = new Date();
+ date.setTime( Number( date ) + ( config.wgCookieExpiration * 1000 ) );
+ options.expires = date;
+ } else if ( typeof options.expires === 'number' ) {
+ // Lifetime in seconds
+ date = new Date();
+ date.setTime( Number( date ) + ( options.expires * 1000 ) );
+ options.expires = date;
+ } else if ( options.expires === null ) {
+ // $.cookie makes a session cookie when options.expires is omitted
+ delete options.expires;
+ }
+
+ // Process prefix
+ key = options.prefix + key;
+ delete options.prefix;
+
+ // Process value
+ if ( value !== null ) {
+ value = String( value );
+ }
+
+ // Other options are handled by $.cookie
+ $.cookie( key, value, options );
+ },
+
+ /**
+ * Get the value of a cookie.
+ *
+ * @param {string} key
+ * @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
+ * `undefined` or `null`, then `wgCookiePrefix` is used
+ * @param {Mixed} [defaultValue=null]
+ * @return {string|null|Mixed} If the cookie exists, then the value of the
+ * cookie, otherwise `defaultValue`
+ */
+ get: function ( key, prefix, defaultValue ) {
+ var result;
+
+ if ( prefix === undefined || prefix === null ) {
+ prefix = mw.config.get( 'wgCookiePrefix' );
+ }
+
+ // Was defaultValue omitted?
+ if ( arguments.length < 3 ) {
+ defaultValue = null;
+ }
+
+ result = $.cookie( prefix + key );
+
+ return result !== null ? result : defaultValue;
+ }
+ };
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+
+ var CONTROL_BUCKET = 'control',
+ MAX_INT32_UNSIGNED = 4294967295;
+
+ /**
+ * An implementation of Jenkins' one-at-a-time hash.
+ *
+ * @see https://en.wikipedia.org/wiki/Jenkins_hash_function
+ *
+ * @param {string} string String to hash
+ * @return {number} The hash as a 32-bit unsigned integer
+ * @ignore
+ *
+ * @author Ori Livneh <ori@wikimedia.org>
+ * @see https://jsbin.com/kejewi/4/watch?js,console
+ */
+ function hashString( string ) {
+ /* eslint-disable no-bitwise */
+ var hash = 0,
+ i = string.length;
+
+ while ( i-- ) {
+ hash += string.charCodeAt( i );
+ hash += ( hash << 10 );
+ hash ^= ( hash >> 6 );
+ }
+ hash += ( hash << 3 );
+ hash ^= ( hash >> 11 );
+ hash += ( hash << 15 );
+
+ return hash >>> 0;
+ /* eslint-enable no-bitwise */
+ }
+
+ /**
+ * Provides an API for bucketing users in experiments.
+ *
+ * @class mw.experiments
+ * @singleton
+ */
+ mw.experiments = {
+
+ /**
+ * Gets the bucket for the experiment given the token.
+ *
+ * The name of the experiment and the token are hashed. The hash is converted
+ * to a number which is then used to get a bucket.
+ *
+ * Consider the following experiment specification:
+ *
+ * ```
+ * {
+ * name: 'My first experiment',
+ * enabled: true,
+ * buckets: {
+ * control: 0.5
+ * A: 0.25,
+ * B: 0.25
+ * }
+ * }
+ * ```
+ *
+ * The experiment has three buckets: control, A, and B. The user has a 50%
+ * chance of being assigned to the control bucket, and a 25% chance of being
+ * assigned to either the A or B buckets. If the experiment were disabled,
+ * then the user would always be assigned to the control bucket.
+ *
+ * @param {Object} experiment
+ * @param {string} experiment.name The name of the experiment
+ * @param {boolean} experiment.enabled Whether or not the experiment is
+ * enabled. If the experiment is disabled, then the user is always assigned
+ * to the control bucket
+ * @param {Object} experiment.buckets A map of bucket name to probability
+ * that the user will be assigned to that bucket
+ * @param {string} token A token that uniquely identifies the user for the
+ * duration of the experiment
+ * @return {string} The bucket
+ */
+ getBucket: function ( experiment, token ) {
+ var buckets = experiment.buckets,
+ key,
+ range = 0,
+ hash,
+ max,
+ acc = 0;
+
+ if ( !experiment.enabled || $.isEmptyObject( experiment.buckets ) ) {
+ return CONTROL_BUCKET;
+ }
+
+ for ( key in buckets ) {
+ range += buckets[ key ];
+ }
+
+ hash = hashString( experiment.name + ':' + token );
+ max = ( hash / MAX_INT32_UNSIGNED ) * range;
+
+ for ( key in buckets ) {
+ acc += buckets[ key ];
+
+ if ( max <= acc ) {
+ return key;
+ }
+ }
+ }
+ };
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * Tools for inspecting page composition and performance.
+ *
+ * @author Ori Livneh
+ * @since 1.22
+ */
+
+/* eslint-disable no-console */
+
+( function ( mw, $ ) {
+
+ var inspect,
+ byteLength = require( 'mediawiki.String' ).byteLength,
+ hasOwn = Object.prototype.hasOwnProperty;
+
+ function sortByProperty( array, prop, descending ) {
+ var order = descending ? -1 : 1;
+ return array.sort( function ( a, b ) {
+ return a[ prop ] > b[ prop ] ? order : a[ prop ] < b[ prop ] ? -order : 0;
+ } );
+ }
+
+ function humanSize( bytes ) {
+ var i,
+ units = [ '', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB' ];
+
+ if ( !$.isNumeric( bytes ) || bytes === 0 ) { return bytes; }
+
+ for ( i = 0; bytes >= 1024; bytes /= 1024 ) { i++; }
+ // Maintain one decimal for kB and above, but don't
+ // add ".0" for bytes.
+ return bytes.toFixed( i > 0 ? 1 : 0 ) + units[ i ];
+ }
+
+ /**
+ * @class mw.inspect
+ * @singleton
+ */
+ inspect = {
+
+ /**
+ * Return a map of all dependency relationships between loaded modules.
+ *
+ * @return {Object} Maps module names to objects. Each sub-object has
+ * two properties, 'requires' and 'requiredBy'.
+ */
+ getDependencyGraph: function () {
+ var modules = inspect.getLoadedModules(),
+ graph = {};
+
+ modules.forEach( function ( moduleName ) {
+ var dependencies = mw.loader.moduleRegistry[ moduleName ].dependencies || [];
+
+ if ( !hasOwn.call( graph, moduleName ) ) {
+ graph[ moduleName ] = { requiredBy: [] };
+ }
+ graph[ moduleName ].requires = dependencies;
+
+ dependencies.forEach( function ( depName ) {
+ if ( !hasOwn.call( graph, depName ) ) {
+ graph[ depName ] = { requiredBy: [] };
+ }
+ graph[ depName ].requiredBy.push( moduleName );
+ } );
+ } );
+ return graph;
+ },
+
+ /**
+ * Calculate the byte size of a ResourceLoader module.
+ *
+ * @param {string} moduleName The name of the module
+ * @return {number|null} Module size in bytes or null
+ */
+ getModuleSize: function ( moduleName ) {
+ var module = mw.loader.moduleRegistry[ moduleName ],
+ args, i, size;
+
+ if ( module.state !== 'ready' ) {
+ return null;
+ }
+
+ if ( !module.style && !module.script ) {
+ return 0;
+ }
+
+ function getFunctionBody( func ) {
+ return String( func )
+ // To ensure a deterministic result, replace the start of the function
+ // declaration with a fixed string. For example, in Chrome 55, it seems
+ // V8 seemingly-at-random decides to sometimes put a line break between
+ // the opening brace and first statement of the function body. T159751.
+ .replace( /^\s*function\s*\([^)]*\)\s*{\s*/, 'function(){' )
+ .replace( /\s*}\s*$/, '}' );
+ }
+
+ // Based on the load.php response for this module.
+ // For example: `mw.loader.implement("example", function(){}, {"css":[".x{color:red}"]});`
+ // @see mw.loader.store.set().
+ args = [
+ moduleName,
+ module.script,
+ module.style,
+ module.messages,
+ module.templates
+ ];
+ // Trim trailing null or empty object, as load.php would have done.
+ // @see ResourceLoader::makeLoaderImplementScript and ResourceLoader::trimArray.
+ i = args.length;
+ while ( i-- ) {
+ if ( args[ i ] === null || ( $.isPlainObject( args[ i ] ) && $.isEmptyObject( args[ i ] ) ) ) {
+ args.splice( i, 1 );
+ } else {
+ break;
+ }
+ }
+
+ size = 0;
+ for ( i = 0; i < args.length; i++ ) {
+ if ( typeof args[ i ] === 'function' ) {
+ size += byteLength( getFunctionBody( args[ i ] ) );
+ } else {
+ size += byteLength( JSON.stringify( args[ i ] ) );
+ }
+ }
+
+ return size;
+ },
+
+ /**
+ * Given CSS source, count both the total number of selectors it
+ * contains and the number which match some element in the current
+ * document.
+ *
+ * @param {string} css CSS source
+ * @return {Object} Selector counts
+ * @return {number} return.selectors Total number of selectors
+ * @return {number} return.matched Number of matched selectors
+ */
+ auditSelectors: function ( css ) {
+ var selectors = { total: 0, matched: 0 },
+ style = document.createElement( 'style' );
+
+ style.textContent = css;
+ document.body.appendChild( style );
+ $.each( style.sheet.cssRules, function ( index, rule ) {
+ selectors.total++;
+ // document.querySelector() on prefixed pseudo-elements can throw exceptions
+ // in Firefox and Safari. Ignore these exceptions.
+ // https://bugs.webkit.org/show_bug.cgi?id=149160
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1204880
+ try {
+ if ( document.querySelector( rule.selectorText ) !== null ) {
+ selectors.matched++;
+ }
+ } catch ( e ) {}
+ } );
+ document.body.removeChild( style );
+ return selectors;
+ },
+
+ /**
+ * Get a list of all loaded ResourceLoader modules.
+ *
+ * @return {Array} List of module names
+ */
+ getLoadedModules: function () {
+ return mw.loader.getModuleNames().filter( function ( module ) {
+ return mw.loader.getState( module ) === 'ready';
+ } );
+ },
+
+ /**
+ * Print tabular data to the console, using console.table, console.log,
+ * or mw.log (in declining order of preference).
+ *
+ * @param {Array} data Tabular data represented as an array of objects
+ * with common properties.
+ */
+ dumpTable: function ( data ) {
+ try {
+ // Bartosz made me put this here.
+ if ( window.opera ) { throw window.opera; }
+ // Use Function.prototype#call to force an exception on Firefox,
+ // which doesn't define console#table but doesn't complain if you
+ // try to invoke it.
+ // eslint-disable-next-line no-useless-call
+ console.table.call( console, data );
+ return;
+ } catch ( e ) {}
+ try {
+ console.log( JSON.stringify( data, null, 2 ) );
+ return;
+ } catch ( e ) {}
+ mw.log( data );
+ },
+
+ /**
+ * Generate and print one more reports. When invoked with no arguments,
+ * print all reports.
+ *
+ * @param {...string} [reports] Report names to run, or unset to print
+ * all available reports.
+ */
+ runReports: function () {
+ var reports = arguments.length > 0 ?
+ Array.prototype.slice.call( arguments ) :
+ $.map( inspect.reports, function ( v, k ) { return k; } );
+
+ reports.forEach( function ( name ) {
+ inspect.dumpTable( inspect.reports[ name ]() );
+ } );
+ },
+
+ /**
+ * @class mw.inspect.reports
+ * @singleton
+ */
+ reports: {
+ /**
+ * Generate a breakdown of all loaded modules and their size in
+ * kilobytes. Modules are ordered from largest to smallest.
+ *
+ * @return {Object[]} Size reports
+ */
+ size: function () {
+ // Map each module to a descriptor object.
+ var modules = inspect.getLoadedModules().map( function ( module ) {
+ return {
+ name: module,
+ size: inspect.getModuleSize( module )
+ };
+ } );
+
+ // Sort module descriptors by size, largest first.
+ sortByProperty( modules, 'size', true );
+
+ // Convert size to human-readable string.
+ modules.forEach( function ( module ) {
+ module.sizeInBytes = module.size;
+ module.size = humanSize( module.size );
+ } );
+
+ return modules;
+ },
+
+ /**
+ * For each module with styles, count the number of selectors, and
+ * count how many match against some element currently in the DOM.
+ *
+ * @return {Object[]} CSS reports
+ */
+ css: function () {
+ var modules = [];
+
+ inspect.getLoadedModules().forEach( function ( name ) {
+ var css, stats, module = mw.loader.moduleRegistry[ name ];
+
+ try {
+ css = module.style.css.join();
+ } catch ( e ) { return; } // skip
+
+ stats = inspect.auditSelectors( css );
+ modules.push( {
+ module: name,
+ allSelectors: stats.total,
+ matchedSelectors: stats.matched,
+ percentMatched: stats.total !== 0 ?
+ ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
+ } );
+ } );
+ sortByProperty( modules, 'allSelectors', true );
+ return modules;
+ },
+
+ /**
+ * Report stats on mw.loader.store: the number of localStorage
+ * cache hits and misses, the number of items purged from the
+ * cache, and the total size of the module blob in localStorage.
+ *
+ * @return {Object[]} Store stats
+ */
+ store: function () {
+ var raw, stats = { enabled: mw.loader.store.enabled };
+ if ( stats.enabled ) {
+ $.extend( stats, mw.loader.store.stats );
+ try {
+ raw = localStorage.getItem( mw.loader.store.getStoreKey() );
+ stats.totalSizeInBytes = byteLength( raw );
+ stats.totalSize = humanSize( byteLength( raw ) );
+ } catch ( e ) {}
+ }
+ return [ stats ];
+ }
+ },
+
+ /**
+ * Perform a string search across the JavaScript and CSS source code
+ * of all loaded modules and return an array of the names of the
+ * modules that matched.
+ *
+ * @param {string|RegExp} pattern String or regexp to match.
+ * @return {Array} Array of the names of modules that matched.
+ */
+ grep: function ( pattern ) {
+ if ( typeof pattern.test !== 'function' ) {
+ pattern = new RegExp( mw.RegExp.escape( pattern ), 'g' );
+ }
+
+ return inspect.getLoadedModules().filter( function ( moduleName ) {
+ var module = mw.loader.moduleRegistry[ moduleName ];
+
+ // Grep module's JavaScript
+ if ( $.isFunction( module.script ) && pattern.test( module.script.toString() ) ) {
+ return true;
+ }
+
+ // Grep module's CSS
+ if (
+ $.isPlainObject( module.style ) && Array.isArray( module.style.css ) &&
+ pattern.test( module.style.css.join( '' ) )
+ ) {
+ // Module's CSS source matches
+ return true;
+ }
+
+ return false;
+ } );
+ }
+ };
+
+ if ( mw.config.get( 'debug' ) ) {
+ mw.log( 'mw.inspect: reports are not available in debug mode.' );
+ }
+
+ mw.inspect = inspect;
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.plugin.notify
+ */
+( function ( mw ) {
+ 'use strict';
+
+ /**
+ * @see mw.notification#notify
+ * @see mw.notification#defaults
+ * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
+ * @param {Object} options See mw.notification#defaults for details.
+ * @return {jQuery.Promise}
+ */
+ mw.notify = function ( message, options ) {
+ // Don't bother loading the whole notification system if we never use it.
+ return mw.loader.using( 'mediawiki.notification' )
+ .then( function () {
+ // Call notify with the notification the user requested of us.
+ return mw.notification.notify( message, options );
+ } );
+ };
+
+ /**
+ * @class mw
+ * @mixins mw.plugin.notify
+ */
+
+}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ 'use strict';
+
+ // Catch exceptions to avoid fatal in Chrome's "Block data storage" mode
+ // which throws when accessing the localStorage property itself, as opposed
+ // to the standard behaviour of throwing on getItem/setItem. (T148998)
+ var
+ localStorage = ( function () {
+ try {
+ return window.localStorage;
+ } catch ( e ) {}
+ }() ),
+ sessionStorage = ( function () {
+ try {
+ return window.sessionStorage;
+ } catch ( e ) {}
+ }() );
+
+ /**
+ * A wrapper for an HTML5 Storage interface (`localStorage` or `sessionStorage`)
+ * that is safe to call on all browsers.
+ *
+ * @class mw.SafeStorage
+ * @private
+ * @param {Object|undefined} store The Storage instance to wrap around
+ */
+ function SafeStorage( store ) {
+ this.store = store;
+ }
+
+ /**
+ * Retrieve value from device storage.
+ *
+ * @param {string} key Key of item to retrieve
+ * @return {string|null|boolean} String value, null if no value exists, or false
+ * if localStorage is not available.
+ */
+ SafeStorage.prototype.get = function ( key ) {
+ try {
+ return this.store.getItem( key );
+ } catch ( e ) {}
+ return false;
+ };
+
+ /**
+ * Set a value in device storage.
+ *
+ * @param {string} key Key name to store under
+ * @param {string} value Value to be stored
+ * @return {boolean} Whether the save succeeded or not
+ */
+ SafeStorage.prototype.set = function ( key, value ) {
+ try {
+ this.store.setItem( key, value );
+ return true;
+ } catch ( e ) {}
+ return false;
+ };
+
+ /**
+ * Remove a value from device storage.
+ *
+ * @param {string} key Key of item to remove
+ * @return {boolean} Whether the save succeeded or not
+ */
+ SafeStorage.prototype.remove = function ( key ) {
+ try {
+ this.store.removeItem( key );
+ return true;
+ } catch ( e ) {}
+ return false;
+ };
+
+ /**
+ * A wrapper for the HTML5 `localStorage` interface
+ * that is safe to call on all browsers.
+ *
+ * @class
+ * @singleton
+ * @extends mw.SafeStorage
+ */
+ mw.storage = new SafeStorage( localStorage );
+
+ /**
+ * A wrapper for the HTML5 `sessionStorage` interface
+ * that is safe to call on all browsers.
+ *
+ * @class
+ * @singleton
+ * @extends mw.SafeStorage
+ */
+ mw.storage.session = new SafeStorage( sessionStorage );
+
+}( mediaWiki ) );
--- /dev/null
+/**
+ * @class mw.user
+ * @singleton
+ */
+/* global Uint32Array */
+( function ( mw, $ ) {
+ var userInfoPromise, stickyRandomSessionId;
+
+ /**
+ * Get the current user's groups or rights
+ *
+ * @private
+ * @return {jQuery.Promise}
+ */
+ function getUserInfo() {
+ if ( !userInfoPromise ) {
+ userInfoPromise = new mw.Api().getUserInfo();
+ }
+ return userInfoPromise;
+ }
+
+ // mw.user with the properties options and tokens gets defined in mediawiki.js.
+ $.extend( mw.user, {
+
+ /**
+ * Generate a random user session ID.
+ *
+ * This information would potentially be stored in a cookie to identify a user during a
+ * session or series of sessions. Its uniqueness should not be depended on unless the
+ * browser supports the crypto API.
+ *
+ * Known problems with Math.random():
+ * Using the Math.random function we have seen sets
+ * with 1% of non uniques among 200,000 values with Safari providing most of these.
+ * Given the prevalence of Safari in mobile the percentage of duplicates in
+ * mobile usages of this code is probably higher.
+ *
+ * Rationale:
+ * We need about 64 bits to make sure that probability of collision
+ * on 500 million (5*10^8) is <= 1%
+ * See https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
+ *
+ * @return {string} 64 bit integer in hex format, padded
+ */
+ generateRandomSessionId: function () {
+ var rnds, i,
+ hexRnds = new Array( 2 ),
+ // Support: IE 11
+ crypto = window.crypto || window.msCrypto;
+
+ if ( crypto && crypto.getRandomValues && typeof Uint32Array === 'function' ) {
+ // Fill an array with 2 random values, each of which is 32 bits.
+ // Note that Uint32Array is array-like but does not implement Array.
+ rnds = new Uint32Array( 2 );
+ crypto.getRandomValues( rnds );
+ } else {
+ rnds = [
+ Math.floor( Math.random() * 0x100000000 ),
+ Math.floor( Math.random() * 0x100000000 )
+ ];
+ }
+ // Convert number to a string with 16 hex characters
+ for ( i = 0; i < 2; i++ ) {
+ // Add 0x100000000 before converting to hex and strip the extra character
+ // after converting to keep the leading zeros.
+ hexRnds[ i ] = ( rnds[ i ] + 0x100000000 ).toString( 16 ).slice( 1 );
+ }
+
+ // Concatenation of two random integers with entropy n and m
+ // returns a string with entropy n+m if those strings are independent
+ return hexRnds.join( '' );
+ },
+
+ /**
+ * A sticky generateRandomSessionId for the current JS execution context,
+ * cached within this class.
+ *
+ * @return {string} 64 bit integer in hex format, padded
+ */
+ stickyRandomId: function () {
+ if ( !stickyRandomSessionId ) {
+ stickyRandomSessionId = mw.user.generateRandomSessionId();
+ }
+
+ return stickyRandomSessionId;
+ },
+
+ /**
+ * Get the current user's database id
+ *
+ * Not to be confused with #id.
+ *
+ * @return {number} Current user's id, or 0 if user is anonymous
+ */
+ getId: function () {
+ return mw.config.get( 'wgUserId' ) || 0;
+ },
+
+ /**
+ * Get the current user's name
+ *
+ * @return {string|null} User name string or null if user is anonymous
+ */
+ getName: function () {
+ return mw.config.get( 'wgUserName' );
+ },
+
+ /**
+ * Get date user registered, if available
+ *
+ * @return {boolean|null|Date} False for anonymous users, null if data is
+ * unavailable, or Date for when the user registered.
+ */
+ getRegistration: function () {
+ var registration;
+ if ( mw.user.isAnon() ) {
+ return false;
+ }
+ registration = mw.config.get( 'wgUserRegistration' );
+ // Registration may be unavailable if the user signed up before MediaWiki
+ // began tracking this.
+ return !registration ? null : new Date( registration );
+ },
+
+ /**
+ * Whether the current user is anonymous
+ *
+ * @return {boolean}
+ */
+ isAnon: function () {
+ return mw.user.getName() === null;
+ },
+
+ /**
+ * Get an automatically generated random ID (persisted in sessionStorage)
+ *
+ * This ID is ephemeral for everyone, staying in their browser only until they
+ * close their browsing session.
+ *
+ * @return {string} Random session ID
+ */
+ sessionId: function () {
+ var sessionId = mw.storage.session.get( 'mwuser-sessionId' );
+ if ( !sessionId ) {
+ sessionId = mw.user.generateRandomSessionId();
+ mw.storage.session.set( 'mwuser-sessionId', sessionId );
+ }
+ return sessionId;
+ },
+
+ /**
+ * Get the current user's name or the session ID
+ *
+ * Not to be confused with #getId.
+ *
+ * @return {string} User name or random session ID
+ */
+ id: function () {
+ return mw.user.getName() || mw.user.sessionId();
+ },
+
+ /**
+ * Get the current user's groups
+ *
+ * @param {Function} [callback]
+ * @return {jQuery.Promise}
+ */
+ getGroups: function ( callback ) {
+ var userGroups = mw.config.get( 'wgUserGroups', [] );
+
+ // Uses promise for backwards compatibility
+ return $.Deferred().resolve( userGroups ).done( callback );
+ },
+
+ /**
+ * Get the current user's rights
+ *
+ * @param {Function} [callback]
+ * @return {jQuery.Promise}
+ */
+ getRights: function ( callback ) {
+ return getUserInfo().then(
+ function ( userInfo ) { return userInfo.rights; },
+ function () { return []; }
+ ).done( callback );
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * Add autocomplete suggestions for names of registered users.
+ */
+( function ( mw, $ ) {
+ var api, config;
+
+ config = {
+ fetch: function ( userInput, response, maxRows ) {
+ var node = this[ 0 ];
+
+ api = api || new mw.Api();
+
+ $.data( node, 'request', api.get( {
+ formatversion: 2,
+ action: 'query',
+ list: 'allusers',
+ // Prefix of list=allusers is case sensitive. Normalise first
+ // character to uppercase so that "fo" may yield "Foo".
+ auprefix: userInput[ 0 ].toUpperCase() + userInput.slice( 1 ),
+ aulimit: maxRows
+ } ).done( function ( data ) {
+ var users = $.map( data.query.allusers, function ( userObj ) {
+ return userObj.name;
+ } );
+ response( users );
+ } ) );
+ },
+ cancel: function () {
+ var node = this[ 0 ],
+ request = $.data( node, 'request' );
+
+ if ( request ) {
+ request.abort();
+ $.removeData( node, 'request' );
+ }
+ }
+ };
+
+ $( function () {
+ $( '.mw-autocomplete-user' ).suggestions( config );
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+ 'use strict';
+
+ var util;
+
+ /**
+ * Encode the string like PHP's rawurlencode
+ * @ignore
+ *
+ * @param {string} str String to be encoded.
+ * @return {string} Encoded string
+ */
+ function rawurlencode( str ) {
+ str = String( str );
+ return encodeURIComponent( str )
+ .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
+ .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' );
+ }
+
+ /**
+ * Private helper function used by util.escapeId*()
+ * @ignore
+ *
+ * @param {string} str String to be encoded
+ * @param {string} mode Encoding mode, see documentation for $wgFragmentMode
+ * in DefaultSettings.php
+ * @return {string} Encoded string
+ */
+ function escapeIdInternal( str, mode ) {
+ str = String( str );
+
+ switch ( mode ) {
+ case 'html5':
+ return str.replace( / /g, '_' );
+ case 'legacy':
+ return rawurlencode( str.replace( / /g, '_' ) )
+ .replace( /%3A/g, ':' )
+ .replace( /%/g, '.' );
+ default:
+ throw new Error( 'Unrecognized ID escaping mode ' + mode );
+ }
+ }
+
+ /**
+ * Utility library
+ * @class mw.util
+ * @singleton
+ */
+ util = {
+
+ /* Main body */
+
+ /**
+ * Encode the string like PHP's rawurlencode
+ *
+ * @param {string} str String to be encoded.
+ * @return {string} Encoded string
+ */
+ rawurlencode: rawurlencode,
+
+ /**
+ * Encode string into HTML id compatible form suitable for use in HTML
+ * Analog to PHP Sanitizer::escapeIdForAttribute()
+ *
+ * @since 1.30
+ *
+ * @param {string} str String to encode
+ * @return {string} Encoded string
+ */
+ escapeIdForAttribute: function ( str ) {
+ var mode = mw.config.get( 'wgFragmentMode' )[ 0 ];
+
+ return escapeIdInternal( str, mode );
+ },
+
+ /**
+ * Encode string into HTML id compatible form suitable for use in links
+ * Analog to PHP Sanitizer::escapeIdForLink()
+ *
+ * @since 1.30
+ *
+ * @param {string} str String to encode
+ * @return {string} Encoded string
+ */
+ escapeIdForLink: function ( str ) {
+ var mode = mw.config.get( 'wgFragmentMode' )[ 0 ];
+
+ return escapeIdInternal( str, mode );
+ },
+
+ /**
+ * Encode page titles for use in a URL
+ *
+ * We want / and : to be included as literal characters in our title URLs
+ * as they otherwise fatally break the title.
+ *
+ * The others are decoded because we can, it's prettier and matches behaviour
+ * of `wfUrlencode` in PHP.
+ *
+ * @param {string} str String to be encoded.
+ * @return {string} Encoded string
+ */
+ wikiUrlencode: function ( str ) {
+ return util.rawurlencode( str )
+ .replace( /%20/g, '_' )
+ // wfUrlencode replacements
+ .replace( /%3B/g, ';' )
+ .replace( /%40/g, '@' )
+ .replace( /%24/g, '$' )
+ .replace( /%21/g, '!' )
+ .replace( /%2A/g, '*' )
+ .replace( /%28/g, '(' )
+ .replace( /%29/g, ')' )
+ .replace( /%2C/g, ',' )
+ .replace( /%2F/g, '/' )
+ .replace( /%7E/g, '~' )
+ .replace( /%3A/g, ':' );
+ },
+
+ /**
+ * Get the link to a page name (relative to `wgServer`),
+ *
+ * @param {string|null} [pageName=wgPageName] Page name
+ * @param {Object} [params] A mapping of query parameter names to values,
+ * e.g. `{ action: 'edit' }`
+ * @return {string} Url of the page with name of `pageName`
+ */
+ getUrl: function ( pageName, params ) {
+ var titleFragmentStart, url, query,
+ fragment = '',
+ title = typeof pageName === 'string' ? pageName : mw.config.get( 'wgPageName' );
+
+ // Find any fragment
+ titleFragmentStart = title.indexOf( '#' );
+ if ( titleFragmentStart !== -1 ) {
+ fragment = title.slice( titleFragmentStart + 1 );
+ // Exclude the fragment from the page name
+ title = title.slice( 0, titleFragmentStart );
+ }
+
+ // Produce query string
+ if ( params ) {
+ query = $.param( params );
+ }
+ if ( query ) {
+ url = title ?
+ util.wikiScript() + '?title=' + util.wikiUrlencode( title ) + '&' + query :
+ util.wikiScript() + '?' + query;
+ } else {
+ url = mw.config.get( 'wgArticlePath' )
+ .replace( '$1', util.wikiUrlencode( title ).replace( /\$/g, '$$$$' ) );
+ }
+
+ // Append the encoded fragment
+ if ( fragment.length ) {
+ url += '#' + util.escapeIdForLink( fragment );
+ }
+
+ return url;
+ },
+
+ /**
+ * Get address to a script in the wiki root.
+ * For index.php use `mw.config.get( 'wgScript' )`.
+ *
+ * @since 1.18
+ * @param {string} str Name of script (e.g. 'api'), defaults to 'index'
+ * @return {string} Address to script (e.g. '/w/api.php' )
+ */
+ wikiScript: function ( str ) {
+ str = str || 'index';
+ if ( str === 'index' ) {
+ return mw.config.get( 'wgScript' );
+ } else if ( str === 'load' ) {
+ return mw.config.get( 'wgLoadScript' );
+ } else {
+ return mw.config.get( 'wgScriptPath' ) + '/' + str + '.php';
+ }
+ },
+
+ /**
+ * Append a new style block to the head and return the CSSStyleSheet object.
+ * Use .ownerNode to access the `<style>` element, or use mw.loader#addStyleTag.
+ * This function returns the styleSheet object for convience (due to cross-browsers
+ * difference as to where it is located).
+ *
+ * var sheet = util.addCSS( '.foobar { display: none; }' );
+ * $( foo ).click( function () {
+ * // Toggle the sheet on and off
+ * sheet.disabled = !sheet.disabled;
+ * } );
+ *
+ * @param {string} text CSS to be appended
+ * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element.
+ */
+ addCSS: function ( text ) {
+ var s = mw.loader.addStyleTag( text );
+ return s.sheet || s.styleSheet || s;
+ },
+
+ /**
+ * Grab the URL parameter value for the given parameter.
+ * Returns null if not found.
+ *
+ * @param {string} param The parameter name.
+ * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
+ * @return {Mixed} Parameter value or null.
+ */
+ getParamValue: function ( param, url ) {
+ // Get last match, stop at hash
+ var re = new RegExp( '^[^#]*[&?]' + mw.RegExp.escape( param ) + '=([^&#]*)' ),
+ m = re.exec( url !== undefined ? url : location.href );
+
+ if ( m ) {
+ // Beware that decodeURIComponent is not required to understand '+'
+ // by spec, as encodeURIComponent does not produce it.
+ return decodeURIComponent( m[ 1 ].replace( /\+/g, '%20' ) );
+ }
+ return null;
+ },
+
+ /**
+ * The content wrapper of the skin (e.g. `.mw-body`).
+ *
+ * Populated on document ready. To use this property,
+ * wait for `$.ready` and be sure to have a module dependency on
+ * `mediawiki.util` which will ensure
+ * your document ready handler fires after initialization.
+ *
+ * Because of the lazy-initialised nature of this property,
+ * you're discouraged from using it.
+ *
+ * If you need just the wikipage content (not any of the
+ * extra elements output by the skin), use `$( '#mw-content-text' )`
+ * instead. Or listen to mw.hook#wikipage_content which will
+ * allow your code to re-run when the page changes (e.g. live preview
+ * or re-render after ajax save).
+ *
+ * @property {jQuery}
+ */
+ $content: null,
+
+ /**
+ * Add a link to a portlet menu on the page, such as:
+ *
+ * p-cactions (Content actions), p-personal (Personal tools),
+ * p-navigation (Navigation), p-tb (Toolbox)
+ *
+ * The first three parameters are required, the others are optional and
+ * may be null. Though providing an id and tooltip is recommended.
+ *
+ * By default the new link will be added to the end of the list. To
+ * add the link before a given existing item, pass the DOM node
+ * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector
+ * (e.g. `'#foobar'`) for that item.
+ *
+ * util.addPortletLink(
+ * 'p-tb', 'https://www.mediawiki.org/',
+ * 'mediawiki.org', 't-mworg', 'Go to mediawiki.org', 'm', '#t-print'
+ * );
+ *
+ * var node = util.addPortletLink(
+ * 'p-tb',
+ * new mw.Title( 'Special:Example' ).getUrl(),
+ * 'Example'
+ * );
+ * $( node ).on( 'click', function ( e ) {
+ * console.log( 'Example' );
+ * e.preventDefault();
+ * } );
+ *
+ * @param {string} portlet ID of the target portlet ( 'p-cactions' or 'p-personal' etc.)
+ * @param {string} href Link URL
+ * @param {string} text Link text
+ * @param {string} [id] ID of the new item, should be unique and preferably have
+ * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' )
+ * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
+ * @param {string} [accesskey] Access key to activate this link (one character, try
+ * to avoid conflicts. Use `$( '[accesskey=x]' ).get()` in the console to
+ * see if 'x' is already used.
+ * @param {HTMLElement|jQuery|string} [nextnode] Element or jQuery-selector string to the item that
+ * the new item should be added before, should be another item in the same
+ * list, it will be ignored otherwise
+ *
+ * @return {HTMLElement|null} The added element (a ListItem or Anchor element,
+ * depending on the skin) or null if no element was added to the document.
+ */
+ addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) {
+ var $item, $link, $portlet, $ul;
+
+ // Check if there's at least 3 arguments to prevent a TypeError
+ if ( arguments.length < 3 ) {
+ return null;
+ }
+ // Setup the anchor tag
+ $link = $( '<a>' ).attr( 'href', href ).text( text );
+ if ( tooltip ) {
+ $link.attr( 'title', tooltip );
+ }
+
+ // Select the specified portlet
+ $portlet = $( '#' + portlet );
+ if ( $portlet.length === 0 ) {
+ return null;
+ }
+ // Select the first (most likely only) unordered list inside the portlet
+ $ul = $portlet.find( 'ul' ).eq( 0 );
+
+ // If it didn't have an unordered list yet, create it
+ if ( $ul.length === 0 ) {
+
+ $ul = $( '<ul>' );
+
+ // If there's no <div> inside, append it to the portlet directly
+ if ( $portlet.find( 'div:first' ).length === 0 ) {
+ $portlet.append( $ul );
+ } else {
+ // otherwise if there's a div (such as div.body or div.pBody)
+ // append the <ul> to last (most likely only) div
+ $portlet.find( 'div' ).eq( -1 ).append( $ul );
+ }
+ }
+ // Just in case..
+ if ( $ul.length === 0 ) {
+ return null;
+ }
+
+ // Unhide portlet if it was hidden before
+ $portlet.removeClass( 'emptyPortlet' );
+
+ // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab)
+ // and back up the selector to the list item
+ if ( $portlet.hasClass( 'vectorTabs' ) ) {
+ $item = $link.wrap( '<li><span></span></li>' ).parent().parent();
+ } else {
+ $item = $link.wrap( '<li></li>' ).parent();
+ }
+
+ // Implement the properties passed to the function
+ if ( id ) {
+ $item.attr( 'id', id );
+ }
+
+ if ( accesskey ) {
+ $link.attr( 'accesskey', accesskey );
+ }
+
+ if ( tooltip ) {
+ $link.attr( 'title', tooltip );
+ }
+
+ if ( nextnode ) {
+ // Case: nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
+ // Case: nextnode is a CSS selector for jQuery
+ if ( nextnode.nodeType || typeof nextnode === 'string' ) {
+ nextnode = $ul.find( nextnode );
+ } else if ( !nextnode.jquery ) {
+ // Error: Invalid nextnode
+ nextnode = undefined;
+ }
+ if ( nextnode && ( nextnode.length !== 1 || nextnode[ 0 ].parentNode !== $ul[ 0 ] ) ) {
+ // Error: nextnode must resolve to a single node
+ // Error: nextnode must have the associated <ul> as its parent
+ nextnode = undefined;
+ }
+ }
+
+ // Case: nextnode is a jQuery-wrapped DOM element
+ if ( nextnode ) {
+ nextnode.before( $item );
+ } else {
+ // Fallback (this is the default behavior)
+ $ul.append( $item );
+ }
+
+ // Update tooltip for the access key after inserting into DOM
+ // to get a localized access key label (T69946).
+ $link.updateTooltipAccessKeys();
+
+ return $item[ 0 ];
+ },
+
+ /**
+ * Validate a string as representing a valid e-mail address
+ * according to HTML5 specification. Please note the specification
+ * does not validate a domain with one character.
+ *
+ * FIXME: should be moved to or replaced by a validation module.
+ *
+ * @param {string} mailtxt E-mail address to be validated.
+ * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false
+ * as determined by validation.
+ */
+ validateEmail: function ( mailtxt ) {
+ var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp;
+
+ if ( mailtxt === '' ) {
+ return null;
+ }
+
+ // HTML5 defines a string as valid e-mail address if it matches
+ // the ABNF:
+ // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
+ // With:
+ // - atext : defined in RFC 5322 section 3.2.3
+ // - ldh-str : defined in RFC 1034 section 3.5
+ //
+ // (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
+ // First, define the RFC 5322 'atext' which is pretty easy:
+ // atext = ALPHA / DIGIT / ; Printable US-ASCII
+ // "!" / "#" / ; characters not including
+ // "$" / "%" / ; specials. Used for atoms.
+ // "&" / "'" /
+ // "*" / "+" /
+ // "-" / "/" /
+ // "=" / "?" /
+ // "^" / "_" /
+ // "`" / "{" /
+ // "|" / "}" /
+ // "~"
+ rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
+
+ // Next define the RFC 1034 'ldh-str'
+ // <domain> ::= <subdomain> | " "
+ // <subdomain> ::= <label> | <subdomain> "." <label>
+ // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
+ // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
+ // <let-dig-hyp> ::= <let-dig> | "-"
+ // <let-dig> ::= <letter> | <digit>
+ rfc1034LdhStr = 'a-z0-9\\-';
+
+ html5EmailRegexp = new RegExp(
+ // start of string
+ '^' +
+ // User part which is liberal :p
+ '[' + rfc5322Atext + '\\.]+' +
+ // 'at'
+ '@' +
+ // Domain first part
+ '[' + rfc1034LdhStr + ']+' +
+ // Optional second part and following are separated by a dot
+ '(?:\\.[' + rfc1034LdhStr + ']+)*' +
+ // End of string
+ '$',
+ // RegExp is case insensitive
+ 'i'
+ );
+ return ( mailtxt.match( html5EmailRegexp ) !== null );
+ },
+
+ /**
+ * Note: borrows from IP::isIPv4
+ *
+ * @param {string} address
+ * @param {boolean} [allowBlock=false]
+ * @return {boolean}
+ */
+ isIPv4Address: function ( address, allowBlock ) {
+ var block, RE_IP_BYTE, RE_IP_ADD;
+
+ if ( typeof address !== 'string' ) {
+ return false;
+ }
+
+ block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '';
+ RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])';
+ RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
+
+ return ( new RegExp( '^' + RE_IP_ADD + block + '$' ).test( address ) );
+ },
+
+ /**
+ * Note: borrows from IP::isIPv6
+ *
+ * @param {string} address
+ * @param {boolean} [allowBlock=false]
+ * @return {boolean}
+ */
+ isIPv6Address: function ( address, allowBlock ) {
+ var block, RE_IPV6_ADD;
+
+ if ( typeof address !== 'string' ) {
+ return false;
+ }
+
+ block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '';
+ RE_IPV6_ADD =
+ '(?:' + // starts with "::" (including "::")
+ ':(?::|(?::' +
+ '[0-9A-Fa-f]{1,4}' +
+ '){1,7})' +
+ '|' + // ends with "::" (except "::")
+ '[0-9A-Fa-f]{1,4}' +
+ '(?::' +
+ '[0-9A-Fa-f]{1,4}' +
+ '){0,6}::' +
+ '|' + // contains no "::"
+ '[0-9A-Fa-f]{1,4}' +
+ '(?::' +
+ '[0-9A-Fa-f]{1,4}' +
+ '){7}' +
+ ')';
+
+ if ( new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) ) {
+ return true;
+ }
+
+ // contains one "::" in the middle (single '::' check below)
+ RE_IPV6_ADD =
+ '[0-9A-Fa-f]{1,4}' +
+ '(?:::?' +
+ '[0-9A-Fa-f]{1,4}' +
+ '){1,6}';
+
+ return (
+ new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) &&
+ /::/.test( address ) &&
+ !/::.*::/.test( address )
+ );
+ },
+
+ /**
+ * Check whether a string is an IP address
+ *
+ * @since 1.25
+ * @param {string} address String to check
+ * @param {boolean} [allowBlock=false] If a block of IPs should be allowed
+ * @return {boolean}
+ */
+ isIPAddress: function ( address, allowBlock ) {
+ return util.isIPv4Address( address, allowBlock ) ||
+ util.isIPv6Address( address, allowBlock );
+ }
+ };
+
+ /**
+ * Add a little box at the top of the screen to inform the user of
+ * something, replacing any previous message.
+ * Calling with no arguments, with an empty string or null will hide the message
+ *
+ * @method jsMessage
+ * @deprecated since 1.20 Use mw#notify
+ * @param {Mixed} message The DOM-element, jQuery object or HTML-string to be put inside the message box.
+ * to allow CSS/JS to hide different boxes. null = no class used.
+ */
+ mw.log.deprecate( util, 'jsMessage', function ( message ) {
+ if ( !arguments.length || message === '' || message === null ) {
+ return true;
+ }
+ if ( typeof message !== 'object' ) {
+ message = $.parseHTML( message );
+ }
+ mw.notify( message, { autoHide: true, tag: 'legacy' } );
+ return true;
+ }, 'Use mw.notify instead.', 'mw.util.jsMessage' );
+
+ /**
+ * Initialisation of mw.util.$content
+ */
+ function init() {
+ util.$content = ( function () {
+ var i, l, $node, selectors;
+
+ selectors = [
+ // The preferred standard is class "mw-body".
+ // You may also use class "mw-body mw-body-primary" if you use
+ // mw-body in multiple locations. Or class "mw-body-primary" if
+ // you use mw-body deeper in the DOM.
+ '.mw-body-primary',
+ '.mw-body',
+
+ // If the skin has no such class, fall back to the parser output
+ '#mw-content-text'
+ ];
+
+ for ( i = 0, l = selectors.length; i < l; i++ ) {
+ $node = $( selectors[ i ] );
+ if ( $node.length ) {
+ return $node.first();
+ }
+ }
+
+ // Should never happen... well, it could if someone is not finished writing a
+ // skin and has not yet inserted bodytext yet.
+ return $( 'body' );
+ }() );
+ }
+
+ /**
+ * Former public initialisation. Now a no-op function.
+ *
+ * @method util_init
+ * @deprecated since 1.30
+ */
+ mw.log.deprecate( util, 'init', $.noop, 'Remove the call of mw.util.init().', 'mw.util.init' );
+
+ $( init );
+
+ mw.util = util;
+ module.exports = util;
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+ 'use strict';
+
+ /**
+ * Utility library for viewport-related functions
+ *
+ * Notable references:
+ * - https://github.com/tuupola/jquery_lazyload
+ * - https://github.com/luis-almeida/unveil
+ *
+ * @class mw.viewport
+ * @singleton
+ */
+ var viewport = {
+
+ /**
+ * This is a private method pulled inside the module for testing purposes.
+ *
+ * @ignore
+ * @private
+ * @return {Object} Viewport positions
+ */
+ makeViewportFromWindow: function () {
+ var $window = $( window ),
+ scrollTop = $window.scrollTop(),
+ scrollLeft = $window.scrollLeft();
+
+ return {
+ top: scrollTop,
+ left: scrollLeft,
+ right: scrollLeft + $window.width(),
+ bottom: ( window.innerHeight ? window.innerHeight : $window.height() ) + scrollTop
+ };
+ },
+
+ /**
+ * Check if any part of a given element is in a given viewport
+ *
+ * @method
+ * @param {HTMLElement} el Element that's being tested
+ * @param {Object} [rectangle] Viewport to test against; structured as such:
+ *
+ * var rectangle = {
+ * top: topEdge,
+ * left: leftEdge,
+ * right: rightEdge,
+ * bottom: bottomEdge
+ * }
+ * Defaults to viewport made from `window`.
+ *
+ * @return {boolean}
+ */
+ isElementInViewport: function ( el, rectangle ) {
+ var $el = $( el ),
+ offset = $el.offset(),
+ rect = {
+ height: $el.height(),
+ width: $el.width(),
+ top: offset.top,
+ left: offset.left
+ },
+ viewport = rectangle || this.makeViewportFromWindow();
+
+ return (
+ // Top border must be above viewport's bottom
+ ( viewport.bottom >= rect.top ) &&
+ // Left border must be before viewport's right border
+ ( viewport.right >= rect.left ) &&
+ // Bottom border must be below viewport's top
+ ( viewport.top <= rect.top + rect.height ) &&
+ // Right border must be after viewport's left border
+ ( viewport.left <= rect.left + rect.width )
+ );
+ },
+
+ /**
+ * Check if an element is a given threshold away in any direction from a given viewport
+ *
+ * @method
+ * @param {HTMLElement} el Element that's being tested
+ * @param {number} [threshold] Pixel distance considered "close". Must be a positive number.
+ * Defaults to 50.
+ * @param {Object} [rectangle] Viewport to test against.
+ * Defaults to viewport made from `window`.
+ * @return {boolean}
+ */
+ isElementCloseToViewport: function ( el, threshold, rectangle ) {
+ var viewport = rectangle ? $.extend( {}, rectangle ) : this.makeViewportFromWindow();
+ threshold = threshold || 50;
+
+ viewport.top -= threshold;
+ viewport.left -= threshold;
+ viewport.right += threshold;
+ viewport.bottom += threshold;
+ return this.isElementInViewport( el, viewport );
+ }
+
+ };
+
+ mw.viewport = viewport;
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, document ) {
+ var hidden, visibilityChange,
+ nextVisibleTimeoutId = 0,
+ activeTimeouts = {},
+ init = function ( overrideDoc ) {
+ if ( overrideDoc !== undefined ) {
+ document = overrideDoc;
+ }
+
+ if ( document.hidden !== undefined ) {
+ hidden = 'hidden';
+ visibilityChange = 'visibilitychange';
+ } else if ( document.mozHidden !== undefined ) {
+ hidden = 'mozHidden';
+ visibilityChange = 'mozvisibilitychange';
+ } else if ( document.msHidden !== undefined ) {
+ hidden = 'msHidden';
+ visibilityChange = 'msvisibilitychange';
+ } else if ( document.webkitHidden !== undefined ) {
+ hidden = 'webkitHidden';
+ visibilityChange = 'webkitvisibilitychange';
+ }
+ };
+
+ init();
+
+ /**
+ * @class mw.visibleTimeout
+ * @singleton
+ */
+ module.exports = {
+ /**
+ * Generally similar to setTimeout, but turns itself on/off on page
+ * visibility changes. The passed function fires after the page has been
+ * cumulatively visible for the specified number of ms.
+ *
+ * @param {Function} fn The action to execute after visible timeout has expired.
+ * @param {number} delay The number of ms the page should be visible before
+ * calling fn.
+ * @return {number} A positive integer value which identifies the timer. This
+ * value can be passed to clearVisibleTimeout() to cancel the timeout.
+ */
+ set: function ( fn, delay ) {
+ var handleVisibilityChange,
+ timeoutId = null,
+ visibleTimeoutId = nextVisibleTimeoutId++,
+ lastStartedAt = mw.now(),
+ clearVisibleTimeout = function () {
+ if ( timeoutId !== null ) {
+ clearTimeout( timeoutId );
+ timeoutId = null;
+ }
+ delete activeTimeouts[ visibleTimeoutId ];
+ if ( hidden !== undefined ) {
+ document.removeEventListener( visibilityChange, handleVisibilityChange, false );
+ }
+ },
+ onComplete = function () {
+ clearVisibleTimeout();
+ fn();
+ };
+
+ handleVisibilityChange = function () {
+ var now = mw.now();
+
+ if ( document[ hidden ] ) {
+ // pause timeout if running
+ if ( timeoutId !== null ) {
+ delay = Math.max( 0, delay - Math.max( 0, now - lastStartedAt ) );
+ if ( delay === 0 ) {
+ onComplete();
+ } else {
+ clearTimeout( timeoutId );
+ timeoutId = null;
+ }
+ }
+ } else {
+ // resume timeout if not running
+ if ( timeoutId === null ) {
+ lastStartedAt = now;
+ timeoutId = setTimeout( onComplete, delay );
+ }
+ }
+ };
+
+ activeTimeouts[ visibleTimeoutId ] = clearVisibleTimeout;
+ if ( hidden !== undefined ) {
+ document.addEventListener( visibilityChange, handleVisibilityChange, false );
+ }
+ handleVisibilityChange();
+
+ return visibleTimeoutId;
+ },
+
+ /**
+ * Cancel a visible timeout previously established by calling set.
+ * Passing an invalid ID silently does nothing.
+ *
+ * @param {number} visibleTimeoutId The identifier of the visible
+ * timeout you want to cancel. This ID was returned by the
+ * corresponding call to set().
+ */
+ clear: function ( visibleTimeoutId ) {
+ if ( activeTimeouts.hasOwnProperty( visibleTimeoutId ) ) {
+ activeTimeouts[ visibleTimeoutId ]();
+ }
+ }
+ };
+
+ if ( window.QUnit ) {
+ module.exports.setDocument = init;
+ }
+
+}( mediaWiki, document ) );
+++ /dev/null
-( function ( mw ) {
- /**
- * @class mw.RegExp
- */
- mw.RegExp = {
- /**
- * Escape string for safe inclusion in regular expression
- *
- * The following characters are escaped:
- *
- * \ { } ( ) | . ? * + - ^ $ [ ]
- *
- * @since 1.26
- * @static
- * @param {string} str String to escape
- * @return {string} Escaped string
- */
- escape: function ( str ) {
- return str.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' ); // eslint-disable-line no-useless-escape
- }
- };
-}( mediaWiki ) );
+++ /dev/null
-( function () {
-
- /**
- * @class mw.String
- * @singleton
- */
-
- /**
- * Calculate the byte length of a string (accounting for UTF-8).
- *
- * @author Jan Paul Posma, 2011
- * @author Timo Tijhof, 2012
- * @author David Chan, 2013
- *
- * @param {string} str
- * @return {number}
- */
- function byteLength( str ) {
- // This basically figures out how many bytes a UTF-16 string (which is what js sees)
- // will take in UTF-8 by replacing a 2 byte character with 2 *'s, etc, and counting that.
- // Note, surrogate (\uD800-\uDFFF) characters are counted as 2 bytes, since there's two of them
- // and the actual character takes 4 bytes in UTF-8 (2*2=4). Might not work perfectly in
- // edge cases such as illegal sequences, but that should never happen.
-
- // https://en.wikipedia.org/wiki/UTF-8#Description
- // The mapping from UTF-16 code units to UTF-8 bytes is as follows:
- // > Range 0000-007F: codepoints that become 1 byte of UTF-8
- // > Range 0080-07FF: codepoints that become 2 bytes of UTF-8
- // > Range 0800-D7FF: codepoints that become 3 bytes of UTF-8
- // > Range D800-DFFF: Surrogates (each pair becomes 4 bytes of UTF-8)
- // > Range E000-FFFF: codepoints that become 3 bytes of UTF-8 (continued)
-
- return str
- .replace( /[\u0080-\u07FF\uD800-\uDFFF]/g, '**' )
- .replace( /[\u0800-\uD7FF\uE000-\uFFFF]/g, '***' )
- .length;
- }
-
- /**
- * Calculate the character length of a string (accounting for UTF-16 surrogates).
- *
- * @param {string} str
- * @return {number}
- */
- function codePointLength( str ) {
- return str
- // Low surrogate + high surrogate pairs represent one character (codepoint) each
- .replace( /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '*' )
- .length;
- }
-
- // Like String#charAt, but return the pair of UTF-16 surrogates for characters outside of BMP.
- function codePointAt( string, offset, backwards ) {
- // We don't need to check for offsets at the beginning or end of string,
- // String#slice will simply return a shorter (or empty) substring.
- var maybePair = backwards ?
- string.slice( offset - 1, offset + 1 ) :
- string.slice( offset, offset + 2 );
- if ( /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( maybePair ) ) {
- return maybePair;
- } else {
- return string.charAt( offset );
- }
- }
-
- function trimLength( safeVal, newVal, length, lengthFn ) {
- var startMatches, endMatches, matchesLen, inpParts, chopOff, oldChar, newChar,
- oldVal = safeVal;
-
- // Run the hook if one was provided, but only on the length
- // assessment. The value itself is not to be affected by the hook.
- if ( lengthFn( newVal ) <= length ) {
- // Limit was not reached, just remember the new value
- // and let the user continue.
- return {
- newVal: newVal,
- trimmed: false
- };
- }
-
- // Current input is longer than the active limit.
- // Figure out what was added and limit the addition.
- startMatches = 0;
- endMatches = 0;
-
- // It is important that we keep the search within the range of
- // the shortest string's length.
- // Imagine a user adds text that matches the end of the old value
- // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without
- // limiting both searches to the shortest length, endMatches would
- // also be 3.
- matchesLen = Math.min( newVal.length, oldVal.length );
-
- // Count same characters from the left, first.
- // (if "foo" -> "foofoo", assume addition was at the end).
- while ( startMatches < matchesLen ) {
- oldChar = codePointAt( oldVal, startMatches, false );
- newChar = codePointAt( newVal, startMatches, false );
- if ( oldChar !== newChar ) {
- break;
- }
- startMatches += oldChar.length;
- }
-
- while ( endMatches < ( matchesLen - startMatches ) ) {
- oldChar = codePointAt( oldVal, oldVal.length - 1 - endMatches, true );
- newChar = codePointAt( newVal, newVal.length - 1 - endMatches, true );
- if ( oldChar !== newChar ) {
- break;
- }
- endMatches += oldChar.length;
- }
-
- inpParts = [
- // Same start
- newVal.slice( 0, startMatches ),
- // Inserted content
- newVal.slice( startMatches, newVal.length - endMatches ),
- // Same end
- newVal.slice( newVal.length - endMatches )
- ];
-
- // Chop off characters from the end of the "inserted content" string
- // until the limit is statisfied.
- // Make sure to stop when there is nothing to slice (T43450).
- while ( lengthFn( inpParts.join( '' ) ) > length && inpParts[ 1 ].length > 0 ) {
- // Do not chop off halves of surrogate pairs
- chopOff = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test( inpParts[ 1 ] ) ? 2 : 1;
- inpParts[ 1 ] = inpParts[ 1 ].slice( 0, -chopOff );
- }
-
- return {
- newVal: inpParts.join( '' ),
- // For pathological lengthFn() that always returns a length greater than the limit, we might have
- // ended up not trimming - check for this case to avoid infinite loops
- trimmed: newVal !== inpParts.join( '' )
- };
- }
-
- /**
- * Utility function to trim down a string, based on byteLimit
- * and given a safe start position. It supports insertion anywhere
- * in the string, so "foo" to "fobaro" if limit is 4 will result in
- * "fobo", not "foba". Basically emulating the native maxlength by
- * reconstructing where the insertion occurred.
- *
- * @param {string} safeVal Known value that was previously returned by this
- * function, if none, pass empty string.
- * @param {string} newVal New value that may have to be trimmed down.
- * @param {number} byteLimit Number of bytes the value may be in size.
- * @param {Function} [filterFn] Function to call on the string before assessing the length.
- * @return {Object}
- * @return {string} return.newVal
- * @return {boolean} return.trimmed
- */
- function trimByteLength( safeVal, newVal, byteLimit, filterFn ) {
- var lengthFn;
- if ( filterFn ) {
- lengthFn = function ( val ) {
- return byteLength( filterFn( val ) );
- };
- } else {
- lengthFn = byteLength;
- }
-
- return trimLength( safeVal, newVal, byteLimit, lengthFn );
- }
-
- /**
- * Utility function to trim down a string, based on codePointLimit
- * and given a safe start position. It supports insertion anywhere
- * in the string, so "foo" to "fobaro" if limit is 4 will result in
- * "fobo", not "foba". Basically emulating the native maxlength by
- * reconstructing where the insertion occurred.
- *
- * @param {string} safeVal Known value that was previously returned by this
- * function, if none, pass empty string.
- * @param {string} newVal New value that may have to be trimmed down.
- * @param {number} codePointLimit Number of characters the value may be in size.
- * @param {Function} [filterFn] Function to call on the string before assessing the length.
- * @return {Object}
- * @return {string} return.newVal
- * @return {boolean} return.trimmed
- */
- function trimCodePointLength( safeVal, newVal, codePointLimit, filterFn ) {
- var lengthFn;
- if ( filterFn ) {
- lengthFn = function ( val ) {
- return codePointLength( filterFn( val ) );
- };
- } else {
- lengthFn = codePointLength;
- }
-
- return trimLength( safeVal, newVal, codePointLimit, lengthFn );
- }
-
- module.exports = {
- byteLength: byteLength,
- codePointLength: codePointLength,
- trimByteLength: trimByteLength,
- trimCodePointLength: trimCodePointLength
- };
-
-}() );
+++ /dev/null
-( function ( mw, $ ) {
- 'use strict';
-
- /**
- * Provides an API for getting and setting cookies that is
- * syntactically and functionally similar to the server-side cookie
- * API (`WebRequest#getCookie` and `WebResponse#setcookie`).
- *
- * @author Sam Smith <samsmith@wikimedia.org>
- * @author Matthew Flaschen <mflaschen@wikimedia.org>
- * @author Timo Tijhof <krinklemail@gmail.com>
- *
- * @class mw.cookie
- * @singleton
- */
- mw.cookie = {
-
- /**
- * Set or delete a cookie.
- *
- * While this is natural in JavaScript, contrary to `WebResponse#setcookie` in PHP, the
- * default values for the `options` properties only apply if that property isn't set
- * already in your options object (e.g. passing `{ secure: null }` or `{ secure: undefined }`
- * overrides the default value for `options.secure`).
- *
- * @param {string} key
- * @param {string|null} value Value of cookie. If `value` is `null` then this method will
- * instead remove a cookie by name of `key`.
- * @param {Object|Date} [options] Options object, or expiry date
- * @param {Date|number|null} [options.expires] The expiry date of the cookie, or lifetime in seconds.
- *
- * If `options.expires` is null, then a session cookie is set.
- *
- * By default cookie expiration is based on `wgCookieExpiration`. Similar to `WebResponse`
- * in PHP, we set a session cookie if `wgCookieExpiration` is 0. And for non-zero values
- * it is interpreted as lifetime in seconds.
- *
- * @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
- * @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
- * @param {string} [options.path=wgCookiePath] The path attribute of the cookie
- * @param {boolean} [options.secure=false] Whether or not to include the secure attribute.
- * (Does **not** use the wgCookieSecure configuration variable)
- */
- set: function ( key, value, options ) {
- var config, defaultOptions, date;
-
- // wgCookieSecure is not used for now, since 'detect' could not work with
- // ResourceLoaderStartUpModule, as module cache is not fragmented by protocol.
- config = mw.config.get( [
- 'wgCookiePrefix',
- 'wgCookieDomain',
- 'wgCookiePath',
- 'wgCookieExpiration'
- ] );
-
- defaultOptions = {
- prefix: config.wgCookiePrefix,
- domain: config.wgCookieDomain,
- path: config.wgCookiePath,
- secure: false
- };
-
- // Options argument can also be a shortcut for the expiry
- // Expiry can be a Date or null
- if ( $.type( options ) !== 'object' ) {
- // Also takes care of options = undefined, in which case we also don't need $.extend()
- defaultOptions.expires = options;
- options = defaultOptions;
- } else {
- options = $.extend( defaultOptions, options );
- }
-
- // Default to using wgCookieExpiration (lifetime in seconds).
- // If wgCookieExpiration is 0, that is considered a special value indicating
- // all cookies should be session cookies by default.
- if ( options.expires === undefined && config.wgCookieExpiration !== 0 ) {
- date = new Date();
- date.setTime( Number( date ) + ( config.wgCookieExpiration * 1000 ) );
- options.expires = date;
- } else if ( typeof options.expires === 'number' ) {
- // Lifetime in seconds
- date = new Date();
- date.setTime( Number( date ) + ( options.expires * 1000 ) );
- options.expires = date;
- } else if ( options.expires === null ) {
- // $.cookie makes a session cookie when options.expires is omitted
- delete options.expires;
- }
-
- // Process prefix
- key = options.prefix + key;
- delete options.prefix;
-
- // Process value
- if ( value !== null ) {
- value = String( value );
- }
-
- // Other options are handled by $.cookie
- $.cookie( key, value, options );
- },
-
- /**
- * Get the value of a cookie.
- *
- * @param {string} key
- * @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
- * `undefined` or `null`, then `wgCookiePrefix` is used
- * @param {Mixed} [defaultValue=null]
- * @return {string|null|Mixed} If the cookie exists, then the value of the
- * cookie, otherwise `defaultValue`
- */
- get: function ( key, prefix, defaultValue ) {
- var result;
-
- if ( prefix === undefined || prefix === null ) {
- prefix = mw.config.get( 'wgCookiePrefix' );
- }
-
- // Was defaultValue omitted?
- if ( arguments.length < 3 ) {
- defaultValue = null;
- }
-
- result = $.cookie( prefix + key );
-
- return result !== null ? result : defaultValue;
- }
- };
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, $ ) {
-
- var CONTROL_BUCKET = 'control',
- MAX_INT32_UNSIGNED = 4294967295;
-
- /**
- * An implementation of Jenkins' one-at-a-time hash.
- *
- * @see https://en.wikipedia.org/wiki/Jenkins_hash_function
- *
- * @param {string} string String to hash
- * @return {number} The hash as a 32-bit unsigned integer
- * @ignore
- *
- * @author Ori Livneh <ori@wikimedia.org>
- * @see https://jsbin.com/kejewi/4/watch?js,console
- */
- function hashString( string ) {
- /* eslint-disable no-bitwise */
- var hash = 0,
- i = string.length;
-
- while ( i-- ) {
- hash += string.charCodeAt( i );
- hash += ( hash << 10 );
- hash ^= ( hash >> 6 );
- }
- hash += ( hash << 3 );
- hash ^= ( hash >> 11 );
- hash += ( hash << 15 );
-
- return hash >>> 0;
- /* eslint-enable no-bitwise */
- }
-
- /**
- * Provides an API for bucketing users in experiments.
- *
- * @class mw.experiments
- * @singleton
- */
- mw.experiments = {
-
- /**
- * Gets the bucket for the experiment given the token.
- *
- * The name of the experiment and the token are hashed. The hash is converted
- * to a number which is then used to get a bucket.
- *
- * Consider the following experiment specification:
- *
- * ```
- * {
- * name: 'My first experiment',
- * enabled: true,
- * buckets: {
- * control: 0.5
- * A: 0.25,
- * B: 0.25
- * }
- * }
- * ```
- *
- * The experiment has three buckets: control, A, and B. The user has a 50%
- * chance of being assigned to the control bucket, and a 25% chance of being
- * assigned to either the A or B buckets. If the experiment were disabled,
- * then the user would always be assigned to the control bucket.
- *
- * @param {Object} experiment
- * @param {string} experiment.name The name of the experiment
- * @param {boolean} experiment.enabled Whether or not the experiment is
- * enabled. If the experiment is disabled, then the user is always assigned
- * to the control bucket
- * @param {Object} experiment.buckets A map of bucket name to probability
- * that the user will be assigned to that bucket
- * @param {string} token A token that uniquely identifies the user for the
- * duration of the experiment
- * @return {string} The bucket
- */
- getBucket: function ( experiment, token ) {
- var buckets = experiment.buckets,
- key,
- range = 0,
- hash,
- max,
- acc = 0;
-
- if ( !experiment.enabled || $.isEmptyObject( experiment.buckets ) ) {
- return CONTROL_BUCKET;
- }
-
- for ( key in buckets ) {
- range += buckets[ key ];
- }
-
- hash = hashString( experiment.name + ':' + token );
- max = ( hash / MAX_INT32_UNSIGNED ) * range;
-
- for ( key in buckets ) {
- acc += buckets[ key ];
-
- if ( max <= acc ) {
- return key;
- }
- }
- }
- };
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * Tools for inspecting page composition and performance.
- *
- * @author Ori Livneh
- * @since 1.22
- */
-
-/* eslint-disable no-console */
-
-( function ( mw, $ ) {
-
- var inspect,
- byteLength = require( 'mediawiki.String' ).byteLength,
- hasOwn = Object.prototype.hasOwnProperty;
-
- function sortByProperty( array, prop, descending ) {
- var order = descending ? -1 : 1;
- return array.sort( function ( a, b ) {
- return a[ prop ] > b[ prop ] ? order : a[ prop ] < b[ prop ] ? -order : 0;
- } );
- }
-
- function humanSize( bytes ) {
- var i,
- units = [ '', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB' ];
-
- if ( !$.isNumeric( bytes ) || bytes === 0 ) { return bytes; }
-
- for ( i = 0; bytes >= 1024; bytes /= 1024 ) { i++; }
- // Maintain one decimal for kB and above, but don't
- // add ".0" for bytes.
- return bytes.toFixed( i > 0 ? 1 : 0 ) + units[ i ];
- }
-
- /**
- * @class mw.inspect
- * @singleton
- */
- inspect = {
-
- /**
- * Return a map of all dependency relationships between loaded modules.
- *
- * @return {Object} Maps module names to objects. Each sub-object has
- * two properties, 'requires' and 'requiredBy'.
- */
- getDependencyGraph: function () {
- var modules = inspect.getLoadedModules(),
- graph = {};
-
- modules.forEach( function ( moduleName ) {
- var dependencies = mw.loader.moduleRegistry[ moduleName ].dependencies || [];
-
- if ( !hasOwn.call( graph, moduleName ) ) {
- graph[ moduleName ] = { requiredBy: [] };
- }
- graph[ moduleName ].requires = dependencies;
-
- dependencies.forEach( function ( depName ) {
- if ( !hasOwn.call( graph, depName ) ) {
- graph[ depName ] = { requiredBy: [] };
- }
- graph[ depName ].requiredBy.push( moduleName );
- } );
- } );
- return graph;
- },
-
- /**
- * Calculate the byte size of a ResourceLoader module.
- *
- * @param {string} moduleName The name of the module
- * @return {number|null} Module size in bytes or null
- */
- getModuleSize: function ( moduleName ) {
- var module = mw.loader.moduleRegistry[ moduleName ],
- args, i, size;
-
- if ( module.state !== 'ready' ) {
- return null;
- }
-
- if ( !module.style && !module.script ) {
- return 0;
- }
-
- function getFunctionBody( func ) {
- return String( func )
- // To ensure a deterministic result, replace the start of the function
- // declaration with a fixed string. For example, in Chrome 55, it seems
- // V8 seemingly-at-random decides to sometimes put a line break between
- // the opening brace and first statement of the function body. T159751.
- .replace( /^\s*function\s*\([^)]*\)\s*{\s*/, 'function(){' )
- .replace( /\s*}\s*$/, '}' );
- }
-
- // Based on the load.php response for this module.
- // For example: `mw.loader.implement("example", function(){}, {"css":[".x{color:red}"]});`
- // @see mw.loader.store.set().
- args = [
- moduleName,
- module.script,
- module.style,
- module.messages,
- module.templates
- ];
- // Trim trailing null or empty object, as load.php would have done.
- // @see ResourceLoader::makeLoaderImplementScript and ResourceLoader::trimArray.
- i = args.length;
- while ( i-- ) {
- if ( args[ i ] === null || ( $.isPlainObject( args[ i ] ) && $.isEmptyObject( args[ i ] ) ) ) {
- args.splice( i, 1 );
- } else {
- break;
- }
- }
-
- size = 0;
- for ( i = 0; i < args.length; i++ ) {
- if ( typeof args[ i ] === 'function' ) {
- size += byteLength( getFunctionBody( args[ i ] ) );
- } else {
- size += byteLength( JSON.stringify( args[ i ] ) );
- }
- }
-
- return size;
- },
-
- /**
- * Given CSS source, count both the total number of selectors it
- * contains and the number which match some element in the current
- * document.
- *
- * @param {string} css CSS source
- * @return {Object} Selector counts
- * @return {number} return.selectors Total number of selectors
- * @return {number} return.matched Number of matched selectors
- */
- auditSelectors: function ( css ) {
- var selectors = { total: 0, matched: 0 },
- style = document.createElement( 'style' );
-
- style.textContent = css;
- document.body.appendChild( style );
- $.each( style.sheet.cssRules, function ( index, rule ) {
- selectors.total++;
- // document.querySelector() on prefixed pseudo-elements can throw exceptions
- // in Firefox and Safari. Ignore these exceptions.
- // https://bugs.webkit.org/show_bug.cgi?id=149160
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1204880
- try {
- if ( document.querySelector( rule.selectorText ) !== null ) {
- selectors.matched++;
- }
- } catch ( e ) {}
- } );
- document.body.removeChild( style );
- return selectors;
- },
-
- /**
- * Get a list of all loaded ResourceLoader modules.
- *
- * @return {Array} List of module names
- */
- getLoadedModules: function () {
- return mw.loader.getModuleNames().filter( function ( module ) {
- return mw.loader.getState( module ) === 'ready';
- } );
- },
-
- /**
- * Print tabular data to the console, using console.table, console.log,
- * or mw.log (in declining order of preference).
- *
- * @param {Array} data Tabular data represented as an array of objects
- * with common properties.
- */
- dumpTable: function ( data ) {
- try {
- // Bartosz made me put this here.
- if ( window.opera ) { throw window.opera; }
- // Use Function.prototype#call to force an exception on Firefox,
- // which doesn't define console#table but doesn't complain if you
- // try to invoke it.
- // eslint-disable-next-line no-useless-call
- console.table.call( console, data );
- return;
- } catch ( e ) {}
- try {
- console.log( JSON.stringify( data, null, 2 ) );
- return;
- } catch ( e ) {}
- mw.log( data );
- },
-
- /**
- * Generate and print one more reports. When invoked with no arguments,
- * print all reports.
- *
- * @param {...string} [reports] Report names to run, or unset to print
- * all available reports.
- */
- runReports: function () {
- var reports = arguments.length > 0 ?
- Array.prototype.slice.call( arguments ) :
- $.map( inspect.reports, function ( v, k ) { return k; } );
-
- reports.forEach( function ( name ) {
- inspect.dumpTable( inspect.reports[ name ]() );
- } );
- },
-
- /**
- * @class mw.inspect.reports
- * @singleton
- */
- reports: {
- /**
- * Generate a breakdown of all loaded modules and their size in
- * kilobytes. Modules are ordered from largest to smallest.
- *
- * @return {Object[]} Size reports
- */
- size: function () {
- // Map each module to a descriptor object.
- var modules = inspect.getLoadedModules().map( function ( module ) {
- return {
- name: module,
- size: inspect.getModuleSize( module )
- };
- } );
-
- // Sort module descriptors by size, largest first.
- sortByProperty( modules, 'size', true );
-
- // Convert size to human-readable string.
- modules.forEach( function ( module ) {
- module.sizeInBytes = module.size;
- module.size = humanSize( module.size );
- } );
-
- return modules;
- },
-
- /**
- * For each module with styles, count the number of selectors, and
- * count how many match against some element currently in the DOM.
- *
- * @return {Object[]} CSS reports
- */
- css: function () {
- var modules = [];
-
- inspect.getLoadedModules().forEach( function ( name ) {
- var css, stats, module = mw.loader.moduleRegistry[ name ];
-
- try {
- css = module.style.css.join();
- } catch ( e ) { return; } // skip
-
- stats = inspect.auditSelectors( css );
- modules.push( {
- module: name,
- allSelectors: stats.total,
- matchedSelectors: stats.matched,
- percentMatched: stats.total !== 0 ?
- ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
- } );
- } );
- sortByProperty( modules, 'allSelectors', true );
- return modules;
- },
-
- /**
- * Report stats on mw.loader.store: the number of localStorage
- * cache hits and misses, the number of items purged from the
- * cache, and the total size of the module blob in localStorage.
- *
- * @return {Object[]} Store stats
- */
- store: function () {
- var raw, stats = { enabled: mw.loader.store.enabled };
- if ( stats.enabled ) {
- $.extend( stats, mw.loader.store.stats );
- try {
- raw = localStorage.getItem( mw.loader.store.getStoreKey() );
- stats.totalSizeInBytes = byteLength( raw );
- stats.totalSize = humanSize( byteLength( raw ) );
- } catch ( e ) {}
- }
- return [ stats ];
- }
- },
-
- /**
- * Perform a string search across the JavaScript and CSS source code
- * of all loaded modules and return an array of the names of the
- * modules that matched.
- *
- * @param {string|RegExp} pattern String or regexp to match.
- * @return {Array} Array of the names of modules that matched.
- */
- grep: function ( pattern ) {
- if ( typeof pattern.test !== 'function' ) {
- pattern = new RegExp( mw.RegExp.escape( pattern ), 'g' );
- }
-
- return inspect.getLoadedModules().filter( function ( moduleName ) {
- var module = mw.loader.moduleRegistry[ moduleName ];
-
- // Grep module's JavaScript
- if ( $.isFunction( module.script ) && pattern.test( module.script.toString() ) ) {
- return true;
- }
-
- // Grep module's CSS
- if (
- $.isPlainObject( module.style ) && Array.isArray( module.style.css ) &&
- pattern.test( module.style.css.join( '' ) )
- ) {
- // Module's CSS source matches
- return true;
- }
-
- return false;
- } );
- }
- };
-
- if ( mw.config.get( 'debug' ) ) {
- mw.log( 'mw.inspect: reports are not available in debug mode.' );
- }
-
- mw.inspect = inspect;
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.plugin.notify
- */
-( function ( mw ) {
- 'use strict';
-
- /**
- * @see mw.notification#notify
- * @see mw.notification#defaults
- * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
- * @param {Object} options See mw.notification#defaults for details.
- * @return {jQuery.Promise}
- */
- mw.notify = function ( message, options ) {
- // Don't bother loading the whole notification system if we never use it.
- return mw.loader.using( 'mediawiki.notification' )
- .then( function () {
- // Call notify with the notification the user requested of us.
- return mw.notification.notify( message, options );
- } );
- };
-
- /**
- * @class mw
- * @mixins mw.plugin.notify
- */
-
-}( mediaWiki ) );
+++ /dev/null
-( function ( mw ) {
- 'use strict';
-
- // Catch exceptions to avoid fatal in Chrome's "Block data storage" mode
- // which throws when accessing the localStorage property itself, as opposed
- // to the standard behaviour of throwing on getItem/setItem. (T148998)
- var
- localStorage = ( function () {
- try {
- return window.localStorage;
- } catch ( e ) {}
- }() ),
- sessionStorage = ( function () {
- try {
- return window.sessionStorage;
- } catch ( e ) {}
- }() );
-
- /**
- * A wrapper for an HTML5 Storage interface (`localStorage` or `sessionStorage`)
- * that is safe to call on all browsers.
- *
- * @class mw.SafeStorage
- * @private
- * @param {Object|undefined} store The Storage instance to wrap around
- */
- function SafeStorage( store ) {
- this.store = store;
- }
-
- /**
- * Retrieve value from device storage.
- *
- * @param {string} key Key of item to retrieve
- * @return {string|null|boolean} String value, null if no value exists, or false
- * if localStorage is not available.
- */
- SafeStorage.prototype.get = function ( key ) {
- try {
- return this.store.getItem( key );
- } catch ( e ) {}
- return false;
- };
-
- /**
- * Set a value in device storage.
- *
- * @param {string} key Key name to store under
- * @param {string} value Value to be stored
- * @return {boolean} Whether the save succeeded or not
- */
- SafeStorage.prototype.set = function ( key, value ) {
- try {
- this.store.setItem( key, value );
- return true;
- } catch ( e ) {}
- return false;
- };
-
- /**
- * Remove a value from device storage.
- *
- * @param {string} key Key of item to remove
- * @return {boolean} Whether the save succeeded or not
- */
- SafeStorage.prototype.remove = function ( key ) {
- try {
- this.store.removeItem( key );
- return true;
- } catch ( e ) {}
- return false;
- };
-
- /**
- * A wrapper for the HTML5 `localStorage` interface
- * that is safe to call on all browsers.
- *
- * @class
- * @singleton
- * @extends mw.SafeStorage
- */
- mw.storage = new SafeStorage( localStorage );
-
- /**
- * A wrapper for the HTML5 `sessionStorage` interface
- * that is safe to call on all browsers.
- *
- * @class
- * @singleton
- * @extends mw.SafeStorage
- */
- mw.storage.session = new SafeStorage( sessionStorage );
-
-}( mediaWiki ) );
+++ /dev/null
-/**
- * @class mw.user
- * @singleton
- */
-/* global Uint32Array */
-( function ( mw, $ ) {
- var userInfoPromise, stickyRandomSessionId;
-
- /**
- * Get the current user's groups or rights
- *
- * @private
- * @return {jQuery.Promise}
- */
- function getUserInfo() {
- if ( !userInfoPromise ) {
- userInfoPromise = new mw.Api().getUserInfo();
- }
- return userInfoPromise;
- }
-
- // mw.user with the properties options and tokens gets defined in mediawiki.js.
- $.extend( mw.user, {
-
- /**
- * Generate a random user session ID.
- *
- * This information would potentially be stored in a cookie to identify a user during a
- * session or series of sessions. Its uniqueness should not be depended on unless the
- * browser supports the crypto API.
- *
- * Known problems with Math.random():
- * Using the Math.random function we have seen sets
- * with 1% of non uniques among 200,000 values with Safari providing most of these.
- * Given the prevalence of Safari in mobile the percentage of duplicates in
- * mobile usages of this code is probably higher.
- *
- * Rationale:
- * We need about 64 bits to make sure that probability of collision
- * on 500 million (5*10^8) is <= 1%
- * See https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
- *
- * @return {string} 64 bit integer in hex format, padded
- */
- generateRandomSessionId: function () {
- var rnds, i,
- hexRnds = new Array( 2 ),
- // Support: IE 11
- crypto = window.crypto || window.msCrypto;
-
- if ( crypto && crypto.getRandomValues && typeof Uint32Array === 'function' ) {
- // Fill an array with 2 random values, each of which is 32 bits.
- // Note that Uint32Array is array-like but does not implement Array.
- rnds = new Uint32Array( 2 );
- crypto.getRandomValues( rnds );
- } else {
- rnds = [
- Math.floor( Math.random() * 0x100000000 ),
- Math.floor( Math.random() * 0x100000000 )
- ];
- }
- // Convert number to a string with 16 hex characters
- for ( i = 0; i < 2; i++ ) {
- // Add 0x100000000 before converting to hex and strip the extra character
- // after converting to keep the leading zeros.
- hexRnds[ i ] = ( rnds[ i ] + 0x100000000 ).toString( 16 ).slice( 1 );
- }
-
- // Concatenation of two random integers with entropy n and m
- // returns a string with entropy n+m if those strings are independent
- return hexRnds.join( '' );
- },
-
- /**
- * A sticky generateRandomSessionId for the current JS execution context,
- * cached within this class.
- *
- * @return {string} 64 bit integer in hex format, padded
- */
- stickyRandomId: function () {
- if ( !stickyRandomSessionId ) {
- stickyRandomSessionId = mw.user.generateRandomSessionId();
- }
-
- return stickyRandomSessionId;
- },
-
- /**
- * Get the current user's database id
- *
- * Not to be confused with #id.
- *
- * @return {number} Current user's id, or 0 if user is anonymous
- */
- getId: function () {
- return mw.config.get( 'wgUserId' ) || 0;
- },
-
- /**
- * Get the current user's name
- *
- * @return {string|null} User name string or null if user is anonymous
- */
- getName: function () {
- return mw.config.get( 'wgUserName' );
- },
-
- /**
- * Get date user registered, if available
- *
- * @return {boolean|null|Date} False for anonymous users, null if data is
- * unavailable, or Date for when the user registered.
- */
- getRegistration: function () {
- var registration;
- if ( mw.user.isAnon() ) {
- return false;
- }
- registration = mw.config.get( 'wgUserRegistration' );
- // Registration may be unavailable if the user signed up before MediaWiki
- // began tracking this.
- return !registration ? null : new Date( registration );
- },
-
- /**
- * Whether the current user is anonymous
- *
- * @return {boolean}
- */
- isAnon: function () {
- return mw.user.getName() === null;
- },
-
- /**
- * Get an automatically generated random ID (persisted in sessionStorage)
- *
- * This ID is ephemeral for everyone, staying in their browser only until they
- * close their browsing session.
- *
- * @return {string} Random session ID
- */
- sessionId: function () {
- var sessionId = mw.storage.session.get( 'mwuser-sessionId' );
- if ( !sessionId ) {
- sessionId = mw.user.generateRandomSessionId();
- mw.storage.session.set( 'mwuser-sessionId', sessionId );
- }
- return sessionId;
- },
-
- /**
- * Get the current user's name or the session ID
- *
- * Not to be confused with #getId.
- *
- * @return {string} User name or random session ID
- */
- id: function () {
- return mw.user.getName() || mw.user.sessionId();
- },
-
- /**
- * Get the current user's groups
- *
- * @param {Function} [callback]
- * @return {jQuery.Promise}
- */
- getGroups: function ( callback ) {
- var userGroups = mw.config.get( 'wgUserGroups', [] );
-
- // Uses promise for backwards compatibility
- return $.Deferred().resolve( userGroups ).done( callback );
- },
-
- /**
- * Get the current user's rights
- *
- * @param {Function} [callback]
- * @return {jQuery.Promise}
- */
- getRights: function ( callback ) {
- return getUserInfo().then(
- function ( userInfo ) { return userInfo.rights; },
- function () { return []; }
- ).done( callback );
- }
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * Add autocomplete suggestions for names of registered users.
- */
-( function ( mw, $ ) {
- var api, config;
-
- config = {
- fetch: function ( userInput, response, maxRows ) {
- var node = this[ 0 ];
-
- api = api || new mw.Api();
-
- $.data( node, 'request', api.get( {
- formatversion: 2,
- action: 'query',
- list: 'allusers',
- // Prefix of list=allusers is case sensitive. Normalise first
- // character to uppercase so that "fo" may yield "Foo".
- auprefix: userInput[ 0 ].toUpperCase() + userInput.slice( 1 ),
- aulimit: maxRows
- } ).done( function ( data ) {
- var users = $.map( data.query.allusers, function ( userObj ) {
- return userObj.name;
- } );
- response( users );
- } ) );
- },
- cancel: function () {
- var node = this[ 0 ],
- request = $.data( node, 'request' );
-
- if ( request ) {
- request.abort();
- $.removeData( node, 'request' );
- }
- }
- };
-
- $( function () {
- $( '.mw-autocomplete-user' ).suggestions( config );
- } );
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, $ ) {
- 'use strict';
-
- var util;
-
- /**
- * Encode the string like PHP's rawurlencode
- * @ignore
- *
- * @param {string} str String to be encoded.
- * @return {string} Encoded string
- */
- function rawurlencode( str ) {
- str = String( str );
- return encodeURIComponent( str )
- .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
- .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' );
- }
-
- /**
- * Private helper function used by util.escapeId*()
- * @ignore
- *
- * @param {string} str String to be encoded
- * @param {string} mode Encoding mode, see documentation for $wgFragmentMode
- * in DefaultSettings.php
- * @return {string} Encoded string
- */
- function escapeIdInternal( str, mode ) {
- str = String( str );
-
- switch ( mode ) {
- case 'html5':
- return str.replace( / /g, '_' );
- case 'legacy':
- return rawurlencode( str.replace( / /g, '_' ) )
- .replace( /%3A/g, ':' )
- .replace( /%/g, '.' );
- default:
- throw new Error( 'Unrecognized ID escaping mode ' + mode );
- }
- }
-
- /**
- * Utility library
- * @class mw.util
- * @singleton
- */
- util = {
-
- /* Main body */
-
- /**
- * Encode the string like PHP's rawurlencode
- *
- * @param {string} str String to be encoded.
- * @return {string} Encoded string
- */
- rawurlencode: rawurlencode,
-
- /**
- * Encode string into HTML id compatible form suitable for use in HTML
- * Analog to PHP Sanitizer::escapeIdForAttribute()
- *
- * @since 1.30
- *
- * @param {string} str String to encode
- * @return {string} Encoded string
- */
- escapeIdForAttribute: function ( str ) {
- var mode = mw.config.get( 'wgFragmentMode' )[ 0 ];
-
- return escapeIdInternal( str, mode );
- },
-
- /**
- * Encode string into HTML id compatible form suitable for use in links
- * Analog to PHP Sanitizer::escapeIdForLink()
- *
- * @since 1.30
- *
- * @param {string} str String to encode
- * @return {string} Encoded string
- */
- escapeIdForLink: function ( str ) {
- var mode = mw.config.get( 'wgFragmentMode' )[ 0 ];
-
- return escapeIdInternal( str, mode );
- },
-
- /**
- * Encode page titles for use in a URL
- *
- * We want / and : to be included as literal characters in our title URLs
- * as they otherwise fatally break the title.
- *
- * The others are decoded because we can, it's prettier and matches behaviour
- * of `wfUrlencode` in PHP.
- *
- * @param {string} str String to be encoded.
- * @return {string} Encoded string
- */
- wikiUrlencode: function ( str ) {
- return util.rawurlencode( str )
- .replace( /%20/g, '_' )
- // wfUrlencode replacements
- .replace( /%3B/g, ';' )
- .replace( /%40/g, '@' )
- .replace( /%24/g, '$' )
- .replace( /%21/g, '!' )
- .replace( /%2A/g, '*' )
- .replace( /%28/g, '(' )
- .replace( /%29/g, ')' )
- .replace( /%2C/g, ',' )
- .replace( /%2F/g, '/' )
- .replace( /%7E/g, '~' )
- .replace( /%3A/g, ':' );
- },
-
- /**
- * Get the link to a page name (relative to `wgServer`),
- *
- * @param {string|null} [pageName=wgPageName] Page name
- * @param {Object} [params] A mapping of query parameter names to values,
- * e.g. `{ action: 'edit' }`
- * @return {string} Url of the page with name of `pageName`
- */
- getUrl: function ( pageName, params ) {
- var titleFragmentStart, url, query,
- fragment = '',
- title = typeof pageName === 'string' ? pageName : mw.config.get( 'wgPageName' );
-
- // Find any fragment
- titleFragmentStart = title.indexOf( '#' );
- if ( titleFragmentStart !== -1 ) {
- fragment = title.slice( titleFragmentStart + 1 );
- // Exclude the fragment from the page name
- title = title.slice( 0, titleFragmentStart );
- }
-
- // Produce query string
- if ( params ) {
- query = $.param( params );
- }
- if ( query ) {
- url = title ?
- util.wikiScript() + '?title=' + util.wikiUrlencode( title ) + '&' + query :
- util.wikiScript() + '?' + query;
- } else {
- url = mw.config.get( 'wgArticlePath' )
- .replace( '$1', util.wikiUrlencode( title ).replace( /\$/g, '$$$$' ) );
- }
-
- // Append the encoded fragment
- if ( fragment.length ) {
- url += '#' + util.escapeIdForLink( fragment );
- }
-
- return url;
- },
-
- /**
- * Get address to a script in the wiki root.
- * For index.php use `mw.config.get( 'wgScript' )`.
- *
- * @since 1.18
- * @param {string} str Name of script (e.g. 'api'), defaults to 'index'
- * @return {string} Address to script (e.g. '/w/api.php' )
- */
- wikiScript: function ( str ) {
- str = str || 'index';
- if ( str === 'index' ) {
- return mw.config.get( 'wgScript' );
- } else if ( str === 'load' ) {
- return mw.config.get( 'wgLoadScript' );
- } else {
- return mw.config.get( 'wgScriptPath' ) + '/' + str + '.php';
- }
- },
-
- /**
- * Append a new style block to the head and return the CSSStyleSheet object.
- * Use .ownerNode to access the `<style>` element, or use mw.loader#addStyleTag.
- * This function returns the styleSheet object for convience (due to cross-browsers
- * difference as to where it is located).
- *
- * var sheet = util.addCSS( '.foobar { display: none; }' );
- * $( foo ).click( function () {
- * // Toggle the sheet on and off
- * sheet.disabled = !sheet.disabled;
- * } );
- *
- * @param {string} text CSS to be appended
- * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element.
- */
- addCSS: function ( text ) {
- var s = mw.loader.addStyleTag( text );
- return s.sheet || s.styleSheet || s;
- },
-
- /**
- * Grab the URL parameter value for the given parameter.
- * Returns null if not found.
- *
- * @param {string} param The parameter name.
- * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
- * @return {Mixed} Parameter value or null.
- */
- getParamValue: function ( param, url ) {
- // Get last match, stop at hash
- var re = new RegExp( '^[^#]*[&?]' + mw.RegExp.escape( param ) + '=([^&#]*)' ),
- m = re.exec( url !== undefined ? url : location.href );
-
- if ( m ) {
- // Beware that decodeURIComponent is not required to understand '+'
- // by spec, as encodeURIComponent does not produce it.
- return decodeURIComponent( m[ 1 ].replace( /\+/g, '%20' ) );
- }
- return null;
- },
-
- /**
- * The content wrapper of the skin (e.g. `.mw-body`).
- *
- * Populated on document ready. To use this property,
- * wait for `$.ready` and be sure to have a module dependency on
- * `mediawiki.util` which will ensure
- * your document ready handler fires after initialization.
- *
- * Because of the lazy-initialised nature of this property,
- * you're discouraged from using it.
- *
- * If you need just the wikipage content (not any of the
- * extra elements output by the skin), use `$( '#mw-content-text' )`
- * instead. Or listen to mw.hook#wikipage_content which will
- * allow your code to re-run when the page changes (e.g. live preview
- * or re-render after ajax save).
- *
- * @property {jQuery}
- */
- $content: null,
-
- /**
- * Add a link to a portlet menu on the page, such as:
- *
- * p-cactions (Content actions), p-personal (Personal tools),
- * p-navigation (Navigation), p-tb (Toolbox)
- *
- * The first three parameters are required, the others are optional and
- * may be null. Though providing an id and tooltip is recommended.
- *
- * By default the new link will be added to the end of the list. To
- * add the link before a given existing item, pass the DOM node
- * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector
- * (e.g. `'#foobar'`) for that item.
- *
- * util.addPortletLink(
- * 'p-tb', 'https://www.mediawiki.org/',
- * 'mediawiki.org', 't-mworg', 'Go to mediawiki.org', 'm', '#t-print'
- * );
- *
- * var node = util.addPortletLink(
- * 'p-tb',
- * new mw.Title( 'Special:Example' ).getUrl(),
- * 'Example'
- * );
- * $( node ).on( 'click', function ( e ) {
- * console.log( 'Example' );
- * e.preventDefault();
- * } );
- *
- * @param {string} portlet ID of the target portlet ( 'p-cactions' or 'p-personal' etc.)
- * @param {string} href Link URL
- * @param {string} text Link text
- * @param {string} [id] ID of the new item, should be unique and preferably have
- * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' )
- * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
- * @param {string} [accesskey] Access key to activate this link (one character, try
- * to avoid conflicts. Use `$( '[accesskey=x]' ).get()` in the console to
- * see if 'x' is already used.
- * @param {HTMLElement|jQuery|string} [nextnode] Element or jQuery-selector string to the item that
- * the new item should be added before, should be another item in the same
- * list, it will be ignored otherwise
- *
- * @return {HTMLElement|null} The added element (a ListItem or Anchor element,
- * depending on the skin) or null if no element was added to the document.
- */
- addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) {
- var $item, $link, $portlet, $ul;
-
- // Check if there's at least 3 arguments to prevent a TypeError
- if ( arguments.length < 3 ) {
- return null;
- }
- // Setup the anchor tag
- $link = $( '<a>' ).attr( 'href', href ).text( text );
- if ( tooltip ) {
- $link.attr( 'title', tooltip );
- }
-
- // Select the specified portlet
- $portlet = $( '#' + portlet );
- if ( $portlet.length === 0 ) {
- return null;
- }
- // Select the first (most likely only) unordered list inside the portlet
- $ul = $portlet.find( 'ul' ).eq( 0 );
-
- // If it didn't have an unordered list yet, create it
- if ( $ul.length === 0 ) {
-
- $ul = $( '<ul>' );
-
- // If there's no <div> inside, append it to the portlet directly
- if ( $portlet.find( 'div:first' ).length === 0 ) {
- $portlet.append( $ul );
- } else {
- // otherwise if there's a div (such as div.body or div.pBody)
- // append the <ul> to last (most likely only) div
- $portlet.find( 'div' ).eq( -1 ).append( $ul );
- }
- }
- // Just in case..
- if ( $ul.length === 0 ) {
- return null;
- }
-
- // Unhide portlet if it was hidden before
- $portlet.removeClass( 'emptyPortlet' );
-
- // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab)
- // and back up the selector to the list item
- if ( $portlet.hasClass( 'vectorTabs' ) ) {
- $item = $link.wrap( '<li><span></span></li>' ).parent().parent();
- } else {
- $item = $link.wrap( '<li></li>' ).parent();
- }
-
- // Implement the properties passed to the function
- if ( id ) {
- $item.attr( 'id', id );
- }
-
- if ( accesskey ) {
- $link.attr( 'accesskey', accesskey );
- }
-
- if ( tooltip ) {
- $link.attr( 'title', tooltip );
- }
-
- if ( nextnode ) {
- // Case: nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
- // Case: nextnode is a CSS selector for jQuery
- if ( nextnode.nodeType || typeof nextnode === 'string' ) {
- nextnode = $ul.find( nextnode );
- } else if ( !nextnode.jquery ) {
- // Error: Invalid nextnode
- nextnode = undefined;
- }
- if ( nextnode && ( nextnode.length !== 1 || nextnode[ 0 ].parentNode !== $ul[ 0 ] ) ) {
- // Error: nextnode must resolve to a single node
- // Error: nextnode must have the associated <ul> as its parent
- nextnode = undefined;
- }
- }
-
- // Case: nextnode is a jQuery-wrapped DOM element
- if ( nextnode ) {
- nextnode.before( $item );
- } else {
- // Fallback (this is the default behavior)
- $ul.append( $item );
- }
-
- // Update tooltip for the access key after inserting into DOM
- // to get a localized access key label (T69946).
- $link.updateTooltipAccessKeys();
-
- return $item[ 0 ];
- },
-
- /**
- * Validate a string as representing a valid e-mail address
- * according to HTML5 specification. Please note the specification
- * does not validate a domain with one character.
- *
- * FIXME: should be moved to or replaced by a validation module.
- *
- * @param {string} mailtxt E-mail address to be validated.
- * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false
- * as determined by validation.
- */
- validateEmail: function ( mailtxt ) {
- var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp;
-
- if ( mailtxt === '' ) {
- return null;
- }
-
- // HTML5 defines a string as valid e-mail address if it matches
- // the ABNF:
- // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
- // With:
- // - atext : defined in RFC 5322 section 3.2.3
- // - ldh-str : defined in RFC 1034 section 3.5
- //
- // (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
- // First, define the RFC 5322 'atext' which is pretty easy:
- // atext = ALPHA / DIGIT / ; Printable US-ASCII
- // "!" / "#" / ; characters not including
- // "$" / "%" / ; specials. Used for atoms.
- // "&" / "'" /
- // "*" / "+" /
- // "-" / "/" /
- // "=" / "?" /
- // "^" / "_" /
- // "`" / "{" /
- // "|" / "}" /
- // "~"
- rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
-
- // Next define the RFC 1034 'ldh-str'
- // <domain> ::= <subdomain> | " "
- // <subdomain> ::= <label> | <subdomain> "." <label>
- // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
- // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
- // <let-dig-hyp> ::= <let-dig> | "-"
- // <let-dig> ::= <letter> | <digit>
- rfc1034LdhStr = 'a-z0-9\\-';
-
- html5EmailRegexp = new RegExp(
- // start of string
- '^' +
- // User part which is liberal :p
- '[' + rfc5322Atext + '\\.]+' +
- // 'at'
- '@' +
- // Domain first part
- '[' + rfc1034LdhStr + ']+' +
- // Optional second part and following are separated by a dot
- '(?:\\.[' + rfc1034LdhStr + ']+)*' +
- // End of string
- '$',
- // RegExp is case insensitive
- 'i'
- );
- return ( mailtxt.match( html5EmailRegexp ) !== null );
- },
-
- /**
- * Note: borrows from IP::isIPv4
- *
- * @param {string} address
- * @param {boolean} [allowBlock=false]
- * @return {boolean}
- */
- isIPv4Address: function ( address, allowBlock ) {
- var block, RE_IP_BYTE, RE_IP_ADD;
-
- if ( typeof address !== 'string' ) {
- return false;
- }
-
- block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '';
- RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])';
- RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
-
- return ( new RegExp( '^' + RE_IP_ADD + block + '$' ).test( address ) );
- },
-
- /**
- * Note: borrows from IP::isIPv6
- *
- * @param {string} address
- * @param {boolean} [allowBlock=false]
- * @return {boolean}
- */
- isIPv6Address: function ( address, allowBlock ) {
- var block, RE_IPV6_ADD;
-
- if ( typeof address !== 'string' ) {
- return false;
- }
-
- block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '';
- RE_IPV6_ADD =
- '(?:' + // starts with "::" (including "::")
- ':(?::|(?::' +
- '[0-9A-Fa-f]{1,4}' +
- '){1,7})' +
- '|' + // ends with "::" (except "::")
- '[0-9A-Fa-f]{1,4}' +
- '(?::' +
- '[0-9A-Fa-f]{1,4}' +
- '){0,6}::' +
- '|' + // contains no "::"
- '[0-9A-Fa-f]{1,4}' +
- '(?::' +
- '[0-9A-Fa-f]{1,4}' +
- '){7}' +
- ')';
-
- if ( new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) ) {
- return true;
- }
-
- // contains one "::" in the middle (single '::' check below)
- RE_IPV6_ADD =
- '[0-9A-Fa-f]{1,4}' +
- '(?:::?' +
- '[0-9A-Fa-f]{1,4}' +
- '){1,6}';
-
- return (
- new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) &&
- /::/.test( address ) &&
- !/::.*::/.test( address )
- );
- },
-
- /**
- * Check whether a string is an IP address
- *
- * @since 1.25
- * @param {string} address String to check
- * @param {boolean} [allowBlock=false] If a block of IPs should be allowed
- * @return {boolean}
- */
- isIPAddress: function ( address, allowBlock ) {
- return util.isIPv4Address( address, allowBlock ) ||
- util.isIPv6Address( address, allowBlock );
- }
- };
-
- /**
- * Add a little box at the top of the screen to inform the user of
- * something, replacing any previous message.
- * Calling with no arguments, with an empty string or null will hide the message
- *
- * @method jsMessage
- * @deprecated since 1.20 Use mw#notify
- * @param {Mixed} message The DOM-element, jQuery object or HTML-string to be put inside the message box.
- * to allow CSS/JS to hide different boxes. null = no class used.
- */
- mw.log.deprecate( util, 'jsMessage', function ( message ) {
- if ( !arguments.length || message === '' || message === null ) {
- return true;
- }
- if ( typeof message !== 'object' ) {
- message = $.parseHTML( message );
- }
- mw.notify( message, { autoHide: true, tag: 'legacy' } );
- return true;
- }, 'Use mw.notify instead.', 'mw.util.jsMessage' );
-
- /**
- * Initialisation of mw.util.$content
- */
- function init() {
- util.$content = ( function () {
- var i, l, $node, selectors;
-
- selectors = [
- // The preferred standard is class "mw-body".
- // You may also use class "mw-body mw-body-primary" if you use
- // mw-body in multiple locations. Or class "mw-body-primary" if
- // you use mw-body deeper in the DOM.
- '.mw-body-primary',
- '.mw-body',
-
- // If the skin has no such class, fall back to the parser output
- '#mw-content-text'
- ];
-
- for ( i = 0, l = selectors.length; i < l; i++ ) {
- $node = $( selectors[ i ] );
- if ( $node.length ) {
- return $node.first();
- }
- }
-
- // Should never happen... well, it could if someone is not finished writing a
- // skin and has not yet inserted bodytext yet.
- return $( 'body' );
- }() );
- }
-
- /**
- * Former public initialisation. Now a no-op function.
- *
- * @method util_init
- * @deprecated since 1.30
- */
- mw.log.deprecate( util, 'init', $.noop, 'Remove the call of mw.util.init().', 'mw.util.init' );
-
- $( init );
-
- mw.util = util;
- module.exports = util;
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, $ ) {
- 'use strict';
-
- /**
- * Utility library for viewport-related functions
- *
- * Notable references:
- * - https://github.com/tuupola/jquery_lazyload
- * - https://github.com/luis-almeida/unveil
- *
- * @class mw.viewport
- * @singleton
- */
- var viewport = {
-
- /**
- * This is a private method pulled inside the module for testing purposes.
- *
- * @ignore
- * @private
- * @return {Object} Viewport positions
- */
- makeViewportFromWindow: function () {
- var $window = $( window ),
- scrollTop = $window.scrollTop(),
- scrollLeft = $window.scrollLeft();
-
- return {
- top: scrollTop,
- left: scrollLeft,
- right: scrollLeft + $window.width(),
- bottom: ( window.innerHeight ? window.innerHeight : $window.height() ) + scrollTop
- };
- },
-
- /**
- * Check if any part of a given element is in a given viewport
- *
- * @method
- * @param {HTMLElement} el Element that's being tested
- * @param {Object} [rectangle] Viewport to test against; structured as such:
- *
- * var rectangle = {
- * top: topEdge,
- * left: leftEdge,
- * right: rightEdge,
- * bottom: bottomEdge
- * }
- * Defaults to viewport made from `window`.
- *
- * @return {boolean}
- */
- isElementInViewport: function ( el, rectangle ) {
- var $el = $( el ),
- offset = $el.offset(),
- rect = {
- height: $el.height(),
- width: $el.width(),
- top: offset.top,
- left: offset.left
- },
- viewport = rectangle || this.makeViewportFromWindow();
-
- return (
- // Top border must be above viewport's bottom
- ( viewport.bottom >= rect.top ) &&
- // Left border must be before viewport's right border
- ( viewport.right >= rect.left ) &&
- // Bottom border must be below viewport's top
- ( viewport.top <= rect.top + rect.height ) &&
- // Right border must be after viewport's left border
- ( viewport.left <= rect.left + rect.width )
- );
- },
-
- /**
- * Check if an element is a given threshold away in any direction from a given viewport
- *
- * @method
- * @param {HTMLElement} el Element that's being tested
- * @param {number} [threshold] Pixel distance considered "close". Must be a positive number.
- * Defaults to 50.
- * @param {Object} [rectangle] Viewport to test against.
- * Defaults to viewport made from `window`.
- * @return {boolean}
- */
- isElementCloseToViewport: function ( el, threshold, rectangle ) {
- var viewport = rectangle ? $.extend( {}, rectangle ) : this.makeViewportFromWindow();
- threshold = threshold || 50;
-
- viewport.top -= threshold;
- viewport.left -= threshold;
- viewport.right += threshold;
- viewport.bottom += threshold;
- return this.isElementInViewport( el, viewport );
- }
-
- };
-
- mw.viewport = viewport;
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, document ) {
- var hidden, visibilityChange,
- nextVisibleTimeoutId = 0,
- activeTimeouts = {},
- init = function ( overrideDoc ) {
- if ( overrideDoc !== undefined ) {
- document = overrideDoc;
- }
-
- if ( document.hidden !== undefined ) {
- hidden = 'hidden';
- visibilityChange = 'visibilitychange';
- } else if ( document.mozHidden !== undefined ) {
- hidden = 'mozHidden';
- visibilityChange = 'mozvisibilitychange';
- } else if ( document.msHidden !== undefined ) {
- hidden = 'msHidden';
- visibilityChange = 'msvisibilitychange';
- } else if ( document.webkitHidden !== undefined ) {
- hidden = 'webkitHidden';
- visibilityChange = 'webkitvisibilitychange';
- }
- };
-
- init();
-
- /**
- * @class mw.visibleTimeout
- * @singleton
- */
- module.exports = {
- /**
- * Generally similar to setTimeout, but turns itself on/off on page
- * visibility changes. The passed function fires after the page has been
- * cumulatively visible for the specified number of ms.
- *
- * @param {Function} fn The action to execute after visible timeout has expired.
- * @param {number} delay The number of ms the page should be visible before
- * calling fn.
- * @return {number} A positive integer value which identifies the timer. This
- * value can be passed to clearVisibleTimeout() to cancel the timeout.
- */
- set: function ( fn, delay ) {
- var handleVisibilityChange,
- timeoutId = null,
- visibleTimeoutId = nextVisibleTimeoutId++,
- lastStartedAt = mw.now(),
- clearVisibleTimeout = function () {
- if ( timeoutId !== null ) {
- clearTimeout( timeoutId );
- timeoutId = null;
- }
- delete activeTimeouts[ visibleTimeoutId ];
- if ( hidden !== undefined ) {
- document.removeEventListener( visibilityChange, handleVisibilityChange, false );
- }
- },
- onComplete = function () {
- clearVisibleTimeout();
- fn();
- };
-
- handleVisibilityChange = function () {
- var now = mw.now();
-
- if ( document[ hidden ] ) {
- // pause timeout if running
- if ( timeoutId !== null ) {
- delay = Math.max( 0, delay - Math.max( 0, now - lastStartedAt ) );
- if ( delay === 0 ) {
- onComplete();
- } else {
- clearTimeout( timeoutId );
- timeoutId = null;
- }
- }
- } else {
- // resume timeout if not running
- if ( timeoutId === null ) {
- lastStartedAt = now;
- timeoutId = setTimeout( onComplete, delay );
- }
- }
- };
-
- activeTimeouts[ visibleTimeoutId ] = clearVisibleTimeout;
- if ( hidden !== undefined ) {
- document.addEventListener( visibilityChange, handleVisibilityChange, false );
- }
- handleVisibilityChange();
-
- return visibleTimeoutId;
- },
-
- /**
- * Cancel a visible timeout previously established by calling set.
- * Passing an invalid ID silently does nothing.
- *
- * @param {number} visibleTimeoutId The identifier of the visible
- * timeout you want to cancel. This ID was returned by the
- * corresponding call to set().
- */
- clear: function ( visibleTimeoutId ) {
- if ( activeTimeouts.hasOwnProperty( visibleTimeoutId ) ) {
- activeTimeouts[ visibleTimeoutId ]();
- }
- }
- };
-
- if ( window.QUnit ) {
- module.exports.setDocument = init;
- }
-
-}( mediaWiki, document ) );