diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2013-12-08 09:55:49 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2013-12-08 09:55:49 +0100 |
commit | 4ac9fa081a7c045f6a9f1cfc529d82423f485b2e (patch) | |
tree | af68743f2f4a47d13f2b0eb05f5c4aaf86d8ea37 /resources/mediawiki | |
parent | af4da56f1ad4d3ef7b06557bae365da2ea27a897 (diff) |
Update to MediaWiki 1.22.0
Diffstat (limited to 'resources/mediawiki')
18 files changed, 1688 insertions, 636 deletions
diff --git a/resources/mediawiki/images/arrow-collapsed-ltr.png b/resources/mediawiki/images/arrow-collapsed-ltr.png Binary files differnew file mode 100644 index 00000000..b17e578b --- /dev/null +++ b/resources/mediawiki/images/arrow-collapsed-ltr.png diff --git a/resources/mediawiki/images/arrow-collapsed-rtl.png b/resources/mediawiki/images/arrow-collapsed-rtl.png Binary files differnew file mode 100644 index 00000000..a834548e --- /dev/null +++ b/resources/mediawiki/images/arrow-collapsed-rtl.png diff --git a/resources/mediawiki/images/arrow-expanded.png b/resources/mediawiki/images/arrow-expanded.png Binary files differnew file mode 100644 index 00000000..2bec798e --- /dev/null +++ b/resources/mediawiki/images/arrow-expanded.png diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index b86a14ba..5038c515 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -1,192 +1,368 @@ /*! * @author Neil Kandalgaonkar, 2010 - * @author Timo Tijhof, 2011 + * @author Timo Tijhof, 2011-2013 * @since 1.18 - * - * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink */ ( function ( mw, $ ) { - /* Local space */ - /** * @class mw.Title * + * Parse titles into an object struture. Note that when using the constructor + * directly, passing invalid titles will result in an exception. Use #newFromText to use the + * logic directly and get null for invalid titles which is easier to work with. + * * @constructor * @param {string} title Title of the page. If no second argument given, - * this will be searched for a namespace. - * @param {number} [namespace] Namespace id. If given, title will be taken as-is. + * this will be searched for a namespace + * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title + * @throws {Error} When the title is invalid */ function Title( title, namespace ) { - this.ns = 0; // integer namespace id - this.name = null; // name in canonical 'database' form - this.ext = null; // extension - - if ( arguments.length === 2 ) { - setNameAndExtension( this, title ); - this.ns = fixNsId( namespace ); - } else if ( arguments.length === 1 ) { - setAll( this, title ); + var parsed = parse( title, namespace ); + if ( !parsed ) { + throw new Error( 'Unable to parse title' ); } + + this.namespace = parsed.namespace; + this.title = parsed.title; + this.ext = parsed.ext; + this.fragment = parsed.fragment; + return this; } -var - /* Public methods (defined later) */ - fn, + /* Private members */ + + var /** - * Strip some illegal chars: control chars, colon, less than, greater than, - * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity - * intact, like unicode bidi chars, but it's a good start.. - * @ignore - * @param {string} s - * @return {string} + * @private + * @static + * @property NS_MAIN */ - clean = function ( s ) { - if ( s !== undefined ) { - return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' ); - } - }, + NS_MAIN = 0, /** - * Convert db-key to readable text. - * @ignore - * @param {string} s - * @return {string} + * @private + * @static + * @property NS_TALK */ - text = function ( s ) { - if ( s !== null && s !== undefined ) { - return s.replace( /_/g, ' ' ); - } else { - return ''; - } - }, + NS_TALK = 1, /** - * Sanitize name. - * @ignore + * @private + * @static + * @property NS_SPECIAL */ - fixName = function ( s ) { - return clean( $.trim( s ) ); - }, + NS_SPECIAL = -1, /** - * Sanitize extension. - * @ignore + * Get the namespace id from a namespace name (either from the localized, canonical or alias + * name). + * + * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or + * even 'Bild'. + * + * @private + * @static + * @method getNsIdByName + * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored) + * @return {number|boolean} Namespace id or boolean false */ - fixExt = function ( s ) { - return clean( s ); + getNsIdByName = function ( ns ) { + var id; + + // Don't cast non-strings to strings, because null or undefined should not result in + // returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki) + // Also, toLowerCase throws exception on null/undefined, because it is a String method. + if ( typeof ns !== 'string' ) { + return false; + } + ns = ns.toLowerCase(); + id = mw.config.get( 'wgNamespaceIds' )[ns]; + if ( id === undefined ) { + return false; + } + return id; }, + rUnderscoreTrim = /^_+|_+$/g, + + rSplit = /^(.+?)_*:_*(.*)$/, + + // See Title.php#getTitleInvalidRegex + rInvalid = new RegExp( + '[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' + + // URL percent encoding sequences interfere with the ability + // to round-trip titles -- you can't link to them consistently. + '|%[0-9A-Fa-f]{2}' + + // XML/HTML character references produce similar issues. + '|&[A-Za-z0-9\u0080-\uFFFF]+;' + + '|&#[0-9]+;' + + '|&#x[0-9A-Fa-f]+;' + ), + /** - * Sanitize namespace id. - * @ignore - * @param id {Number} Namespace id. - * @return {Number|Boolean} The id as-is or boolean false if invalid. + * Internal helper for #constructor and #newFromtext. + * + * Based on Title.php#secureAndSplit + * + * @private + * @static + * @method parse + * @param {string} title + * @param {number} [defaultNamespace=NS_MAIN] + * @return {Object|boolean} */ - fixNsId = function ( id ) { - // wgFormattedNamespaces is an object of *string* key-vals (ie. arr["0"] not arr[0] ) - var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()]; + parse = function ( title, defaultNamespace ) { + var namespace, m, id, i, fragment, ext; - // Check only undefined (may be false-y, such as '' (main namespace) ). - if ( ns === undefined ) { + namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace; + + title = title + // Normalise whitespace to underscores and remove duplicates + .replace( /[ _\s]+/g, '_' ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + + if ( title === '' ) { return false; + } + + // Process initial colon + if ( title.charAt( 0 ) === ':' ) { + // Initial colon means main namespace instead of specified default + namespace = NS_MAIN; + title = title + // Strip colon + .substr( 1 ) + // Trim underscores + .replace( rUnderscoreTrim, '' ); + } + + // Process namespace prefix (if any) + m = title.match( rSplit ); + if ( m ) { + id = getNsIdByName( m[1] ); + if ( id !== false ) { + // Ordinary namespace + namespace = id; + title = m[2]; + + // For Talk:X pages, make sure X has no "namespace" prefix + if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) { + // Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x) + if ( getNsIdByName( m[1] ) !== false ) { + return false; + } + } + } + } + + // Process fragment + i = title.indexOf( '#' ); + if ( i === -1 ) { + fragment = null; } else { - return Number( id ); + fragment = title + // Get segment starting after the hash + .substr( i + 1 ) + // Convert to text + // NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo") + .replace( /_/g, ' ' ); + + title = title + // Strip hash + .substr( 0, i ) + // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux") + .replace( rUnderscoreTrim, '' ); } - }, - /** - * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias). - * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. - * @ignore - * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). - * @return {Number|Boolean} Namespace id or boolean false if unrecognized. - */ - getNsIdByName = function ( ns ) { - // Don't cast non-strings to strings, because null or undefined - // should not result in returning the id of a potential namespace - // called "Null:" (e.g. on nullwiki.example.org) - // Also, toLowerCase throws exception on null/undefined, because - // it is a String.prototype method. - if ( typeof ns !== 'string' ) { + + // Reject illegal characters + if ( title.match( rInvalid ) ) { return false; } - ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize - var id = mw.config.get( 'wgNamespaceIds' )[ns]; - if ( id === undefined ) { - mw.log( 'mw.Title: Unrecognized namespace: ' + ns ); + + // Disallow titles that browsers or servers might resolve as directory navigation + if ( + title.indexOf( '.' ) !== -1 && ( + title === '.' || title === '..' || + title.indexOf( './' ) === 0 || + title.indexOf( '../' ) === 0 || + title.indexOf( '/./' ) !== -1 || + title.indexOf( '/../' ) !== -1 || + title.substr( -2 ) === '/.' || + title.substr( -3 ) === '/..' + ) + ) { + return false; + } + + // Disallow magic tilde sequence + if ( title.indexOf( '~~~' ) !== -1 ) { + return false; + } + + // Disallow titles exceeding the 255 byte size limit (size of underlying database field) + // Except for special pages, e.g. [[Special:Block/Long name]] + // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should + // be less than 512 bytes. + if ( namespace !== NS_SPECIAL && $.byteLength( title ) > 255 ) { + return false; + } + + // Can't make a link to a namespace alone. + if ( title === '' && namespace !== NS_MAIN ) { + return false; + } + + // Any remaining initial :s are illegal. + if ( title.charAt( 0 ) === ':' ) { return false; } - return fixNsId( id ); + + // For backwards-compatibility with old mw.Title, we separate the extension from the + // rest of the title. + i = title.lastIndexOf( '.' ); + if ( i === -1 || title.length <= i + 1 ) { + // Extensions are the non-empty segment after the last dot + ext = null; + } else { + ext = title.substr( i + 1 ); + title = title.substr( 0, i ); + } + + return { + namespace: namespace, + title: title, + ext: ext, + fragment: fragment + }; }, /** - * Helper to extract namespace, name and extension from a string. + * Convert db-key to readable text. * - * @ignore - * @param {mw.Title} title - * @param {string} raw - * @return {mw.Title} + * @private + * @static + * @method text + * @param {string} s + * @return {string} */ - setAll = function ( title, s ) { - // In normal browsers the match-array contains null/undefined if there's no match, - // IE returns an empty string. - var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ), - nsMatch = getNsIdByName( matches[1] ); - - // Namespace must be valid, and title must be a non-empty string. - if ( nsMatch && typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ns = nsMatch; - title.name = fixName( matches[2] ); - if ( typeof matches[3] === 'string' && matches[3] !== '' ) { - title.ext = fixExt( matches[3] ); - } + text = function ( s ) { + if ( s !== null && s !== undefined ) { + return s.replace( /_/g, ' ' ); } else { - // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace. - title.ns = 0; - setNameAndExtension( title, s ); + return ''; } - return title; }, + // Polyfill for ES5 Object.create + createObject = Object.create || ( function () { + return function ( o ) { + function Title() {} + if ( o !== Object( o ) ) { + throw new Error( 'Cannot inherit from a non-object' ); + } + Title.prototype = o; + return new Title(); + }; + }() ); + + + /* Static members */ + /** - * Helper to extract name and extension from a string. + * Constructor for Title objects with a null return instead of an exception for invalid titles. * - * @ignore - * @param {mw.Title} title - * @param {string} raw - * @return {mw.Title} + * @static + * @method + * @param {string} title + * @param {number} [namespace=NS_MAIN] Default namespace + * @return {mw.Title|null} A valid Title object or null if the title is invalid */ - setNameAndExtension = function ( title, raw ) { - // In normal browsers the match-array contains null/undefined if there's no match, - // IE returns an empty string. - var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ ); - - // Title must be a non-empty string. - if ( typeof matches[1] === 'string' && matches[1] !== '' ) { - title.name = fixName( matches[1] ); - if ( typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ext = fixExt( matches[2] ); - } - } else { - throw new Error( 'mw.Title: Could not parse title "' + raw + '"' ); + Title.newFromText = function ( title, namespace ) { + var t, parsed = parse( title, namespace ); + if ( !parsed ) { + return null; } - return title; + + t = createObject( Title.prototype ); + t.namespace = parsed.namespace; + t.title = parsed.title; + t.ext = parsed.ext; + t.fragment = parsed.fragment; + + return t; }; + /** + * Get the file title from an image element + * + * var title = mw.Title.newFromImg( $( 'img:first' ) ); + * + * @static + * @param {HTMLElement|jQuery} img The image to use as a base + * @return {mw.Title|null} The file title or null if unsuccessful + */ + Title.newFromImg = function ( img ) { + var matches, i, regex, src, decodedSrc, + + // thumb.php-generated thumbnails + thumbPhpRegex = /thumb\.php/, + + regexes = [ + // Thumbnails + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/, + + // Thumbnails in non-hashed upload directories + /\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/, + + // Full size images + /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/, + + // Full-size images in non-hashed upload directories + /\/([^\s\/]+)$/ + ], - /* Static space */ + recount = regexes.length; + + src = img.jquery ? img[0].src : img.src; + + matches = src.match( thumbPhpRegex ); + + if ( matches ) { + return mw.Title.newFromText( 'File:' + mw.util.getParamValue( 'f', src ) ); + } + + decodedSrc = decodeURIComponent( src ); + + for ( i = 0; i < recount; i++ ) { + regex = regexes[i]; + matches = decodedSrc.match( regex ); + + if ( matches && matches[1] ) { + return mw.Title.newFromText( 'File:' + matches[1] ); + } + } + + return null; + }; /** * Whether this title exists on the wiki. + * * @static - * @param {Mixed} title prefixed db-key name (string) or instance of Title - * @return {Mixed} Boolean true/false if the information is available. Otherwise null. + * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title + * @return {boolean|null} Boolean if the information is available, otherwise null */ Title.exists = function ( title ) { - var type = $.type( title ), obj = Title.exist.pages, match; + var match, + type = $.type( title ), + obj = Title.exist.pages; + if ( type === 'string' ) { match = obj[title]; } else if ( type === 'object' && title instanceof Title ) { @@ -194,23 +370,23 @@ var } else { throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' ); } + if ( typeof match === 'boolean' ) { return match; } + return null; }; - /** - * @static - * @property - */ Title.exist = { /** + * Boolean true value indicates page does exist. + * * @static * @property {Object} exist.pages Keyed by PrefixedDb title. - * Boolean true value indicates page does exist. */ pages: {}, + /** * Example to declare existing titles: * Title.exist.set(['User:John_Doe', ...]); @@ -219,8 +395,8 @@ var * * @static * @property exist.set - * @param {string|Array} titles Title(s) in strict prefixedDb title form. - * @param {boolean} [state] State of the given titles. Defaults to true. + * @param {string|Array} titles Title(s) in strict prefixedDb title form + * @param {boolean} [state=true] State of the given titles * @return {boolean} */ set: function ( titles, state ) { @@ -234,42 +410,60 @@ var } }; - /* Public methods */ + /* Public members */ - fn = { + Title.prototype = { constructor: Title, /** - * Get the namespace number. + * Get the namespace number + * + * Example: 6 for "File:Example_image.svg". + * * @return {number} */ - getNamespaceId: function (){ - return this.ns; + getNamespaceId: function () { + return this.namespace; }, /** - * Get the namespace prefix (in the content-language). - * In NS_MAIN this is '', otherwise namespace name plus ':' + * Get the namespace prefix (in the content language) + * + * Example: "File:" for "File:Example_image.svg". + * In #NS_MAIN this is '', otherwise namespace name plus ':' + * * @return {string} */ - getNamespacePrefix: function (){ - return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); + getNamespacePrefix: function () { + return this.namespace === NS_MAIN ? + '' : + ( mw.config.get( 'wgFormattedNamespaces' )[ this.namespace ].replace( / /g, '_' ) + ':' ); }, /** - * The name, like "Foo_bar" + * Get the page name without extension or namespace prefix + * + * Example: "Example_image" for "File:Example_image.svg". + * + * For the page title (full page name without namespace prefix), see #getMain. + * * @return {string} */ getName: function () { - if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { - return this.name; + if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this.title; } else { - return $.ucFirst( this.name ); + return $.ucFirst( this.title ); } }, /** - * The name, like "Foo bar" + * Get the page name (transformed by #text) + * + * Example: "Example image" for "File:Example_image.svg". + * + * For the page title (full page name without namespace prefix), see #getMainText. + * * @return {string} */ getNameText: function () { @@ -277,24 +471,30 @@ var }, /** - * Get full name in prefixed DB form, like File:Foo_bar.jpg, - * most useful for API calls, anything that must identify the "title". - * @return {string} + * Get the extension of the page name (if any) + * + * @return {string|null} Name extension or null if there is none */ - getPrefixedDb: function () { - return this.getNamespacePrefix() + this.getMain(); + getExtension: function () { + return this.ext; }, /** - * Get full name in text form, like "File:Foo bar.jpg". + * Shortcut for appendable string to form the main page name. + * + * Returns a string like ".json", or "" if no extension. + * * @return {string} */ - getPrefixedText: function () { - return text( this.getPrefixedDb() ); + getDotExtension: function () { + return this.ext === null ? '' : '.' + this.ext; }, /** - * The main title (without namespace), like "Foo_bar.jpg" + * Get the main page name (transformed by #text) + * + * Example: "Example_image.svg" for "File:Example_image.svg". + * * @return {string} */ getMain: function () { @@ -302,7 +502,10 @@ var }, /** - * The "text" form, like "Foo bar.jpg" + * Get the main page name (transformed by #text) + * + * Example: "Example image.svg" for "File:Example_image.svg". + * * @return {string} */ getMainText: function () { @@ -310,46 +513,73 @@ var }, /** - * Get the extension (returns null if there was none) - * @return {string|null} + * Get the full page name + * + * Eaxample: "File:Example_image.svg". + * Most useful for API calls, anything that must identify the "title". + * + * @return {string} */ - getExtension: function () { - return this.ext; + getPrefixedDb: function () { + return this.getNamespacePrefix() + this.getMain(); }, /** - * Convenience method: return string like ".jpg", or "" if no extension + * Get the full page name (transformed by #text) + * + * Example: "File:Example image.svg" for "File:Example_image.svg". + * * @return {string} */ - getDotExtension: function () { - return this.ext === null ? '' : '.' + this.ext; + getPrefixedText: function () { + return text( this.getPrefixedDb() ); + }, + + /** + * Get the fragment (if any). + * + * Note that this method (by design) does not include the hash character and + * the value is not url encoded. + * + * @return {string|null} + */ + getFragment: function () { + return this.fragment; }, /** - * Return the URL to this title - * @see mw.util#wikiGetlink + * Get the URL to this title + * + * @see mw.util#getUrl * @return {string} */ getUrl: function () { - return mw.util.wikiGetlink( this.toString() ); + return mw.util.getUrl( this.toString() ); }, /** * Whether this title exists on the wiki. + * * @see #static-method-exists - * @return {boolean|null} If the information is available. Otherwise null. + * @return {boolean|null} Boolean if the information is available, otherwise null */ exists: function () { return Title.exists( this ); } }; - // Alias - fn.toString = fn.getPrefixedDb; - fn.toText = fn.getPrefixedText; + /** + * @alias #getPrefixedDb + * @method + */ + Title.prototype.toString = Title.prototype.getPrefixedDb; + - // Assign - Title.prototype = fn; + /** + * @alias #getPrefixedText + * @method + */ + Title.prototype.toText = Title.prototype.getPrefixedText; // Expose mw.Title = Title; diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index 643e5c3e..a2d4d6cb 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -201,7 +201,7 @@ uri = this, matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); $.each( properties, function ( i, property ) { - uri[ property ] = matches[ i+1 ]; + uri[ property ] = matches[ i + 1 ]; } ); // uri.query starts out as the query string; we will parse it into key-val pairs then make @@ -210,7 +210,7 @@ q = {}; // using replace to iterate over a string if ( uri.query ) { - uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { + uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( $0, $1, $2, $3 ) { var k, v; if ( $1 ) { k = Uri.decode( $1 ); diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js index 88af3c65..986917a1 100644 --- a/resources/mediawiki/mediawiki.debug.js +++ b/resources/mediawiki/mediawiki.debug.js @@ -229,7 +229,7 @@ $( '<colgroup>' ).css( 'width', 350 ).appendTo( $table ); - entryTypeText = function( entryType ) { + entryTypeText = function ( entryType ) { switch ( entryType ) { case 'log': return 'Log'; diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index 83bf2e3a..de068598 100644 --- a/resources/mediawiki/mediawiki.htmlform.js +++ b/resources/mediawiki/mediawiki.htmlform.js @@ -1,7 +1,7 @@ /** * Utility functions for jazzing up HTMLForm elements. */ -( function ( $ ) { +( function ( mw, $ ) { /** * jQuery plugin to fade or snap to visible state. @@ -59,4 +59,70 @@ } ); -}( jQuery ) ); + function addMulti( $oldContainer, $container ) { + var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ), + oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ), + $select = $( '<select>' ), + dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' ); + oldClass = $.trim( oldClass ); + $select.attr( { + name: name, + multiple: 'multiple', + 'data-placeholder': dataPlaceholder.plain(), + 'class': 'htmlform-chzn-select mw-input ' + oldClass + } ); + $oldContainer.find( 'input' ).each( function () { + var $oldInput = $(this), + checked = $oldInput.prop( 'checked' ), + $option = $( '<option>' ); + $option.prop( 'value', $oldInput.prop( 'value' ) ); + if ( checked ) { + $option.prop( 'selected', true ); + } + $option.text( $oldInput.prop( 'value' ) ); + $select.append( $option ); + } ); + $container.append( $select ); + } + + function convertCheckboxesToMulti( $oldContainer, type ) { + var $fieldLabel = $( '<td>' ), + $td = $( '<td>' ), + $fieldLabelText = $( '<label>' ), + $container; + if ( type === 'tr' ) { + addMulti( $oldContainer, $td ); + $container = $( '<tr>' ); + $container.append( $td ); + } else if ( type === 'div' ) { + $fieldLabel = $( '<div>' ); + $container = $( '<div>' ); + addMulti( $oldContainer, $container ); + } + $fieldLabel.attr( 'class', 'mw-label' ); + $fieldLabelText.text( $oldContainer.find( '.mw-label label' ).text() ); + $fieldLabel.append( $fieldLabelText ); + $container.prepend( $fieldLabel ); + $oldContainer.replaceWith( $container ); + return $container; + } + + if ( $( '.mw-chosen' ).length ) { + mw.loader.using( 'jquery.chosen', function () { + $( '.mw-chosen' ).each( function () { + var type = this.nodeName.toLowerCase(), + $converted = convertCheckboxesToMulti( $( this ), type ); + $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } ); + } ); + } ); + } + + $( function () { + var $matrixTooltips = $( '.mw-htmlform-matrix .mw-htmlform-tooltip' ); + if ( $matrixTooltips.length ) { + mw.loader.using( 'jquery.tipsy', function () { + $matrixTooltips.tipsy( { gravity: 's' } ); + } ); + } + } ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.icon.css b/resources/mediawiki/mediawiki.icon.css new file mode 100644 index 00000000..f61b7257 --- /dev/null +++ b/resources/mediawiki/mediawiki.icon.css @@ -0,0 +1,15 @@ +/* General-purpose icons via CSS. Classes here should be named "mw-icon-*". */ + +/* For the collapsed and expanded arrows, we also provide selectors to make it + * easy to use them with jquery.makeCollapsible. */ +.mw-icon-arrow-collapsed, +.mw-collapsible-arrow.mw-collapsible-toggle-collapsed { + /* @embed */ + background: url(images/arrow-collapsed-ltr.png) no-repeat left bottom; +} + +.mw-icon-arrow-expanded, +.mw-collapsible-arrow.mw-collapsible-toggle-expanded { + /* @embed */ + background: url(images/arrow-expanded.png) no-repeat left bottom; +} diff --git a/resources/mediawiki/mediawiki.inspect.js b/resources/mediawiki/mediawiki.inspect.js new file mode 100644 index 00000000..2f2ca335 --- /dev/null +++ b/resources/mediawiki/mediawiki.inspect.js @@ -0,0 +1,204 @@ +/*! + * Tools for inspecting page composition and performance. + * + * @author Ori Livneh + * @since 1.22 + */ +/*jshint devel:true */ +( function ( mw, $ ) { + + 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; + } ); + } + + /** + * @class mw.inspect + * @singleton + */ + var inspect = { + + /** + * 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 ], + payload = 0; + + if ( mw.loader.getState( moduleName ) !== 'ready' ) { + return null; + } + + if ( !module.style && !module.script ) { + return null; + } + + // Tally CSS + if ( module.style && $.isArray( module.style.css ) ) { + $.each( module.style.css, function ( i, stylesheet ) { + payload += $.byteLength( stylesheet ); + } ); + } + + // Tally JavaScript + if ( $.isFunction( module.script ) ) { + payload += $.byteLength( module.script.toString() ); + } + + return payload; + }, + + /** + * 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 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' ), + sheet, rules; + + style.textContent = css; + document.body.appendChild( style ); + // Standards-compliant browsers use .sheet.cssRules, IE8 uses .styleSheet.rules… + sheet = style.sheet || style.styleSheet; + rules = sheet.cssRules || sheet.rules; + $.each( rules, function ( index, rule ) { + selectors.total++; + if ( document.querySelector( rule.selectorText ) !== null ) { + selectors.matched++; + } + } ); + document.body.removeChild( style ); + return selectors; + }, + + /** + * Get a list of all loaded ResourceLoader modules. + * + * @return {Array} List of module names + */ + getLoadedModules: function () { + return $.grep( mw.loader.getModuleNames(), 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. + console.table.call( console, data ); + return; + } catch (e) {} + try { + console.log( $.toJSON( 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; } ); + + $.each( reports, function ( index, 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. + */ + size: function () { + // Map each module to a descriptor object. + var modules = $.map( inspect.getLoadedModules(), 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. + $.each( modules, function ( i, module ) { + module.size = module.size > 1024 ? + ( module.size / 1024 ).toFixed( 2 ) + ' KB' : + ( module.size !== null ? module.size + ' B' : null ); + } ); + + return modules; + }, + + /** + * For each module with styles, count the number of selectors, and + * count how many match against some element currently in the DOM. + */ + css: function () { + var modules = []; + + $.each( inspect.getLoadedModules(), function ( index, 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; + }, + } + }; + + if ( mw.config.get( 'debug' ) ) { + mw.log( 'mw.inspect: reports are not available in debug mode.' ); + } + + mw.inspect = inspect; + +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 183b525e..70b9be93 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -3,6 +3,7 @@ * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs * * @author neilk@wikimedia.org +* @author mflaschen@wikimedia.org */ ( function ( mw, $ ) { var oldParser, @@ -11,6 +12,31 @@ magic : { 'SITENAME' : mw.config.get( 'wgSiteName' ) }, + // This is a whitelist based on, but simpler than, Sanitizer.php. + // Self-closing tags are not currently supported. + allowedHtmlElements : [ + 'b', + 'i' + ], + // Key tag name, value allowed attributes for that tag. + // See Sanitizer::setupAttributeWhitelist + allowedHtmlCommonAttributes : [ + // HTML + 'id', + 'class', + 'style', + 'lang', + 'dir', + 'title', + + // WAI-ARIA + 'role' + ], + + // Attributes allowed for specific elements. + // Key is element name in lower case + // Value is array of allowed attributes for that element + allowedHtmlAttributesByElement : {}, messages : mw.messages, language : mw.language, @@ -18,7 +44,7 @@ // // Only 'text', 'parse', and 'escaped' are supported, and the // actual escaping for 'escaped' is done by other code (generally - // through jqueryMsg). + // through mediawiki.js). // // However, note that this default only // applies to direct calls to jqueryMsg. The default for mediawiki.js itself @@ -28,6 +54,47 @@ }; /** + * Wrapper around jQuery append that converts all non-objects to TextNode so append will not + * convert what it detects as an htmlString to an element. + * + * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is. + * + * @param {jQuery} $parent Parent node wrapped by jQuery + * @param {Object|string|Array} children What to append, with the same possible types as jQuery + * @return {jQuery} $parent + */ + function appendWithoutParsing( $parent, children ) { + var i, len; + + if ( !$.isArray( children ) ) { + children = [children]; + } + + for ( i = 0, len = children.length; i < len; i++ ) { + if ( typeof children[i] !== 'object' ) { + children[i] = document.createTextNode( children[i] ); + } + } + + return $parent.append( children ); + } + + /** + * Decodes the main HTML entities, those encoded by mw.html.escape. + * + * @param {string} encode Encoded string + * @return {string} String with those entities decoded + */ + function decodePrimaryHtmlEntities( encoded ) { + return encoded + .replace( /'/g, '\'' ) + .replace( /"/g, '"' ) + .replace( /</g, '<' ) + .replace( />/g, '>' ) + .replace( /&/g, '&' ); + } + + /** * Given parser options, return a function that parses a key and replacements, returning jQuery object * @param {Object} parser options * @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery} @@ -48,7 +115,7 @@ try { return parser.parse( key, argsArray ); } catch ( e ) { - return $( '<span>' ).append( key + ': ' + e.message ); + return $( '<span>' ).text( key + ': ' + e.message ); } }; } @@ -125,10 +192,10 @@ */ return function () { var $target = this.empty(); - // TODO: Simply $target.append( failableParserFn( arguments ).contents() ) - // or Simply $target.append( failableParserFn( arguments ) ) + // TODO: Simply appendWithoutParsing( $target, failableParserFn( arguments ).contents() ) + // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) ) $.each( failableParserFn( arguments ).contents(), function ( i, node ) { - $target.append( node ); + appendWithoutParsing( $target, node ); } ); return $target; }; @@ -206,11 +273,13 @@ * @return {Mixed} abstract syntax tree */ wikiTextToAst: function ( input ) { - var pos, + var pos, settings = this.settings, concat = Array.prototype.concat, regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets, - backslash, anyCharacter, escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, - whitespace, dollar, digits, - openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon, + doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral, + escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, + whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue, + htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag, + openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon, templateContents, openTemplate, closeTemplate, nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result; @@ -289,6 +358,15 @@ return result; }; } + + /** + * Makes a regex parser, given a RegExp object. + * The regex being passed in should start with a ^ to anchor it to the start + * of the string. + * + * @param {RegExp} regex anchored regex + * @return {Function} function to parse input based on the regex + */ function makeRegexParser( regex ) { return function () { var matches = input.substr( pos ).match( regex ); @@ -315,12 +393,23 @@ // but some debuggers can't tell you exactly where they come from. Also the mutually // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF) // This may be because, to save code, memoization was removed - regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); + + regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ ); regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ ); + backslash = makeStringParser( '\\' ); + doubleQuote = makeStringParser( '"' ); + singleQuote = makeStringParser( '\'' ); anyCharacter = makeRegexParser( /^./ ); + + openHtmlStartTag = makeStringParser( '<' ); + optionalForwardSlash = makeRegexParser( /^\/?/ ); + openHtmlEndTag = makeStringParser( '</' ); + htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ ); + closeHtmlTag = makeRegexParser( /^\s*>/ ); + function escapedLiteral() { var result = sequence( [ backslash, @@ -369,6 +458,10 @@ return result === null ? null : result.join(''); } + asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ ); + htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ ); + htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ ); + whitespace = makeRegexParser( /^\s+/ ); dollar = makeStringParser( '$' ); digits = makeRegexParser( /^\d+/ ); @@ -385,7 +478,7 @@ } openExtlink = makeStringParser( '[' ); closeExtlink = makeStringParser( ']' ); - // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed + // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed function extlink() { var result, parsedResult; result = null; @@ -393,11 +486,18 @@ openExtlink, nonWhitespaceExpression, whitespace, - expression, + nOrMore( 1, expression ), closeExtlink ] ); if ( parsedResult !== null ) { - result = [ 'LINK', parsedResult[1], parsedResult[3] ]; + result = [ 'EXTLINK', parsedResult[1] ]; + // TODO (mattflaschen, 2013-03-22): Clean this up if possible. + // It's avoiding CONCAT for single nodes, so they at least doesn't get the htmlEmitter span. + if ( parsedResult[3].length === 1 ) { + result.push( parsedResult[3][0] ); + } else { + result.push( ['CONCAT'].concat( parsedResult[3] ) ); + } } return result; } @@ -414,10 +514,10 @@ if ( result === null ) { return null; } - return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; + return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; } - openLink = makeStringParser( '[[' ); - closeLink = makeStringParser( ']]' ); + openWikilink = makeStringParser( '[[' ); + closeWikilink = makeStringParser( ']]' ); pipe = makeStringParser( '|' ); function template() { @@ -448,21 +548,158 @@ wikilinkPage // unpiped link ] ); - function link() { + function wikilink() { var result, parsedResult, parsedLinkContents; result = null; parsedResult = sequence( [ - openLink, + openWikilink, wikilinkContents, - closeLink + closeWikilink ] ); if ( parsedResult !== null ) { parsedLinkContents = parsedResult[1]; - result = [ 'WLINK' ].concat( parsedLinkContents ); + result = [ 'WIKILINK' ].concat( parsedLinkContents ); + } + return result; + } + + // TODO: Support data- if appropriate + function doubleQuotedHtmlAttributeValue() { + var parsedResult = sequence( [ + doubleQuote, + htmlDoubleQuoteAttributeValue, + doubleQuote + ] ); + return parsedResult === null ? null : parsedResult[1]; + } + + function singleQuotedHtmlAttributeValue() { + var parsedResult = sequence( [ + singleQuote, + htmlSingleQuoteAttributeValue, + singleQuote + ] ); + return parsedResult === null ? null : parsedResult[1]; + } + + function htmlAttribute() { + var parsedResult = sequence( [ + whitespace, + asciiAlphabetLiteral, + htmlAttributeEquals, + choice( [ + doubleQuotedHtmlAttributeValue, + singleQuotedHtmlAttributeValue + ] ) + ] ); + return parsedResult === null ? null : [parsedResult[1], parsedResult[3]]; + } + + /** + * Checks if HTML is allowed + * + * @param {string} startTagName HTML start tag name + * @param {string} endTagName HTML start tag name + * @param {Object} attributes array of consecutive key value pairs, + * with index 2 * n being a name and 2 * n + 1 the associated value + * @return {boolean} true if this is HTML is allowed, false otherwise + */ + function isAllowedHtml( startTagName, endTagName, attributes ) { + var i, len, attributeName; + + startTagName = startTagName.toLowerCase(); + endTagName = endTagName.toLowerCase(); + if ( startTagName !== endTagName || $.inArray( startTagName, settings.allowedHtmlElements ) === -1 ) { + return false; + } + + for ( i = 0, len = attributes.length; i < len; i += 2 ) { + attributeName = attributes[i]; + if ( $.inArray( attributeName, settings.allowedHtmlCommonAttributes ) === -1 && + $.inArray( attributeName, settings.allowedHtmlAttributesByElement[startTagName] || [] ) === -1 ) { + return false; + } + } + + return true; + } + + function htmlAttributes() { + var parsedResult = nOrMore( 0, htmlAttribute )(); + // Un-nest attributes array due to structure of jQueryMsg operations (see emit). + return concat.apply( ['HTMLATTRIBUTES'], parsedResult ); + } + + // Subset of allowed HTML markup. + // Most elements and many attributes allowed on the server are not supported yet. + function html() { + var result = null, parsedOpenTagResult, parsedHtmlContents, + parsedCloseTagResult, wrappedAttributes, attributes, + startTagName, endTagName, startOpenTagPos, startCloseTagPos, + endOpenTagPos, endCloseTagPos; + + // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match. + // 1. open through closeHtmlTag + // 2. expression + // 3. openHtmlEnd through close + // This will allow recording the positions to reconstruct if HTML is to be treated as text. + + startOpenTagPos = pos; + parsedOpenTagResult = sequence( [ + openHtmlStartTag, + asciiAlphabetLiteral, + htmlAttributes, + optionalForwardSlash, + closeHtmlTag + ] ); + + if ( parsedOpenTagResult === null ) { + return null; } + + endOpenTagPos = pos; + startTagName = parsedOpenTagResult[1]; + + parsedHtmlContents = nOrMore( 0, expression )(); + + startCloseTagPos = pos; + parsedCloseTagResult = sequence( [ + openHtmlEndTag, + asciiAlphabetLiteral, + closeHtmlTag + ] ); + + if ( parsedCloseTagResult === null ) { + // Closing tag failed. Return the start tag and contents. + return [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ].concat( parsedHtmlContents ); + } + + endCloseTagPos = pos; + endTagName = parsedCloseTagResult[1]; + wrappedAttributes = parsedOpenTagResult[2]; + attributes = wrappedAttributes.slice( 1 ); + if ( isAllowedHtml( startTagName, endTagName, attributes) ) { + result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ].concat( parsedHtmlContents ); + } else { + // HTML is not allowed, so contents will remain how + // it was, while HTML markup at this level will be + // treated as text + // E.g. assuming script tags are not allowed: + // + // <script>[[Foo|bar]]</script> + // + // results in '<script>' and '</script>' + // (not treated as an HTML tag), surrounding a fully + // parsed HTML link. + // + // Concatenate everything from the tag, flattening the contents. + result = [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ].concat( parsedHtmlContents, input.substring( startCloseTagPos, endCloseTagPos ) ); + } + return result; } + templateName = transform( // see $wgLegalTitleChars // not allowing : due to the need to catch "PLURAL:$1" @@ -525,7 +762,7 @@ closeTemplate = makeStringParser('}}'); nonWhitespaceExpression = choice( [ template, - link, + wikilink, extLinkParam, extlink, replacement, @@ -533,7 +770,7 @@ ] ); paramExpression = choice( [ template, - link, + wikilink, extLinkParam, extlink, replacement, @@ -542,10 +779,11 @@ expression = choice( [ template, - link, + wikilink, extLinkParam, extlink, replacement, + html, literal ] ); @@ -659,12 +897,12 @@ $.each( nodes, function ( i, node ) { if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) { $.each( node.contents(), function ( j, childNode ) { - $span.append( childNode ); + appendWithoutParsing( $span, childNode ); } ); } else { // Let jQuery append nodes, arrays of nodes and jQuery objects // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings) - $span.append( $.type( node ) === 'object' ? node : document.createTextNode( node ) ); + appendWithoutParsing( $span, node ); } } ); return $span; @@ -704,11 +942,11 @@ * * @param nodes */ - wlink: function ( nodes ) { + wikilink: function ( nodes ) { var page, anchor, url; page = nodes[0]; - url = mw.util.wikiGetlink( page ); + url = mw.util.getUrl( page ); // [[Some Page]] or [[Namespace:Some Page]] if ( nodes.length === 1 ) { @@ -730,6 +968,36 @@ }, /** + * Converts array of HTML element key value pairs to object + * + * @param {Array} nodes array of consecutive key value pairs, with index 2 * n being a name and 2 * n + 1 the associated value + * @return {Object} object mapping attribute name to attribute value + */ + htmlattributes: function ( nodes ) { + var i, len, mapping = {}; + for ( i = 0, len = nodes.length; i < len; i += 2 ) { + mapping[nodes[i]] = decodePrimaryHtmlEntities( nodes[i + 1] ); + } + return mapping; + }, + + /** + * Handles an (already-validated) HTML element. + * + * @param {Array} nodes nodes to process when creating element + * @return {jQuery|Array} jQuery node for valid HTML or array for disallowed element + */ + htmlelement: function ( nodes ) { + var tagName, attributes, contents, $element; + + tagName = nodes.shift(); + attributes = nodes.shift(); + contents = nodes; + $element = $( document.createElement( tagName ) ).attr( attributes ); + return appendWithoutParsing( $element, contents ); + }, + + /** * Transform parsed structure into external link * If the href is a jQuery object, treat it as "enclosing" the link text. * ... function, treat it as the click handler @@ -738,7 +1006,7 @@ * @param {Array} of two elements, {jQuery|Function|String} and {String} * @return {jQuery} */ - link: function ( nodes ) { + extlink: function ( nodes ) { var $el, arg = nodes[0], contents = nodes[1]; @@ -752,12 +1020,11 @@ $el.attr( 'href', arg.toString() ); } } - $el.append( contents ); - return $el; + return appendWithoutParsing( $el, contents ); }, /** - * This is basically use a combination of replace + link (link with parameter + * This is basically use a combination of replace + external link (link with parameter * as url), but we don't want to run the regular replace here-on: inserting a * url as href-attribute of a link will automatically escape it already, so * we don't want replace to (manually) escape it as well. @@ -765,7 +1032,7 @@ * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ - linkparam: function ( nodes, replacements ) { + extlinkparam: function ( nodes, replacements ) { var replacement, index = parseInt( nodes[0], 10 ); if ( index < replacements.length) { @@ -773,7 +1040,7 @@ } else { replacement = '$' + ( index + 1 ); } - return this.link( [ replacement, nodes[1] ] ); + return this.extlink( [ replacement, nodes[1] ] ); }, /** @@ -865,7 +1132,7 @@ // Caching is somewhat problematic, because we do need different message functions for different maps, so // we'd have to cache the parser as a member of this.map, which sounds a bit ugly. // Do not use mw.jqueryMsg unless required - if ( this.format === 'plain' || !/\{\{|\[/.test(this.map.get( this.key ) ) ) { + if ( this.format === 'plain' || !/\{\{|[\[<>]/.test(this.map.get( this.key ) ) ) { // Fall back to mw.msg's simple parser return oldParser.apply( this ); } diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index ca987543..80223e5d 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -1,5 +1,9 @@ -/* - * Core MediaWiki JavaScript Library +/** + * Base library for MediaWiki. + * + * @class mw + * @alternateClassName mediaWiki + * @singleton */ var mw = ( function ( $, undefined ) { @@ -10,15 +14,67 @@ var mw = ( function ( $, undefined ) { var hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice; + /** + * Log a message to window.console, if possible. Useful to force logging of some + * errors that are otherwise hard to detect (I.e., this logs also in production mode). + * Gets console references in each invocation, so that delayed debugging tools work + * fine. No need for optimization here, which would only result in losing logs. + * + * @private + * @param {string} msg text for the log entry. + * @param {Error} [e] + */ + function log( msg, e ) { + var console = window.console; + if ( console && console.log ) { + console.log( msg ); + // If we have an exception object, log it through .error() to trigger + // proper stacktraces in browsers that support it. There are no (known) + // browsers that don't support .error(), that do support .log() and + // have useful exception handling through .log(). + if ( e && console.error ) { + console.error( String( e ), e ); + } + } + } + /* Object constructors */ /** * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. + * + * @example + * + * var addies, wanted, results; + * + * // Create your address book + * addies = new mw.Map(); + * + * // This data could be coming from an external source (eg. API/AJAX) + * addies.set( { + * 'John Doe' : '10 Wall Street, New York, USA', + * 'Jane Jackson' : '21 Oxford St, London, UK', + * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL' + * } ); + * + * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson']; + * + * // You can detect missing keys first + * if ( !addies.exists( wanted ) ) { + * // One or more are missing (in this case: "George Johnson") + * mw.log( 'One or more names were not found in your address book' ); + * } + * + * // Or just let it give you what it can + * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' ); + * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK" + * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US" + * * @class mw.Map * * @constructor - * @param {boolean} global Whether to store the values in the global window + * @param {boolean} [global=false] Whether to store the values in the global window * object or a exclusively in the object property 'values'. */ function Map( global ) { @@ -32,8 +88,8 @@ var mw = ( function ( $, undefined ) { * * If called with no arguments, all values will be returned. * - * @param selection mixed String key or array of keys to get values for. - * @param fallback mixed Value to use in case key(s) do not exist (optional). + * @param {string|Array} selection String key or array of keys to get values for. + * @param {Mixed} [fallback] Value to use in case key(s) do not exist. * @return mixed If selection was a string returns the value or null, * If selection was an array, returns an object of key/values (value is null if not found), * If selection was not passed or invalid, will return the 'values' object member (be careful as @@ -73,8 +129,8 @@ var mw = ( function ( $, undefined ) { /** * Sets one or multiple key/value pairs. * - * @param selection {mixed} String key or array of keys to set values for. - * @param value {mixed} Value to set (optional, only in use when key is a string) + * @param {string|Object} selection String key to set value for, or object mapping keys to values. + * @param {Mixed} [value] Value to set (optional, only in use when key is a string) * @return {Boolean} This returns true on success, false on failure. */ set: function ( selection, value ) { @@ -96,7 +152,7 @@ var mw = ( function ( $, undefined ) { /** * Checks if one or multiple keys exist. * - * @param selection {mixed} String key or array of keys to check + * @param {Mixed} selection String key or array of keys to check * @return {boolean} Existence of key(s) */ exists: function ( selection ) { @@ -115,8 +171,12 @@ var mw = ( function ( $, undefined ) { }; /** - * Object constructor for messages, - * similar to the Message class in MediaWiki PHP. + * Object constructor for messages. + * + * Similar to the Message class in MediaWiki PHP. + * + * Format defaults to 'text'. + * * @class mw.Message * * @constructor @@ -134,8 +194,7 @@ var mw = ( function ( $, undefined ) { Message.prototype = { /** - * Simple message parser, does $N replacement, HTML-escaping (only for - * 'escaped' format), and nothing else. + * Simple message parser, does $N replacement and nothing else. * * This may be overridden to provide a more complex message parser. * @@ -259,19 +318,21 @@ var mw = ( function ( $, undefined ) { } }; - /** - * @class mw - * @alternateClassName mediaWiki - * @singleton - */ return { /* Public Members */ /** - * Dummy function which in debug mode can be replaced with a function that - * emulates console.log in console-less environments. + * Dummy placeholder for {@link mw.log} + * @method */ - log: function () { }, + log: ( function () { + var log = function () {}; + log.warn = function () {}; + log.deprecate = function ( obj, key, val ) { + obj[key] = val; + }; + return log; + }() ), // Make the Map constructor publicly available. Map: Map, @@ -280,13 +341,17 @@ var mw = ( function ( $, undefined ) { Message: Message, /** - * List of configuration values + * Map of configuration values * - * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). - * If `$wgLegacyJavaScriptGlobals` is true, this Map will have its values - * in the global window object. - * @property + * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config) + * on MediaWiki.org. + * + * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the + * global window object. + * + * @property {mw.Map} config */ + // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`. config: null, /** @@ -295,9 +360,15 @@ var mw = ( function ( $, undefined ) { */ libs: {}, - /* Extension points */ - /** + * Access container for deprecated functionality that can be moved from + * from their legacy location and attached to this object (e.g. a global + * function that is deprecated and as stop-gap can be exposed through here). + * + * This was reserved for future use but never ended up being used. + * + * @deprecated since 1.22: Let deprecated identifiers keep their original name + * and use mw.log#deprecate to create an access container for tracking. * @property */ legacy: {}, @@ -311,7 +382,9 @@ var mw = ( function ( $, undefined ) { /* Public Methods */ /** - * Gets a message object, similar to wfMessage(). + * Get a message object. + * + * Similar to wfMessage() in MediaWiki PHP. * * @param {string} key Key of message to get * @param {Mixed...} parameters Parameters for the $N replacements in messages. @@ -324,14 +397,16 @@ var mw = ( function ( $, undefined ) { }, /** - * Gets a message string, similar to wfMessage() + * Get a message string using 'text' format. + * + * Similar to wfMsg() in MediaWiki PHP. * - * @see mw.Message#toString + * @see mw.Message * @param {string} key Key of message to get * @param {Mixed...} parameters Parameters for the $N replacements in messages. * @return {string} */ - msg: function ( /* key, parameters... */ ) { + msg: function () { return mw.message.apply( mw.message, arguments ).toString(); }, @@ -420,11 +495,11 @@ var mw = ( function ( $, undefined ) { * * @private * @param {string} text CSS text - * @param {Mixed} [nextnode] An Element or jQuery object for an element where - * the style tag should be inserted before. Otherwise appended to the `<head>`. - * @return {HTMLElement} Node reference to the created `<style>` tag. + * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be + * inserted before. Otherwise it will be appended to `<head>`. + * @return {HTMLElement} Reference to the created `<style>` element. */ - function addStyleTag( text, nextnode ) { + function newStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); // Insert into document before setting cssText (bug 33305) if ( nextnode ) { @@ -457,7 +532,7 @@ var mw = ( function ( $, undefined ) { /** * Checks whether it is safe to add this css to a stylesheet. - * + * * @private * @param {string} cssText * @return {boolean} False if a new one must be created. @@ -470,8 +545,13 @@ var mw = ( function ( $, undefined ) { } /** + * Add a bit of CSS text to the current browser page. + * + * The CSS will be appended to an existing ResourceLoader-created `<style>` tag + * or create a new one based on whether the given `cssText` is safe for extension. + * * @param {string} [cssText=cssBuffer] If called without cssText, - * the internal buffer will be inserted instead. + * the internal buffer will be inserted instead. * @param {Function} [callback] */ function addEmbeddedCSS( cssText, callback ) { @@ -533,7 +613,7 @@ var mw = ( function ( $, undefined ) { try { styleEl.styleSheet.cssText += cssText; // IE } catch ( e ) { - log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e ); + log( 'addEmbeddedCSS fail', e ); } } else { styleEl.appendChild( document.createTextNode( String( cssText ) ) ); @@ -543,7 +623,7 @@ var mw = ( function ( $, undefined ) { } } - $( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); + $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); cssCallbacks.fire().empty(); } @@ -659,7 +739,7 @@ var mw = ( function ( $, undefined ) { * * @private * @param {string|string[]} states Module states to filter by - * @param {Array} modules List of module names to filter (optional, by default the entire + * @param {Array} [modules] List of module names to filter (optional, by default the entire * registry is used) * @return {Array} List of filtered module names */ @@ -712,30 +792,6 @@ var mw = ( function ( $, undefined ) { } /** - * Log a message to window.console, if possible. Useful to force logging of some - * errors that are otherwise hard to detect (I.e., this logs also in production mode). - * Gets console references in each invocation, so that delayed debugging tools work - * fine. No need for optimization here, which would only result in losing logs. - * - * @private - * @param {string} msg text for the log entry. - * @param {Error} [e] - */ - function log( msg, e ) { - var console = window.console; - if ( console && console.log ) { - console.log( msg ); - // If we have an exception object, log it through .error() to trigger - // proper stacktraces in browsers that support it. There are no (known) - // browsers that don't support .error(), that do support .log() and - // have useful exception handling through .log(). - if ( e && console.error ) { - console.error( e ); - } - } - } - - /** * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs * and modules that depend upon this module. if the given module failed, propagate the 'error' * state up the dependency tree; otherwise, execute all jobs/modules that now have all their @@ -775,22 +831,18 @@ var mw = ( function ( $, undefined ) { j -= 1; try { if ( hasErrors ) { - throw new Error( 'Module ' + module + ' failed.'); + if ( $.isFunction( job.error ) ) { + job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] ); + } } else { if ( $.isFunction( job.ready ) ) { job.ready(); } } } catch ( e ) { - if ( $.isFunction( job.error ) ) { - try { - job.error( e, [module] ); - } catch ( ex ) { - // A user-defined operation raised an exception. Swallow to protect - // our state machine! - log( 'Exception thrown by job.error()', ex ); - } - } + // A user-defined callback raised an exception. + // Swallow it to protect our state machine! + log( 'Exception thrown by job.error', e ); } } } @@ -816,8 +868,7 @@ var mw = ( function ( $, undefined ) { */ function addScript( src, callback, async ) { /*jshint evil:true */ - var script, head, - done = false; + var script, head, done; // Using isReady directly instead of storing it locally from // a $.fn.ready callback (bug 31895). @@ -829,6 +880,7 @@ var mw = ( function ( $, undefined ) { // IE-safe way of getting the <head>. document.head isn't supported // in old IE, and doesn't work when in the <head>. + done = false; head = document.getElementsByTagName( 'head' )[0] || document.body; script = document.createElement( 'script' ); @@ -848,12 +900,12 @@ var mw = ( function ( $, undefined ) { // Handle memory leak in IE script.onload = script.onreadystatechange = null; - // Remove the script + // Detach the element from the document if ( script.parentNode ) { script.parentNode.removeChild( script ); } - // Dereference the script + // Dereference the element from javascript script = undefined; callback(); @@ -950,7 +1002,7 @@ var mw = ( function ( $, undefined ) { } catch ( e ) { // This needs to NOT use mw.log because these errors are common in production mode // and not in debug mode, such as when a symbol that should be global isn't exported - log( 'Exception thrown by ' + module + ': ' + e.message, e ); + log( 'Exception thrown by ' + module, e ); registry[module].state = 'error'; handlePending( module ); } @@ -967,30 +1019,37 @@ var mw = ( function ( $, undefined ) { mw.messages.set( registry[module].messages ); } - // Make sure we don't run the scripts until all (potentially asynchronous) - // stylesheet insertions have completed. - ( function () { - var pending = 0; - checkCssHandles = function () { - // cssHandlesRegistered ensures we don't take off too soon, e.g. when - // one of the cssHandles is fired while we're still creating more handles. - if ( cssHandlesRegistered && pending === 0 && runScript ) { - runScript(); - runScript = undefined; // Revoke - } - }; - cssHandle = function () { - var check = checkCssHandles; - pending++; - return function () { - if (check) { - pending--; - check(); - check = undefined; // Revoke + if ( $.isReady || registry[module].async ) { + // Make sure we don't run the scripts until all (potentially asynchronous) + // stylesheet insertions have completed. + ( function () { + var pending = 0; + checkCssHandles = function () { + // cssHandlesRegistered ensures we don't take off too soon, e.g. when + // one of the cssHandles is fired while we're still creating more handles. + if ( cssHandlesRegistered && pending === 0 && runScript ) { + runScript(); + runScript = undefined; // Revoke } }; - }; - }() ); + cssHandle = function () { + var check = checkCssHandles; + pending++; + return function () { + if (check) { + pending--; + check(); + check = undefined; // Revoke + } + }; + }; + }() ); + } else { + // We are in blocking mode, and so we can't afford to wait for CSS + cssHandle = function () {}; + // Run immediately + checkCssHandles = runScript; + } // Process styles (see also mw.loader.implement) // * back-compat: { <media>: css } @@ -1131,7 +1190,7 @@ var mw = ( function ( $, undefined ) { * @param {Object} moduleMap Module map, see #buildModulesString * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request * @param {string} sourceLoadScript URL of load.php - * @param {boolean} async If true, use an asynchrounous request even if document ready has not yet occurred + * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred */ function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { var request = $.extend( @@ -1146,10 +1205,24 @@ var mw = ( function ( $, undefined ) { /* Public Methods */ return { - addStyleTag: addStyleTag, + /** + * The module registry is exposed as an aid for debugging and inspecting page + * state; it is not a public interface for modifying the registry. + * + * @see #registry + * @property + * @private + */ + moduleRegistry: registry, /** - * Requests dependencies from server, loading and executing when things when ready. + * @inheritdoc #newStyleTag + * @method + */ + addStyleTag: newStyleTag, + + /** + * Batch-request queued dependencies from the server. */ work: function () { var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup, @@ -1311,15 +1384,15 @@ var mw = ( function ( $, undefined ) { }, /** - * Registers a module, letting the system know about it and its + * Register a module, letting the system know about it and its * properties. Startup modules contain calls to this function. * - * @param module {String}: Module name - * @param version {Number}: Module version number as a timestamp (falls backs to 0) - * @param dependencies {String|Array|Function}: One string or array of strings of module + * @param {string} module Module name + * @param {number} version Module version number as a timestamp (falls backs to 0) + * @param {string|Array|Function} dependencies One string or array of strings of module * names on which this module depends, or a function that returns that array. - * @param group {String}: Group which the module is in (optional, defaults to null) - * @param source {String}: Name of the source. Defaults to local. + * @param {string} [group=null] Group which the module is in + * @param {string} [source='local'] Name of the source */ register: function ( module, version, dependencies, group, source ) { var m; @@ -1362,9 +1435,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Implements a module, giving the system a course of action to take - * upon loading. Results of a request for one or more modules contain - * calls to this function. + * Implement a module given the components that make up the module. + * + * When #load or #using requests one or more modules, the server + * response contain calls to this function. * * All arguments are required. * @@ -1419,12 +1493,12 @@ var mw = ( function ( $, undefined ) { }, /** - * Executes a function as soon as one or more required modules are ready + * Execute a function as soon as one or more required modules are ready. * - * @param dependencies {String|Array} Module name or array of modules names the callback + * @param {string|Array} dependencies Module name or array of modules names the callback * dependends on to be ready before executing - * @param ready {Function} callback to execute when all dependencies are ready (optional) - * @param error {Function} callback to execute when if dependencies have a errors (optional) + * @param {Function} [ready] callback to execute when all dependencies are ready + * @param {Function} [error] callback to execute when if dependencies have a errors */ using: function ( dependencies, ready, error ) { var tod = typeof dependencies; @@ -1456,17 +1530,17 @@ var mw = ( function ( $, undefined ) { }, /** - * Loads an external script or one or more modules for future use + * Load an external script or one or more modules. * - * @param modules {mixed} Either the name of a module, array of modules, + * @param {string|Array} modules Either the name of a module, array of modules, * or a URL of an external script or style - * @param type {String} mime-type to use if calling with a URL of an + * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an * external script or style; acceptable values are "text/css" and * "text/javascript"; if no type is provided, text/javascript is assumed. - * @param async {Boolean} (optional) If true, load modules asynchronously - * even if document ready has not yet occurred. If false (default), - * block before document ready and load async after. If not set, true will - * be assumed if loading a URL, and false will be assumed otherwise. + * @param {boolean} [async] If true, load modules asynchronously + * even if document ready has not yet occurred. If false, block before + * document ready and load async after. If not set, true will be + * assumed if loading a URL, and false will be assumed otherwise. */ load: function ( modules, type, async ) { var filtered, m, module, l; @@ -1536,10 +1610,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the state of a module + * Change the state of one or more modules. * - * @param module {String|Object} module name or object of module name/state pairs - * @param state {String} state name + * @param {string|Object} module module name or object of module name/state pairs + * @param {string} state state name */ state: function ( module, state ) { var m; @@ -1565,9 +1639,9 @@ var mw = ( function ( $, undefined ) { }, /** - * Gets the version of a module + * Get the version of a module. * - * @param module string name of module to get version for + * @param {string} module Name of module to get version for */ getVersion: function ( module ) { if ( registry[module] !== undefined && registry[module].version !== undefined ) { @@ -1577,16 +1651,17 @@ var mw = ( function ( $, undefined ) { }, /** - * @deprecated since 1.18 use mw.loader.getVersion() instead + * @inheritdoc #getVersion + * @deprecated since 1.18 use #getVersion instead */ version: function () { return mw.loader.getVersion.apply( mw.loader, arguments ); }, /** - * Gets the state of a module + * Get the state of a module. * - * @param module string name of module to get state for + * @param {string} module name of module to get state for */ getState: function ( module ) { if ( registry[module] !== undefined && registry[module].state !== undefined ) { @@ -1607,16 +1682,45 @@ var mw = ( function ( $, undefined ) { }, /** - * For backwards-compatibility with Squid-cached pages. Loads mw.user + * Load the `mediawiki.user` module. + * + * For backwards-compatibility with cached pages from before 2013 where: + * + * - the `mediawiki.user` module didn't exist yet + * - `mw.user` was still part of mediawiki.js + * - `mw.loader.go` still existed and called after `mw.loader.load()` */ go: function () { mw.loader.load( 'mediawiki.user' ); + }, + + /** + * @inheritdoc mw.inspect#runReports + * @method + */ + inspect: function () { + var args = slice.call( arguments ); + mw.loader.using( 'mediawiki.inspect', function () { + mw.inspect.runReports.apply( mw.inspect, args ); + } ); } + }; }() ), /** * HTML construction helper functions + * + * @example + * + * var Html, output; + * + * Html = mw.html; + * output = Html.element( 'div', {}, new Html.Raw( + * Html.element( 'img', { src: '<' } ) + * ) ); + * mw.log( output ); // <div><img src="<"/></div> + * * @class mw.html * @singleton */ @@ -1646,39 +1750,17 @@ var mw = ( function ( $, undefined ) { }, /** - * Wrapper object for raw HTML passed to mw.html.element(). - * @class mw.html.Raw - */ - Raw: function ( value ) { - this.value = value; - }, - - /** - * Wrapper object for CDATA element contents passed to mw.html.element() - * @class mw.html.Cdata - */ - Cdata: function ( value ) { - this.value = value; - }, - - /** * Create an HTML element string, with safe escaping. * - * @param name The tag name. - * @param attrs An object with members mapping element names to values - * @param contents The contents of the element. May be either: + * @param {string} name The tag name. + * @param {Object} attrs An object with members mapping element names to values + * @param {Mixed} contents The contents of the element. May be either: * - string: The string is escaped. * - null or undefined: The short closing form is used, e.g. <br/>. * - this.Raw: The value attribute is included without escaping. * - this.Cdata: The value attribute is included, and an exception is * thrown if it contains an illegal ETAGO delimiter. * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2 - * - * Example: - * var h = mw.html; - * return h.element( 'div', {}, - * new h.Raw( h.element( 'img', {src: '<'} ) ) ); - * Returns <div><img src="<"/></div> */ element: function ( name, attrs, contents ) { var v, attrName, s = '<' + name; @@ -1727,6 +1809,22 @@ var mw = ( function ( $, undefined ) { } s += '</' + name + '>'; return s; + }, + + /** + * Wrapper object for raw HTML passed to mw.html.element(). + * @class mw.html.Raw + */ + Raw: function ( value ) { + this.value = value; + }, + + /** + * Wrapper object for CDATA element contents passed to mw.html.element() + * @class mw.html.Cdata + */ + Cdata: function ( value ) { + this.value = value; } }; }() ), @@ -1735,7 +1833,87 @@ var mw = ( function ( $, undefined ) { user: { options: new Map(), tokens: new Map() - } + }, + + /** + * Registry and firing of events. + * + * MediaWiki has various interface components that are extended, enhanced + * or manipulated in some other way by extensions, gadgets and even + * in core itself. + * + * This framework helps streamlining the timing of when these other + * code paths fire their plugins (instead of using document-ready, + * which can and should be limited to firing only once). + * + * Features like navigating to other wiki pages, previewing an edit + * and editing itself – without a refresh – can then retrigger these + * hooks accordingly to ensure everything still works as expected. + * + * Example usage: + * + * mw.hook( 'wikipage.content' ).add( fn ).remove( fn ); + * mw.hook( 'wikipage.content' ).fire( $content ); + * + * Handlers can be added and fired for arbitrary event names at any time. The same + * event can be fired multiple times. The last run of an event is memorized + * (similar to `$(document).ready` and `$.Deferred().done`). + * This means if an event is fired, and a handler added afterwards, the added + * function will be fired right away with the last given event data. + * + * Like Deferreds and Promises, the mw.hook object is both detachable and chainable. + * Thus allowing flexible use and optimal maintainability and authority control. + * You can pass around the `add` and/or `fire` method to another piece of code + * without it having to know the event name (or `mw.hook` for that matter). + * + * var h = mw.hook( 'bar.ready' ); + * new mw.Foo( .. ).fetch( { callback: h.fire } ); + * + * Note: Events are documented with an underscore instead of a dot in the event + * name due to jsduck not supporting dots in that position. + * + * @class mw.hook + */ + hook: ( function () { + var lists = {}; + + /** + * Create an instance of mw.hook. + * + * @method hook + * @member mw + * @param {string} name Name of hook. + * @return {mw.hook} + */ + return function ( name ) { + var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) ); + + return { + /** + * Register a hook handler + * @param {Function...} handler Function to bind. + * @chainable + */ + add: list.add, + + /** + * Unregister a hook handler + * @param {Function...} handler Function to unbind. + * @chainable + */ + remove: list.remove, + + /** + * Run a hook. + * @param {Mixed...} data + * @chainable + */ + fire: function () { + return list.fireWith( null, slice.call( arguments ) ); + } + }; + }; + }() ) }; }( jQuery ) ); diff --git a/resources/mediawiki/mediawiki.log.js b/resources/mediawiki/mediawiki.log.js index ee08b12b..75e4c961 100644 --- a/resources/mediawiki/mediawiki.log.js +++ b/resources/mediawiki/mediawiki.log.js @@ -1,4 +1,4 @@ -/** +/*! * Logger for MediaWiki javascript. * Implements the stub left by the main 'mediawiki' module. * @@ -9,15 +9,20 @@ ( function ( mw, $ ) { /** + * @class mw.log + * @singleton + */ + + /** * Logs a message to the console. * * In the case the browser does not have a console API, a console is created on-the-fly by appending - * a <div id="mw-log-console"> element to the bottom of the body and then appending this and future + * a `<div id="mw-log-console">` element to the bottom of the body and then appending this and future * messages to that, instead of the console. * - * @param {String} First in list of variadic messages to output to console. + * @param {string...} msg Messages to output to console. */ - mw.log = function ( /* logmsg, logmsg, */ ) { + mw.log = function () { // Turn arguments into an array var args = Array.prototype.slice.call( arguments ), // Allow log messages to use a configured prefix to identify the source window (ie. frame) @@ -54,7 +59,7 @@ hovzer.update(); } $log.append( - $( '<div></div>' ) + $( '<div>' ) .css( { borderBottom: 'solid 1px #DDDDDD', fontSize: 'small', @@ -68,4 +73,54 @@ } ); }; + /** + * Write a message the console's warning channel. + * Also logs a stacktrace for easier debugging. + * Each action is silently ignored if the browser doesn't support it. + * + * @param {string...} msg Messages to output to console + */ + mw.log.warn = function () { + var console = window.console; + if ( console && console.warn ) { + console.warn.apply( console, arguments ); + if ( console.trace ) { + console.trace(); + } + } + }; + + /** + * Create a property in a host object that, when accessed, will produce + * a deprecation warning in the console with backtrace. + * + * @param {Object} obj Host object of deprecated property + * @param {string} key Name of property to create in `obj` + * @param {Mixed} val The value this property should return when accessed + * @param {string} [msg] Optional text to include in the deprecation message. + */ + mw.log.deprecate = !Object.defineProperty ? function ( obj, key, val ) { + obj[key] = val; + } : function ( obj, key, val, msg ) { + msg = 'MWDeprecationWarning: Use of "' + key + '" property is deprecated.' + + ( msg ? ( ' ' + msg ) : '' ); + try { + Object.defineProperty( obj, key, { + configurable: true, + enumerable: true, + get: function () { + mw.log.warn( msg ); + return val; + }, + set: function ( newVal ) { + mw.log.warn( msg ); + val = newVal; + } + } ); + } catch ( err ) { + // IE8 can throw on Object.defineProperty + obj[key] = val; + } + }; + }( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.notification.css b/resources/mediawiki/mediawiki.notification.css index 9a7b651d..3aa358ac 100644 --- a/resources/mediawiki/mediawiki.notification.css +++ b/resources/mediawiki/mediawiki.notification.css @@ -2,15 +2,25 @@ * Stylesheet for mediawiki.notification module */ -#mw-notification-area { +.mw-notification-area { position: absolute; - top: 1em; - right: 1em; + top: 0; + right: 0; + padding: 1em 1em 0 0; width: 20em; line-height: 1.35; z-index: 10000; } +.mw-notification-area-floating { + position: fixed; +} + +* html .mw-notification-area-floating { + /* Make it at least 'absolute' in IE6 since 'fixed' is not supported */ + position: absolute; +} + .mw-notification { padding: 0.25em 1em; margin-bottom: 0.5em; diff --git a/resources/mediawiki/mediawiki.notification.js b/resources/mediawiki/mediawiki.notification.js index fd34e7ee..4ede8096 100644 --- a/resources/mediawiki/mediawiki.notification.js +++ b/resources/mediawiki/mediawiki.notification.js @@ -2,10 +2,10 @@ 'use strict'; var notification, - isPageReady = false, - preReadyNotifQueue = [], // The #mw-notification-area div that all notifications are contained inside. - $area = null; + $area, + isPageReady = false, + preReadyNotifQueue = []; /** * Creates a Notification object for 1 message. @@ -350,7 +350,9 @@ * @ignore */ function init() { - $area = $( '<div id="mw-notification-area"></div>' ) + var offset, $window = $( window ); + + $area = $( '<div id="mw-notification-area" class="mw-notification-area mw-notification-area-layout"></div>' ) // Pause auto-hide timers when the mouse is in the notification area. .on( { mouseenter: notification.pause, @@ -371,6 +373,19 @@ // Prepend the notification area to the content area and save it's object. mw.util.$content.prepend( $area ); + offset = $area.offset(); + + function updateAreaMode() { + var isFloating = $window.scrollTop() > offset.top; + $area + .toggleClass( 'mw-notification-area-floating', isFloating ) + .toggleClass( 'mw-notification-area-layout', !isFloating ); + } + + $window.on( 'scroll', updateAreaMode ); + + // Initial mode + updateAreaMode(); } /** @@ -411,6 +426,7 @@ * @param {HTMLElement|jQuery|mw.Message|string} message * @param {Object} options The options to use for the notification. * See #defaults for details. + * @return {Object} Object with a close function to close the notification */ notify: function ( message, options ) { var notif; @@ -423,6 +439,7 @@ } else { preReadyNotifQueue.push( notif ); } + return { close: $.proxy( notif.close, notif ) }; }, /** diff --git a/resources/mediawiki/mediawiki.notify.js b/resources/mediawiki/mediawiki.notify.js index 83d95b61..743d6517 100644 --- a/resources/mediawiki/mediawiki.notify.js +++ b/resources/mediawiki/mediawiki.notify.js @@ -1,22 +1,23 @@ /** * @class mw.plugin.notify */ -( function ( mw ) { +( function ( mw, $ ) { 'use strict'; /** * @see mw.notification#notify * @param message * @param options + * @return {jQuery.Promise} */ mw.notify = function ( message, options ) { + var d = $.Deferred(); // Don't bother loading the whole notification system if we never use it. mw.loader.using( 'mediawiki.notification', function () { - // Don't bother calling mw.loader.using a second time after we've already loaded mw.notification. - mw.notify = mw.notification.notify; // Call notify with the notification the user requested of us. - mw.notify( message, options ); - } ); + d.resolve( mw.notification.notify( message, options ) ); + }, d.reject ); + return d.promise(); }; /** @@ -24,4 +25,4 @@ * @mixins mw.plugin.notify */ -}( mediaWiki ) ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js index 2bc7cea9..7f078626 100644 --- a/resources/mediawiki/mediawiki.searchSuggest.js +++ b/resources/mediawiki/mediawiki.searchSuggest.js @@ -2,7 +2,7 @@ * Add search suggestions to the search form. */ ( function ( mw, $ ) { - $( document ).ready( function ( $ ) { + $( function () { var map, resultRenderCache, searchboxesSelectors, // Region where the suggestions box will appear directly below // (using the same width). Can be a container element or the input @@ -130,8 +130,6 @@ searchboxesSelectors = [ // Primary searchbox on every page in standard skins '#searchInput', - // Secondary searchbox in legacy skins (LegacyTemplate::searchForm uses id "searchInput + unique id") - '#searchInput2', // Special:Search '#powerSearchText', '#searchText', @@ -141,36 +139,27 @@ $( searchboxesSelectors.join(', ') ) .suggestions( { fetch: function ( query ) { - var $el, jqXhr; + var $el; if ( query.length !== 0 ) { - $el = $(this); - jqXhr = $.ajax( { - url: mw.util.wikiScript( 'api' ), - data: { - format: 'json', - action: 'opensearch', - search: query, - namespace: 0, - suggest: '' - }, - dataType: 'json', - success: function ( data ) { - if ( $.isArray( data ) && data.length ) { - $el.suggestions( 'suggestions', data[1] ); - } - } - }); - $el.data( 'request', jqXhr ); + $el = $( this ); + $el.data( 'request', ( new mw.Api() ).get( { + action: 'opensearch', + search: query, + namespace: 0, + suggest: '' + } ).done( function ( data ) { + $el.suggestions( 'suggestions', data[1] ); + } ) ); } }, cancel: function () { - var jqXhr = $(this).data( 'request' ); + var apiPromise = $( this ).data( 'request' ); // If the delay setting has caused the fetch to have not even happened - // yet, the jqXHR object will have never been set. - if ( jqXhr && $.isFunction( jqXhr.abort ) ) { - jqXhr.abort(); - $(this).removeData( 'request' ); + // yet, the apiPromise object will have never been set. + if ( apiPromise && $.isFunction( apiPromise.abort ) ) { + apiPromise.abort(); + $( this ).removeData( 'request' ); } }, result: { @@ -196,11 +185,6 @@ return; } - // Placeholder text for search box - $searchInput - .attr( 'placeholder', mw.msg( 'searchsuggest-search' ) ) - .placeholder(); - // Special suggestions functionality for skin-provided search box $searchInput.suggestions( { result: { diff --git a/resources/mediawiki/mediawiki.user.js b/resources/mediawiki/mediawiki.user.js index e0329597..3e375fb6 100644 --- a/resources/mediawiki/mediawiki.user.js +++ b/resources/mediawiki/mediawiki.user.js @@ -1,67 +1,60 @@ -/* - * Implementation for mediaWiki.user +/** + * @class mw.user + * @singleton */ - ( function ( mw, $ ) { + var user, + callbacks = {}, + // Extend the skeleton mw.user from mediawiki.js + // This is kind of ugly but we're stuck with this for b/c reasons + options = mw.user.options || new mw.Map(), + tokens = mw.user.tokens || new mw.Map(); /** - * User object + * Get the current user's groups or rights + * + * @private + * @param {string} info One of 'groups' or 'rights' + * @param {Function} callback */ - function User( options, tokens ) { - var user, callbacks; - - /* Private Members */ - - user = this; - callbacks = {}; - - /** - * Gets the current user's groups or rights. - * @param {String} info: One of 'groups' or 'rights'. - * @param {Function} callback - */ - function getUserInfo( info, callback ) { - var api; - if ( callbacks[info] ) { - callbacks[info].add( callback ); - return; - } - callbacks.rights = $.Callbacks('once memory'); - callbacks.groups = $.Callbacks('once memory'); + function getUserInfo( info, callback ) { + var api; + if ( callbacks[info] ) { callbacks[info].add( callback ); - api = new mw.Api(); - api.get( { - action: 'query', - meta: 'userinfo', - uiprop: 'rights|groups' - } ).always( function ( data ) { - var rights, groups; - if ( data.query && data.query.userinfo ) { - rights = data.query.userinfo.rights; - groups = data.query.userinfo.groups; - } - callbacks.rights.fire( rights || [] ); - callbacks.groups.fire( groups || [] ); - } ); + return; } + callbacks.rights = $.Callbacks('once memory'); + callbacks.groups = $.Callbacks('once memory'); + callbacks[info].add( callback ); + api = new mw.Api(); + api.get( { + action: 'query', + meta: 'userinfo', + uiprop: 'rights|groups' + } ).always( function ( data ) { + var rights, groups; + if ( data.query && data.query.userinfo ) { + rights = data.query.userinfo.rights; + groups = data.query.userinfo.groups; + } + callbacks.rights.fire( rights || [] ); + callbacks.groups.fire( groups || [] ); + } ); + } - /* Public Members */ - - this.options = options || new mw.Map(); - - this.tokens = tokens || new mw.Map(); - - /* Public Methods */ + mw.user = user = { + options: options, + tokens: tokens, /** - * Generates a random user session ID (32 alpha-numeric characters). + * Generate a random user session ID (32 alpha-numeric characters) * * 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. * - * @return String: Random set of 32 alpha-numeric characters + * @return {string} Random set of 32 alpha-numeric characters */ - this.generateRandomSessionId = function () { + generateRandomSessionId: function () { var i, r, id = '', seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -70,33 +63,45 @@ id += seed.substring( r, r + 1 ); } return id; - }; + }, + + /** + * 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 ); + }, /** - * Gets the current user's name. + * Get the current user's name * - * @return Mixed: User name string or null if users is anonymous + * @return {string|null} User name string or null if user is anonymous */ - this.getName = function () { + getName: function () { return mw.config.get( 'wgUserName' ); - }; + }, /** - * @deprecated since 1.20 use mw.user.getName() instead + * @inheritdoc #getName + * @deprecated since 1.20 use #getName instead */ - this.name = function () { - return this.getName(); - }; + name: function () { + return user.getName(); + }, /** - * Get date user registered, if available. + * Get date user registered, if available * - * @return {Date|false|null} date user registered, or false for anonymous users, or + * @return {Date|boolean|null} Date user registered, or false for anonymous users, or * null when data is not available */ - this.getRegistration = function () { + getRegistration: function () { var registration = mw.config.get( 'wgUserRegistration' ); - if ( this.isAnon() ) { + if ( user.isAnon() ) { return false; } else if ( registration === null ) { // Information may not be available if they signed up before @@ -105,110 +110,109 @@ } else { return new Date( registration ); } - }; + }, /** - * Checks if the current user is anonymous. + * Whether the current user is anonymous * - * @return Boolean + * @return {boolean} */ - this.isAnon = function () { + isAnon: function () { return user.getName() === null; - }; + }, /** - * @deprecated since 1.20 use mw.user.isAnon() instead + * @inheritdoc #isAnon + * @deprecated since 1.20 use #isAnon instead */ - this.anonymous = function () { + anonymous: function () { return user.isAnon(); - }; + }, /** - * Gets a random session ID automatically generated and kept in a cookie. + * Get an automatically generated random ID (stored in a session cookie) * * This ID is ephemeral for everyone, staying in their browser only until they close * their browser. * - * @return String: User name or random session ID + * @return {string} Random session ID */ - this.sessionId = function () { + sessionId: function () { var sessionId = $.cookie( 'mediaWiki.user.sessionId' ); - if ( typeof sessionId === 'undefined' || sessionId === null ) { + if ( sessionId === undefined || sessionId === null ) { sessionId = user.generateRandomSessionId(); - $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': null, 'path': '/' } ); + $.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } ); } return sessionId; - }; + }, /** - * Gets the current user's name or the session ID + * Get the current user's name or the session ID * - * @return String: User name or random session ID + * Not to be confused with #getId. + * + * @return {string} User name or random session ID */ - this.id = function() { - var name = user.getName(); - if ( name ) { - return name; - } - return user.sessionId(); - }; + id: function () { + return user.getName() || user.sessionId(); + }, /** - * Gets the user's bucket, placing them in one at random based on set odds if needed. - * - * @param key String: Name of bucket - * @param options Object: Bucket configuration options - * @param options.buckets Object: List of bucket-name/relative-probability pairs (required, - * must have at least one pair) - * @param options.version Number: Version of bucket test, changing this forces rebucketing - * (optional, default: 0) - * @param options.tracked Boolean: Track the event of bucketing through the API module of - * the ClickTracking extension (optional, default: false) - * @param options.expires Number: Length of time (in days) until the user gets rebucketed - * (optional, default: 30) - * @return String: Bucket name - the randomly chosen key of the options.buckets object + * Get the user's bucket (place them in one if not done already) * - * @example * mw.user.bucket( 'test', { - * 'buckets': { 'ignored': 50, 'control': 25, 'test': 25 }, - * 'version': 1, - * 'tracked': true, - * 'expires': 7 + * buckets: { ignored: 50, control: 25, test: 25 }, + * version: 1, + * expires: 7 * } ); + * + * @param {string} key Name of bucket + * @param {Object} options Bucket configuration options + * @param {Object} options.buckets List of bucket-name/relative-probability pairs (required, + * must have at least one pair) + * @param {number} [options.version=0] Version of bucket test, changing this forces + * rebucketing + * @param {number} [options.expires=30] Length of time (in days) until the user gets + * rebucketed + * @return {string} Bucket name - the randomly chosen key of the `options.buckets` object */ - this.bucket = function ( key, options ) { + bucket: function ( key, options ) { var cookie, parts, version, bucket, range, k, rand, total; options = $.extend( { buckets: {}, version: 0, - tracked: false, expires: 30 }, options || {} ); cookie = $.cookie( 'mediaWiki.user.bucket:' + key ); // Bucket information is stored as 2 integers, together as version:bucket like: "1:2" - if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) > 0 ) { + if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) !== -1 ) { parts = cookie.split( ':' ); if ( parts.length > 1 && Number( parts[0] ) === options.version ) { version = Number( parts[0] ); bucket = String( parts[1] ); } } + if ( bucket === undefined ) { if ( !$.isPlainObject( options.buckets ) ) { - throw 'Invalid buckets error. Object expected for options.buckets.'; + throw new Error( 'Invalid bucket. Object expected for options.buckets.' ); } + version = Number( options.version ); + // Find range range = 0; for ( k in options.buckets ) { range += options.buckets[k]; } + // Select random value within range rand = Math.random() * range; + // Determine which bucket the value landed in total = 0; for ( k in options.buckets ) { @@ -218,39 +222,34 @@ break; } } - if ( options.tracked ) { - mw.loader.using( 'jquery.clickTracking', function () { - $.trackAction( - 'mediaWiki.user.bucket:' + key + '@' + version + ':' + bucket - ); - } ); - } + $.cookie( 'mediaWiki.user.bucket:' + key, version + ':' + bucket, - { 'path': '/', 'expires': Number( options.expires ) } + { path: '/', expires: Number( options.expires ) } ); } + return bucket; - }; + }, /** - * Gets the current user's groups. + * Get the current user's groups + * + * @param {Function} callback */ - this.getGroups = function ( callback ) { + getGroups: function ( callback ) { getUserInfo( 'groups', callback ); - }; + }, /** - * Gets the current user's rights. + * Get the current user's rights + * + * @param {Function} callback */ - this.getRights = function ( callback ) { + getRights: function ( callback ) { getUserInfo( 'rights', callback ); - }; - } - - // Extend the skeleton mw.user from mediawiki.js - // This is kind of ugly but we're stuck with this for b/c reasons - mw.user = new User( mw.user.options, mw.user.tokens ); + } + }; }( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.util.js b/resources/mediawiki/mediawiki.util.js index 5211b0d0..7383df2d 100644 --- a/resources/mediawiki/mediawiki.util.js +++ b/resources/mediawiki/mediawiki.util.js @@ -13,7 +13,7 @@ * (don't call before document ready) */ init: function () { - var profile, $tocTitle, $tocToggleLink, hideTocCookie; + var profile; /* Set tooltipAccessKeyPrefix */ profile = $.client.profile(); @@ -53,8 +53,9 @@ || profile.name === 'konqueror' ) ) { util.tooltipAccessKeyPrefix = 'ctrl-'; - // Firefox 2.x and later - } else if ( profile.name === 'firefox' && profile.versionBase > '1' ) { + // Firefox/Iceweasel 2.x and later + } else if ( ( profile.name === 'firefox' || profile.name === 'iceweasel' ) + && profile.versionBase > '1' ) { util.tooltipAccessKeyPrefix = 'alt-shift-'; } @@ -105,29 +106,32 @@ } )(); // Table of contents toggle - $tocTitle = $( '#toctitle' ); - $tocToggleLink = $( '#togglelink' ); - // Only add it if there is a TOC and there is no toggle added already - if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) { - hideTocCookie = $.cookie( 'mw_hidetoc' ); + mw.hook( 'wikipage.content' ).add( function () { + var $tocTitle, $tocToggleLink, hideTocCookie; + $tocTitle = $( '#toctitle' ); + $tocToggleLink = $( '#togglelink' ); + // Only add it if there is a TOC and there is no toggle added already + if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) { + hideTocCookie = $.cookie( 'mw_hidetoc' ); $tocToggleLink = $( '<a href="#" class="internal" id="togglelink"></a>' ) .text( mw.msg( 'hidetoc' ) ) .click( function ( e ) { e.preventDefault(); util.toggleToc( $(this) ); } ); - $tocTitle.append( - $tocToggleLink - .wrap( '<span class="toctoggle"></span>' ) - .parent() - .prepend( ' [' ) - .append( '] ' ) - ); - - if ( hideTocCookie === '1' ) { - util.toggleToc( $tocToggleLink ); + $tocTitle.append( + $tocToggleLink + .wrap( '<span class="toctoggle"></span>' ) + .parent() + .prepend( ' [' ) + .append( '] ' ) + ); + + if ( hideTocCookie === '1' ) { + util.toggleToc( $tocToggleLink ); + } } - } + } ); }, /* Main body */ @@ -160,11 +164,18 @@ * Get the link to a page name (relative to `wgServer`), * * @param {string} str Page name to get the link for. + * @param {Object} params A mapping of query parameter names to values, + * e.g. { action: 'edit' }. Optional. * @return {string} Location for a page with name of `str` or boolean false on error. */ - wikiGetlink: function ( str ) { - return mw.config.get( 'wgArticlePath' ).replace( '$1', + getUrl: function ( str, params ) { + var url = mw.config.get( 'wgArticlePath' ).replace( '$1', util.wikiUrlencode( typeof str === 'string' ? str : mw.config.get( 'wgPageName' ) ) ); + if ( params && !$.isEmptyObject( params ) ) { + url += url.indexOf( '?' ) !== -1 ? '&' : '?'; + url += $.param( params ); + } + return url; }, /** @@ -251,7 +262,7 @@ * Returns null if not found. * * @param {string} param The parameter name. - * @param {string} [url] URL to search through. + * @param {string} [url=document.location.href] URL to search through, defaulting to the current document's URL. * @return {Mixed} Parameter value or null. */ getParamValue: function ( param, url ) { @@ -279,8 +290,17 @@ /** * @property {RegExp} * Regex to match accesskey tooltips. + * + * Should match: + * + * - "ctrl-option-" + * - "alt-shift-" + * - "ctrl-alt-" + * - "ctrl-" + * + * The accesskey is matched in group $6. */ - tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/, + tooltipAccessKeyRegexp: /\[(ctrl-)?(option-)?(alt-)?(shift-)?(esc-)?(.)\]$/, /** * Add the appropriate prefix to the accesskey shown in the tooltip. @@ -301,9 +321,9 @@ } $nodes.attr( 'title', function ( i, val ) { - if ( val && util.tooltipAccessKeyRegexp.exec( val ) ) { + if ( val && util.tooltipAccessKeyRegexp.test( val ) ) { return val.replace( util.tooltipAccessKeyRegexp, - '[' + util.tooltipAccessKeyPrefix + '$5]' ); + '[' + util.tooltipAccessKeyPrefix + '$6]' ); } return val; } ); @@ -364,87 +384,86 @@ $link.attr( 'title', tooltip ); } - // Some skins don't have any portlets - // just add it to the bottom of their 'sidebar' element as a fallback - switch ( mw.config.get( 'skin' ) ) { - case 'standard': - $( '#quickbar' ).append( $link.after( '<br/>' ) ); - return $link[0]; - case 'nostalgia': - $( '#searchform' ).before( $link ).before( ' | ' ); - return $link[0]; - default: // Skins like chick, modern, monobook, myskin, simple, vector... - - // 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 ); + // 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 ) { + // If it didn't have an unordered list yet, create it + if ( $ul.length === 0 ) { - $ul = $( '<ul>' ); + $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; + // 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' ); + // 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(); - } + // 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 ); - } + // Implement the properties passed to the function + if ( id ) { + $item.attr( 'id', id ); + } + + if ( tooltip ) { + // Trim any existing accesskey hint and the trailing space + tooltip = $.trim( tooltip.replace( util.tooltipAccessKeyRegexp, '' ) ); if ( accesskey ) { - $link.attr( 'accesskey', accesskey ); tooltip += ' [' + accesskey + ']'; - $link.attr( 'title', tooltip ); } - if ( accesskey && tooltip ) { + $link.attr( 'title', tooltip ); + if ( accesskey ) { util.updateTooltipAccessKeys( $link ); } + } - // Where to put our node ? - // - nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js) - if ( nextnode && nextnode.parentNode === $ul[0] ) { - $(nextnode).before( $item ); - - // - nextnode is a CSS selector for jQuery - } else if ( typeof nextnode === 'string' && $ul.find( nextnode ).length !== 0 ) { - $ul.find( nextnode ).eq( 0 ).before( $item ); + if ( accesskey ) { + $link.attr( 'accesskey', accesskey ); + } - // If the jQuery selector isn't found within the <ul>, - // or if nextnode was invalid or not passed at all, - // then just append it at the end of the <ul> (this is the default behavior) - } else { + if ( nextnode ) { + if ( nextnode.nodeType || typeof nextnode === 'string' ) { + // nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js) + // or nextnode is a CSS selector for jQuery + nextnode = $ul.find( nextnode ); + } else if ( !nextnode.jquery || ( nextnode.length && nextnode[0].parentNode !== $ul[0] ) ) { + // Fallback $ul.append( $item ); + return $item[0]; } + if ( nextnode.length === 1 ) { + // nextnode is a jQuery object that represents exactly one element + nextnode.before( $item ); + return $item[0]; + } + } + // Fallback (this is the default behavior) + $ul.append( $item ); + return $item[0]; - return $item[0]; - } }, /** @@ -454,7 +473,7 @@ * * @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. - * @deprecated Use mw#notify + * @deprecated since 1.20 Use mw#notify */ jsMessage: function ( message ) { if ( !arguments.length || message === '' || message === null ) { @@ -593,6 +612,13 @@ } }; + /** + * @method wikiGetlink + * @inheritdoc #getUrl + * @deprecated since 1.23 Use #getUrl instead. + */ + mw.log.deprecate( util, 'wikiGetlink', util.getUrl, 'Use mw.util.getUrl instead.' ); + mw.util = util; }( mediaWiki, jQuery ) ); |