summaryrefslogtreecommitdiff
path: root/resources/mediawiki
diff options
context:
space:
mode:
Diffstat (limited to 'resources/mediawiki')
-rw-r--r--resources/mediawiki/mediawiki.Title.js104
-rw-r--r--resources/mediawiki/mediawiki.Uri.js103
-rw-r--r--resources/mediawiki/mediawiki.debug.css1
-rw-r--r--resources/mediawiki/mediawiki.debug.js68
-rw-r--r--resources/mediawiki/mediawiki.feedback.js182
-rw-r--r--resources/mediawiki/mediawiki.htmlform.js20
-rw-r--r--resources/mediawiki/mediawiki.jqueryMsg.js411
-rw-r--r--resources/mediawiki/mediawiki.jqueryMsg.peg4
-rw-r--r--resources/mediawiki/mediawiki.js751
-rw-r--r--resources/mediawiki/mediawiki.log.js11
-rw-r--r--resources/mediawiki/mediawiki.notification.css26
-rw-r--r--resources/mediawiki/mediawiki.notification.js480
-rw-r--r--resources/mediawiki/mediawiki.notify.js20
-rw-r--r--resources/mediawiki/mediawiki.searchSuggest.js166
-rw-r--r--resources/mediawiki/mediawiki.user.js128
-rw-r--r--resources/mediawiki/mediawiki.util.js171
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 ) );