diff options
Diffstat (limited to 'resources/mediawiki')
-rw-r--r-- | resources/mediawiki/mediawiki.Title.js | 104 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.Uri.js | 103 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.css | 1 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.js | 68 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.feedback.js | 182 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.htmlform.js | 20 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.js | 411 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.peg | 4 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.js | 751 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.log.js | 11 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.notification.css | 26 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.notification.js | 480 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.notify.js | 20 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.searchSuggest.js | 166 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.user.js | 128 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.util.js | 171 |
16 files changed, 1850 insertions, 796 deletions
diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index 8d7996cb..33cca585 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -7,7 +7,7 @@ * * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink */ -( function( $ ) { +( function ( mw, $ ) { /* Local space */ @@ -20,19 +20,25 @@ * @param namespace {Number} (optional) Namespace id. If given, title will be taken as-is. * @return {Title} this */ -var Title = function( title, namespace ) { - this._ns = 0; // integer namespace id - this._name = null; // name in canonical 'database' form - this._ext = null; // extension + 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 ); + this.ns = fixNsId( namespace ); } else if ( arguments.length === 1 ) { setAll( this, title ); } return this; - }, + } + +var + /** + * Public methods (defined later) + */ + fn, /** * Strip some illegal chars: control chars, colon, less than, greater than, @@ -41,7 +47,7 @@ var Title = function( title, namespace ) { * @param s {String} * @return {String} */ - clean = function( s ) { + clean = function ( s ) { if ( s !== undefined ) { return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' ); } @@ -63,14 +69,14 @@ var Title = function( title, namespace ) { /** * Sanitize name. */ - fixName = function( s ) { + fixName = function ( s ) { return clean( $.trim( s ) ); }, /** * Sanitize name. */ - fixExt = function( s ) { + fixExt = function ( s ) { return clean( s ); }, @@ -79,7 +85,7 @@ var Title = function( title, namespace ) { * @param id {Number} Namespace id. * @return {Number|Boolean} The id as-is or boolean false if invalid. */ - fixNsId = function( id ) { + 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()]; @@ -98,9 +104,13 @@ var Title = function( title, namespace ) { * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). * @return {Number|Boolean} Namespace id or boolean false if unrecognized. */ - getNsIdByName = function( ns ) { - // toLowerCase throws exception on null/undefined. Return early. - if ( ns == null ) { + 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' ) { return false; } ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize @@ -119,22 +129,22 @@ var Title = function( title, namespace ) { * @param raw {String} * @return {mw.Title} */ - setAll = function( title, s ) { + 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{1,5}))?$/ ), + var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ), ns_match = getNsIdByName( matches[1] ); // Namespace must be valid, and title must be a non-empty string. if ( ns_match && typeof matches[2] === 'string' && matches[2] !== '' ) { - title._ns = ns_match; - title._name = fixName( matches[2] ); + title.ns = ns_match; + title.name = fixName( matches[2] ); if ( typeof matches[3] === 'string' && matches[3] !== '' ) { - title._ext = fixExt( matches[3] ); + title.ext = fixExt( matches[3] ); } } else { // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace. - title._ns = 0; + title.ns = 0; setNameAndExtension( title, s ); } return title; @@ -147,16 +157,16 @@ var Title = function( title, namespace ) { * @param raw {String} * @return {mw.Title} */ - setNameAndExtension = function( title, raw ) { + 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{1,5}))?$/ ); + var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ ); // Title must be a non-empty string. if ( typeof matches[1] === 'string' && matches[1] !== '' ) { - title._name = fixName( matches[1] ); + title.name = fixName( matches[1] ); if ( typeof matches[2] === 'string' && matches[2] !== '' ) { - title._ext = fixExt( matches[2] ); + title.ext = fixExt( matches[2] ); } } else { throw new Error( 'mw.Title: Could not parse title "' + raw + '"' ); @@ -172,7 +182,7 @@ var Title = function( title, namespace ) { * @param title {mixed} prefixed db-key name (string) or instance of Title * @return {mixed} Boolean true/false if the information is available. Otherwise null. */ - Title.exists = function( title ) { + Title.exists = function ( title ) { var type = $.type( title ), obj = Title.exist.pages, match; if ( type === 'string' ) { match = obj[title]; @@ -203,7 +213,7 @@ var Title = function( title, namespace ) { * @param state {Boolean} (optional) State of the given titles. Defaults to true. * @return {Boolean} */ - set: function( titles, state ) { + set: function ( titles, state ) { titles = $.isArray( titles ) ? titles : [titles]; state = state === undefined ? true : !!state; var pages = this.pages, i, len = titles.length; @@ -216,15 +226,15 @@ var Title = function( title, namespace ) { /* Public methods */ - var fn = { + fn = { constructor: Title, /** * Get the namespace number. * @return {Number} */ - getNamespaceId: function(){ - return this._ns; + getNamespaceId: function (){ + return this.ns; }, /** @@ -232,19 +242,19 @@ var Title = function( title, namespace ) { * 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 mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); }, /** * The name, like "Foo_bar" * @return {String} */ - getName: function() { - if ( $.inArray( this._ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { - return this._name; + getName: function () { + if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this.name; } else { - return $.ucFirst( this._name ); + return $.ucFirst( this.name ); } }, @@ -252,7 +262,7 @@ var Title = function( title, namespace ) { * The name, like "Foo bar" * @return {String} */ - getNameText: function() { + getNameText: function () { return text( this.getName() ); }, @@ -260,7 +270,7 @@ var Title = function( title, namespace ) { * Get full name in prefixed DB form, like File:Foo_bar.jpg, * most useful for API calls, anything that must identify the "title". */ - getPrefixedDb: function() { + getPrefixedDb: function () { return this.getNamespacePrefix() + this.getMain(); }, @@ -268,7 +278,7 @@ var Title = function( title, namespace ) { * Get full name in text form, like "File:Foo bar.jpg". * @return {String} */ - getPrefixedText: function() { + getPrefixedText: function () { return text( this.getPrefixedDb() ); }, @@ -276,7 +286,7 @@ var Title = function( title, namespace ) { * The main title (without namespace), like "Foo_bar.jpg" * @return {String} */ - getMain: function() { + getMain: function () { return this.getName() + this.getDotExtension(); }, @@ -284,7 +294,7 @@ var Title = function( title, namespace ) { * The "text" form, like "Foo bar.jpg" * @return {String} */ - getMainText: function() { + getMainText: function () { return text( this.getMain() ); }, @@ -292,23 +302,23 @@ var Title = function( title, namespace ) { * Get the extension (returns null if there was none) * @return {String|null} extension */ - getExtension: function() { - return this._ext; + getExtension: function () { + return this.ext; }, /** * Convenience method: return string like ".jpg", or "" if no extension * @return {String} */ - getDotExtension: function() { - return this._ext === null ? '' : '.' + this._ext; + getDotExtension: function () { + return this.ext === null ? '' : '.' + this.ext; }, /** * Return the URL to this title * @return {String} */ - getUrl: function() { + getUrl: function () { return mw.util.wikiGetlink( this.toString() ); }, @@ -316,7 +326,7 @@ var Title = function( title, namespace ) { * Whether this title exists on the wiki. * @return {mixed} Boolean true/false if the information is available. Otherwise null. */ - exists: function() { + exists: function () { return Title.exists( this ); } }; @@ -331,4 +341,4 @@ var Title = function( title, namespace ) { // Expose mw.Title = Title; -})(jQuery); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index 26fdfa9e..bd12b214 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -56,7 +56,7 @@ * */ -( function( $, mw ) { +( function ( mw, $ ) { /** * Function that's useful when constructing the URI string -- we frequently encounter the pattern of @@ -70,9 +70,8 @@ function cat( pre, val, post, raw ) { if ( val === undefined || val === null || val === '' ) { return ''; - } else { - return pre + ( raw ? val : mw.Uri.encode( val ) ) + post; } + return pre + ( raw ? val : mw.Uri.encode( val ) ) + post; } // Regular expressions to parse many common URIs. @@ -98,13 +97,16 @@ * We use a factory to inject a document location, for relative URLs, including protocol-relative URLs. * so the library is still testable & purely functional. */ - mw.UriRelative = function( documentLocation ) { + mw.UriRelative = function ( documentLocation ) { + var defaultUri; /** * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse. * @constructor * @param {Object|String} URI string, or an Object with appropriate properties (especially another URI object to clone). * Object must have non-blank 'protocol', 'host', and 'path' properties. + * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created + * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core). * @param {Object|Boolean} Object with options, or (backwards compatibility) a boolean for strictMode * - strictMode {Boolean} Trigger strict mode parsing of the url. Default: false * - overrideKeys {Boolean} Wether to let duplicate query parameters override eachother (true) or automagically @@ -117,25 +119,48 @@ overrideKeys: false }, options ); - if ( uri !== undefined && uri !== null || uri !== '' ) { + if ( uri !== undefined && uri !== null && uri !== '' ) { if ( typeof uri === 'string' ) { - this._parse( uri, options ); + this.parse( uri, options ); } else if ( typeof uri === 'object' ) { - var _this = this; - $.each( properties, function( i, property ) { - _this[property] = uri[property]; - } ); - if ( this.query === undefined ) { + // Copy data over from existing URI object + for ( var prop in uri ) { + // Only copy direct properties, not inherited ones + if ( uri.hasOwnProperty( prop ) ) { + // Deep copy object properties + if ( $.isArray( uri[prop] ) || $.isPlainObject( uri[prop] ) ) { + this[prop] = $.extend( true, {}, uri[prop] ); + } else { + this[prop] = uri[prop]; + } + } + } + if ( !this.query ) { this.query = {}; } } + } else { + // If we didn't get a URI in the constructor, use the default one. + return defaultUri.clone(); } // protocol-relative URLs if ( !this.protocol ) { - this.protocol = defaultProtocol; + this.protocol = defaultUri.protocol; + } + // No host given: + if ( !this.host ) { + this.host = defaultUri.host; + // port ? + if ( !this.port ) { + this.port = defaultUri.port; + } + } + if ( this.path && this.path.charAt( 0 ) !== '/' ) { + // A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot + // figure out whether the last path compoennt of defaultUri.path is a directory or a file. + throw new Error( 'Bad constructor arguments' ); } - if ( !( this.protocol && this.host && this.path ) ) { throw new Error( 'Bad constructor arguments' ); } @@ -147,7 +172,7 @@ * @param {String} string * @return {String} encoded for URI */ - Uri.encode = function( s ) { + Uri.encode = function ( s ) { return encodeURIComponent( s ) .replace( /!/g, '%21').replace( /'/g, '%27').replace( /\(/g, '%28') .replace( /\)/g, '%29').replace( /\*/g, '%2A') @@ -159,7 +184,7 @@ * @param {String} string encoded for URI * @return {String} decoded string */ - Uri.decode = function( s ) { + Uri.decode = function ( s ) { return decodeURIComponent( s.replace( /\+/g, '%20' ) ); }; @@ -171,23 +196,25 @@ * @param {Object} options * @return {Boolean} success */ - _parse: function( str, options ) { - var matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); - var uri = this; - $.each( properties, function( i, property ) { + parse: function ( str, options ) { + var q, + uri = this, + matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); + $.each( properties, function ( i, property ) { uri[ property ] = matches[ i+1 ]; } ); // uri.query starts out as the query string; we will parse it into key-val pairs then make // that object the "query" property. // we overwrite query in uri way to make cloning easier, it can use the same list of properties. - var q = {}; + q = {}; // using replace to iterate over a string if ( uri.query ) { uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { + var k, v; if ( $1 ) { - var k = Uri.decode( $1 ); - var v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); + k = Uri.decode( $1 ); + v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); // If overrideKeys, always (re)set top level value. // If not overrideKeys but this key wasn't set before, then we set it as well. @@ -215,7 +242,7 @@ * Returns user and password portion of a URI. * @return {String} */ - getUserInfo: function() { + getUserInfo: function () { return cat( '', this.user, cat( ':', this.password, '' ) ); }, @@ -223,7 +250,7 @@ * Gets host and port portion of a URI. * @return {String} */ - getHostPort: function() { + getHostPort: function () { return this.host + cat( ':', this.port, '' ); }, @@ -232,7 +259,7 @@ * In most real-world URLs, this is simply the hostname, but it is more general. * @return {String} */ - getAuthority: function() { + getAuthority: function () { return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); }, @@ -241,12 +268,12 @@ * Does not preserve the order of arguments passed into the URI. Does handle escaping. * @return {String} */ - getQueryString: function() { + getQueryString: function () { var args = []; - $.each( this.query, function( key, val ) { - var k = Uri.encode( key ); - var vals = val === null ? [ null ] : $.makeArray( val ); - $.each( vals, function( i, v ) { + $.each( this.query, function ( key, val ) { + var k = Uri.encode( key ), + vals = $.isArray( val ) ? val : [ val ]; + $.each( vals, function ( i, v ) { args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); } ); } ); @@ -257,7 +284,7 @@ * Returns everything after the authority section of the URI * @return {String} */ - getRelativePath: function() { + getRelativePath: function () { return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); }, @@ -265,7 +292,7 @@ * Gets the entire URI string. May not be precisely the same as input due to order of query arguments. * @return {String} the URI string */ - toString: function() { + toString: function () { return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); }, @@ -273,7 +300,7 @@ * Clone this URI * @return {Object} new URI object with same properties */ - clone: function() { + clone: function () { return new Uri( this ); }, @@ -282,20 +309,20 @@ * @param {Object} query parameters in key-val form to override or add * @return {Object} this URI object */ - extend: function( parameters ) { + extend: function ( parameters ) { $.extend( this.query, parameters ); return this; } }; - var defaultProtocol = ( new Uri( documentLocation ) ).protocol; + defaultUri = new Uri( documentLocation ); - return Uri; + return Uri; }; // if we are running in a browser, inject the current document location, for relative URLs - if ( document && document.location && document.location.href ) { + if ( document && document.location && document.location.href ) { mw.Uri = mw.UriRelative( document.location.href ); } -} )( jQuery, mediaWiki ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css index 923d4a47..149e1bff 100644 --- a/resources/mediawiki/mediawiki.debug.css +++ b/resources/mediawiki/mediawiki.debug.css @@ -6,7 +6,6 @@ } .mw-debug pre { - font-family: Monaco, "Consolas", "Lucida Console", "Courier New", monospace; font-size: 11px; padding: 0; margin: 0; diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js index a2bfbcbe..1ad1a623 100644 --- a/resources/mediawiki/mediawiki.debug.js +++ b/resources/mediawiki/mediawiki.debug.js @@ -5,12 +5,13 @@ * @since 1.19 */ -( function ( $, mw, undefined ) { -"use strict"; +( function ( mw, $ ) { + 'use strict'; - var hovzer = $.getFootHovzer(); + var debug, + hovzer = $.getFootHovzer(); - var debug = mw.Debug = { + debug = mw.Debug = { /** * Toolbar container element * @@ -93,7 +94,7 @@ * Constructs the HTML for the debugging toolbar */ buildHtml: function () { - var $container, $bits, panes, id; + var $container, $bits, panes, id, gitInfo; $container = $( '<div id="mw-debug-toolbar" class="mw-debug"></div>' ); @@ -106,9 +107,9 @@ * @return {jQuery} */ function bitDiv( id ) { - return $( '<div>' ).attr({ + return $( '<div>' ).prop({ id: 'mw-debug-' + id, - 'class': 'mw-debug-bit' + className: 'mw-debug-bit' }) .appendTo( $bits ); } @@ -122,8 +123,8 @@ */ function paneLabel( id, text ) { return $( '<a>' ) - .attr({ - 'class': 'mw-debug-panelabel', + .prop({ + className: 'mw-debug-panelabel', href: '#mw-debug-pane-' + id }) .text( text ); @@ -138,12 +139,12 @@ * @return {jQuery} */ function paneTriggerBitDiv( id, text, count ) { - if( count ) { + if ( count ) { text = text + ' (' + count + ')'; } - return $( '<div>' ).attr({ + return $( '<div>' ).prop({ id: 'mw-debug-' + id, - 'class': 'mw-debug-bit mw-debug-panelink' + className: 'mw-debug-bit mw-debug-panelink' }) .append( paneLabel( id, text ) ) .appendTo( $bits ); @@ -159,9 +160,24 @@ paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length ); + gitInfo = ''; + if ( this.data.gitRevision !== false ) { + gitInfo = '(' + this.data.gitRevision.substring( 0, 7 ) + ')'; + if ( this.data.gitViewUrl !== false ) { + gitInfo = $( '<a>' ) + .attr( 'href', this.data.gitViewUrl ) + .text( gitInfo ); + } + } + bitDiv( 'mwversion' ) - .append( $( '<a href="//www.mediawiki.org/"></a>' ).text( 'MediaWiki' ) ) - .append( ': ' + this.data.mwVersion ); + .append( $( '<a href="//www.mediawiki.org/">MediaWiki</a>' ) ) + .append( document.createTextNode( ': ' + this.data.mwVersion + ' ' ) ) + .append( gitInfo ); + + if ( this.data.gitBranch !== false ) { + bitDiv( 'gitbranch' ).text( 'Git branch: ' + this.data.gitBranch ); + } bitDiv( 'phpversion' ) .append( $( '<a href="//www.php.net/"></a>' ).text( 'PHP' ) ) @@ -191,8 +207,8 @@ } $( '<div>' ) - .attr({ - 'class': 'mw-debug-pane', + .prop({ + className: 'mw-debug-pane', id: 'mw-debug-pane-' + id }) .append( panes[id] ) @@ -210,9 +226,9 @@ $table = $( '<table id="mw-debug-console">' ); - $('<colgroup>').css( 'width', /*padding=*/20 + ( 10*/*fontSize*/11 ) ).appendTo( $table ); - $('<colgroup>').appendTo( $table ); - $('<colgroup>').css( 'width', 350 ).appendTo( $table ); + $( '<colgroup>' ).css( 'width', /* padding = */ 20 + ( 10 * /* fontSize = */ 11 ) ).appendTo( $table ); + $( '<colgroup>' ).appendTo( $table ); + $( '<colgroup>' ).css( 'width', 350 ).appendTo( $table ); entryTypeText = function( entryType ) { @@ -235,7 +251,7 @@ $( '<tr>' ) .append( $( '<td>' ) .text( entry.typeText ) - .attr( 'class', 'mw-debug-console-' + entry.type ) + .addClass( 'mw-debug-console-' + entry.type ) ) .append( $( '<td>' ).html( entry.msg ) ) .append( $( '<td>' ).text( entry.caller ) ) @@ -254,10 +270,10 @@ $table = $( '<table id="mw-debug-querylist"></table>' ); $( '<tr>' ) - .append( $('<th>#</th>').css( 'width', '4em' ) ) - .append( $('<th>SQL</th>') ) - .append( $('<th>Time</th>').css( 'width', '8em' ) ) - .append( $('<th>Call</th>').css( 'width', '18em' ) ) + .append( $( '<th>#</th>' ).css( 'width', '4em' ) ) + .append( $( '<th>SQL</th>' ) ) + .append( $( '<th>Time</th>' ).css( 'width', '8em' ) ) + .append( $( '<th>Call</th>' ).css( 'width', '18em' ) ) .appendTo( $table ); for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) { @@ -285,7 +301,7 @@ for ( i = 0, length = this.data.debugLog.length; i < length; i += 1 ) { line = this.data.debugLog[i]; $( '<li>' ) - .html( mw.html.escape( line ).replace( /\n/g, "<br />\n" ) ) + .html( mw.html.escape( line ).replace( /\n/g, '<br />\n' ) ) .appendTo( $list ); } @@ -348,4 +364,4 @@ } }; -} )( jQuery, mediaWiki ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js index 9a4a7298..634d02b1 100644 --- a/resources/mediawiki/mediawiki.feedback.js +++ b/resources/mediawiki/mediawiki.feedback.js @@ -22,22 +22,22 @@ * Minimal example in how to use it: * * var feedback = new mw.Feedback(); - * $( '#myButton' ).click( function() { feedback.launch(); } ); + * $( '#myButton' ).click( function () { feedback.launch(); } ); * * You can also launch the feedback form with a prefilled subject and body. * See the docs for the launch() method. */ -( function( mw, $, undefined ) { +( function ( mw, $ ) { /** * Thingy for collecting user feedback on a wiki page * @param {Array} options -- optional, all properties optional. - * api: {mw.Api} if omitted, will just create a standard API - * title: {mw.Title} the title of the page where you collect feedback. Defaults to "Feedback". - * dialogTitleMessageKey: {String} message key for the title of the dialog box - * bugsLink: {mw.Uri|String} url where bugs can be posted - * bugsListLink: {mw.Uri|String} url where bugs can be listed + * api: {mw.Api} if omitted, will just create a standard API + * title: {mw.Title} the title of the page where you collect feedback. Defaults to "Feedback". + * dialogTitleMessageKey: {String} message key for the title of the dialog box + * bugsLink: {mw.Uri|String} url where bugs can be posted + * bugsListLink: {mw.Uri|String} url where bugs can be listed */ - mw.Feedback = function( options ) { + mw.Feedback = function ( options ) { if ( options === undefined ) { options = {}; } @@ -67,64 +67,69 @@ }; mw.Feedback.prototype = { - setup: function() { - var _this = this; + setup: function () { + var fb = this; - var $feedbackPageLink = $( '<a></a>' ) - .attr( { 'href': _this.title.getUrl(), 'target': '_blank' } ) + var $feedbackPageLink = $( '<a>' ) + .attr( { 'href': fb.title.getUrl(), 'target': '_blank' } ) .css( { 'white-space': 'nowrap' } ); - var $bugNoteLink = $( '<a></a>' ).attr( { 'href': '#' } ).click( function() { _this.displayBugs(); } ); + var $bugNoteLink = $( '<a>' ).attr( { 'href': '#' } ).click( function () { + fb.displayBugs(); + } ); - var $bugsListLink = $( '<a></a>' ).attr( { 'href': _this.bugsListLink, 'target': '_blank' } ); + var $bugsListLink = $( '<a>' ).attr( { 'href': fb.bugsListLink, 'target': '_blank' } ); + // TODO: Use a stylesheet instead of these inline styles this.$dialog = - $( '<div style="position:relative;"></div>' ).append( + $( '<div style="position: relative;"></div>' ).append( $( '<div class="feedback-mode feedback-form"></div>' ).append( - $( '<small></small>' ).append( - $( '<p></p>' ).msg( + $( '<small>' ).append( + $( '<p>' ).msg( 'feedback-bugornote', $bugNoteLink, - _this.title.getNameText(), + fb.title.getNameText(), $feedbackPageLink.clone() ) ), - $( '<div style="margin-top:1em;"></div>' ).append( + $( '<div style="margin-top: 1em;"></div>' ).append( mw.msg( 'feedback-subject' ), - $( '<br/>' ), - $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width:99%;"/>' ) + $( '<br>' ), + $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width: 99%;"/>' ) ), - $( '<div style="margin-top:0.4em;"></div>' ).append( + $( '<div style="margin-top: 0.4em;"></div>' ).append( mw.msg( 'feedback-message' ), - $( '<br/>' ), - $( '<textarea name="message" class="feedback-message" style="width:99%;" rows="5" cols="60"></textarea>' ) + $( '<br>' ), + $( '<textarea name="message" class="feedback-message" style="width: 99%;" rows="5" cols="60"></textarea>' ) ) ), $( '<div class="feedback-mode feedback-bugs"></div>' ).append( $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink ) ), - $( '<div class="feedback-mode feedback-submitting" style="text-align:center;margin:3em 0;"></div>' ).append( + $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' ).append( mw.msg( 'feedback-adding' ), $( '<br/>' ), $( '<span class="feedback-spinner"></span>' ) ), - $( '<div class="feedback-mode feedback-thanks" style="text-align:center;margin:1em"></div>' ).msg( - 'feedback-thanks', _this.title.getNameText(), $feedbackPageLink.clone() + $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg( + 'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone() ), - $( '<div class="feedback-mode feedback-error" style="position:relative;"></div>' ).append( - $( '<div class="feedback-error-msg style="color:#990000;margin-top:0.4em;"></div>' ) + $( '<div class="feedback-mode feedback-error" style="position: relative;"></div>' ).append( + $( '<div class="feedback-error-msg style="color: #990000; margin-top: 0.4em;"></div>' ) ) ); // undo some damage from dialog css - this.$dialog.find( 'a' ).css( { 'color': '#0645ad' } ); + this.$dialog.find( 'a' ).css( { + color: '#0645ad' + } ); this.$dialog.dialog({ width: 500, autoOpen: false, title: mw.msg( this.dialogTitleMessageKey ), modal: true, - buttons: _this.buttons + buttons: fb.buttons }); this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get(0); @@ -132,98 +137,119 @@ }, - display: function( s ) { + display: function ( s ) { this.$dialog.dialog( { buttons:{} } ); // hide the buttons this.$dialog.find( '.feedback-mode' ).hide(); // hide everything this.$dialog.find( '.feedback-' + s ).show(); // show the desired div }, - displaySubmitting: function() { + displaySubmitting: function () { this.display( 'submitting' ); }, - displayBugs: function() { - var _this = this; + displayBugs: function () { + var fb = this; this.display( 'bugs' ); var bugsButtons = {}; - bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function() { window.open( _this.bugsLink, '_blank' ); }; - bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function() { _this.cancel(); }; - this.$dialog.dialog( { buttons: bugsButtons } ); + bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () { + window.open( fb.bugsLink, '_blank' ); + }; + bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function () { + fb.cancel(); + }; + this.$dialog.dialog( { + buttons: bugsButtons + } ); }, - displayThanks: function() { - var _this = this; + displayThanks: function () { + var fb = this; this.display( 'thanks' ); var closeButton = {}; - closeButton[ mw.msg( 'feedback-close' ) ] = function() { _this.$dialog.dialog( 'close' ); }; - this.$dialog.dialog( { buttons: closeButton } ); + closeButton[ mw.msg( 'feedback-close' ) ] = function () { + fb.$dialog.dialog( 'close' ); + }; + this.$dialog.dialog( { + buttons: closeButton + } ); }, /** * Display the feedback form * @param {Object} optional prefilled contents for the feedback form. Object with properties: - * subject: {String} - * message: {String} + * subject: {String} + * message: {String} */ - displayForm: function( contents ) { - var _this = this; - this.subjectInput.value = (contents && contents.subject) ? contents.subject : ''; - this.messageInput.value = (contents && contents.message) ? contents.message : ''; + displayForm: function ( contents ) { + var fb = this; + this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : ''; + this.messageInput.value = ( contents && contents.message ) ? contents.message : ''; this.display( 'form' ); // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized var formButtons = {}; - formButtons[ mw.msg( 'feedback-submit' ) ] = function() { _this.submit(); }; - formButtons[ mw.msg( 'feedback-cancel' ) ] = function() { _this.cancel(); }; + formButtons[ mw.msg( 'feedback-submit' ) ] = function () { + fb.submit(); + }; + formButtons[ mw.msg( 'feedback-cancel' ) ] = function () { + fb.cancel(); + }; this.$dialog.dialog( { buttons: formButtons } ); // put the buttons back }, - displayError: function( message ) { - var _this = this; + displayError: function ( message ) { + var fb = this; this.display( 'error' ); this.$dialog.find( '.feedback-error-msg' ).msg( message ); var closeButton = {}; - closeButton[ mw.msg( 'feedback-close' ) ] = function() { _this.$dialog.dialog( 'close' ); }; + closeButton[ mw.msg( 'feedback-close' ) ] = function () { + fb.$dialog.dialog( 'close' ); + }; this.$dialog.dialog( { buttons: closeButton } ); }, - cancel: function() { + cancel: function () { this.$dialog.dialog( 'close' ); }, - submit: function() { - var _this = this; - - // get the values to submit - var subject = this.subjectInput.value; - - var message = "<small>User agent: " + navigator.userAgent + "</small>\n\n" - + this.messageInput.value; - if ( message.indexOf( '~~~' ) == -1 ) { - message += " ~~~~"; - } - - this.displaySubmitting(); + submit: function () { + var subject, message, + fb = this; - var ok = function( result ) { + function ok( result ) { if ( result.edit !== undefined ) { if ( result.edit.result === 'Success' ) { - _this.displayThanks(); + fb.displayThanks(); } else { - _this.displayError( 'feedback-error1' ); // unknown API result + // unknown API result + fb.displayError( 'feedback-error1' ); } } else { - _this.displayError( 'feedback-error2' ); // edit failed + // edit failed + fb.displayError( 'feedback-error2' ); } - }; + } - var err = function( code, info ) { - _this.displayError( 'feedback-error3' ); // ajax request failed - }; + function err( code, info ) { + // ajax request failed + fb.displayError( 'feedback-error3' ); + } + + // Get the values to submit. + subject = this.subjectInput.value; + + // We used to include "mw.html.escape( navigator.userAgent )" but there are legal issues + // with posting this without their explicit consent + message = this.messageInput.value; + if ( message.indexOf( '~~~' ) === -1 ) { + message += ' ~~~~'; + } + + this.displaySubmitting(); this.api.newSection( this.title, subject, message, ok, err ); - }, // close submit button function + }, /** * Modify the display form, and then open it, focusing interface on the subject. @@ -231,7 +257,7 @@ * subject: {String} * message: {String} */ - launch: function( contents ) { + launch: function ( contents ) { this.displayForm( contents ); this.$dialog.dialog( 'open' ); this.subjectInput.focus(); @@ -239,4 +265,4 @@ }; -} )( window.mediaWiki, jQuery ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index 17a02cf4..a4753b99 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 ( $ ) { /** * jQuery plugin to fade or snap to visible state. @@ -9,7 +9,7 @@ * @param boolean instantToggle (optional) * @return jQuery */ -$.fn.goIn = function( instantToggle ) { +$.fn.goIn = function ( instantToggle ) { if ( instantToggle === true ) { return $(this).show(); } @@ -22,7 +22,7 @@ $.fn.goIn = function( instantToggle ) { * @param boolean instantToggle (optional) * @return jQuery */ -$.fn.goOut = function( instantToggle ) { +$.fn.goOut = function ( instantToggle ) { if ( instantToggle === true ) { return $(this).hide(); } @@ -31,24 +31,24 @@ $.fn.goOut = function( instantToggle ) { /** * Bind a function to the jQuery object via live(), and also immediately trigger - * the function on the objects with an 'instant' paramter set to true - * @param callback function taking one paramter, which is Bool true when the event + * the function on the objects with an 'instant' parameter set to true + * @param callback function taking one parameter, which is Bool true when the event * is called immediately, and the EventArgs object when triggered from an event */ -$.fn.liveAndTestAtStart = function( callback ){ +$.fn.liveAndTestAtStart = function ( callback ){ $(this) .live( 'change', callback ) - .each( function( index, element ){ + .each( function ( index, element ){ callback.call( this, true ); } ); }; // Document ready: -$( function() { +$( function () { // Animate the SelectOrOther fields, to only show the text field when // 'other' is selected. - $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function( instant ) { + $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); $other = $other.add( $other.siblings( 'br' ) ); if ( $(this).val() === 'other' ) { @@ -61,4 +61,4 @@ $( function() { }); -})( jQuery ); +}( jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 6c00bd15..86af31ff 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -1,22 +1,27 @@ /** - * Experimental advanced wikitext parser-emitter. - * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs - * - * @author neilk@wikimedia.org - */ - -( function( mw, $, undefined ) { - - mw.jqueryMsg = {}; +* Experimental advanced wikitext parser-emitter. +* See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs +* +* @author neilk@wikimedia.org +*/ +( function ( mw, $ ) { + var slice = Array.prototype.slice, + parserDefaults = { + magic : { + 'SITENAME' : mw.config.get( 'wgSiteName' ) + }, + messages : mw.messages, + language : mw.language + }; /** * 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} */ - function getFailableParserFn( options ) { - var parser = new mw.jqueryMsg.parser( options ); - /** + function getFailableParserFn( options ) { + var parser = new mw.jqueryMsg.parser( options ); + /** * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes. * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it. @@ -24,24 +29,23 @@ * @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements. * @return {jQuery} */ - return function( args ) { + return function ( args ) { var key = args[0]; - var argsArray = $.isArray( args[1] ) ? args[1] : $.makeArray( args ).slice( 1 ); - var escapedArgsArray = $.map( argsArray, function( arg ) { - return typeof arg === 'string' ? mw.html.escape( arg ) : arg; - } ); + var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); try { - return parser.parse( key, escapedArgsArray ); + return parser.parse( key, argsArray ); } catch ( e ) { - return $( '<span></span>' ).append( key + ': ' + e.message ); + return $( '<span>' ).append( key + ': ' + e.message ); } }; } + mw.jqueryMsg = {}; + /** - * Class method. + * Class method. * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements). - * e.g. + * e.g. * window.gM = mediaWiki.parser.getMessageFunction( options ); * $( 'p#headline' ).html( gM( 'hello-user', username ) ); * @@ -51,75 +55,68 @@ * @param {Array} parser options * @return {Function} function suitable for assigning to window.gM */ - mw.jqueryMsg.getMessageFunction = function( options ) { + mw.jqueryMsg.getMessageFunction = function ( options ) { var failableParserFn = getFailableParserFn( options ); - /** + /** * N.B. replacements are variadic arguments or an array in second parameter. In other words: - * somefunction(a, b, c, d) - * is equivalent to + * somefunction(a, b, c, d) + * is equivalent to * somefunction(a, [b, c, d]) * * @param {String} message key * @param {Array} optional replacements (can also specify variadically) * @return {String} rendered HTML as string */ - return function( /* key, replacements */ ) { + return function ( /* key, replacements */ ) { return failableParserFn( arguments ).html(); }; }; /** - * Class method. - * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to - * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links. - * e.g. + * Class method. + * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to + * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links. + * e.g. * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options ); - * var userlink = $( '<a>' ).click( function() { alert( "hello!!") } ); + * var userlink = $( '<a>' ).click( function () { alert( "hello!!") } ); * $( 'p#headline' ).msg( 'hello-user', userlink ); * * @param {Array} parser options * @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg */ - mw.jqueryMsg.getPlugin = function( options ) { + mw.jqueryMsg.getPlugin = function ( options ) { var failableParserFn = getFailableParserFn( options ); - /** + /** * N.B. replacements are variadic arguments or an array in second parameter. In other words: - * somefunction(a, b, c, d) - * is equivalent to + * somefunction(a, b, c, d) + * is equivalent to * somefunction(a, [b, c, d]) - * + * * We append to 'this', which in a jQuery plugin context will be the selected elements. * @param {String} message key * @param {Array} optional replacements (can also specify variadically) * @return {jQuery} this */ - return function( /* key, replacements */ ) { + return function ( /* key, replacements */ ) { var $target = this.empty(); - $.each( failableParserFn( arguments ).contents(), function( i, node ) { + $.each( failableParserFn( arguments ).contents(), function ( i, node ) { $target.append( node ); } ); return $target; }; }; - var parserDefaults = { - 'magic' : {}, - 'messages' : mw.messages, - 'language' : mw.language - }; - /** * The parser itself. * Describes an object, whose primary duty is to .parse() message keys. * @param {Array} options */ - mw.jqueryMsg.parser = function( options ) { + mw.jqueryMsg.parser = function ( options ) { this.settings = $.extend( {}, parserDefaults, options ); this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic ); }; mw.jqueryMsg.parser.prototype = { - // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical. // (This is why we would like to move this functionality server-side). astCache: {}, @@ -132,51 +129,46 @@ * @param {Array} replacements for $1, $2... $n * @return {jQuery} */ - parse: function( key, replacements ) { + parse: function ( key, replacements ) { return this.emitter.emit( this.getAst( key ), replacements ); }, - /** - * Fetch the message string associated with a key, return parsed structure. Memoized. - * Note that we pass '[' + key + ']' back for a missing message here. - * @param {String} key + * Fetch the message string associated with a key, return parsed structure. Memoized. + * Note that we pass '[' + key + ']' back for a missing message here. + * @param {String} key * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing */ - getAst: function( key ) { - if ( this.astCache[ key ] === undefined ) { + getAst: function ( key ) { + if ( this.astCache[ key ] === undefined ) { var wikiText = this.settings.messages.get( key ); if ( typeof wikiText !== 'string' ) { wikiText = "\\[" + key + "\\]"; } this.astCache[ key ] = this.wikiTextToAst( wikiText ); } - return this.astCache[ key ]; + return this.astCache[ key ]; }, - /* * Parses the input wikiText into an abstract syntax tree, essentially an s-expression. * * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already. * n.b. We want to move this functionality to the server. Nothing here is required to be on the client. - * + * * @param {String} message string wikitext * @throws Error * @return {Mixed} abstract syntax tree */ - wikiTextToAst: function( input ) { - - // Indicates current position in input as we parse through it. - // Shared among all parsing functions below. - var pos = 0; + wikiTextToAst: function ( input ) { + // Indicates current position in input as we parse through it. + // Shared among all parsing functions below. + var pos = 0; // ========================================================= // parsing combinators - could be a library on its own // ========================================================= - - - // Try parsers until one works, if none work return null + // Try parsers until one works, if none work return null function choice( ps ) { - return function() { + return function () { for ( var i = 0; i < ps.length; i++ ) { var result = ps[i](); if ( result !== null ) { @@ -186,27 +178,25 @@ return null; }; } - // try several ps in a row, all must succeed or return null // this is the only eager one function sequence( ps ) { var originalPos = pos; var result = []; - for ( var i = 0; i < ps.length; i++ ) { + for ( var i = 0; i < ps.length; i++ ) { var res = ps[i](); if ( res === null ) { pos = originalPos; return null; - } + } result.push( res ); } return result; } - // run the same parser over and over until it fails. // must succeed a minimum of n times or return null function nOrMore( n, p ) { - return function() { + return function () { var originalPos = pos; var result = []; var parsed = p(); @@ -217,26 +207,23 @@ if ( result.length < n ) { pos = originalPos; return null; - } + } return result; }; } - // There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null. // But using this as a combinator seems to cause problems when combined with nOrMore(). // May be some scoping issue function transform( p, fn ) { - return function() { + return function () { var result = p(); return result === null ? null : fn( result ); }; } - // Helpers -- just make ps out of simpler JS builtin types - - function makeStringParser( s ) { + function makeStringParser( s ) { var len = s.length; - return function() { + return function () { var result = null; if ( input.substr( pos, len ) === s ) { result = s; @@ -245,105 +232,87 @@ return result; }; } - function makeRegexParser( regex ) { - return function() { + return function () { var matches = input.substr( pos ).match( regex ); - if ( matches === null ) { + if ( matches === null ) { return null; - } + } pos += matches[0].length; return matches[0]; }; } - - /** - * =================================================================== + /** + * =================================================================== * General patterns above this line -- wikitext specific parsers below - * =================================================================== + * =================================================================== */ - // Parsing functions follow. All parsing functions work like this: // They don't accept any arguments. // Instead, they just operate non destructively on the string 'input' // As they can consume parts of the string, they advance the shared variable pos, // and return tokens (or whatever else they want to return). - // some things are defined as closures and other things as ordinary functions // converting everything to a closure makes it a lot harder to debug... errors pop up // 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 - - - var regularLiteral = makeRegexParser( /^[^{}[\]$\\]/ ); - var regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/); - var regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/); - + var regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); + var regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); + var regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); var backslash = makeStringParser( "\\" ); var anyCharacter = makeRegexParser( /^./ ); - function escapedLiteral() { var result = sequence( [ - backslash, + backslash, anyCharacter ] ); return result === null ? null : result[1]; } - var escapedOrLiteralWithoutSpace = choice( [ escapedLiteral, regularLiteralWithoutSpace ] ); - var escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] ); - - var escapedOrRegularLiteral = choice( [ + var escapedOrRegularLiteral = choice( [ escapedLiteral, regularLiteral ] ); - // Used to define "literals" without spaces, in space-delimited situations function literalWithoutSpace() { var result = nOrMore( 1, escapedOrLiteralWithoutSpace )(); return result === null ? null : result.join(''); } - - // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default + // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default // it is not a literal in the parameter function literalWithoutBar() { var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); return result === null ? null : result.join(''); } - function literal() { var result = nOrMore( 1, escapedOrRegularLiteral )(); return result === null ? null : result.join(''); } - - var whitespace = makeRegexParser( /^\s+/ ); + var whitespace = makeRegexParser( /^\s+/ ); var dollar = makeStringParser( '$' ); - var digits = makeRegexParser( /^\d+/ ); + var digits = makeRegexParser( /^\d+/ ); function replacement() { var result = sequence( [ dollar, digits ] ); - if ( result === null ) { + if ( result === null ) { return null; } return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; } - - var openExtlink = makeStringParser( '[' ); var closeExtlink = makeStringParser( ']' ); - // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed function extlink() { var result = null; @@ -359,10 +328,23 @@ } return result; } - + // this is the same as the above extlink, except that the url is being passed on as a parameter + function extLinkParam() { + var result = sequence( [ + openExtlink, + dollar, + digits, + whitespace, + expression, + closeExtlink + ] ); + if ( result === null ) { + return null; + } + return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; + } var openLink = makeStringParser( '[[' ); var closeLink = makeStringParser( ']]' ); - function link() { var result = null; var parsedResult = sequence( [ @@ -375,16 +357,14 @@ } return result; } - - var templateName = transform( + var templateName = transform( // see $wgLegalTitleChars // not allowing : due to the need to catch "PLURAL:$1" - makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+-]+/ ), - function( result ) { return result.toString(); } + makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ), + function ( result ) { return result.toString(); } ); - function templateParam() { - var result = sequence( [ + var result = sequence( [ pipe, nOrMore( 0, paramExpression ) ] ); @@ -395,9 +375,7 @@ // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw. return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0]; } - var pipe = makeStringParser( '|' ); - function templateWithReplacement() { var result = sequence( [ templateName, @@ -406,21 +384,29 @@ ] ); return result === null ? null : [ result[0], result[2] ]; } - + function templateWithOutReplacement() { + var result = sequence( [ + templateName, + colon, + paramExpression + ] ); + return result === null ? null : [ result[0], result[2] ]; + } var colon = makeStringParser(':'); - var templateContents = choice( [ - function() { + function () { var res = sequence( [ - templateWithReplacement, + // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}} + // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}} + choice( [ templateWithReplacement, templateWithOutReplacement ] ), nOrMore( 0, templateParam ) ] ); return res === null ? null : res[0].concat( res[1] ); }, - function() { + function () { var res = sequence( [ templateName, - nOrMore( 0, templateParam ) + nOrMore( 0, templateParam ) ] ); if ( res === null ) { return null; @@ -428,10 +414,8 @@ return [ res[0] ].concat( res[1] ); } ] ); - var openTemplate = makeStringParser('{{'); var closeTemplate = makeStringParser('}}'); - function template() { var result = sequence( [ openTemplate, @@ -440,31 +424,30 @@ ] ); return result === null ? null : result[1]; } - var nonWhitespaceExpression = choice( [ - template, + template, link, + extLinkParam, extlink, replacement, literalWithoutSpace ] ); - var paramExpression = choice( [ - template, + template, link, + extLinkParam, extlink, replacement, literalWithoutBar ] ); - - var expression = choice( [ + var expression = choice( [ template, link, + extLinkParam, extlink, replacement, - literal + literal ] ); - function start() { var result = nOrMore( 0, expression )(); if ( result === null ) { @@ -472,16 +455,13 @@ } return [ "CONCAT" ].concat( result ); } - // everything above this point is supposed to be stateless/static, but // I am deferring the work of turning it into prototypes & objects. It's quite fast enough - // finally let's do some actual work... - var result = start(); - + /* - * For success, the p must have gotten to the end of the input + * For success, the p must have gotten to the end of the input * and returned a non-null. * n.b. This is part of language infrastructure, so we do not throw an internationalizable message. */ @@ -490,20 +470,19 @@ } return result; } - - }; + }; /** * htmlEmitter - object which primarily exists to emit HTML from parser ASTs */ - mw.jqueryMsg.htmlEmitter = function( language, magic ) { + mw.jqueryMsg.htmlEmitter = function ( language, magic ) { this.language = language; - var _this = this; - - $.each( magic, function( key, val ) { - _this[ key.toLowerCase() ] = function() { return val; }; + var jmsg = this; + $.each( magic, function ( key, val ) { + jmsg[ key.toLowerCase() ] = function () { + return val; + }; } ); - /** * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.) * Walk entire node structure, applying replacements and template functions when appropriate @@ -511,23 +490,23 @@ * @param {Array} replacements for $1, $2, ... $n * @return {Mixed} single-string node or array of nodes suitable for jQuery appending */ - this.emit = function( node, replacements ) { + this.emit = function ( node, replacements ) { var ret = null; - var _this = this; - switch( typeof node ) { + var jmsg = this; + switch ( typeof node ) { case 'string': case 'number': ret = node; break; case 'object': // node is an array of nodes - var subnodes = $.map( node.slice( 1 ), function( n ) { - return _this.emit( n, replacements ); + var subnodes = $.map( node.slice( 1 ), function ( n ) { + return jmsg.emit( n, replacements ); } ); var operation = node[0].toLowerCase(); - if ( typeof _this[operation] === 'function' ) { - ret = _this[ operation ]( subnodes, replacements ); + if ( typeof jmsg[operation] === 'function' ) { + ret = jmsg[ operation ]( subnodes, replacements ); } else { - throw new Error( 'unknown operation "' + operation + '"' ); + throw new Error( 'Unknown operation "' + operation + '"' ); } break; case 'undefined': @@ -537,21 +516,18 @@ ret = ''; break; default: - throw new Error( 'unexpected type in AST: ' + typeof node ); + throw new Error( 'Unexpected type in AST: ' + typeof node ); } return ret; }; - }; - // For everything in input that follows double-open-curly braces, there should be an equivalent parser - // function. For instance {{PLURAL ... }} will be processed by 'plural'. + // function. For instance {{PLURAL ... }} will be processed by 'plural'. // If you have 'magic words' then configure the parser to have them upon creation. // // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to). // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on) mw.jqueryMsg.htmlEmitter.prototype = { - /** * Parsing has been applied depth-first we can assume that all nodes here are single nodes * Must return a single node to parents -- a jQuery with synthetic span @@ -559,23 +535,23 @@ * @param {Array} nodes - mixed, some single nodes, some arrays of nodes * @return {jQuery} */ - concat: function( nodes ) { - var span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' ); - $.each( nodes, function( i, node ) { + concat: function ( nodes ) { + var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' ); + $.each( nodes, function ( i, node ) { if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) { - $.each( node.contents(), function( j, childNode ) { - span.append( childNode ); + $.each( node.contents(), function ( j, childNode ) { + $span.append( childNode ); } ); } else { // strings, integers, anything else - span.append( node ); + $span.append( node ); } } ); - return span; + return $span; }, /** - * Return replacement of correct index, or string if unavailable. + * Return escaped replacement of correct index, or string if unavailable. * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ]. * if the specified parameter is not found return the same string * (e.g. "$99" -> parameter 98 -> not found -> return "$99" ) @@ -583,17 +559,29 @@ * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ - replace: function( nodes, replacements ) { + replace: function ( nodes, replacements ) { var index = parseInt( nodes[0], 10 ); - return index < replacements.length ? replacements[index] : '$' + ( index + 1 ); + + if ( index < replacements.length ) { + if ( typeof arg === 'string' ) { + // replacement is a string, escape it + return mw.html.escape( replacements[index] ); + } else { + // replacement is no string, don't touch! + return replacements[index]; + } + } else { + // index not found, fallback to displaying variable + return '$' + ( index + 1 ); + } }, - /** + /** * Transform wiki-link - * TODO unimplemented + * TODO unimplemented */ - wlink: function( nodes ) { - return "unimplemented"; + wlink: function ( nodes ) { + return 'unimplemented'; }, /** @@ -601,14 +589,14 @@ * If the href is a jQuery object, treat it as "enclosing" the link text. * ... function, treat it as the click handler * ... string, treat it as a URI - * TODO: throw an error if nodes.length > 2 ? + * TODO: throw an error if nodes.length > 2 ? * @param {Array} of two elements, {jQuery|Function|String} and {String} * @return {jQuery} */ - link: function( nodes ) { + link: function ( nodes ) { var arg = nodes[0]; var contents = nodes[1]; - var $el; + var $el; if ( arg instanceof jQuery ) { $el = arg; } else { @@ -619,19 +607,39 @@ $el.attr( 'href', arg.toString() ); } } - $el.append( contents ); + $el.append( contents ); return $el; }, /** + * This is basically use a combination of replace + 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. + * TODO throw error if nodes.length > 1 ? + * @param {Array} of one element, integer, n >= 0 + * @return {String} replacement + */ + linkparam: function ( nodes, replacements ) { + var replacement, + index = parseInt( nodes[0], 10 ); + if ( index < replacements.length) { + replacement = replacements[index]; + } else { + replacement = '$' + ( index + 1 ); + } + return this.link( [ replacement, nodes[1] ] ); + }, + + /** * Transform parsed structure into pluralization * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number). * So convert it back with the current language's convertNumber. - * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ] + * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ] * @return {String} selected pluralized form according to current language */ - plural: function( nodes ) { - var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 ); + plural: function ( nodes ) { + var count = parseFloat( this.language.convertNumber( nodes[0], true ) ); var forms = nodes.slice(1); return forms.length ? this.language.convertPlural( count, forms ) : ''; }, @@ -639,10 +647,10 @@ /** * Transform parsed structure into gender * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}. - * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] + * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] * @return {String} selected gender form according to current language */ - gender: function( nodes ) { + gender: function ( nodes ) { var gender; if ( nodes[0] && nodes[0].options instanceof mw.Map ){ gender = nodes[0].options.get( 'gender' ); @@ -651,35 +659,40 @@ } var forms = nodes.slice(1); return this.language.gender( gender, forms ); - } + }, + /** + * Transform parsed structure into grammar conversion. + * Invoked by putting {{grammar:form|word}} in a message + * @param {Array} of nodes [{Grammar case eg: genitive}, {String word}] + * @return {String} selected grammatical form according to current language + */ + grammar: function ( nodes ) { + var form = nodes[0]; + var word = nodes[1]; + return word && form && this.language.convertGrammar( word, form ); + } }; - - // TODO figure out a way to make magic work with common globals like wgSiteName, without requiring init from library users... - // var options = { magic: { 'SITENAME' : mw.config.get( 'wgSiteName' ) } }; - - // deprecated! don't rely on gM existing. - // the window.gM ought not to be required - or if required, not required here. But moving it to extensions breaks it (?!) + // Deprecated! don't rely on gM existing. + // The window.gM ought not to be required - or if required, not required here. + // But moving it to extensions breaks it (?!) // Need to fix plugin so it could do attributes as well, then will be okay to remove this. - window.gM = mw.jqueryMsg.getMessageFunction(); - + window.gM = mw.jqueryMsg.getMessageFunction(); $.fn.msg = mw.jqueryMsg.getPlugin(); - + // Replace the default message parser with jqueryMsg var oldParser = mw.Message.prototype.parser; - mw.Message.prototype.parser = function() { + mw.Message.prototype.parser = function () { // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe? // 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.map.get( this.key ).indexOf( '{{' ) < 0 ) { // Fall back to mw.msg's simple parser return oldParser.apply( this ); } - var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); return messageFunction( this.key, this.parameters ); }; -} )( mediaWiki, jQuery ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg index 74c57e4b..e059ed1d 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.peg +++ b/resources/mediawiki/mediawiki.jqueryMsg.peg @@ -22,11 +22,15 @@ template templateContents = twr:templateWithReplacement p:templateParam* { return twr.concat(p) } + / twr:templateWithOutReplacement p:templateParam* { return twr.concat(p) } / t:templateName p:templateParam* { return p.length ? [ t, p ] : [ t ] } templateWithReplacement = t:templateName ":" r:replacement { return [ t, r ] } +templateWithOutReplacement + = t:templateName ":" p:paramExpression { return [ t, p ] } + templateParam = "|" e:paramExpression* { return e.length > 1 ? [ "CONCAT" ].concat(e) : e[0]; } diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 121d5399..1a72ed13 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -3,11 +3,13 @@ */ var mw = ( function ( $, undefined ) { -"use strict"; + "use strict"; /* Private Members */ - var hasOwn = Object.prototype.hasOwnProperty; + var hasOwn = Object.prototype.hasOwnProperty, + slice = Array.prototype.slice; + /* Object constructors */ /** @@ -43,13 +45,15 @@ var mw = ( function ( $, undefined ) { var results, i; if ( $.isArray( selection ) ) { - selection = $.makeArray( selection ); + selection = slice.call( selection ); results = {}; for ( i = 0; i < selection.length; i += 1 ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; - } else if ( typeof selection === 'string' ) { + } + + if ( typeof selection === 'string' ) { if ( this.values[selection] === undefined ) { if ( fallback !== undefined ) { return fallback; @@ -58,11 +62,13 @@ var mw = ( function ( $, undefined ) { } return this.values[selection]; } + if ( selection === undefined ) { return this.values; - } else { - return null; // invalid selection key } + + // invalid selection key + return null; }, /** @@ -80,7 +86,8 @@ var mw = ( function ( $, undefined ) { this.values[s] = selection[s]; } return true; - } else if ( typeof selection === 'string' && value !== undefined ) { + } + if ( typeof selection === 'string' && value !== undefined ) { this.values[selection] = value; return true; } @@ -103,9 +110,8 @@ var mw = ( function ( $, undefined ) { } } return true; - } else { - return this.values[selection] !== undefined; } + return this.values[selection] !== undefined; } }; @@ -124,7 +130,7 @@ var mw = ( function ( $, undefined ) { this.format = 'plain'; this.map = map; this.key = key; - this.parameters = parameters === undefined ? [] : $.makeArray( parameters ); + this.parameters = parameters === undefined ? [] : slice.call( parameters ); return this; } @@ -132,17 +138,17 @@ var mw = ( function ( $, undefined ) { /** * Simple message parser, does $N replacement and nothing else. * This may be overridden to provide a more complex message parser. - * + * * This function will not be called for nonexistent messages. */ - parser: function() { + parser: function () { var parameters = this.parameters; return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) { var index = parseInt( match, 10 ) - 1; return parameters[index] !== undefined ? parameters[index] : '$' + match; } ); }, - + /** * Appends (does not replace) parameters for replacement to the .parameters property. * @@ -162,7 +168,7 @@ var mw = ( function ( $, undefined ) { * * @return string Message as a string in the current form or <key> if key does not exist. */ - toString: function() { + toString: function () { var text; if ( !this.exists() ) { @@ -186,7 +192,7 @@ var mw = ( function ( $, undefined ) { text = this.parser(); text = mw.html.escape( text ); } - + if ( this.format === 'parse' ) { text = this.parser(); } @@ -199,7 +205,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of parsed message */ - parse: function() { + parse: function () { this.format = 'parse'; return this.toString(); }, @@ -209,7 +215,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of plain message */ - plain: function() { + plain: function () { this.format = 'plain'; return this.toString(); }, @@ -219,7 +225,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of html escaped message */ - escaped: function() { + escaped: function () { this.format = 'escaped'; return this.toString(); }, @@ -229,7 +235,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of parsed message */ - exists: function() { + exists: function () { return this.map.exists( this.key ); } }; @@ -241,8 +247,8 @@ var mw = ( function ( $, undefined ) { * Dummy function which in debug mode can be replaced with a function that * emulates console.log in console-less environments. */ - log: function() { }, - + log: function () { }, + /** * @var constructor Make the Map constructor publicly available. */ @@ -252,7 +258,7 @@ var mw = ( function ( $, undefined ) { * @var constructor Make the Message constructor publicly available. */ Message: Message, - + /** * List of configuration values * @@ -261,25 +267,25 @@ var mw = ( function ( $, undefined ) { * in the global window object. */ config: null, - + /** * @var object * * Empty object that plugins can be installed in. */ libs: {}, - + /* Extension points */ - + legacy: {}, - + /** * Localization system */ messages: new Map(), - + /* Public Methods */ - + /** * Gets a message object, similar to wfMessage() * @@ -292,33 +298,33 @@ var mw = ( function ( $, undefined ) { var parameters; // Support variadic arguments if ( parameter_1 !== undefined ) { - parameters = $.makeArray( arguments ); + parameters = slice.call( arguments ); parameters.shift(); } else { parameters = []; } return new Message( mw.messages, key, parameters ); }, - + /** - * Gets a message string, similar to wfMsg() + * Gets a message string, similar to wfMessage() * * @param key string Key of message to get * @param parameters mixed First argument in a list of variadic arguments, * each a parameter for $N replacement in messages. * @return String. */ - msg: function ( key, parameters ) { + msg: function ( /* key, parameter_1, parameter_2, .. */ ) { return mw.message.apply( mw.message, arguments ).toString(); }, - + /** * Client-side module loader which integrates with the MediaWiki ResourceLoader */ - loader: ( function() { - + loader: ( function () { + /* Private Members */ - + /** * Mapping of registered modules * @@ -335,7 +341,7 @@ var mw = ( function ( $, undefined ) { * { * 'moduleName': { * 'version': ############## (unix timestamp), - * 'dependencies': ['required.foo', 'bar.also', ...], (or) function() {} + * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null, * 'source': 'local', 'someforeignwiki', (or) null * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' @@ -345,7 +351,7 @@ var mw = ( function ( $, undefined ) { * } * } */ - var registry = {}, + var registry = {}, /** * Mapping of sources, keyed by source-id, values are objects. * Format: @@ -362,68 +368,111 @@ var mw = ( function ( $, undefined ) { queue = [], // List of callback functions waiting for modules to be ready to be called jobs = [], - // Flag indicating that document ready has occured - ready = false, // Selector cache for the marker element. Use getMarker() to get/use the marker! $marker = null; - - /* Cache document ready status */ - - $(document).ready( function () { - ready = true; - } ); - + /* Private methods */ - + function getMarker() { // Cached ? if ( $marker ) { return $marker; - } else { - $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); - if ( $marker.length ) { - return $marker; - } - mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' ); - $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); + } + + $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); + if ( $marker.length ) { return $marker; } + mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' ); + $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); + + return $marker; + } + + /** + * Create a new style tag and add it to the DOM. + * + * @param text String: CSS text + * @param nextnode mixed: [optional] An Element or jQuery object for an element where + * the style tag should be inserted before. Otherwise appended to the <head>. + * @return HTMLStyleElement + */ + function addStyleTag( text, nextnode ) { + var s = document.createElement( 'style' ); + // Insert into document before setting cssText (bug 33305) + if ( nextnode ) { + // Must be inserted with native insertBefore, not $.fn.before. + // When using jQuery to insert it, like $nextnode.before( s ), + // then IE6 will throw "Access is denied" when trying to append + // to .cssText later. Some kind of weird security measure. + // http://stackoverflow.com/q/12586482/319266 + // Works: jsfiddle.net/zJzMy/1 + // Fails: jsfiddle.net/uJTQz + // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines) + if ( nextnode.jquery ) { + nextnode = nextnode.get( 0 ); + } + nextnode.parentNode.insertBefore( s, nextnode ); + } else { + document.getElementsByTagName( 'head' )[0].appendChild( s ); + } + if ( s.styleSheet ) { + // IE + s.styleSheet.cssText = text; + } else { + // Other browsers. + // (Safari sometimes borks on non-string values, + // play safe by casting to a string, just in case.) + s.appendChild( document.createTextNode( String( text ) ) ); + } + return s; + } + + /** + * Checks if certain cssText is safe to append to + * a stylesheet. + * + * Right now it only makes sure that cssText containing @import + * rules will end up in a new stylesheet (as those only work when + * placed at the start of a stylesheet; bug 35562). + * This could later be extended to take care of other bugs, such as + * the IE cssRules limit - not the same as the IE styleSheets limit). + */ + function canExpandStylesheetWith( $style, cssText ) { + return cssText.indexOf( '@import' ) === -1; } - - function addInlineCSS( css, media ) { - var $style = getMarker().prev(), - $newStyle, - attrs = { 'type': 'text/css', 'media': media }; - if ( $style.is( 'style' ) && $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { - // There's already a dynamic <style> tag present, append to it - // This recycling of <style> tags is for bug 31676 (can't have - // more than 32 <style> tags in IE) - - // Also, calling .append() on a <style> tag explodes with a JS error in IE, - // so if the .append() fails we fall back to building a new <style> tag and - // replacing the existing one - try { - // Do cdata sanitization on the provided CSS, and prepend a double newline - css = $( mw.html.element( 'style', {}, new mw.html.Cdata( "\n\n" + css ) ) ).html(); - $style.append( css ); - } catch ( e ) { - // Generate a new tag with the combined CSS - css = $style.html() + "\n\n" + css; - $newStyle = $( mw.html.element( 'style', attrs, new mw.html.Cdata( css ) ) ) - .data( 'ResourceLoaderDynamicStyleTag', true ); - // Prevent a flash of unstyled content by inserting the new tag - // before removing the old one - $style.after( $newStyle ); - $style.remove(); + + function addEmbeddedCSS( cssText ) { + var $style, styleEl; + $style = getMarker().prev(); + // Re-use <style> tags if possible, this to try to stay + // under the IE stylesheet limit (bug 31676). + // Also verify that the the element before Marker actually is one + // that came from ResourceLoader, and not a style tag that some + // other script inserted before our marker, or, more importantly, + // it may not be a style tag at all (could be <meta> or <script>). + if ( + $style.data( 'ResourceLoaderDynamicStyleTag' ) === true && + canExpandStylesheetWith( $style, cssText ) + ) { + // There's already a dynamic <style> tag present and + // canExpandStylesheetWith() gave a green light to append more to it. + styleEl = $style.get( 0 ); + if ( styleEl.styleSheet ) { + try { + styleEl.styleSheet.cssText += cssText; // IE + } catch ( e ) { + log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e ); + } + } else { + styleEl.appendChild( document.createTextNode( String( cssText ) ) ); } } else { - // Create a new <style> tag and insert it - $style = $( mw.html.element( 'style', attrs, new mw.html.Cdata( css ) ) ); - $style.data( 'ResourceLoaderDynamicStyleTag', true ); - getMarker().before( $style ); + $( addStyleTag( cssText, getMarker() ) ) + .data( 'ResourceLoaderDynamicStyleTag', true ); } } - + function compare( a, b ) { var i; if ( a.length !== b.length ) { @@ -441,7 +490,7 @@ var mw = ( function ( $, undefined ) { } return true; } - + /** * Generates an ISO8601 "basic" string from a UNIX timestamp */ @@ -456,13 +505,23 @@ var mw = ( function ( $, undefined ) { pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z' ].join( '' ); } - + /** - * Recursively resolves dependencies and detects circular references + * Resolves dependencies and detects circular references. + * + * @param module String Name of the top-level module whose dependencies shall be + * resolved and sorted. + * @param resolved Array Returns a topological sort of the given module and its + * dependencies, such that later modules depend on earlier modules. The array + * contains the module names. If the array contains already some module names, + * this function appends its result to the pre-existing array. + * @param unresolved Object [optional] Hash used to track the current dependency + * chain; used to report loops in the dependency graph. + * @throws Error if any unregistered module or a dependency loop is encountered */ - function recurse( module, resolved, unresolved ) { + function sortDependencies( module, resolved, unresolved ) { var n, deps, len; - + if ( registry[module] === undefined ) { throw new Error( 'Unknown dependency: ' + module ); } @@ -474,12 +533,20 @@ var mw = ( function ( $, undefined ) { registry[module].dependencies = [registry[module].dependencies]; } } + if ( $.inArray( module, resolved ) !== -1 ) { + // Module already resolved; nothing to do. + return; + } + // unresolved is optional, supply it if not passed in + if ( !unresolved ) { + unresolved = {}; + } // Tracks down dependencies deps = registry[module].dependencies; len = deps.length; for ( n = 0; n < len; n += 1 ) { if ( $.inArray( deps[n], resolved ) === -1 ) { - if ( $.inArray( deps[n], unresolved ) !== -1 ) { + if ( unresolved[deps[n]] ) { throw new Error( 'Circular reference detected: ' + module + ' -> ' + deps[n] @@ -487,43 +554,43 @@ var mw = ( function ( $, undefined ) { } // Add to unresolved - unresolved[unresolved.length] = module; - recurse( deps[n], resolved, unresolved ); - // module is at the end of unresolved - unresolved.pop(); + unresolved[module] = true; + sortDependencies( deps[n], resolved, unresolved ); + delete unresolved[module]; } } resolved[resolved.length] = module; } - + /** - * Gets a list of module names that a module depends on in their proper dependency order + * Gets a list of module names that a module depends on in their proper dependency + * order. * * @param module string module name or array of string module names * @return list of dependencies, including 'module'. * @throws Error if circular reference is detected */ function resolve( module ) { - var modules, m, deps, n, resolved; - + var m, resolved; + // Allow calling with an array of module names if ( $.isArray( module ) ) { - modules = []; + resolved = []; for ( m = 0; m < module.length; m += 1 ) { - deps = resolve( module[m] ); - for ( n = 0; n < deps.length; n += 1 ) { - modules[modules.length] = deps[n]; - } + sortDependencies( module[m], resolved ); } - return modules; - } else if ( typeof module === 'string' ) { + return resolved; + } + + if ( typeof module === 'string' ) { resolved = []; - recurse( module, resolved, [] ); + sortDependencies( module, resolved ); return resolved; } + throw new Error( 'Invalid module argument: ' + module ); } - + /** * Narrows a list of module names down to those matching a specific * state (see comment on top of this scope for a list of valid states). @@ -537,7 +604,7 @@ var mw = ( function ( $, undefined ) { */ function filter( states, modules ) { var list, module, s, m; - + // Allow states to be given as a string if ( typeof states === 'string' ) { states = [states]; @@ -570,66 +637,127 @@ var mw = ( function ( $, undefined ) { } return list; } - + + /** + * Determine whether all dependencies are in state 'ready', which means we may + * execute the module or job now. + * + * @param dependencies Array dependencies (module names) to be checked. + * + * @return Boolean true if all dependencies are in state 'ready', false otherwise + */ + function allReady( dependencies ) { + return filter( 'ready', dependencies ).length === dependencies.length; + } + /** - * Automatically executes jobs and modules which are pending with satistifed dependencies. + * 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. * - * This is used when dependencies are satisfied, such as when a module is executed. + * @param msg String text for the log entry. + * @param e Error [optional] to also log. + */ + function log( msg, e ) { + var console = window.console; + if ( console && console.log ) { + console.log( msg ); + // If we have an exception object, log it through .error() to trigger + // proper stacktraces in browsers that support it. There are no (known) + // browsers that don't support .error(), that do support .log() and + // have useful exception handling through .log(). + if ( e && console.error ) { + console.error( e ); + } + } + } + + /** + * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs + * and modules that depend upon this module. if the given module failed, propagate the 'error' + * state up the dependency tree; otherwise, execute all jobs/modules that now have all their + * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any. + * + * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'. */ function handlePending( module ) { - var j, r; - - try { - // Run jobs whose dependencies have just been met - for ( j = 0; j < jobs.length; j += 1 ) { - if ( compare( - filter( 'ready', jobs[j].dependencies ), - jobs[j].dependencies ) ) - { - var callback = jobs[j].ready; - jobs.splice( j, 1 ); - j -= 1; - if ( $.isFunction( callback ) ) { - callback(); + var j, job, hasErrors, m, stateChange; + + // Modules. + if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) { + // If the current module failed, mark all dependent modules also as failed. + // Iterate until steady-state to propagate the error state upwards in the + // dependency tree. + do { + stateChange = false; + for ( m in registry ) { + if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) { + if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) { + registry[m].state = 'error'; + stateChange = true; + } } } - } - // Execute modules whose dependencies have just been met - for ( r in registry ) { - if ( registry[r].state === 'loaded' ) { - if ( compare( - filter( ['ready'], registry[r].dependencies ), - registry[r].dependencies ) ) - { - execute( r ); + } while ( stateChange ); + } + + // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module. + for ( j = 0; j < jobs.length; j += 1 ) { + hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0; + if ( hasErrors || allReady( jobs[j].dependencies ) ) { + // All dependencies satisfied, or some have errors + job = jobs[j]; + jobs.splice( j, 1 ); + j -= 1; + try { + if ( hasErrors ) { + throw new Error ("Module " + module + " failed."); + } 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 ); + } } } } - } catch ( e ) { - // Run error callbacks of jobs affected by this condition - for ( j = 0; j < jobs.length; j += 1 ) { - if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) { - if ( $.isFunction( jobs[j].error ) ) { - jobs[j].error( e, module ); - } - jobs.splice( j, 1 ); - j -= 1; + } + + if ( registry[module].state === 'ready' ) { + // The current module became 'ready'. Recursively execute all dependent modules that are loaded + // and now have all dependencies satisfied. + for ( m in registry ) { + if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) { + execute( m ); } } - throw e; } } - + /** * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, - * depending on whether document-ready has occured yet and whether we are in async mode. + * depending on whether document-ready has occurred yet and whether we are in async mode. * * @param src String: URL to script, will be used as the src attribute in the script tag * @param callback Function: Optional callback which will be run when the script is done */ function addScript( src, callback, async ) { - var done = false, script, head; - if ( ready || async ) { + /*jshint evil:true */ + var script, head, + done = false; + + // Using isReady directly instead of storing it locally from + // a $.fn.ready callback (bug 31895). + if ( $.isReady || async ) { // jQuery's getScript method is NOT better than doing this the old-fashioned way // because jQuery will eval the script's code, and errors will not have sane // line numbers. @@ -638,8 +766,8 @@ var mw = ( function ( $, undefined ) { script.setAttribute( 'type', 'text/javascript' ); if ( $.isFunction( callback ) ) { // Attach handlers for all browsers (based on jQuery.ajax) - script.onload = script.onreadystatechange = function() { - + script.onload = script.onreadystatechange = function () { + if ( !done && ( @@ -647,11 +775,11 @@ var mw = ( function ( $, undefined ) { || /loaded|complete/.test( script.readyState ) ) ) { - + done = true; - + callback(); - + // Handle memory leak in IE. This seems to fail in // IE7 sometimes (Permission Denied error when // accessing script.parentNode) so wrap it in @@ -661,21 +789,21 @@ var mw = ( function ( $, undefined ) { if ( script.parentNode ) { script.parentNode.removeChild( script ); } - + // Dereference the script script = undefined; } catch ( e ) { } } }; } - + if ( window.opera ) { // Appending to the <head> blocks rendering completely in Opera, // so append to the <body> after document ready. This means the // scripts only start loading after the document has been rendered, // but so be it. Opera users don't deserve faster web pages if their // browser makes it impossible - $( function() { document.body.appendChild( script ); } ); + $( function () { document.body.appendChild( script ); } ); } else { // IE-safe way of getting the <head> . document.documentElement.head doesn't // work in scripts that run in the <head> @@ -693,15 +821,15 @@ var mw = ( function ( $, undefined ) { } } } - + /** * Executes a loaded module, making it ready to use * * @param module string module name to execute */ - function execute( module, callback ) { - var style, media, i, script, markModuleReady, nestedAddScript; - + function execute( module ) { + var key, value, media, i, urls, script, markModuleReady, nestedAddScript; + if ( registry[module] === undefined ) { throw new Error( 'Module has not been registered yet: ' + module ); } else if ( registry[module].state === 'registered' ) { @@ -711,38 +839,84 @@ var mw = ( function ( $, undefined ) { } else if ( registry[module].state === 'ready' ) { throw new Error( 'Module has already been loaded: ' + module ); } - - // Add styles + + /** + * Define loop-function here for efficiency + * and to avoid re-using badly scoped variables. + */ + function addLink( media, url ) { + var el = document.createElement( 'link' ); + getMarker().before( el ); // IE: Insert in dom before setting href + el.rel = 'stylesheet'; + if ( media && media !== 'all' ) { + el.media = media; + } + el.href = url; + } + + // Process styles (see also mw.loader.implement) + // * back-compat: { <media>: css } + // * back-compat: { <media>: [url, ..] } + // * { "css": [css, ..] } + // * { "url": { <media>: [url, ..] } } if ( $.isPlainObject( registry[module].style ) ) { - for ( media in registry[module].style ) { - style = registry[module].style[media]; - if ( $.isArray( style ) ) { - for ( i = 0; i < style.length; i += 1 ) { - getMarker().before( mw.html.element( 'link', { - 'type': 'text/css', - 'media': media, - 'rel': 'stylesheet', - 'href': style[i] - } ) ); + for ( key in registry[module].style ) { + value = registry[module].style[key]; + media = undefined; + + if ( key !== 'url' && key !== 'css' ) { + // Backwards compatibility, key is a media-type + if ( typeof value === 'string' ) { + // back-compat: { <media>: css } + // Ignore 'media' because it isn't supported (nor was it used). + // Strings are pre-wrapped in "@media". The media-type was just "" + // (because it had to be set to something). + // This is one of the reasons why this format is no longer used. + addEmbeddedCSS( value ); + } else { + // back-compat: { <media>: [url, ..] } + media = key; + key = 'bc-url'; + } + } + + // Array of css strings in key 'css', + // or back-compat array of urls from media-type + if ( $.isArray( value ) ) { + for ( i = 0; i < value.length; i += 1 ) { + if ( key === 'bc-url' ) { + // back-compat: { <media>: [url, ..] } + addLink( media, value[i] ); + } else if ( key === 'css' ) { + // { "css": [css, ..] } + addEmbeddedCSS( value[i] ); + } + } + // Not an array, but a regular object + // Array of urls inside media-type key + } else if ( typeof value === 'object' ) { + // { "url": { <media>: [url, ..] } } + for ( media in value ) { + urls = value[media]; + for ( i = 0; i < urls.length; i += 1 ) { + addLink( media, urls[i] ); + } } - } else if ( typeof style === 'string' ) { - addInlineCSS( style, media ); } } } + // Add localizations to message system if ( $.isPlainObject( registry[module].messages ) ) { mw.messages.set( registry[module].messages ); } + // Execute script try { script = registry[module].script; - markModuleReady = function() { + markModuleReady = function () { registry[module].state = 'ready'; handlePending( module ); - if ( $.isFunction( callback ) ) { - callback(); - } }; nestedAddScript = function ( arr, callback, async, i ) { // Recursively call addScript() in its own callback @@ -752,29 +926,29 @@ var mw = ( function ( $, undefined ) { callback(); return; } - - addScript( arr[i], function() { + + addScript( arr[i], function () { nestedAddScript( arr, callback, async, i + 1 ); }, async ); }; - + if ( $.isArray( script ) ) { registry[module].state = 'loading'; nestedAddScript( script, markModuleReady, registry[module].async, 0 ); } else if ( $.isFunction( script ) ) { + registry[module].state = 'ready'; script( $ ); - markModuleReady(); + handlePending( module ); } } catch ( e ) { // This needs to NOT use mw.log because these errors are common in production mode // and not in debug mode, such as when a symbol that should be global isn't exported - if ( window.console && typeof window.console.log === 'function' ) { - console.log( 'mw.loader::execute> Exception thrown by ' + module + ': ' + e.message ); - } + log( 'Exception thrown by ' + module + ': ' + e.message, e ); registry[module].state = 'error'; + handlePending( module ); } } - + /** * Adds a dependencies to the queue with optional callbacks to be run * when the dependencies are ready or fail @@ -787,22 +961,14 @@ var mw = ( function ( $, undefined ) { */ function request( dependencies, ready, error, async ) { var regItemDeps, regItemDepLen, n; - + // Allow calling by single module name if ( typeof dependencies === 'string' ) { dependencies = [dependencies]; - if ( registry[dependencies[0]] !== undefined ) { - // Cache repetitively accessed deep level object member - regItemDeps = registry[dependencies[0]].dependencies; - // Cache to avoid looped access to length property - regItemDepLen = regItemDeps.length; - for ( n = 0; n < regItemDepLen; n += 1 ) { - dependencies[dependencies.length] = regItemDeps[n]; - } - } } + // Add ready and error callbacks if they were given - if ( arguments.length > 1 ) { + if ( ready !== undefined || error !== undefined ) { jobs[jobs.length] = { 'dependencies': filter( ['registered', 'loading', 'loaded'], @@ -812,6 +978,7 @@ var mw = ( function ( $, undefined ) { 'error': error }; } + // Queue up any dependencies that are registered dependencies = filter( ['registered'], dependencies ); for ( n = 0; n < dependencies.length; n += 1 ) { @@ -823,10 +990,11 @@ var mw = ( function ( $, undefined ) { } } } + // Work the queue mw.loader.work(); } - + function sortQuery(o) { var sorted = {}, key, a = []; for ( key in o ) { @@ -840,7 +1008,7 @@ var mw = ( function ( $, undefined ) { } return sorted; } - + /** * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] } * to a query string of the form foo.bar,baz|bar.baz,quux @@ -853,7 +1021,7 @@ var mw = ( function ( $, undefined ) { } return arr.join( '|' ); } - + /** * Asynchronously append a script tag to the end of the body * that invokes load.php @@ -872,9 +1040,11 @@ var mw = ( function ( $, undefined ) { // Append &* to avoid triggering the IE6 extension check addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async ); } - + /* Public Methods */ return { + addStyleTag: addStyleTag, + /** * Requests dependencies from server, loading and executing when things when ready. */ @@ -883,7 +1053,7 @@ var mw = ( function ( $, undefined ) { source, group, g, i, modules, maxVersion, sourceLoadScript, currReqBase, currReqBaseLength, moduleMap, l, lastDotIndex, prefix, suffix, bytesAdded, async; - + // Build a list of request parameters common to all requests. reqBase = { skin: mw.config.get( 'skin' ), @@ -893,7 +1063,7 @@ var mw = ( function ( $, undefined ) { // Split module batch by source and by group. splits = {}; maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 ); - + // Appends a list of modules from the queue to the batch for ( q = 0; q < queue.length; q += 1 ) { // Only request modules which are registered @@ -910,14 +1080,14 @@ var mw = ( function ( $, undefined ) { if ( !batch.length ) { return; } - + // The queue has been processed into the batch, clear up the queue. queue = []; - + // Always order modules alphabetically to help reduce cache // misses for otherwise identical content. batch.sort(); - + // Split batch by source and by group. for ( b = 0; b < batch.length; b += 1 ) { bSource = registry[batch[b]].source; @@ -931,24 +1101,24 @@ var mw = ( function ( $, undefined ) { bSourceGroup = splits[bSource][bGroup]; bSourceGroup[bSourceGroup.length] = batch[b]; } - + // Clear the batch - this MUST happen before we append any // script elements to the body or it's possible that a script // will be locally cached, instantly load, and work the batch // again, all before we've cleared it causing each request to // include modules which are already loaded. batch = []; - + for ( source in splits ) { - + sourceLoadScript = sources[source].loadScript; - + for ( group in splits[source] ) { - + // Cache access to currently selected list of // modules for this group from this source. modules = splits[source][group]; - + // Calculate the highest timestamp maxVersion = 0; for ( g = 0; g < modules.length; g += 1 ) { @@ -956,16 +1126,20 @@ var mw = ( function ( $, undefined ) { maxVersion = registry[modules[g]].version; } } - + currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase ); + // For user modules append a user name to the request. + if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) { + currReqBase.user = mw.config.get( 'wgUserName' ); + } currReqBaseLength = $.param( currReqBase ).length; async = true; // We may need to split up the request to honor the query string length limit, // so build it piece by piece. l = currReqBaseLength + 9; // '&modules='.length == 9 - + moduleMap = {}; // { prefix: [ suffixes ] } - + for ( i = 0; i < modules.length; i += 1 ) { // Determine how many bytes this module would add to the query string lastDotIndex = modules[i].lastIndexOf( '.' ); @@ -975,7 +1149,7 @@ var mw = ( function ( $, undefined ) { bytesAdded = moduleMap[prefix] !== undefined ? suffix.length + 3 // '%2C'.length == 3 : modules[i].length + 3; // '%7C'.length == 3 - + // If the request would become too long, create a new one, // but don't create empty requests if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) { @@ -1005,7 +1179,7 @@ var mw = ( function ( $, undefined ) { } } }, - + /** * Register a source. * @@ -1023,16 +1197,16 @@ var mw = ( function ( $, undefined ) { } return true; } - + if ( sources[id] !== undefined ) { throw new Error( 'source already registered: ' + id ); } - + sources[id] = props; - + return true; }, - + /** * Registers a module, letting the system know about it and its * properties. Startup modules contain calls to this function. @@ -1083,7 +1257,7 @@ var mw = ( function ( $, undefined ) { registry[module].dependencies = dependencies; } }, - + /** * Implements a module, giving the system a course of action to take * upon loading. Results of a request for one or more modules contain @@ -1091,12 +1265,20 @@ var mw = ( function ( $, undefined ) { * * All arguments are required. * - * @param module String: Name of module - * @param script Mixed: Function of module code or String of URL to be used as the src - * attribute when adding a script element to the body - * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs - * keyed by media-type - * @param msgs Object: List of key/value pairs to be passed through mw.messages.set + * @param {String} module Name of module + * @param {Function|Array} script Function with module code or Array of URLs to + * be used as the src attribute of a new <script> tag. + * @param {Object} style Should follow one of the following patterns: + * { "css": [css, ..] } + * { "url": { <media>: [url, ..] } } + * And for backwards compatibility (needs to be supported forever due to caching): + * { <media>: css } + * { <media>: [url, ..] } + * + * The reason css strings are not concatenated anymore is bug 31676. We now check + * whether it's safe to extend the stylesheet (see canExpandStylesheetWith). + * + * @param {Object} msgs List of key/value pairs to be passed through mw.messages.set */ implement: function ( module, script, style, msgs ) { // Validate input @@ -1120,21 +1302,19 @@ var mw = ( function ( $, undefined ) { if ( registry[module] !== undefined && registry[module].script !== undefined ) { throw new Error( 'module already implemented: ' + module ); } - // Mark module as loaded - registry[module].state = 'loaded'; // Attach components registry[module].script = script; registry[module].style = style; registry[module].messages = msgs; - // Execute or queue callback - if ( compare( - filter( ['ready'], registry[module].dependencies ), - registry[module].dependencies ) ) - { - execute( module ); + // The module may already have been marked as erroneous + if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) { + registry[module].state = 'loaded'; + if ( allReady( registry[module].dependencies ) ) { + execute( module ); + } } }, - + /** * Executes a function as soon as one or more required modules are ready * @@ -1155,25 +1335,23 @@ var mw = ( function ( $, undefined ) { } // Resolve entire dependency map dependencies = resolve( dependencies ); - // If all dependencies are met, execute ready immediately - if ( compare( filter( ['ready'], dependencies ), dependencies ) ) { + if ( allReady( dependencies ) ) { + // Run ready immediately if ( $.isFunction( ready ) ) { ready(); } - } - // If any dependencies have errors execute error immediately - else if ( filter( ['error'], dependencies ).length ) { + } else if ( filter( ['error', 'missing'], dependencies ).length ) { + // Execute error immediately if any dependencies have errors if ( $.isFunction( error ) ) { - error( new Error( 'one or more dependencies have state "error"' ), + error( new Error( 'one or more dependencies have state "error" or "missing"' ), dependencies ); } - } - // Since some dependencies are not yet ready, queue up a request - else { + } else { + // Not all dependencies are ready: queue up a request request( dependencies, ready, error ); } }, - + /** * Loads an external script or one or more modules for future use * @@ -1188,7 +1366,7 @@ var mw = ( function ( $, undefined ) { * be assumed if loading a URL, and false will be assumed otherwise. */ load: function ( modules, type, async ) { - var filtered, m; + var filtered, m, module; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { @@ -1209,7 +1387,8 @@ var mw = ( function ( $, undefined ) { href: modules } ) ); return; - } else if ( type === 'text/javascript' || type === undefined ) { + } + if ( type === 'text/javascript' || type === undefined ) { addScript( modules, null, async ); return; } @@ -1226,28 +1405,31 @@ var mw = ( function ( $, undefined ) { // an array of unrelated modules, whereas the modules passed to // using() are related and must all be loaded. for ( filtered = [], m = 0; m < modules.length; m += 1 ) { - if ( registry[modules[m]] !== undefined ) { - filtered[filtered.length] = modules[m]; + module = registry[modules[m]]; + if ( module !== undefined ) { + if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) { + filtered[filtered.length] = modules[m]; + } } } - // Resolve entire dependency map - filtered = resolve( filtered ); - // If all modules are ready, nothing dependency be done - if ( compare( filter( ['ready'], filtered ), filtered ) ) { + if ( filtered.length === 0 ) { return; } - // If any modules have errors - else if ( filter( ['error'], filtered ).length ) { + // Resolve entire dependency map + filtered = resolve( filtered ); + // If all modules are ready, nothing to be done + if ( allReady( filtered ) ) { return; } - // Since some modules are not yet ready, queue up a request - else { - request( filtered, null, null, async ); + // If any modules have errors: also quit. + if ( filter( ['error', 'missing'], filtered ).length ) { return; } + // Since some modules are not yet ready, queue up a request. + request( filtered, null, null, async ); }, - + /** * Changes the state of a module * @@ -1256,6 +1438,7 @@ var mw = ( function ( $, undefined ) { */ state: function ( module, state ) { var m; + if ( typeof module === 'object' ) { for ( m in module ) { mw.loader.state( m, module[m] ); @@ -1265,9 +1448,17 @@ var mw = ( function ( $, undefined ) { if ( registry[module] === undefined ) { mw.loader.register( module ); } - registry[module].state = state; + if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1 + && registry[module].state !== state ) { + // Make sure pending modules depending on this one get executed if their + // dependencies are now fulfilled! + registry[module].state = state; + handlePending( module ); + } else { + registry[module].state = state; + } }, - + /** * Gets the version of a module * @@ -1279,14 +1470,14 @@ var mw = ( function ( $, undefined ) { } return null; }, - + /** * @deprecated since 1.18 use mw.loader.getVersion() instead */ version: function () { return mw.loader.getVersion.apply( mw.loader, arguments ); }, - + /** * Gets the state of a module * @@ -1298,7 +1489,7 @@ var mw = ( function ( $, undefined ) { } return null; }, - + /** * Get names of all registered modules. * @@ -1309,7 +1500,7 @@ var mw = ( function ( $, undefined ) { return key; } ); }, - + /** * For backwards-compatibility with Squid-cached pages. Loads mw.user */ @@ -1318,7 +1509,7 @@ var mw = ( function ( $, undefined ) { } }; }() ), - + /** HTML construction helper functions */ html: ( function () { function escapeCallback( s ) { @@ -1344,7 +1535,7 @@ var mw = ( function ( $, undefined ) { escape: function ( s ) { return s.replace( /['"<>&]/g, escapeCallback ); }, - + /** * Wrapper object for raw HTML passed to mw.html.element(). * @constructor @@ -1352,7 +1543,7 @@ var mw = ( function ( $, undefined ) { Raw: function ( value ) { this.value = value; }, - + /** * Wrapper object for CDATA element contents passed to mw.html.element() * @constructor @@ -1360,7 +1551,7 @@ var mw = ( function ( $, undefined ) { Cdata: function ( value ) { this.value = value; }, - + /** * Create an HTML element string, with safe escaping. * @@ -1382,7 +1573,7 @@ var mw = ( function ( $, undefined ) { */ element: function ( name, attrs, contents ) { var v, attrName, s = '<' + name; - + for ( attrName in attrs ) { v = attrs[attrName]; // Convert name=true, to name=name @@ -1429,7 +1620,7 @@ var mw = ( function ( $, undefined ) { return s; } }; - })(), + }() ), // Skeleton user object. mediawiki.user.js extends this user: { @@ -1437,8 +1628,8 @@ var mw = ( function ( $, undefined ) { tokens: new Map() } }; - -})( jQuery ); + +}( jQuery ) ); // Alias $j to jQuery for backwards compatibility window.$j = jQuery; @@ -1447,7 +1638,7 @@ window.$j = jQuery; window.mw = window.mediaWiki = mw; // Auto-register from pre-loaded startup scripts -if ( typeof startUp !== 'undefined' && jQuery.isFunction( startUp ) ) { - startUp(); - startUp = undefined; +if ( jQuery.isFunction( window.startUp ) ) { + window.startUp(); + window.startUp = undefined; } diff --git a/resources/mediawiki/mediawiki.log.js b/resources/mediawiki/mediawiki.log.js index ad4c73df..4ea1a881 100644 --- a/resources/mediawiki/mediawiki.log.js +++ b/resources/mediawiki/mediawiki.log.js @@ -6,7 +6,7 @@ * @author Trevor Parscal <tparscal@wikimedia.org> */ -( function ( $ ) { +( function ( mw, $ ) { /** * Logs a message to the console. @@ -17,7 +17,7 @@ * * @param {String} First in list of variadic messages to output to console. */ - mw.log = function( /* logmsg, logmsg, */ ) { + mw.log = function ( /* logmsg, logmsg, */ ) { // 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) @@ -33,7 +33,8 @@ // If there is no console, use our own log box mw.loader.using( 'jquery.footHovzer', function () { - var d = new Date(), + var hovzer, + d = new Date(), // Create HH:MM:SS.MIL timestamp time = ( d.getHours() < 10 ? '0' + d.getHours() : d.getHours() ) + ':' + ( d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes() ) + @@ -48,7 +49,7 @@ backgroundColor: 'white', borderTop: 'solid 2px #ADADAD' } ); - var hovzer = $.getFootHovzer(); + hovzer = $.getFootHovzer(); hovzer.$.append( $log ); hovzer.update(); } @@ -67,4 +68,4 @@ } ); }; -})( jQuery ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.notification.css b/resources/mediawiki/mediawiki.notification.css new file mode 100644 index 00000000..9a7b651d --- /dev/null +++ b/resources/mediawiki/mediawiki.notification.css @@ -0,0 +1,26 @@ +/** + * Stylesheet for mediawiki.notification module + */ + +#mw-notification-area { + position: absolute; + top: 1em; + right: 1em; + width: 20em; + line-height: 1.35; + z-index: 10000; +} + +.mw-notification { + padding: 0.25em 1em; + margin-bottom: 0.5em; + border: solid 1px #ddd; + background-color: #fcfcfc; + /* Message hides on-click */ + /* See also mediawiki.notification.js */ + cursor: pointer; +} + +.mw-notification-title { + font-weight: bold; +} diff --git a/resources/mediawiki/mediawiki.notification.js b/resources/mediawiki/mediawiki.notification.js new file mode 100644 index 00000000..58a3ab6a --- /dev/null +++ b/resources/mediawiki/mediawiki.notification.js @@ -0,0 +1,480 @@ +/** + * Implements mediaWiki.notification library + */ +( function ( mw, $ ) { + 'use strict'; + + var isPageReady = false, + isInitialized = false, + preReadyNotifQueue = [], + /** + * @var {jQuery} + * The #mw-notification-area div that all notifications are contained inside. + */ + $area = null; + + /** + * Creates a Notification object for 1 message. + * Does not insert anything into the document (see .start()). + * + * @constructor + * @see mw.notification.notify + */ + function Notification( message, options ) { + var $notification, $notificationTitle, $notificationContent; + + $notification = $( '<div class="mw-notification"></div>' ) + .data( 'mw.notification', this ) + .addClass( options.autoHide ? 'mw-notification-autohide' : 'mw-notification-noautohide' ); + + if ( options.tag ) { + // Sanitize options.tag before it is used by any code. (Including Notification class methods) + options.tag = options.tag.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' ); + if ( options.tag ) { + $notification.addClass( 'mw-notification-tag-' + options.tag ); + } else { + delete options.tag; + } + } + + if ( options.title ) { + $notificationTitle = $( '<div class="mw-notification-title"></div>' ) + .text( options.title ) + .appendTo( $notification ); + } + + $notificationContent = $( '<div class="mw-notification-content"></div>' ); + + if ( typeof message === 'object' ) { + // Handle mw.Message objects separately from DOM nodes and jQuery objects + if ( message instanceof mw.Message ) { + $notificationContent.html( message.parse() ); + } else { + $notificationContent.append( message ); + } + } else { + $notificationContent.text( message ); + } + + $notificationContent.appendTo( $notification ); + + // Private state parameters, meant for internal use only + // isOpen: Set to true after .start() is called to avoid double calls. + // Set back to false after .close() to avoid duplicating the close animation. + // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts. + // Set to true initially so .start() can call .resume(). + // message: The message passed to the notification. Unused now but may be used in the future + // to stop replacement of a tagged notification with another notification using the same message. + // options: The options passed to the notification with a little sanitization. Used by various methods. + // $notification: jQuery object containing the notification DOM node. + this.isOpen = false; + this.isPaused = true; + this.message = message; + this.options = options; + this.$notification = $notification; + } + + /** + * Start the notification. + * This inserts it into the page, closes any matching tagged notifications, + * handles the fadeIn animations and repacement transitions, and starts autoHide timers. + */ + Notification.prototype.start = function () { + var + // Local references + $notification, options, + // Original opacity so that we can animate back to it later + opacity, + // Other notification elements matching the same tag + $tagMatches, + outerHeight, + placeholderHeight; + + if ( this.isOpen ) { + return; + } + + this.isOpen = true; + + options = this.options; + $notification = this.$notification; + + opacity = this.$notification.css( 'opacity' ); + + // Set the opacity to 0 so we can fade in later. + $notification.css( 'opacity', 0 ); + + if ( options.tag ) { + // Check to see if there are any tagged notifications with the same tag as the new one + $tagMatches = $area.find( '.mw-notification-tag-' + options.tag ); + } + + // If we found a tagged notification use the replacement pattern instead of the new + // notification fade-in pattern. + if ( options.tag && $tagMatches.length ) { + + // Iterate over the tag matches to find the outerHeight we should use + // for the placeholder. + outerHeight = 0; + $tagMatches.each( function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif ) { + // Use the notification's height + padding + border + margins + // as the placeholder height. + outerHeight = notif.$notification.outerHeight( true ); + if ( notif.$replacementPlaceholder ) { + // Grab the height of a placeholder that has not finished animating. + placeholderHeight = notif.$replacementPlaceholder.height(); + // Remove any placeholders added by a previous tagged + // notification that was in the middle of replacing another. + // This also makes sure that we only grab the placeholderHeight + // for the most recent notification. + notif.$replacementPlaceholder.remove(); + delete notif.$replacementPlaceholder; + } + // Close the previous tagged notification + // Since we're replacing it do this with a fast speed and don't output a placeholder + // since we're taking care of that transition ourselves. + notif.close( { speed: 'fast', placeholder: false } ); + } + } ); + if ( placeholderHeight !== undefined ) { + // If the other tagged notification was in the middle of replacing another + // tagged notification, continue from the placeholder's height instead of + // using the outerHeight of the notification. + outerHeight = placeholderHeight; + } + + $notification + // Insert the new notification before the tagged notification(s) + .insertBefore( $tagMatches.first() ) + .css( { + // Use an absolute position so that we can use a placeholder to gracefully push other notifications + // into the right spot. + position: 'absolute', + width: $notification.width() + } ) + // Fade-in the notification + .animate( { opacity: opacity }, + { + duration: 'slow', + complete: function () { + // After we've faded in clear the opacity and let css take over + $( this ).css( { opacity: '' } ); + } + } ); + + // Create a clear placeholder we can use to make the notifications around the notification that is being + // replaced expand or contract gracefully to fit the height of the new notification. + var self = this; + self.$replacementPlaceholder = $( '<div>' ) + // Set the height to the space the previous notification or placeholder took + .css( 'height', outerHeight ) + // Make sure that this placeholder is at the very end of this tagged notification group + .insertAfter( $tagMatches.eq( -1 ) ) + // Animate the placeholder height to the space that this new notification will take up + .animate( { height: $notification.outerHeight( true ) }, + { + // Do space animations fast + speed: 'fast', + complete: function () { + // Reset the notification position after we've finished the space animation + // However do not do it if the placeholder was removed because another tagged + // notification went and closed this one. + if ( self.$replacementPlaceholder ) { + $notification.css( 'position', '' ); + } + // Finally, remove the placeholder from the DOM + $( this ).remove(); + } + } ); + } else { + // Append to the notification area and fade in to the original opacity. + $notification + .appendTo( $area ) + .animate( { opacity: opacity }, + { + duration: 'fast', + complete: function () { + // After we've faded in clear the opacity and let css take over + $( this ).css( 'opacity', '' ); + } + } + ); + } + + // By default a notification is paused. + // If this notification is within the first {autoHideLimit} notifications then + // start the auto-hide timer as soon as it's created. + var autohideCount = $area.find( '.mw-notification-autohide' ).length; + if ( autohideCount <= notification.autoHideLimit ) { + this.resume(); + } + }; + + /** + * Pause any running auto-hide timer for this notification + */ + Notification.prototype.pause = function () { + if ( this.isPaused ) { + return; + } + this.isPaused = true; + + if ( this.timeout ) { + clearTimeout( this.timeout ); + delete this.timeout; + } + }; + + /** + * Start autoHide timer if not already started. + * Does nothing if autoHide is disabled. + * Either to resume from pause or to make the first start. + */ + Notification.prototype.resume = function () { + var notif = this; + if ( !notif.isPaused ) { + return; + } + // Start any autoHide timeouts + if ( notif.options.autoHide ) { + notif.isPaused = false; + notif.timeout = setTimeout( function () { + // Already finished, so don't try to re-clear it + delete notif.timeout; + notif.close(); + }, notification.autoHideSeconds * 1000 ); + } + }; + + /** + * Close/hide the notification. + * + * @param {Object} options An object containing options for the closing of the notification. + * These are typically only used internally. + * - speed: Use a close speed different than the default 'slow'. + * - placeholder: Set to false to disable the placeholder transition. + */ + Notification.prototype.close = function ( options ) { + if ( !this.isOpen ) { + return; + } + this.isOpen = false; + // Clear any remaining timeout on close + this.pause(); + + options = $.extend( { + speed: 'slow', + placeholder: true + }, options ); + + // Remove the mw-notification-autohide class from the notification to avoid + // having a half-closed notification counted as a notification to resume + // when handling {autoHideLimit}. + this.$notification.removeClass( 'mw-notification-autohide' ); + + // Now that a notification is being closed. Start auto-hide timers for any + // notification that has now become one of the first {autoHideLimit} notifications. + notification.resume(); + + this.$notification + .css( { + // Don't trigger any mouse events while fading out, just in case the cursor + // happens to be right above us when we transition upwards. + pointerEvents: 'none', + // Set an absolute position so we can move upwards in the animation. + // Notification replacement doesn't look right unless we use an animation like this. + position: 'absolute', + // We must fix the width to avoid it shrinking horizontally. + width: this.$notification.width() + } ) + // Fix the top/left position to the current computed position from which we + // can animate upwards. + .css( this.$notification.position() ); + + // This needs to be done *after* notification's position has been made absolute. + if ( options.placeholder ) { + // Insert a placeholder with a height equal to the height of the + // notification plus it's vertical margins in place of the notification + var $placeholder = $( '<div>' ) + .css( 'height', this.$notification.outerHeight( true ) ) + .insertBefore( this.$notification ); + } + + // Animate opacity and top to create fade upwards animation for notification closing + this.$notification + .animate( { + opacity: 0, + top: '-=35' + }, { + duration: options.speed, + complete: function () { + // Remove the notification + $( this ).remove(); + if ( options.placeholder ) { + // Use a fast slide up animation after closing to make it look like the notifications + // below slide up into place when the notification disappears + $placeholder.slideUp( 'fast', function () { + // Remove the placeholder + $( this ).remove(); + } ); + } + } + } ); + }; + + /** + * Helper function, take a list of notification divs and call + * a function on the Notification instance attached to them + * + * @param {jQuery} $notifications A jQuery object containing notification divs + * @param {string} fn The name of the function to call on the Notification instance + */ + function callEachNotification( $notifications, fn ) { + $notifications.each( function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif ) { + notif[fn](); + } + } ); + } + + /** + * Initialisation + * (don't call before document ready) + */ + function init() { + if ( !isInitialized ) { + isInitialized = true; + $area = $( '<div id="mw-notification-area"></div>' ) + // Pause auto-hide timers when the mouse is in the notification area. + .on( { + mouseenter: notification.pause, + mouseleave: notification.resume + } ) + // When clicking on a notification close it. + .on( 'click', '.mw-notification', function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif ) { + notif.close(); + } + } ) + // Stop click events from <a> tags from propogating to prevent clicking. + // on links from hiding a notification. + .on( 'click', 'a', function ( e ) { + e.stopPropagation(); + } ); + + // Prepend the notification area to the content area and save it's object. + mw.util.$content.prepend( $area ); + } + } + + var notification = { + /** + * Pause auto-hide timers for all notifications. + * Notifications will not auto-hide until resume is called. + */ + pause: function () { + callEachNotification( + $area.children( '.mw-notification' ), + 'pause' + ); + }, + + /** + * Resume any paused auto-hide timers from the beginning. + * Only the first {autoHideLimit} timers will be resumed. + */ + resume: function () { + callEachNotification( + // Only call resume on the first {autoHideLimit} notifications. + // Exclude noautohide notifications to avoid bugs where {autoHideLimit} + // { autoHide: false } notifications are at the start preventing any + // auto-hide notifications from being autohidden. + $area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ), + 'resume' + ); + }, + + /** + * Display a notification message to the user. + * + * @param {mixed} message The DOM-element, jQuery object, mw.Message instance, + * or plaintext string to be used as the message. + * @param {Object} options The options to use for the notification. + * See mw.notification.defaults for details. + */ + notify: function ( message, options ) { + var notif; + options = $.extend( {}, notification.defaults, options ); + + notif = new Notification( message, options ); + + if ( isPageReady ) { + notif.start(); + } else { + preReadyNotifQueue.push( notif ); + } + }, + + /** + * @var {Object} + * The defaults for mw.notification.notify's options parameter + * autoHide: + * A boolean indicating whether the notifification should automatically + * be hidden after shown. Or if it should persist. + * + * tag: + * An optional string. When a notification is tagged only one message + * with that tag will be displayed. Trying to display a new notification + * with the same tag as one already being displayed will cause the other + * notification to be closed and this new notification to open up inside + * the same place as the previous notification. + * + * title: + * An optional title for the notification. Will be displayed above the + * content. Usually in bold. + */ + defaults: { + autoHide: true, + tag: false, + title: undefined + }, + + /** + * @var {number} + * Number of seconds to wait before auto-hiding notifications. + */ + autoHideSeconds: 5, + + /** + * @var {number} + * Maximum number of notifications to count down auto-hide timers for. + * Only the first {autoHideLimit} notifications being displayed will + * auto-hide. Any notifications further down in the list will only start + * counting down to auto-hide after the first few messages have closed. + * + * This basically represents the number of notifications the user should + * be able to process in {autoHideSeconds} time. + */ + autoHideLimit: 3 + }; + + $( function () { + var notif; + + init(); + + // Handle pre-ready queue. + isPageReady = true; + while ( preReadyNotifQueue.length ) { + notif = preReadyNotifQueue.shift(); + notif.start(); + } + } ); + + mw.notification = notification; + +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.notify.js b/resources/mediawiki/mediawiki.notify.js new file mode 100644 index 00000000..3bf2a896 --- /dev/null +++ b/resources/mediawiki/mediawiki.notify.js @@ -0,0 +1,20 @@ +/** + * Implements mediaWiki.notify function + */ +( function ( mw ) { + 'use strict'; + + /** + * @see mw.notification.notify + */ + mw.notify = function ( message, options ) { + // 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 ); + } ); + }; + +}( mediaWiki ) );
\ No newline at end of file diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js new file mode 100644 index 00000000..99a55576 --- /dev/null +++ b/resources/mediawiki/mediawiki.searchSuggest.js @@ -0,0 +1,166 @@ +/** + * Add search suggestions to the search form. + */ +( function ( mw, $ ) { + $( document ).ready( function ( $ ) { + var map, searchboxesSelectors, + // Region where the suggestions box will appear directly below + // (using the same width). Can be a container element or the input + // itself, depending on what suits best in the environment. + // For Vector the suggestion box should align with the simpleSearch + // container's borders, in other skins it should align with the input + // element (not the search form, as that would leave the buttons + // vertically between the input and the suggestions). + $searchRegion = $( '#simpleSearch, #searchInput' ).first(), + $searchInput = $( '#searchInput' ); + + // Compatibility map + map = { + browsers: { + // Left-to-right languages + ltr: { + // SimpleSearch is broken in Opera < 9.6 + opera: [['>=', 9.6]], + docomo: false, + blackberry: false, + ipod: false, + iphone: false + }, + // Right-to-left languages + rtl: { + opera: [['>=', 9.6]], + docomo: false, + blackberry: false, + ipod: false, + iphone: false + } + } + }; + + if ( !$.client.test( map ) ) { + return; + } + + // General suggestions functionality for all search boxes + 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', + // Generic selector for skins with multiple searchboxes (used by CologneBlue) + '.mw-searchInput' + ]; + $( searchboxesSelectors.join(', ') ) + .suggestions( { + fetch: function ( query ) { + var $el, jqXhr; + + 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 ); + } + }, + cancel: function () { + var jqXhr = $(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' ); + } + }, + result: { + select: function ( $input ) { + $input.closest( 'form' ).submit(); + } + }, + delay: 120, + highlightInput: true + } ) + .bind( 'paste cut drop', function () { + // make sure paste and cut events from the mouse and drag&drop events + // trigger the keypress handler and cause the suggestions to update + $( this ).trigger( 'keypress' ); + } ); + + // Ensure that the thing is actually present! + if ( $searchRegion.length === 0 ) { + // Don't try to set anything up if simpleSearch is disabled sitewide. + // The loader code loads us if the option is present, even if we're + // not actually enabled (anymore). + 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: { + select: function ( $input ) { + $input.closest( 'form' ).submit(); + } + }, + special: { + render: function ( query ) { + var $el = this; + if ( $el.children().length === 0 ) { + $el + .append( + $( '<div>' ) + .addClass( 'special-label' ) + .text( mw.msg( 'searchsuggest-containing' ) ), + $( '<div>' ) + .addClass( 'special-query' ) + .text( query ) + .autoEllipsis() + ) + .show(); + } else { + $el.find( '.special-query' ) + .text( query ) + .autoEllipsis(); + } + }, + select: function ( $input ) { + $input.closest( 'form' ).append( + $( '<input type="hidden" name="fulltext" value="1"/>' ) + ); + $input.closest( 'form' ).submit(); + } + }, + $region: $searchRegion + } ); + + // In most skins (at least Monobook and Vector), the font-size is messed up in <body>. + // (they use 2 elements to get a sane font-height). So, instead of making exceptions for + // each skin or adding more stylesheets, just copy it from the active element so auto-fit. + $searchInput + .data( 'suggestions-context' ) + .data.$container + .css( 'fontSize', $searchInput.css( 'fontSize' ) ); + + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.user.js b/resources/mediawiki/mediawiki.user.js index 7f881b0e..5c5c87e2 100644 --- a/resources/mediawiki/mediawiki.user.js +++ b/resources/mediawiki/mediawiki.user.js @@ -2,16 +2,48 @@ * Implementation for mediaWiki.user */ -(function( $ ) { +( function ( mw, $ ) { /** * User object */ function User( options, tokens ) { + var user, callbacks; /* Private Members */ - var that = this; + 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'); + 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 */ @@ -25,14 +57,15 @@ * Generates 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. It's uniqueness should not be depended on. + * session or series of sessions. Its uniqueness should not be depended on. * * @return String: Random set of 32 alpha-numeric characters */ function generateId() { - var id = ''; - var seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - for ( var i = 0, r; i < 32; i++ ) { + var i, r, + id = '', + seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + for ( i = 0; i < 32; i++ ) { r = Math.floor( Math.random() * seed.length ); id += seed.substring( r, r + 1 ); } @@ -44,17 +77,31 @@ * * @return Mixed: User name string or null if users is anonymous */ - this.name = function() { + this.getName = function () { return mw.config.get( 'wgUserName' ); }; /** + * @deprecated since 1.20 use mw.user.getName() instead + */ + this.name = function () { + return this.getName(); + }; + + /** * Checks if the current user is anonymous. * * @return Boolean */ - this.anonymous = function() { - return that.name() ? false : true; + this.isAnon = function () { + return user.getName() === null; + }; + + /** + * @deprecated since 1.20 use mw.user.isAnon() instead + */ + this.anonymous = function () { + return user.isAnon(); }; /** @@ -67,7 +114,7 @@ */ this.sessionId = function () { var sessionId = $.cookie( 'mediaWiki.user.sessionId' ); - if ( typeof sessionId == 'undefined' || sessionId === null ) { + if ( typeof sessionId === 'undefined' || sessionId === null ) { sessionId = generateId(); $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': null, 'path': '/' } ); } @@ -84,16 +131,20 @@ * @return String: User name or random session ID */ this.id = function() { - var name = that.name(); + var id, + name = user.getName(); if ( name ) { return name; } - var id = $.cookie( 'mediaWiki.user.id' ); - if ( typeof id == 'undefined' || id === null ) { + id = $.cookie( 'mediaWiki.user.id' ); + if ( typeof id === 'undefined' || id === null ) { id = generateId(); } // Set cookie if not set, or renew it if already set - $.cookie( 'mediaWiki.user.id', id, { 'expires': 365, 'path': '/' } ); + $.cookie( 'mediaWiki.user.id', id, { + expires: 365, + path: '/' + } ); return id; }; @@ -120,38 +171,41 @@ * 'expires': 7 * } ); */ - this.bucket = function( key, options ) { + this.bucket = function ( key, options ) { + var cookie, parts, version, bucket, + range, k, rand, total; + options = $.extend( { - 'buckets': {}, - 'version': 0, - 'tracked': false, - 'expires': 30 + buckets: {}, + version: 0, + tracked: false, + expires: 30 }, options || {} ); - var cookie = $.cookie( 'mediaWiki.user.bucket:' + key ); - var bucket = null; - var version = 0; + + 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 ) { - var parts = cookie.split( ':' ); - if ( parts.length > 1 && parts[0] == options.version ) { + parts = cookie.split( ':' ); + if ( parts.length > 1 && Number( parts[0] ) === options.version ) { version = Number( parts[0] ); bucket = String( parts[1] ); } } - if ( bucket === null ) { + if ( bucket === undefined ) { if ( !$.isPlainObject( options.buckets ) ) { throw 'Invalid buckets error. Object expected for options.buckets.'; } version = Number( options.version ); // Find range - var range = 0, k; + range = 0; for ( k in options.buckets ) { range += options.buckets[k]; } // Select random value within range - var rand = Math.random() * range; + rand = Math.random() * range; // Determine which bucket the value landed in - var total = 0; + total = 0; for ( k in options.buckets ) { bucket = k; total += options.buckets[k]; @@ -160,7 +214,7 @@ } } if ( options.tracked ) { - mw.loader.using( 'jquery.clickTracking', function() { + mw.loader.using( 'jquery.clickTracking', function () { $.trackAction( 'mediaWiki.user.bucket:' + key + '@' + version + ':' + bucket ); @@ -174,10 +228,24 @@ } return bucket; }; + + /** + * Gets the current user's groups. + */ + this.getGroups = function ( callback ) { + getUserInfo( 'groups', callback ); + }; + + /** + * Gets the current user's rights. + */ + this.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 ); -})(jQuery);
\ No newline at end of file +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.util.js b/resources/mediawiki/mediawiki.util.js index 0a95d102..29284384 100644 --- a/resources/mediawiki/mediawiki.util.js +++ b/resources/mediawiki/mediawiki.util.js @@ -1,8 +1,8 @@ /** * Implements mediaWiki.util library */ -( function ( $, mw ) { - "use strict"; +( function ( mw, $ ) { + 'use strict'; // Local cache and alias var util = { @@ -14,12 +14,6 @@ init: function () { var profile, $tocTitle, $tocToggleLink, hideTocCookie; - /* Set up $.messageBox */ - $.messageBoxNew( { - id: 'mw-js-message', - parent: '#content' - } ); - /* Set tooltipAccessKeyPrefix */ profile = $.client.profile(); @@ -48,7 +42,11 @@ && profile.name === 'safari' && profile.layoutVersion > 526 ) { util.tooltipAccessKeyPrefix = 'ctrl-alt-'; - + // Firefox 14+ on Mac + } else if ( profile.platform === 'mac' + && profile.name === 'firefox' + && profile.versionNumber >= 14 ) { + util.tooltipAccessKeyPrefix = 'ctrl-option-'; // Safari/Konqueror on any platform, or any browser on Mac // (but not Safari on Windows) } else if ( !( profile.platform === 'win' && profile.name === 'safari' ) @@ -63,25 +61,49 @@ } /* Fill $content var */ - if ( $( '#bodyContent' ).length ) { - // Vector, Monobook, Chick etc. - util.$content = $( '#bodyContent' ); - - } else if ( $( '#mw_contentholder' ).length ) { - // Modern - util.$content = $( '#mw_contentholder' ); - - } else if ( $( '#article' ).length ) { - // Standard, CologneBlue - util.$content = $( '#article' ); + util.$content = ( function () { + var $content, selectors = [ + // The preferred standard for setting $content (class="mw-body") + // You may also use (class="mw-body mw-body-primary") if you use + // mw-body in multiple locations. + // Or class="mw-body-primary" if you want $content to be deeper + // in the dom than mw-body + '.mw-body-primary', + '.mw-body', + + /* Legacy fallbacks for setting the content */ + // Vector, Monobook, Chick, etc... based skins + '#bodyContent', + + // Modern based skins + '#mw_contentholder', + + // Standard, CologneBlue + '#article', + + // #content is present on almost all if not all skins. Most skins (the above cases) + // have #content too, but as an outer wrapper instead of the article text container. + // The skins that don't have an outer wrapper do have #content for everything + // so it's a good fallback + '#content', + + // If nothing better is found fall back to our bodytext div that is guaranteed to be here + '#mw-content-text', + + // Should never happen... well, it could if someone is not finished writing a skin and has + // not inserted bodytext yet. But in any case <body> should always exist + 'body' + ]; + for ( var i = 0, l = selectors.length; i < l; i++ ) { + $content = $( selectors[i] ).first(); + if ( $content.length ) { + return $content; + } + } - } else { - // #content is present on almost all if not all skins. Most skins (the above cases) - // have #content too, but as an outer wrapper instead of the article text container. - // The skins that don't have an outer wrapper do have #content for everything - // so it's a good fallback - util.$content = $( '#content' ); - } + // Make sure we don't unset util.$content if it was preset and we don't find anything + return util.$content; + } )(); // Table of contents toggle $tocTitle = $( '#toctitle' ); @@ -150,32 +172,41 @@ * Get address to a script in the wiki root. * For index.php use mw.config.get( 'wgScript' ) * + * @since 1.18 * @param str string Name of script (eg. 'api'), defaults to 'index' * @return string Address to script (eg. '/w/api.php' ) */ wikiScript: function ( str ) { - return mw.config.get( 'wgScriptPath' ) + '/' + ( str || 'index' ) + - mw.config.get( 'wgScriptExtension' ); + str = str || 'index'; + if ( str === 'index' ) { + return mw.config.get( 'wgScript' ); + } else if ( str === 'load' ) { + return mw.config.get( 'wgLoadScript' ); + } else { + return mw.config.get( 'wgScriptPath' ) + '/' + str + + mw.config.get( 'wgScriptExtension' ); + } }, /** - * Append a new style block to the head + * Append a new style block to the head and return the CSSStyleSheet object. + * Use .ownerNode to access the <style> element, or use mw.loader.addStyleTag. + * This function returns the styleSheet object for convience (due to cross-browsers + * difference as to where it is located). + * @example + * <code> + * var sheet = mw.util.addCSS('.foobar { display: none; }'); + * $(foo).click(function () { + * // Toggle the sheet on and off + * sheet.disabled = !sheet.disabled; + * }); + * </code> * * @param text string CSS to be appended - * @return CSSStyleSheet + * @return CSSStyleSheet (use .ownerNode to get to the <style> element) */ addCSS: function ( text ) { - var s = document.createElement( 'style' ); - s.type = 'text/css'; - s.rel = 'stylesheet'; - // Insert into document before setting cssText (bug 33305) - document.getElementsByTagName('head')[0].appendChild( s ); - if ( s.styleSheet ) { - s.styleSheet.cssText = text; // IE - } else { - // Safari sometimes borks on null - s.appendChild( document.createTextNode( String( text ) ) ); - } + var s = mw.loader.addStyleTag( text ); return s.sheet || s; }, @@ -231,7 +262,7 @@ // Get last match, stop at hash var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ), m = re.exec( url ); - if ( m && m.length > 1 ) { + if ( m ) { // Beware that decodeURIComponent is not required to understand '+' // by spec, as encodeURIComponent does not produce it. return decodeURIComponent( m[1].replace( /\+/g, '%20' ) ); @@ -282,7 +313,7 @@ /* * @var jQuery - * A jQuery object that refers to the page-content element + * A jQuery object that refers to the content area element * Populated by init(). */ $content: null, @@ -353,20 +384,21 @@ return null; } // Select the first (most likely only) unordered list inside the portlet - $ul = $portlet.find( 'ul' ); + $ul = $portlet.find( 'ul' ).eq( 0 ); // If it didn't have an unordered list yet, create it if ( $ul.length === 0 ) { + + $ul = $( '<ul>' ); + // If there's no <div> inside, append it to the portlet directly if ( $portlet.find( 'div:first' ).length === 0 ) { - $portlet.append( '<ul></ul>' ); + $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></ul>' ); + $portlet.find( 'div' ).eq( -1 ).append( $ul ); } - // Select the created element - $ul = $portlet.find( 'ul' ).eq( 0 ); } // Just in case.. if ( $ul.length === 0 ) { @@ -424,43 +456,18 @@ * Calling with no arguments, with an empty string or null will hide the message * * @param message {mixed} The DOM-element, jQuery object or HTML-string to be put inside the message box. - * @param className {String} Used in adding a class; should be different for each call * to allow CSS/JS to hide different boxes. null = no class used. - * @return {Boolean} True on success, false on failure. + * @depreceated Use mw.notify */ - jsMessage: function ( message, className ) { + jsMessage: function ( message ) { if ( !arguments.length || message === '' || message === null ) { - $( '#mw-js-message' ).empty().hide(); - return true; // Emptying and hiding message is intended behaviour, return true - - } else { - // We special-case skin structures provided by the software. Skins that - // choose to abandon or significantly modify our formatting can just define - // an mw-js-message div to start with. - var $messageDiv = $( '#mw-js-message' ); - if ( !$messageDiv.length ) { - $messageDiv = $( '<div id="mw-js-message"></div>' ); - if ( util.$content.parent().length ) { - util.$content.parent().prepend( $messageDiv ); - } else { - return false; - } - } - - if ( className ) { - $messageDiv.prop( 'class', 'mw-js-message-' + className ); - } - - if ( typeof message === 'object' ) { - $messageDiv.empty(); - $messageDiv.append( message ); - } else { - $messageDiv.html( message ); - } - - $messageDiv.slideDown(); return true; } + if ( typeof message !== 'object' ) { + message = $.parseHTML( message ); + } + mw.notify( message, { autoHide: true, tag: 'legacy' } ); + return true; }, /** @@ -598,4 +605,4 @@ mw.util = util; -} )( jQuery, mediaWiki ); +}( mediaWiki, jQuery ) ); |