diff options
Diffstat (limited to 'resources/mediawiki')
-rw-r--r-- | resources/mediawiki/mediawiki.Title.js | 103 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.Uri.js | 64 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.css | 1 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.js | 6 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.feedback.js | 45 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.hidpi.js | 5 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.htmlform.js | 112 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.js | 423 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.peg | 1 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.js | 699 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.log.js | 2 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.notification.js | 145 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.notify.js | 13 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.searchSuggest.css | 16 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.searchSuggest.js | 110 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.user.js | 25 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.util.js | 236 |
17 files changed, 1218 insertions, 788 deletions
diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index 33cca585..b86a14ba 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -1,6 +1,4 @@ -/** - * mediaWiki.Title - * +/*! * @author Neil Kandalgaonkar, 2010 * @author Timo Tijhof, 2011 * @since 1.18 @@ -12,13 +10,12 @@ /* Local space */ /** - * Title - * @constructor + * @class mw.Title * - * @param title {String} Title of the page. If no second argument given, + * @constructor + * @param {string} title Title of the page. If no second argument given, * this will be searched for a namespace. - * @param namespace {Number} (optional) Namespace id. If given, title will be taken as-is. - * @return {Title} this + * @param {number} [namespace] Namespace id. If given, title will be taken as-is. */ function Title( title, namespace ) { this.ns = 0; // integer namespace id @@ -35,17 +32,16 @@ } var - /** - * Public methods (defined later) - */ + /* Public methods (defined later) */ fn, /** * Strip some illegal chars: control chars, colon, less than, greater than, * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity * intact, like unicode bidi chars, but it's a good start.. - * @param s {String} - * @return {String} + * @ignore + * @param {string} s + * @return {string} */ clean = function ( s ) { if ( s !== undefined ) { @@ -55,8 +51,9 @@ var /** * Convert db-key to readable text. - * @param s {String} - * @return {String} + * @ignore + * @param {string} s + * @return {string} */ text = function ( s ) { if ( s !== null && s !== undefined ) { @@ -68,13 +65,15 @@ var /** * Sanitize name. + * @ignore */ fixName = function ( s ) { return clean( $.trim( s ) ); }, /** - * Sanitize name. + * Sanitize extension. + * @ignore */ fixExt = function ( s ) { return clean( s ); @@ -82,6 +81,7 @@ var /** * Sanitize namespace id. + * @ignore * @param id {Number} Namespace id. * @return {Number|Boolean} The id as-is or boolean false if invalid. */ @@ -99,8 +99,8 @@ var /** * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias). - * - * @example On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. + * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. + * @ignore * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). * @return {Number|Boolean} Namespace id or boolean false if unrecognized. */ @@ -125,19 +125,20 @@ var /** * Helper to extract namespace, name and extension from a string. * - * @param title {mw.Title} - * @param raw {String} + * @ignore + * @param {mw.Title} title + * @param {string} raw * @return {mw.Title} */ 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+))?$/ ), - ns_match = getNsIdByName( matches[1] ); + nsMatch = 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; + if ( nsMatch && typeof matches[2] === 'string' && matches[2] !== '' ) { + title.ns = nsMatch; title.name = fixName( matches[2] ); if ( typeof matches[3] === 'string' && matches[3] !== '' ) { title.ext = fixExt( matches[3] ); @@ -153,8 +154,9 @@ var /** * Helper to extract name and extension from a string. * - * @param title {mw.Title} - * @param raw {String} + * @ignore + * @param {mw.Title} title + * @param {string} raw * @return {mw.Title} */ setNameAndExtension = function ( title, raw ) { @@ -179,8 +181,9 @@ var /** * Whether this title exists on the wiki. - * @param title {mixed} prefixed db-key name (string) or instance of Title - * @return {mixed} Boolean true/false if the information is available. Otherwise null. + * @static + * @param {Mixed} title prefixed db-key name (string) or instance of Title + * @return {Mixed} Boolean true/false if the information is available. Otherwise null. */ Title.exists = function ( title ) { var type = $.type( title ), obj = Title.exist.pages, match; @@ -198,20 +201,27 @@ var }; /** - * @var Title.exist {Object} + * @static + * @property */ Title.exist = { /** - * @var Title.exist.pages {Object} Keyed by PrefixedDb title. + * @static + * @property {Object} exist.pages Keyed by PrefixedDb title. * Boolean true value indicates page does exist. */ pages: {}, /** - * @example Declare existing titles: Title.exist.set(['User:John_Doe', ...]); - * @example Declare titles nonexistent: Title.exist.set(['File:Foo_bar.jpg', ...], false); - * @param titles {String|Array} Title(s) in strict prefixedDb title form. - * @param state {Boolean} (optional) State of the given titles. Defaults to true. - * @return {Boolean} + * Example to declare existing titles: + * Title.exist.set(['User:John_Doe', ...]); + * Eample to declare titles nonexistent: + * Title.exist.set(['File:Foo_bar.jpg', ...], false); + * + * @static + * @property exist.set + * @param {string|Array} titles Title(s) in strict prefixedDb title form. + * @param {boolean} [state] State of the given titles. Defaults to true. + * @return {boolean} */ set: function ( titles, state ) { titles = $.isArray( titles ) ? titles : [titles]; @@ -231,7 +241,7 @@ var /** * Get the namespace number. - * @return {Number} + * @return {number} */ getNamespaceId: function (){ return this.ns; @@ -240,7 +250,7 @@ var /** * Get the namespace prefix (in the content-language). * In NS_MAIN this is '', otherwise namespace name plus ':' - * @return {String} + * @return {string} */ getNamespacePrefix: function (){ return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); @@ -248,7 +258,7 @@ var /** * The name, like "Foo_bar" - * @return {String} + * @return {string} */ getName: function () { if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { @@ -260,7 +270,7 @@ var /** * The name, like "Foo bar" - * @return {String} + * @return {string} */ getNameText: function () { return text( this.getName() ); @@ -269,6 +279,7 @@ var /** * Get full name in prefixed DB form, like File:Foo_bar.jpg, * most useful for API calls, anything that must identify the "title". + * @return {string} */ getPrefixedDb: function () { return this.getNamespacePrefix() + this.getMain(); @@ -276,7 +287,7 @@ var /** * Get full name in text form, like "File:Foo bar.jpg". - * @return {String} + * @return {string} */ getPrefixedText: function () { return text( this.getPrefixedDb() ); @@ -284,7 +295,7 @@ var /** * The main title (without namespace), like "Foo_bar.jpg" - * @return {String} + * @return {string} */ getMain: function () { return this.getName() + this.getDotExtension(); @@ -292,7 +303,7 @@ var /** * The "text" form, like "Foo bar.jpg" - * @return {String} + * @return {string} */ getMainText: function () { return text( this.getMain() ); @@ -300,7 +311,7 @@ var /** * Get the extension (returns null if there was none) - * @return {String|null} extension + * @return {string|null} */ getExtension: function () { return this.ext; @@ -308,7 +319,7 @@ var /** * Convenience method: return string like ".jpg", or "" if no extension - * @return {String} + * @return {string} */ getDotExtension: function () { return this.ext === null ? '' : '.' + this.ext; @@ -316,7 +327,8 @@ var /** * Return the URL to this title - * @return {String} + * @see mw.util#wikiGetlink + * @return {string} */ getUrl: function () { return mw.util.wikiGetlink( this.toString() ); @@ -324,7 +336,8 @@ var /** * Whether this title exists on the wiki. - * @return {mixed} Boolean true/false if the information is available. Otherwise null. + * @see #static-method-exists + * @return {boolean|null} If the information is available. Otherwise null. */ exists: function () { return Title.exists( this ); diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index bd12b214..643e5c3e 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -61,11 +61,11 @@ /** * Function that's useful when constructing the URI string -- we frequently encounter the pattern of * having to add something to the URI as we go, but only if it's present, and to include a character before or after if so. - * @param {String} to prepend, if value not empty - * @param {String} value to include, if not empty - * @param {String} to append, if value not empty - * @param {Boolean} raw -- if true, do not URI encode - * @return {String} + * @param {string|undefined} pre To prepend. + * @param {string} val To include. + * @param {string} post To append. + * @param {boolean} raw If true, val will not be encoded. + * @return {string} Result. */ function cat( pre, val, post, raw ) { if ( val === undefined || val === null || val === '' ) { @@ -76,8 +76,8 @@ // Regular expressions to parse many common URIs. var parser = { - strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, - loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ + strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ }, // The order here matches the order of captured matches in the above parser regexes. @@ -103,14 +103,14 @@ /** * 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). + * @param {Object|string} uri 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 - * convert to an array (false, default). + * 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 + * - {boolean} strictMode Trigger strict mode parsing of the url. Default: false + * - {boolean} overrideKeys Wether to let duplicate query parameters override eachother (true) or automagically + * convert to an array (false, default). */ function Uri( uri, options ) { options = typeof options === 'object' ? options : { strictMode: !!options }; @@ -158,7 +158,7 @@ } 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. + // figure out whether the last path component of defaultUri.path is a directory or a file. throw new Error( 'Bad constructor arguments' ); } if ( !( this.protocol && this.host && this.path ) ) { @@ -169,8 +169,8 @@ /** * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more compliant with RFC 3986 * Similar to rawurlencode from PHP and our JS library mw.util.rawurlencode, but we also replace space with a + - * @param {String} string - * @return {String} encoded for URI + * @param {string} s String to encode. + * @return {string} Encoded string for URI. */ Uri.encode = function ( s ) { return encodeURIComponent( s ) @@ -180,9 +180,9 @@ }; /** - * Standard decodeURIComponent, with '+' to space - * @param {String} string encoded for URI - * @return {String} decoded string + * Standard decodeURIComponent, with '+' to space. + * @param {string} s String encoded for URI. + * @return {string} Decoded string. */ Uri.decode = function ( s ) { return decodeURIComponent( s.replace( /\+/g, '%20' ) ); @@ -192,9 +192,9 @@ /** * Parse a string and set our properties accordingly. - * @param {String} URI + * @param {string} str URI * @param {Object} options - * @return {Boolean} success + * @return {boolean} Success. */ parse: function ( str, options ) { var q, @@ -240,7 +240,7 @@ /** * Returns user and password portion of a URI. - * @return {String} + * @return {string} */ getUserInfo: function () { return cat( '', this.user, cat( ':', this.password, '' ) ); @@ -248,7 +248,7 @@ /** * Gets host and port portion of a URI. - * @return {String} + * @return {string} */ getHostPort: function () { return this.host + cat( ':', this.port, '' ); @@ -257,7 +257,7 @@ /** * Returns the userInfo and host and port portion of the URI. * In most real-world URLs, this is simply the hostname, but it is more general. - * @return {String} + * @return {string} */ getAuthority: function () { return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); @@ -266,7 +266,7 @@ /** * Returns the query arguments of the URL, encoded into a string * Does not preserve the order of arguments passed into the URI. Does handle escaping. - * @return {String} + * @return {string} */ getQueryString: function () { var args = []; @@ -274,7 +274,13 @@ var k = Uri.encode( key ), vals = $.isArray( val ) ? val : [ val ]; $.each( vals, function ( i, v ) { - args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); + if ( v === null ) { + args.push( k ); + } else if ( k === 'title' ) { + args.push( k + '=' + mw.util.wikiUrlencode( v ) ); + } else { + args.push( k + '=' + Uri.encode( v ) ); + } } ); } ); return args.join( '&' ); @@ -282,7 +288,7 @@ /** * Returns everything after the authority section of the URI - * @return {String} + * @return {string} */ getRelativePath: function () { return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); @@ -290,7 +296,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 + * @return {string} The URI string. */ toString: function () { return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css index 149e1bff..513cb847 100644 --- a/resources/mediawiki/mediawiki.debug.css +++ b/resources/mediawiki/mediawiki.debug.css @@ -1,6 +1,5 @@ .mw-debug { width: 100%; - text-align: left; background-color: #eee; border-top: 1px solid #aaa; } diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js index 1ad1a623..88af3c65 100644 --- a/resources/mediawiki/mediawiki.debug.js +++ b/resources/mediawiki/mediawiki.debug.js @@ -96,7 +96,7 @@ buildHtml: function () { var $container, $bits, panes, id, gitInfo; - $container = $( '<div id="mw-debug-toolbar" class="mw-debug"></div>' ); + $container = $( '<div id="mw-debug-toolbar" class="mw-debug" lang="en" dir="ltr"></div>' ); $bits = $( '<div class="mw-debug-bits"></div>' ); @@ -187,9 +187,7 @@ .text( 'Time: ' + this.data.time.toFixed( 5 ) ); bitDiv( 'memory' ) - .text( 'Memory: ' + this.data.memory ) - .append( $( '<span title="Peak usage"></span>' ).text( ' (' + this.data.memoryPeak + ')' ) ); - + .text( 'Memory: ' + this.data.memory + ' (Peak: ' + this.data.memoryPeak + ')' ); $bits.appendTo( $container ); diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js index 634d02b1..1afe51ef 100644 --- a/resources/mediawiki/mediawiki.feedback.js +++ b/resources/mediawiki/mediawiki.feedback.js @@ -1,5 +1,5 @@ /** - * mediawiki.Feedback + * mediawiki.feedback * * @author Ryan Kaldari, 2010 * @author Neil Kandalgaonkar, 2010-11 @@ -68,17 +68,28 @@ mw.Feedback.prototype = { setup: function () { - var fb = this; + var $feedbackPageLink, + $bugNoteLink, + $bugsListLink, + fb = this; - var $feedbackPageLink = $( '<a>' ) - .attr( { 'href': fb.title.getUrl(), 'target': '_blank' } ) - .css( { 'white-space': 'nowrap' } ); + $feedbackPageLink = $( '<a>' ) + .attr( { + href: fb.title.getUrl(), + target: '_blank' + } ) + .css( { + whiteSpace: 'nowrap' + } ); - var $bugNoteLink = $( '<a>' ).attr( { 'href': '#' } ).click( function () { + $bugNoteLink = $( '<a>' ).attr( { href: '#' } ).click( function () { fb.displayBugs(); } ); - var $bugsListLink = $( '<a>' ).attr( { 'href': fb.bugsListLink, 'target': '_blank' } ); + $bugsListLink = $( '<a>' ).attr( { + href: fb.bugsListLink, + target: '_blank' + } ); // TODO: Use a stylesheet instead of these inline styles this.$dialog = @@ -108,7 +119,7 @@ ), $( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' ).append( mw.msg( 'feedback-adding' ), - $( '<br/>' ), + $( '<br>' ), $( '<span class="feedback-spinner"></span>' ) ), $( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg( @@ -148,9 +159,9 @@ }, displayBugs: function () { - var fb = this; + var fb = this, + bugsButtons = {}; this.display( 'bugs' ); - var bugsButtons = {}; bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () { window.open( fb.bugsLink, '_blank' ); }; @@ -163,9 +174,9 @@ }, displayThanks: function () { - var fb = this; + var fb = this, + closeButton = {}; this.display( 'thanks' ); - var closeButton = {}; closeButton[ mw.msg( 'feedback-close' ) ] = function () { fb.$dialog.dialog( 'close' ); }; @@ -181,14 +192,14 @@ * message: {String} */ displayForm: function ( contents ) { - var fb = this; + var fb = this, + formButtons = {}; 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 () { fb.submit(); }; @@ -199,10 +210,10 @@ }, displayError: function ( message ) { - var fb = this; + var fb = this, + closeButton = {}; this.display( 'error' ); this.$dialog.find( '.feedback-error-msg' ).msg( message ); - var closeButton = {}; closeButton[ mw.msg( 'feedback-close' ) ] = function () { fb.$dialog.dialog( 'close' ); }; @@ -231,7 +242,7 @@ } } - function err( code, info ) { + function err() { // ajax request failed fb.displayError( 'feedback-error3' ); } diff --git a/resources/mediawiki/mediawiki.hidpi.js b/resources/mediawiki/mediawiki.hidpi.js new file mode 100644 index 00000000..ecee450c --- /dev/null +++ b/resources/mediawiki/mediawiki.hidpi.js @@ -0,0 +1,5 @@ +jQuery( function ( $ ) { + // Apply hidpi images on DOM-ready + // Some may have already partly preloaded at low resolution. + $( 'body' ).hidpi(); +} ); diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index a4753b99..83bf2e3a 100644 --- a/resources/mediawiki/mediawiki.htmlform.js +++ b/resources/mediawiki/mediawiki.htmlform.js @@ -1,64 +1,62 @@ /** - * Utility functions for jazzing up HTMLForm elements + * Utility functions for jazzing up HTMLForm elements. */ ( function ( $ ) { -/** - * jQuery plugin to fade or snap to visible state. - * - * @param boolean instantToggle (optional) - * @return jQuery - */ -$.fn.goIn = function ( instantToggle ) { - if ( instantToggle === true ) { - return $(this).show(); - } - return $(this).stop( true, true ).fadeIn(); -}; - -/** - * jQuery plugin to fade or snap to hiding state. - * - * @param boolean instantToggle (optional) - * @return jQuery - */ -$.fn.goOut = function ( instantToggle ) { - if ( instantToggle === true ) { - return $(this).hide(); - } - return $(this).stop( true, true ).fadeOut(); -}; - -/** - * Bind a function to the jQuery object via live(), and also immediately trigger - * 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 ){ - $(this) - .live( 'change', callback ) - .each( function ( index, element ){ - callback.call( this, true ); - } ); -}; - -// Document ready: -$( function () { - - // Animate the SelectOrOther fields, to only show the text field when - // 'other' is selected. - $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { - var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); - $other = $other.add( $other.siblings( 'br' ) ); - if ( $(this).val() === 'other' ) { - $other.goIn( instant ); - } else { - $other.goOut( instant ); + /** + * jQuery plugin to fade or snap to visible state. + * + * @param {boolean} instantToggle [optional] + * @return {jQuery} + */ + $.fn.goIn = function ( instantToggle ) { + if ( instantToggle === true ) { + return $(this).show(); } - }); - -}); - + return $(this).stop( true, true ).fadeIn(); + }; + + /** + * jQuery plugin to fade or snap to hiding state. + * + * @param {boolean} instantToggle [optional] + * @return jQuery + */ + $.fn.goOut = function ( instantToggle ) { + if ( instantToggle === true ) { + return $(this).hide(); + } + return $(this).stop( true, true ).fadeOut(); + }; + + /** + * Bind a function to the jQuery object via live(), and also immediately trigger + * the function on the objects with an 'instant' parameter set to true. + * @param {Function} callback Takes one parameter, which is {true} when the + * event is called immediately, and {jQuery.Event} when triggered from an event. + */ + $.fn.liveAndTestAtStart = function ( callback ){ + $(this) + .live( 'change', callback ) + .each( function () { + callback.call( this, true ); + } ); + }; + + $( function () { + + // Animate the SelectOrOther fields, to only show the text field when + // 'other' is selected. + $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { + var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); + $other = $other.add( $other.siblings( 'br' ) ); + if ( $(this).val() === 'other' ) { + $other.goIn( instant ); + } else { + $other.goOut( instant ); + } + }); + + } ); }( jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 86af31ff..183b525e 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -5,13 +5,26 @@ * @author neilk@wikimedia.org */ ( function ( mw, $ ) { - var slice = Array.prototype.slice, + var oldParser, + slice = Array.prototype.slice, parserDefaults = { magic : { 'SITENAME' : mw.config.get( 'wgSiteName' ) }, messages : mw.messages, - language : mw.language + language : mw.language, + + // Same meaning as in mediawiki.js. + // + // Only 'text', 'parse', and 'escaped' are supported, and the + // actual escaping for 'escaped' is done by other code (generally + // through jqueryMsg). + // + // However, note that this default only + // applies to direct calls to jqueryMsg. The default for mediawiki.js itself + // is 'text', including when it uses jqueryMsg. + format: 'parse' + }; /** @@ -30,8 +43,8 @@ * @return {jQuery} */ return function ( args ) { - var key = args[0]; - var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); + var key = args[0], + argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); try { return parser.parse( key, argsArray ); } catch ( e ) { @@ -56,19 +69,32 @@ * @return {Function} function suitable for assigning to window.gM */ mw.jqueryMsg.getMessageFunction = function ( options ) { - var failableParserFn = getFailableParserFn( options ); + var failableParserFn = getFailableParserFn( options ), + format; + + if ( options && options.format !== undefined ) { + format = options.format; + } else { + format = parserDefaults.format; + } + /** * 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]) * - * @param {String} message key - * @param {Array} optional replacements (can also specify variadically) - * @return {String} rendered HTML as string + * @param {string} key Message key. + * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). + * @return {string} Rendered HTML. */ - return function ( /* key, replacements */ ) { - return failableParserFn( arguments ).html(); + return function () { + var failableResult = failableParserFn( arguments ); + if ( format === 'text' || format === 'escaped' ) { + return failableResult.text(); + } else { + return failableResult.html(); + } }; }; @@ -93,12 +119,14 @@ * 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) + * @param {string} key Message key. + * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). * @return {jQuery} this */ - return function ( /* key, replacements */ ) { + return function () { var $target = this.empty(); + // TODO: Simply $target.append( failableParserFn( arguments ).contents() ) + // or Simply $target.append( failableParserFn( arguments ) ) $.each( failableParserFn( arguments ).contents(), function ( i, node ) { $target.append( node ); } ); @@ -113,20 +141,36 @@ */ mw.jqueryMsg.parser = function ( options ) { this.settings = $.extend( {}, parserDefaults, options ); + this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' ); + 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). + /** + * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, 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). + * + * The two parts of the key are separated by colon. For example: + * + * "message-key:true": ast + * + * if they key is "message-key" and onlyCurlyBraceTransform is true. + * + * This cache is shared by all instances of mw.jqueryMsg.parser. + * + * @static + */ astCache: {}, /** * Where the magic happens. * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery * If an error is thrown, returns original key, and logs the error - * @param {String} message key - * @param {Array} replacements for $1, $2... $n + * @param {String} key Message key. + * @param {Array} replacements Variable replacements for $1, $2... $n * @return {jQuery} */ parse: function ( key, replacements ) { @@ -139,16 +183,19 @@ * @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 ) { - var wikiText = this.settings.messages.get( key ); + var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText; + + if ( this.astCache[ cacheKey ] === undefined ) { + wikiText = this.settings.messages.get( key ); if ( typeof wikiText !== 'string' ) { - wikiText = "\\[" + key + "\\]"; + wikiText = '\\[' + key + '\\]'; } - this.astCache[ key ] = this.wikiTextToAst( wikiText ); + this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText ); } - return this.astCache[ key ]; + return this.astCache[ cacheKey ]; }, - /* + + /** * 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. @@ -159,18 +206,27 @@ * @return {Mixed} abstract syntax tree */ wikiTextToAst: function ( input ) { + var pos, + regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets, + backslash, anyCharacter, escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, + whitespace, dollar, digits, + openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon, + templateContents, openTemplate, closeTemplate, + nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result; // Indicates current position in input as we parse through it. // Shared among all parsing functions below. - var pos = 0; + pos = 0; + // ========================================================= // parsing combinators - could be a library on its own // ========================================================= // Try parsers until one works, if none work return null function choice( ps ) { return function () { - for ( var i = 0; i < ps.length; i++ ) { - var result = ps[i](); + var i, result; + for ( i = 0; i < ps.length; i++ ) { + result = ps[i](); if ( result !== null ) { return result; } @@ -181,10 +237,11 @@ // 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++ ) { - var res = ps[i](); + var i, res, + originalPos = pos, + result = []; + for ( i = 0; i < ps.length; i++ ) { + res = ps[i](); if ( res === null ) { pos = originalPos; return null; @@ -197,9 +254,9 @@ // must succeed a minimum of n times or return null function nOrMore( n, p ) { return function () { - var originalPos = pos; - var result = []; - var parsed = p(); + var originalPos = pos, + result = [], + parsed = p(); while ( parsed !== null ) { result.push( parsed ); parsed = p(); @@ -258,11 +315,12 @@ // 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 backslash = makeStringParser( "\\" ); - var anyCharacter = makeRegexParser( /^./ ); + regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); + regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); + regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); + regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ ); + backslash = makeStringParser( '\\' ); + anyCharacter = makeRegexParser( /^./ ); function escapedLiteral() { var result = sequence( [ backslash, @@ -270,36 +328,50 @@ ] ); return result === null ? null : result[1]; } - var escapedOrLiteralWithoutSpace = choice( [ + escapedOrLiteralWithoutSpace = choice( [ escapedLiteral, regularLiteralWithoutSpace ] ); - var escapedOrLiteralWithoutBar = choice( [ + escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] ); - var escapedOrRegularLiteral = choice( [ + 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(''); + 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 // it is not a literal in the parameter function literalWithoutBar() { - var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); + return result === null ? null : result.join(''); } + + // Used for wikilink page names. Like literalWithoutBar, but + // without allowing escapes. + function unescapedLiteralWithoutBar() { + var result = nOrMore( 1, regularLiteralWithoutBar )(); + return result === null ? null : result.join(''); + } + function literal() { - var result = nOrMore( 1, escapedOrRegularLiteral )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrRegularLiteral )(); + return result === null ? null : result.join(''); } - var whitespace = makeRegexParser( /^\s+/ ); - var dollar = makeStringParser( '$' ); - var digits = makeRegexParser( /^\d+/ ); + + function curlyBraceTransformExpressionLiteral() { + var result = nOrMore( 1, regularLiteralWithSquareBrackets )(); + return result === null ? null : result.join(''); + } + + whitespace = makeRegexParser( /^\s+/ ); + dollar = makeStringParser( '$' ); + digits = makeRegexParser( /^\d+/ ); function replacement() { var result = sequence( [ @@ -311,12 +383,13 @@ } return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; } - var openExtlink = makeStringParser( '[' ); - var closeExtlink = makeStringParser( ']' ); + openExtlink = makeStringParser( '[' ); + closeExtlink = makeStringParser( ']' ); // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed function extlink() { - var result = null; - var parsedResult = sequence( [ + var result, parsedResult; + result = null; + parsedResult = sequence( [ openExtlink, nonWhitespaceExpression, whitespace, @@ -343,39 +416,73 @@ } return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; } - var openLink = makeStringParser( '[[' ); - var closeLink = makeStringParser( ']]' ); + openLink = makeStringParser( '[[' ); + closeLink = makeStringParser( ']]' ); + pipe = makeStringParser( '|' ); + + function template() { + var result = sequence( [ + openTemplate, + templateContents, + closeTemplate + ] ); + return result === null ? null : result[1]; + } + + wikilinkPage = choice( [ + unescapedLiteralWithoutBar, + template + ] ); + + function pipedWikilink() { + var result = sequence( [ + wikilinkPage, + pipe, + expression + ] ); + return result === null ? null : [ result[0], result[2] ]; + } + + wikilinkContents = choice( [ + pipedWikilink, + wikilinkPage // unpiped link + ] ); + function link() { - var result = null; - var parsedResult = sequence( [ + var result, parsedResult, parsedLinkContents; + result = null; + + parsedResult = sequence( [ openLink, - expression, + wikilinkContents, closeLink ] ); if ( parsedResult !== null ) { - result = [ 'WLINK', parsedResult[1] ]; + parsedLinkContents = parsedResult[1]; + result = [ 'WLINK' ].concat( parsedLinkContents ); } return result; } - var templateName = transform( + 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(); } ); function templateParam() { - var result = sequence( [ + var expr, result; + result = sequence( [ pipe, nOrMore( 0, paramExpression ) ] ); if ( result === null ) { return null; } - var expr = result[1]; - // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw. - return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0]; + expr = result[1]; + // 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, @@ -392,8 +499,8 @@ ] ); return result === null ? null : [ result[0], result[2] ]; } - var colon = makeStringParser(':'); - var templateContents = choice( [ + colon = makeStringParser(':'); + templateContents = choice( [ function () { var res = sequence( [ // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}} @@ -414,17 +521,9 @@ return [ res[0] ].concat( res[1] ); } ] ); - var openTemplate = makeStringParser('{{'); - var closeTemplate = makeStringParser('}}'); - function template() { - var result = sequence( [ - openTemplate, - templateContents, - closeTemplate - ] ); - return result === null ? null : result[1]; - } - var nonWhitespaceExpression = choice( [ + openTemplate = makeStringParser('{{'); + closeTemplate = makeStringParser('}}'); + nonWhitespaceExpression = choice( [ template, link, extLinkParam, @@ -432,7 +531,7 @@ replacement, literalWithoutSpace ] ); - var paramExpression = choice( [ + paramExpression = choice( [ template, link, extLinkParam, @@ -440,7 +539,8 @@ replacement, literalWithoutBar ] ); - var expression = choice( [ + + expression = choice( [ template, link, extLinkParam, @@ -448,25 +548,42 @@ replacement, literal ] ); - function start() { - var result = nOrMore( 0, expression )(); + + // Used when only {{-transformation is wanted, for 'text' + // or 'escaped' formats + curlyBraceTransformExpression = choice( [ + template, + replacement, + curlyBraceTransformExpressionLiteral + ] ); + + + /** + * Starts the parse + * + * @param {Function} rootExpression root parse function + */ + function start( rootExpression ) { + var result = nOrMore( 0, rootExpression )(); if ( result === null ) { return null; } - return [ "CONCAT" ].concat( result ); + 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(); + + // If you add another possible rootExpression, you must update the astCache key scheme. + result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression ); /* * 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. */ - if (result === null || pos !== input.length) { - throw new Error( "Parse error at position " + pos.toString() + " in input: " + input ); + if ( result === null || pos !== input.length ) { + throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input ); } return result; } @@ -491,18 +608,20 @@ * @return {Mixed} single-string node or array of nodes suitable for jQuery appending */ this.emit = function ( node, replacements ) { - var ret = null; - var jmsg = this; + var ret, subnodes, operation, + 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 ) { + // typeof returns object for arrays + case 'object': + // node is an array of nodes + subnodes = $.map( node.slice( 1 ), function ( n ) { return jmsg.emit( n, replacements ); } ); - var operation = node[0].toLowerCase(); + operation = node[0].toLowerCase(); if ( typeof jmsg[operation] === 'function' ) { ret = jmsg[ operation ]( subnodes, replacements ); } else { @@ -543,8 +662,9 @@ $span.append( childNode ); } ); } else { - // strings, integers, anything else - $span.append( node ); + // Let jQuery append nodes, arrays of nodes and jQuery objects + // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings) + $span.append( $.type( node ) === 'object' ? node : document.createTextNode( node ) ); } } ); return $span; @@ -555,7 +675,7 @@ * 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" ) - * TODO throw error if nodes.length > 1 ? + * TODO: Throw error if nodes.length > 1 ? * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ @@ -563,13 +683,7 @@ var index = parseInt( nodes[0], 10 ); 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]; - } + return replacements[index]; } else { // index not found, fallback to displaying variable return '$' + ( index + 1 ); @@ -578,10 +692,41 @@ /** * Transform wiki-link - * TODO unimplemented + * + * TODO: + * It only handles basic cases, either no pipe, or a pipe with an explicit + * anchor. + * + * It does not attempt to handle features like the pipe trick. + * However, the pipe trick should usually not be present in wikitext retrieved + * from the server, since the replacement is done at save time. + * It may, though, if the wikitext appears in extension-controlled content. + * + * @param nodes */ wlink: function ( nodes ) { - return 'unimplemented'; + var page, anchor, url; + + page = nodes[0]; + url = mw.util.wikiGetlink( page ); + + // [[Some Page]] or [[Namespace:Some Page]] + if ( nodes.length === 1 ) { + anchor = page; + } + + /* + * [[Some Page|anchor text]] or + * [[Namespace:Some Page|anchor] + */ + else { + anchor = nodes[1]; + } + + return $( '<a />' ).attr( { + title: page, + href: url + } ).text( anchor ); }, /** @@ -594,9 +739,9 @@ * @return {jQuery} */ link: function ( nodes ) { - var arg = nodes[0]; - var contents = nodes[1]; - var $el; + var $el, + arg = nodes[0], + contents = nodes[1]; if ( arg instanceof jQuery ) { $el = arg; } else { @@ -639,25 +784,32 @@ * @return {String} selected pluralized form according to current language */ plural: function ( nodes ) { - var count = parseFloat( this.language.convertNumber( nodes[0], true ) ); - var forms = nodes.slice(1); + var forms, count; + count = parseFloat( this.language.convertNumber( nodes[0], true ) ); + forms = nodes.slice(1); return forms.length ? this.language.convertPlural( count, forms ) : ''; }, /** - * Transform parsed structure into gender - * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}. - * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] + * Transform parsed structure according to gender. + * Usage {{gender:[ gender | mw.user object ] | masculine form|feminine form|neutral form}}. + * The first node is either a string, which can be "male" or "female", + * or a User object (not a username). + * + * @param {Array} of nodes, [ {String|mw.User}, {String}, {String}, {String} ] * @return {String} selected gender form according to current language */ gender: function ( nodes ) { - var gender; - if ( nodes[0] && nodes[0].options instanceof mw.Map ){ + var gender, forms; + + if ( nodes[0] && nodes[0].options instanceof mw.Map ) { gender = nodes[0].options.get( 'gender' ); } else { gender = nodes[0]; } - var forms = nodes.slice(1); + + forms = nodes.slice( 1 ); + return this.language.gender( gender, forms ); }, @@ -668,9 +820,33 @@ * @return {String} selected grammatical form according to current language */ grammar: function ( nodes ) { - var form = nodes[0]; - var word = nodes[1]; + var form = nodes[0], + word = nodes[1]; return word && form && this.language.convertGrammar( word, form ); + }, + + /** + * Tranform parsed structure into a int: (interface language) message include + * Invoked by putting {{int:othermessage}} into a message + * @param {Array} of nodes + * @return {string} Other message + */ + int: function ( nodes ) { + return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() ); + }, + + /** + * Takes an unformatted number (arab, no group separators and . as decimal separator) + * and outputs it in the localized digit script and formatted with decimal + * separator, according to the current language + * @param {Array} of nodes + * @return {Number|String} formatted number + */ + formatnum: function ( nodes ) { + var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false, + number = nodes[0]; + + return this.language.convertNumber( number, isInteger ); } }; // Deprecated! don't rely on gM existing. @@ -681,17 +857,24 @@ $.fn.msg = mw.jqueryMsg.getPlugin(); // Replace the default message parser with jqueryMsg - var oldParser = mw.Message.prototype.parser; + oldParser = mw.Message.prototype.parser; mw.Message.prototype.parser = function () { + var messageFunction; + // 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 ) { + if ( this.format === 'plain' || !/\{\{|\[/.test(this.map.get( this.key ) ) ) { // Fall back to mw.msg's simple parser return oldParser.apply( this ); } - var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); + + messageFunction = mw.jqueryMsg.getMessageFunction( { + 'messages': this.map, + // For format 'escaped', escaping part is handled by mediawiki.js + 'format': this.format + } ); return messageFunction( this.key, this.parameters ); }; diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg index e059ed1d..7879d6fa 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.peg +++ b/resources/mediawiki/mediawiki.jqueryMsg.peg @@ -37,6 +37,7 @@ templateParam templateName = tn:[A-Za-z_]+ { return tn.join('').toUpperCase() } +/* TODO: Update to reflect separate piped and unpiped handling */ link = "[[" w:expression "]]" { return [ 'WLINK', w ]; } diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 19112aed..ca987543 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -1,9 +1,9 @@ /* * Core MediaWiki JavaScript Library */ -/*global mw:true */ + var mw = ( function ( $, undefined ) { - "use strict"; + 'use strict'; /* Private Members */ @@ -13,14 +13,13 @@ var mw = ( function ( $, undefined ) { /* Object constructors */ /** - * Map - * * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. + * @class mw.Map * - * @param global boolean Whether to store the values in the global window + * @constructor + * @param {boolean} global Whether to store the values in the global window * object or a exclusively in the object property 'values'. - * @return Map */ function Map( global ) { this.values = global === true ? window : {}; @@ -39,26 +38,26 @@ var mw = ( function ( $, undefined ) { * If selection was an array, returns an object of key/values (value is null if not found), * If selection was not passed or invalid, will return the 'values' object member (be careful as * objects are always passed by reference in JavaScript!). - * @return Values as a string or object, null if invalid/inexistant. + * @return {string|Object|null} Values as a string or object, null if invalid/inexistant. */ get: function ( selection, fallback ) { var results, i; + // If we only do this in the `return` block, it'll fail for the + // call to get() from the mutli-selection block. + fallback = arguments.length > 1 ? fallback : null; if ( $.isArray( selection ) ) { selection = slice.call( selection ); results = {}; - for ( i = 0; i < selection.length; i += 1 ) { + for ( i = 0; i < selection.length; i++ ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { - if ( this.values[selection] === undefined ) { - if ( fallback !== undefined ) { - return fallback; - } - return null; + if ( !hasOwn.call( this.values, selection ) ) { + return fallback; } return this.values[selection]; } @@ -87,7 +86,7 @@ var mw = ( function ( $, undefined ) { } return true; } - if ( typeof selection === 'string' && value !== undefined ) { + if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[selection] = value; return true; } @@ -98,36 +97,35 @@ var mw = ( function ( $, undefined ) { * Checks if one or multiple keys exist. * * @param selection {mixed} String key or array of keys to check - * @return {Boolean} Existence of key(s) + * @return {boolean} Existence of key(s) */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { - for ( s = 0; s < selection.length; s += 1 ) { - if ( this.values[selection[s]] === undefined ) { + for ( s = 0; s < selection.length; s++ ) { + if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { return false; } } return true; } - return this.values[selection] !== undefined; + return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** - * Message - * * Object constructor for messages, * similar to the Message class in MediaWiki PHP. + * @class mw.Message * - * @param map Map Instance of mw.Map - * @param key String - * @param parameters Array - * @return Message + * @constructor + * @param {mw.Map} map Message storage + * @param {string} key + * @param {Array} [parameters] */ function Message( map, key, parameters ) { - this.format = 'plain'; + this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); @@ -136,9 +134,13 @@ var mw = ( function ( $, undefined ) { Message.prototype = { /** - * Simple message parser, does $N replacement and nothing else. + * Simple message parser, does $N replacement, HTML-escaping (only for + * 'escaped' format), and nothing else. + * * This may be overridden to provide a more complex message parser. * + * The primary override is in mediawiki.jqueryMsg. + * * This function will not be called for nonexistent messages. */ parser: function () { @@ -152,8 +154,8 @@ var mw = ( function ( $, undefined ) { /** * Appends (does not replace) parameters for replacement to the .parameters property. * - * @param parameters Array - * @return Message + * @param {Array} parameters + * @chainable */ params: function ( parameters ) { var i; @@ -166,25 +168,21 @@ var mw = ( function ( $, undefined ) { /** * Converts message object to it's string form based on the state of format. * - * @return string Message as a string in the current form or <key> if key does not exist. + * @return {string} Message as a string in the current form or `<key>` if key does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use <key> as text if key does not exist - if ( this.format !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped + if ( this.format === 'escaped' || this.format === 'parse' ) { + // format 'escaped' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } - if ( this.format === 'plain' ) { - // @todo FIXME: Although not applicable to core Message, - // Plugins like jQueryMsg should be able to distinguish - // between 'plain' (only variable replacement and plural/gender) - // and actually parsing wikitext to HTML. + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } @@ -193,15 +191,16 @@ var mw = ( function ( $, undefined ) { text = mw.html.escape( text ); } - if ( this.format === 'parse' ) { - text = this.parser(); - } - return text; }, /** - * Changes format to parse and converts message to string + * Changes format to 'parse' and converts message to string + * + * If jqueryMsg is loaded, this parses the message text from wikitext + * (where supported) to HTML + * + * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ @@ -211,7 +210,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to plain and converts message to string + * Changes format to 'plain' and converts message to string + * + * This substitutes parameters, but otherwise does not change the + * message text. * * @return {string} String form of plain message */ @@ -221,7 +223,23 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the format to html escaped and converts message to string + * Changes format to 'text' and converts message to string + * + * If jqueryMsg is loaded, {{-transformation is done where supported + * (such as {{plural:}}, {{gender:}}, {{int:}}). + * + * Otherwise, it is equivalent to plain. + */ + text: function () { + this.format = 'text'; + return this.toString(); + }, + + /** + * Changes the format to 'escaped' and converts message to string + * + * This is equivalent to using the 'text' format (see text method), then + * HTML-escaping the output. * * @return {string} String form of html escaped message */ @@ -233,13 +251,19 @@ var mw = ( function ( $, undefined ) { /** * Checks if message exists * - * @return {string} String form of parsed message + * @see mw.Map#exists + * @return {boolean} */ exists: function () { return this.map.exists( this.key ); } }; + /** + * @class mw + * @alternateClassName mediaWiki + * @singleton + */ return { /* Public Members */ @@ -249,77 +273,72 @@ var mw = ( function ( $, undefined ) { */ log: function () { }, - /** - * @var constructor Make the Map constructor publicly available. - */ + // Make the Map constructor publicly available. Map: Map, - /** - * @var constructor Make the Message constructor publicly available. - */ + // Make the Message constructor publicly available. Message: Message, /** * List of configuration values * * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). - * If $wgLegacyJavaScriptGlobals is true, this Map will have its values + * If `$wgLegacyJavaScriptGlobals` is true, this Map will have its values * in the global window object. + * @property */ config: null, /** - * @var object - * * Empty object that plugins can be installed in. + * @property */ libs: {}, /* Extension points */ + /** + * @property + */ legacy: {}, /** * Localization system + * @property {mw.Map} */ messages: new Map(), /* Public Methods */ /** - * Gets a message object, similar to wfMessage() + * Gets a message object, similar to wfMessage(). * - * @param key string Key of message to get - * @param parameter_1 mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return Message + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {mw.Message} */ - message: function ( key, parameter_1 /* [, parameter_2] */ ) { - var parameters; - // Support variadic arguments - if ( parameter_1 !== undefined ) { - parameters = slice.call( arguments ); - parameters.shift(); - } else { - parameters = []; - } + message: function ( key ) { + // Variadic arguments + var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** * 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. + * @see mw.Message#toString + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {string} */ - msg: function ( /* key, parameter_1, parameter_2, .. */ ) { + msg: function ( /* key, parameters... */ ) { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Client-side module loader which integrates with the MediaWiki ResourceLoader + * @class mw.loader + * @singleton */ loader: ( function () { @@ -338,29 +357,32 @@ var mw = ( function ( $, undefined ) { * mw.loader.implement. * * Format: - * { - * 'moduleName': { - * 'version': ############## (unix timestamp), - * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} - * 'group': 'somegroup', (or) null, - * 'source': 'local', 'someforeignwiki', (or) null - * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' - * 'script': ..., - * 'style': ..., - * 'messages': { 'key': 'value' }, - * } - * } + * { + * 'moduleName': { + * 'version': ############## (unix timestamp), + * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} + * 'group': 'somegroup', (or) null, + * 'source': 'local', 'someforeignwiki', (or) null + * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing' + * 'script': ..., + * 'style': ..., + * 'messages': { 'key': 'value' }, + * } + * } + * + * @property + * @private */ var registry = {}, - /** - * Mapping of sources, keyed by source-id, values are objects. - * Format: - * { - * 'sourceId': { - * 'loadScript': 'http://foo.bar/w/load.php' - * } - * } - */ + // + // Mapping of sources, keyed by source-id, values are objects. + // Format: + // { + // 'sourceId': { + // 'loadScript': 'http://foo.bar/w/load.php' + // } + // } + // sources = {}, // List of modules which will be loaded as when ready batch = [], @@ -369,7 +391,11 @@ var mw = ( function ( $, undefined ) { // List of callback functions waiting for modules to be ready to be called jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! - $marker = null; + $marker = null, + // Buffer for addEmbeddedCSS. + cssBuffer = '', + // Callbacks for addEmbeddedCSS. + cssCallbacks = $.Callbacks(); /* Private methods */ @@ -392,10 +418,11 @@ var mw = ( function ( $, undefined ) { /** * 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 + * @private + * @param {string} text CSS text + * @param {Mixed} [nextnode] An Element or jQuery object for an element where + * the style tag should be inserted before. Otherwise appended to the `<head>`. + * @return {HTMLElement} Node reference to the created `<style>` tag. */ function addStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); @@ -429,76 +456,107 @@ var mw = ( function ( $, undefined ) { } /** - * 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). + * Checks whether it is safe to add this css to a stylesheet. + * + * @private + * @param {string} cssText + * @return {boolean} False if a new one must be created. */ - function canExpandStylesheetWith( $style, cssText ) { + function canExpandStylesheetWith( cssText ) { + // 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). return cssText.indexOf( '@import' ) === -1; } - function addEmbeddedCSS( cssText ) { + /** + * @param {string} [cssText=cssBuffer] If called without cssText, + * the internal buffer will be inserted instead. + * @param {Function} [callback] + */ + function addEmbeddedCSS( cssText, callback ) { 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 ) ) ); + + if ( callback ) { + cssCallbacks.add( callback ); + } + + // Yield once before inserting the <style> tag. There are likely + // more calls coming up which we can combine this way. + // Appending a stylesheet and waiting for the browser to repaint + // is fairly expensive, this reduces it (bug 45810) + if ( cssText ) { + // Be careful not to extend the buffer with css that needs a new stylesheet + if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) { + // Linebreak for somewhat distinguishable sections + // (the rl-cachekey comment separating each) + cssBuffer += '\n' + cssText; + // TODO: Use requestAnimationFrame in the future which will + // perform even better by not injecting styles while the browser + // is paiting. + setTimeout( function () { + // Can't pass addEmbeddedCSS to setTimeout directly because Firefox + // (below version 13) has the non-standard behaviour of passing a + // numerical "lateness" value as first argument to this callback + // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/ + addEmbeddedCSS(); + } ); + return; } + + // This is a delayed call and we got a buffer still + } else if ( cssBuffer ) { + cssText = cssBuffer; + cssBuffer = ''; } else { - $( addStyleTag( cssText, getMarker() ) ) - .data( 'ResourceLoaderDynamicStyleTag', true ); + // This is a delayed call, but buffer is already cleared by + // another delayed call. + return; } - } - function compare( a, b ) { - var i; - if ( a.length !== b.length ) { - return false; - } - for ( i = 0; i < b.length; i += 1 ) { - if ( $.isArray( a[i] ) ) { - if ( !compare( a[i], b[i] ) ) { - return false; + // By default, always create a new <style>. Appending text + // to a <style> tag means the contents have to be re-parsed (bug 45810). + // Except, of course, in IE below 9, in there we default to + // re-using and appending to a <style> tag due to the + // IE stylesheet limit (bug 31676). + if ( 'documentMode' in document && document.documentMode <= 9 ) { + + $style = getMarker().prev(); + // Verify that the the element before Marker actually is a + // <style> tag and one that came from ResourceLoader + // (not some other style tag or even a `<meta>` or `<script>`). + if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { + // 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 ) ) ); } - } - if ( a[i] !== b[i] ) { - return false; + cssCallbacks.fire().empty(); + return; } } - return true; + + $( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true ); + + cssCallbacks.fire().empty(); } /** * Generates an ISO8601 "basic" string from a UNIX timestamp + * @private */ function formatVersionNumber( timestamp ) { - var pad = function ( a, b, c ) { - return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); - }, - d = new Date(); + var d = new Date(); + function pad( a, b, c ) { + return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); + } d.setTime( timestamp * 1000 ); return [ pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', @@ -509,15 +567,16 @@ var mw = ( function ( $, undefined ) { /** * Resolves dependencies and detects circular references. * - * @param module String Name of the top-level module whose dependencies shall be + * @private + * @param {string} module 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 + * @param {Array} resolved 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 + * @param {Object} [unresolved] 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 + * @throws {Error} If any unregistered module or a dependency loop is encountered */ function sortDependencies( module, resolved, unresolved ) { var n, deps, len; @@ -566,9 +625,10 @@ var mw = ( function ( $, undefined ) { * 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 + * @private + * @param {string} module Module name or array of string module names + * @return {Array} list of dependencies, including 'module'. + * @throws {Error} If circular reference is detected */ function resolve( module ) { var m, resolved; @@ -597,10 +657,11 @@ var mw = ( function ( $, undefined ) { * One can also filter for 'unregistered', which will return the * modules names that don't have a registry entry. * - * @param states string or array of strings of module states to filter by - * @param modules array list of module names to filter (optional, by default the entire + * @private + * @param {string|string[]} states Module states to filter by + * @param {Array} modules List of module names to filter (optional, by default the entire * registry is used) - * @return array list of filtered module names + * @return {Array} List of filtered module names */ function filter( states, modules ) { var list, module, s, m; @@ -642,9 +703,9 @@ var mw = ( function ( $, undefined ) { * 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 + * @private + * @param {Array} dependencies 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; @@ -656,8 +717,9 @@ var mw = ( function ( $, undefined ) { * 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. * - * @param msg String text for the log entry. - * @param e Error [optional] to also log. + * @private + * @param {string} msg text for the log entry. + * @param {Error} [e] */ function log( msg, e ) { var console = window.console; @@ -679,7 +741,8 @@ var mw = ( function ( $, undefined ) { * 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'. + * @private + * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'. */ function handlePending( module ) { var j, job, hasErrors, m, stateChange; @@ -712,7 +775,7 @@ var mw = ( function ( $, undefined ) { j -= 1; try { if ( hasErrors ) { - throw new Error ("Module " + module + " failed."); + throw new Error( 'Module ' + module + ' failed.'); } else { if ( $.isFunction( job.ready ) ) { job.ready(); @@ -747,8 +810,9 @@ var mw = ( function ( $, undefined ) { * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, * 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 + * @private + * @param {string} src URL to script, will be used as the src attribute in the script tag + * @param {Function} [callback] Callback which will be run when the script is done */ function addScript( src, callback, async ) { /*jshint evil:true */ @@ -758,16 +822,20 @@ var mw = ( function ( $, undefined ) { // 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. + // Can't use jQuery.getScript because that only uses <script> for cross-domain, + // it uses XHR and eval for same-domain scripts, which we don't want because it + // messes up line numbers. + // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js) + + // IE-safe way of getting the <head>. document.head isn't supported + // in old IE, and doesn't work when in the <head>. + head = document.getElementsByTagName( 'head' )[0] || document.body; + script = document.createElement( 'script' ); - script.setAttribute( 'src', src ); - script.setAttribute( 'type', 'text/javascript' ); + script.async = true; + script.src = src; if ( $.isFunction( callback ) ) { - // Attach handlers for all browsers (based on jQuery.ajax) script.onload = script.onreadystatechange = function () { - if ( !done && ( @@ -775,24 +843,20 @@ var mw = ( function ( $, undefined ) { || /loaded|complete/.test( script.readyState ) ) ) { - done = true; - callback(); + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; - // Handle memory leak in IE. This seems to fail in - // IE7 sometimes (Permission Denied error when - // accessing script.parentNode) so wrap it in - // a try catch. - try { - script.onload = script.onreadystatechange = null; - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = undefined; - } catch ( e ) { } + // Remove the script + if ( script.parentNode ) { + script.parentNode.removeChild( script ); + } + + // Dereference the script + script = undefined; + + callback(); } }; } @@ -800,20 +864,17 @@ var mw = ( function ( $, undefined ) { 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, + // 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 ); } ); + // browser makes it impossible. + $( 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> - head = document.getElementsByTagName( 'head' )[0]; - ( document.body || head ).appendChild( script ); + head.appendChild( script ); } } else { - document.write( mw.html.element( - 'script', { 'type': 'text/javascript', 'src': src }, '' - ) ); + document.write( mw.html.element( 'script', { 'src': src }, '' ) ); if ( $.isFunction( callback ) ) { // Document.write is synchronous, so this is called when it's done // FIXME: that's a lie. doc.write isn't actually synchronous @@ -825,10 +886,12 @@ var mw = ( function ( $, undefined ) { /** * Executes a loaded module, making it ready to use * - * @param module string module name to execute + * @private + * @param {string} module Module name to execute */ function execute( module ) { - var key, value, media, i, urls, script, markModuleReady, nestedAddScript; + var key, value, media, i, urls, cssHandle, checkCssHandles, + cssHandlesRegistered = false; if ( registry[module] === undefined ) { throw new Error( 'Module has not been registered yet: ' + module ); @@ -837,12 +900,13 @@ var mw = ( function ( $, undefined ) { } else if ( registry[module].state === 'loading' ) { throw new Error( 'Module has not completed loading yet: ' + module ); } else if ( registry[module].state === 'ready' ) { - throw new Error( 'Module has already been loaded: ' + module ); + throw new Error( 'Module has already been executed: ' + module ); } /** * Define loop-function here for efficiency * and to avoid re-using badly scoped variables. + * @ignore */ function addLink( media, url ) { var el = document.createElement( 'link' ); @@ -854,6 +918,80 @@ var mw = ( function ( $, undefined ) { el.href = url; } + function runScript() { + var script, markModuleReady, nestedAddScript; + try { + script = registry[module].script; + markModuleReady = function () { + registry[module].state = 'ready'; + handlePending( module ); + }; + nestedAddScript = function ( arr, callback, async, i ) { + // Recursively call addScript() in its own callback + // for each element of arr. + if ( i >= arr.length ) { + // We're at the end of the array + callback(); + return; + } + + addScript( arr[i], function () { + nestedAddScript( arr, callback, async, i + 1 ); + }, async ); + }; + + if ( $.isArray( script ) ) { + nestedAddScript( script, markModuleReady, registry[module].async, 0 ); + } else if ( $.isFunction( script ) ) { + registry[module].state = 'ready'; + script( $ ); + 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 + log( 'Exception thrown by ' + module + ': ' + e.message, e ); + registry[module].state = 'error'; + handlePending( module ); + } + } + + // This used to be inside runScript, but since that is now fired asychronously + // (after CSS is loaded) we need to set it here right away. It is crucial that + // when execute() is called this is set synchronously, otherwise modules will get + // executed multiple times as the registry will state that it isn't loading yet. + registry[module].state = 'loading'; + + // Add localizations to message system + if ( $.isPlainObject( registry[module].messages ) ) { + mw.messages.set( registry[module].messages ); + } + + // Make sure we don't run the scripts until all (potentially asynchronous) + // stylesheet insertions have completed. + ( function () { + var pending = 0; + checkCssHandles = function () { + // cssHandlesRegistered ensures we don't take off too soon, e.g. when + // one of the cssHandles is fired while we're still creating more handles. + if ( cssHandlesRegistered && pending === 0 && runScript ) { + runScript(); + runScript = undefined; // Revoke + } + }; + cssHandle = function () { + var check = checkCssHandles; + pending++; + return function () { + if (check) { + pending--; + check(); + check = undefined; // Revoke + } + }; + }; + }() ); + // Process styles (see also mw.loader.implement) // * back-compat: { <media>: css } // * back-compat: { <media>: [url, ..] } @@ -872,7 +1010,7 @@ var mw = ( function ( $, undefined ) { // 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 ); + addEmbeddedCSS( value, cssHandle() ); } else { // back-compat: { <media>: [url, ..] } media = key; @@ -889,7 +1027,7 @@ var mw = ( function ( $, undefined ) { addLink( media, value[i] ); } else if ( key === 'css' ) { // { "css": [css, ..] } - addEmbeddedCSS( value[i] ); + addEmbeddedCSS( value[i], cssHandle() ); } } // Not an array, but a regular object @@ -906,61 +1044,24 @@ var mw = ( function ( $, undefined ) { } } - // 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 () { - registry[module].state = 'ready'; - handlePending( module ); - }; - nestedAddScript = function ( arr, callback, async, i ) { - // Recursively call addScript() in its own callback - // for each element of arr. - if ( i >= arr.length ) { - // We're at the end of the array - callback(); - return; - } - - 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( $ ); - 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 - log( 'Exception thrown by ' + module + ': ' + e.message, e ); - registry[module].state = 'error'; - handlePending( module ); - } + // Kick off. + cssHandlesRegistered = true; + checkCssHandles(); } /** * Adds a dependencies to the queue with optional callbacks to be run * when the dependencies are ready or fail * - * @param dependencies string module name or array of string module names - * @param ready function callback to execute when all dependencies are ready - * @param error function callback to execute when any dependency fails - * @param async (optional) If true, load modules asynchronously even if - * document ready has not yet occurred + * @private + * @param {string|string[]} dependencies Module name or array of string module names + * @param {Function} [ready] Callback to execute when all dependencies are ready + * @param {Function} [error] Callback to execute when any dependency fails + * @param {boolean} [async] If true, load modules asynchronously even if + * document ready has not yet occurred. */ function request( dependencies, ready, error, async ) { - var regItemDeps, regItemDepLen, n; + var n; // Allow calling by single module name if ( typeof dependencies === 'string' ) { @@ -1012,6 +1113,7 @@ var mw = ( function ( $, undefined ) { /** * 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 + * @private */ function buildModulesString( moduleMap ) { var arr = [], p, prefix; @@ -1025,14 +1127,15 @@ var mw = ( function ( $, undefined ) { /** * Asynchronously append a script tag to the end of the body * that invokes load.php - * @param moduleMap {Object}: Module map, see buildModulesString() - * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request - * @param sourceLoadScript {String}: URL of load.php - * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred + * @private + * @param {Object} moduleMap Module map, see #buildModulesString + * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request + * @param {string} sourceLoadScript URL of load.php + * @param {boolean} async If true, use an asynchrounous request even if document ready has not yet occurred */ function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { var request = $.extend( - { 'modules': buildModulesString( moduleMap ) }, + { modules: buildModulesString( moduleMap ) }, currReqBase ); request = sortQuery( request ); @@ -1127,9 +1230,9 @@ var mw = ( function ( $, undefined ) { } } - currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase ); + currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase ); // For user modules append a user name to the request. - if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) { + if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) { currReqBase.user = mw.config.get( 'wgUserName' ); } currReqBaseLength = $.param( currReqBase ).length; @@ -1183,10 +1286,10 @@ var mw = ( function ( $, undefined ) { /** * Register a source. * - * @param id {String}: Short lowercase a-Z string representing a source, only used internally. - * @param props {Object}: Object containing only the loadScript property which is a url to - * the load.php location of the source. - * @return {Boolean} + * @param {string} id Short lowercase a-Z string representing a source, only used internally. + * @param {Object} props Object containing only the loadScript property which is a url to + * the load.php location of the source. + * @return {boolean} */ addSource: function ( id, props ) { var source; @@ -1242,15 +1345,15 @@ var mw = ( function ( $, undefined ) { } // List the module as registered registry[module] = { - 'version': version !== undefined ? parseInt( version, 10 ) : 0, - 'dependencies': [], - 'group': typeof group === 'string' ? group : null, - 'source': typeof source === 'string' ? source: 'local', - 'state': 'registered' + version: version !== undefined ? parseInt( version, 10 ) : 0, + dependencies: [], + group: typeof group === 'string' ? group : null, + source: typeof source === 'string' ? source: 'local', + state: 'registered' }; if ( typeof dependencies === 'string' ) { // Allow dependencies to be given as a single module name - registry[module].dependencies = [dependencies]; + registry[module].dependencies = [ dependencies ]; } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) { // Allow dependencies to be given as an array of module names // or a function which returns an array @@ -1265,20 +1368,20 @@ var mw = ( function ( $, undefined ) { * * All arguments are required. * - * @param {String} module Name of module + * @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. + * 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, ..] } + * { "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). + * 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 + * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}. */ implement: function ( module, script, style, msgs ) { // Validate input @@ -1331,7 +1434,7 @@ var mw = ( function ( $, undefined ) { } // Allow calling with a single dependency as a string if ( tod === 'string' ) { - dependencies = [dependencies]; + dependencies = [ dependencies ]; } // Resolve entire dependency map dependencies = resolve( dependencies ); @@ -1366,7 +1469,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, module; + var filtered, m, module, l; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { @@ -1381,11 +1484,13 @@ var mw = ( function ( $, undefined ) { async = true; } if ( type === 'text/css' ) { - $( 'head' ).append( $( '<link>', { - rel: 'stylesheet', - type: 'text/css', - href: modules - } ) ); + // IE7-8 throws security warnings when inserting a <link> tag + // with a protocol-relative URL set though attributes (instead of + // properties) - when on HTTPS. See also bug #. + l = document.createElement( 'link' ); + l.rel = 'stylesheet'; + l.href = modules; + $( 'head' ).append( l ); return; } if ( type === 'text/javascript' || type === undefined ) { @@ -1396,7 +1501,7 @@ var mw = ( function ( $, undefined ) { throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type ); } // Called with single module - modules = [modules]; + modules = [ modules ]; } // Filter out undefined modules, otherwise resolve() will throw @@ -1427,7 +1532,7 @@ var mw = ( function ( $, undefined ) { return; } // Since some modules are not yet ready, queue up a request. - request( filtered, null, null, async ); + request( filtered, undefined, undefined, async ); }, /** @@ -1448,7 +1553,7 @@ var mw = ( function ( $, undefined ) { if ( registry[module] === undefined ) { mw.loader.register( module ); } - if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1 + 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! @@ -1510,11 +1615,15 @@ var mw = ( function ( $, undefined ) { }; }() ), - /** HTML construction helper functions */ + /** + * HTML construction helper functions + * @class mw.html + * @singleton + */ html: ( function () { function escapeCallback( s ) { switch ( s ) { - case "'": + case '\'': return '''; case '"': return '"'; @@ -1530,7 +1639,7 @@ var mw = ( function ( $, undefined ) { return { /** * Escape a string for HTML. Converts special characters to HTML entities. - * @param s The string to escape + * @param {string} s The string to escape */ escape: function ( s ) { return s.replace( /['"<>&]/g, escapeCallback ); @@ -1538,7 +1647,7 @@ var mw = ( function ( $, undefined ) { /** * Wrapper object for raw HTML passed to mw.html.element(). - * @constructor + * @class mw.html.Raw */ Raw: function ( value ) { this.value = value; @@ -1546,7 +1655,7 @@ var mw = ( function ( $, undefined ) { /** * Wrapper object for CDATA element contents passed to mw.html.element() - * @constructor + * @class mw.html.Cdata */ Cdata: function ( value ) { this.value = value; diff --git a/resources/mediawiki/mediawiki.log.js b/resources/mediawiki/mediawiki.log.js index 4ea1a881..ee08b12b 100644 --- a/resources/mediawiki/mediawiki.log.js +++ b/resources/mediawiki/mediawiki.log.js @@ -41,7 +41,7 @@ ':' + ( d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds() ) + '.' + ( d.getMilliseconds() < 10 ? '00' + d.getMilliseconds() : ( d.getMilliseconds() < 100 ? '0' + d.getMilliseconds() : d.getMilliseconds() ) ), $log = $( '#mw-log-console' ); - + if ( !$log.length ) { $log = $( '<div id="mw-log-console"></div>' ).css( { overflow: 'auto', diff --git a/resources/mediawiki/mediawiki.notification.js b/resources/mediawiki/mediawiki.notification.js index 58a3ab6a..fd34e7ee 100644 --- a/resources/mediawiki/mediawiki.notification.js +++ b/resources/mediawiki/mediawiki.notification.js @@ -1,24 +1,24 @@ -/** - * Implements mediaWiki.notification library - */ ( function ( mw, $ ) { 'use strict'; - var isPageReady = false, - isInitialized = false, + var notification, + isPageReady = false, preReadyNotifQueue = [], - /** - * @var {jQuery} - * The #mw-notification-area div that all notifications are contained inside. - */ + // 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()). + * Does not insert anything into the document (see #start). + * + * The "_" in the name is to avoid a bug (http://github.com/senchalabs/jsduck/issues/304) + * It is not part of the actual class name. + * + * @class mw.Notification_ + * @alternateClassName mw.Notification + * @private * * @constructor - * @see mw.notification.notify */ function Notification( message, options ) { var $notification, $notificationTitle, $notificationContent; @@ -88,7 +88,9 @@ // Other notification elements matching the same tag $tagMatches, outerHeight, - placeholderHeight; + placeholderHeight, + autohideCount, + notif; if ( this.isOpen ) { return; @@ -164,10 +166,11 @@ } } ); + notif = this; + // 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>' ) + notif.$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 @@ -181,7 +184,7 @@ // 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 ) { + if ( notif.$replacementPlaceholder ) { $notification.css( 'position', '' ); } // Finally, remove the placeholder from the DOM @@ -206,7 +209,7 @@ // 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; + autohideCount = $area.find( '.mw-notification-autohide' ).length; if ( autohideCount <= notification.autoHideLimit ) { this.resume(); } @@ -253,6 +256,7 @@ * * @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. */ @@ -326,7 +330,7 @@ /** * Helper function, take a list of notification divs and call - * a function on the Notification instance attached to them + * 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 @@ -341,40 +345,43 @@ } /** - * Initialisation - * (don't call before document ready) + * Initialisation. + * Must only be called once, and not before the document is ready. + * @ignore */ 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 ); - } + $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 = { + /** + * @class mw.notification + * @singleton + */ + notification = { /** * Pause auto-hide timers for all notifications. * Notifications will not auto-hide until resume is called. + * @see mw.Notification#pause */ pause: function () { callEachNotification( @@ -385,13 +392,13 @@ /** * Resume any paused auto-hide timers from the beginning. - * Only the first {autoHideLimit} timers will be resumed. + * 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 + // 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' @@ -401,10 +408,9 @@ /** * 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 {HTMLElement|jQuery|mw.Message|string} message * @param {Object} options The options to use for the notification. - * See mw.notification.defaults for details. + * See #defaults for details. */ notify: function ( message, options ) { var notif; @@ -420,22 +426,23 @@ }, /** - * @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. + * @property {Object} + * The defaults for #notify 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. + * - 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. + * - title: + * An optional title for the notification. Will be displayed above the + * content. Usually in bold. */ defaults: { autoHide: true, @@ -444,20 +451,20 @@ }, /** - * @var {number} + * @property {number} * Number of seconds to wait before auto-hiding notifications. */ autoHideSeconds: 5, /** - * @var {number} + * @property {number} * Maximum number of notifications to count down auto-hide timers for. - * Only the first {autoHideLimit} notifications being displayed will + * 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. + * be able to process in #autoHideSeconds time. */ autoHideLimit: 3 }; diff --git a/resources/mediawiki/mediawiki.notify.js b/resources/mediawiki/mediawiki.notify.js index 3bf2a896..83d95b61 100644 --- a/resources/mediawiki/mediawiki.notify.js +++ b/resources/mediawiki/mediawiki.notify.js @@ -1,11 +1,13 @@ /** - * Implements mediaWiki.notify function + * @class mw.plugin.notify */ ( function ( mw ) { 'use strict'; /** - * @see mw.notification.notify + * @see mw.notification#notify + * @param message + * @param options */ mw.notify = function ( message, options ) { // Don't bother loading the whole notification system if we never use it. @@ -17,4 +19,9 @@ } ); }; -}( mediaWiki ) );
\ No newline at end of file + /** + * @class mw + * @mixins mw.plugin.notify + */ + +}( mediaWiki ) ); diff --git a/resources/mediawiki/mediawiki.searchSuggest.css b/resources/mediawiki/mediawiki.searchSuggest.css new file mode 100644 index 00000000..0fb862b9 --- /dev/null +++ b/resources/mediawiki/mediawiki.searchSuggest.css @@ -0,0 +1,16 @@ +/* Make sure the links are not underlined or colored, ever. */ +/* There is already a :focus / :hover indication on the <div>. */ +.suggestions a.mw-searchSuggest-link, +.suggestions a.mw-searchSuggest-link:hover, +.suggestions a.mw-searchSuggest-link:active, +.suggestions a.mw-searchSuggest-link:focus { + text-decoration: none; + color: black; +} + +.suggestions-result-current a.mw-searchSuggest-link, +.suggestions-result-current a.mw-searchSuggest-link:hover, +.suggestions-result-current a.mw-searchSuggest-link:active, +.suggestions-result-current a.mw-searchSuggest-link:focus { + color: white; +} diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js index 99a55576..2bc7cea9 100644 --- a/resources/mediawiki/mediawiki.searchSuggest.js +++ b/resources/mediawiki/mediawiki.searchSuggest.js @@ -3,7 +3,7 @@ */ ( function ( mw, $ ) { $( document ).ready( function ( $ ) { - var map, searchboxesSelectors, + var map, resultRenderCache, searchboxesSelectors, // Region where the suggestions box will appear directly below // (using the same width). Can be a container element or the input // itself, depending on what suits best in the environment. @@ -41,6 +41,91 @@ return; } + // Compute form data for search suggestions functionality. + function computeResultRenderCache( context ) { + var $form, formAction, baseHref, linkParams; + + // Compute common parameters for links' hrefs + $form = context.config.$region.closest( 'form' ); + + formAction = $form.attr( 'action' ); + baseHref = formAction + ( formAction.match(/\?/) ? '&' : '?' ); + + linkParams = {}; + $.each( $form.serializeArray(), function ( idx, obj ) { + linkParams[ obj.name ] = obj.value; + } ); + + return { + textParam: context.data.$textbox.attr( 'name' ), + linkParams: linkParams, + baseHref: baseHref + }; + } + + // The function used to render the suggestions. + function renderFunction( text, context ) { + if ( !resultRenderCache ) { + resultRenderCache = computeResultRenderCache( context ); + } + + // linkParams object is modified and reused + resultRenderCache.linkParams[ resultRenderCache.textParam ] = text; + + // this is the container <div>, jQueryfied + this + .append( + // the <span> is needed for $.autoEllipsis to work + $( '<span>' ) + .css( 'whiteSpace', 'nowrap' ) + .text( text ) + ) + .wrap( + $( '<a>' ) + .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) ) + .addClass( 'mw-searchSuggest-link' ) + ); + } + + function specialRenderFunction( query, context ) { + var $el = this; + + if ( !resultRenderCache ) { + resultRenderCache = computeResultRenderCache( context ); + } + + // linkParams object is modified and reused + resultRenderCache.linkParams[ resultRenderCache.textParam ] = query; + + 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(); + } + + if ( $el.parent().hasClass( 'mw-searchSuggest-link' ) ) { + $el.parent().attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' ); + } else { + $el.wrap( + $( '<a>' ) + .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' ) + .addClass( 'mw-searchSuggest-link' ) + ); + } + } + // General suggestions functionality for all search boxes searchboxesSelectors = [ // Primary searchbox on every page in standard skins @@ -89,6 +174,7 @@ } }, result: { + render: renderFunction, select: function ( $input ) { $input.closest( 'form' ).submit(); } @@ -118,31 +204,13 @@ // Special suggestions functionality for skin-provided search box $searchInput.suggestions( { result: { + render: renderFunction, 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(); - } - }, + render: specialRenderFunction, select: function ( $input ) { $input.closest( 'form' ).append( $( '<input type="hidden" name="fulltext" value="1"/>' ) diff --git a/resources/mediawiki/mediawiki.user.js b/resources/mediawiki/mediawiki.user.js index e64d2e84..e0329597 100644 --- a/resources/mediawiki/mediawiki.user.js +++ b/resources/mediawiki/mediawiki.user.js @@ -61,7 +61,7 @@ * * @return String: Random set of 32 alpha-numeric characters */ - function generateId() { + this.generateRandomSessionId = function () { var i, r, id = '', seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -70,7 +70,7 @@ id += seed.substring( r, r + 1 ); } return id; - } + }; /** * Gets the current user's name. @@ -89,6 +89,25 @@ }; /** + * Get date user registered, if available. + * + * @return {Date|false|null} date user registered, or false for anonymous users, or + * null when data is not available + */ + this.getRegistration = function () { + var registration = mw.config.get( 'wgUserRegistration' ); + if ( this.isAnon() ) { + return false; + } else if ( registration === null ) { + // Information may not be available if they signed up before + // MW began storing this. + return null; + } else { + return new Date( registration ); + } + }; + + /** * Checks if the current user is anonymous. * * @return Boolean @@ -115,7 +134,7 @@ this.sessionId = function () { var sessionId = $.cookie( 'mediaWiki.user.sessionId' ); if ( typeof sessionId === 'undefined' || sessionId === null ) { - sessionId = generateId(); + sessionId = user.generateRandomSessionId(); $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': null, 'path': '/' } ); } return sessionId; diff --git a/resources/mediawiki/mediawiki.util.js b/resources/mediawiki/mediawiki.util.js index 29284384..5211b0d0 100644 --- a/resources/mediawiki/mediawiki.util.js +++ b/resources/mediawiki/mediawiki.util.js @@ -1,10 +1,11 @@ -/** - * Implements mediaWiki.util library - */ ( function ( mw, $ ) { 'use strict'; - // Local cache and alias + /** + * Utility library + * @class mw.util + * @singleton + */ var util = { /** @@ -28,13 +29,10 @@ profile.platform === 'mac' // Chrome on Mac ? 'ctrl-option-' - : profile.platform === 'win' - // Chrome on Windows - // (both alt- and alt-shift work, but alt-f triggers Chrome wrench menu - // which alt-shift-f does not) - ? 'alt-shift-' - // Chrome on other (Ubuntu?) - : 'alt-' + // Chrome on Windows or Linux + // (both alt- and alt-shift work, but alt with E, D, F etc does not + // work since they are browser shortcuts) + : 'alt-shift-' ); // Non-Windows Safari with webkit_version > 526 @@ -62,7 +60,8 @@ /* Fill $content var */ util.$content = ( function () { - var $content, selectors = [ + var i, l, $content, selectors; + 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. @@ -94,7 +93,7 @@ // not inserted bodytext yet. But in any case <body> should always exist 'body' ]; - for ( var i = 0, l = selectors.length; i < l; i++ ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { $content = $( selectors[i] ).first(); if ( $content.length ) { return $content; @@ -136,7 +135,7 @@ /** * Encode the string like PHP's rawurlencode * - * @param str string String to be encoded + * @param {string} str String to be encoded. */ rawurlencode: function ( str ) { str = String( str ); @@ -150,7 +149,7 @@ * We want / and : to be included as literal characters in our title URLs * as they otherwise fatally break the title * - * @param str string String to be encoded + * @param {string} str String to be encoded. */ wikiUrlencode: function ( str ) { return util.rawurlencode( str ) @@ -158,10 +157,10 @@ }, /** - * Get the link to a page name (relative to wgServer) + * Get the link to a page name (relative to `wgServer`), * - * @param str String: Page name to get the link for. - * @return String: Location for a page with name of 'str' or boolean false on error. + * @param {string} str Page name to get the link for. + * @return {string} Location for a page with name of `str` or boolean false on error. */ wikiGetlink: function ( str ) { return mw.config.get( 'wgArticlePath' ).replace( '$1', @@ -170,7 +169,7 @@ /** * Get address to a script in the wiki root. - * For index.php use mw.config.get( 'wgScript' ) + * For index.php use `mw.config.get( 'wgScript' )`. * * @since 1.18 * @param str string Name of script (eg. 'api'), defaults to 'index' @@ -190,20 +189,18 @@ /** * 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. + * 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 (use .ownerNode to get to the <style> element) + * var sheet = mw.util.addCSS('.foobar { display: none; }'); + * $(foo).click(function () { + * // Toggle the sheet on and off + * sheet.disabled = !sheet.disabled; + * }); + * + * @param {string} text CSS to be appended + * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element. */ addCSS: function ( text ) { var s = mw.loader.addStyleTag( text ); @@ -213,10 +210,10 @@ /** * Hide/show the table of contents element * - * @param $toggleLink jQuery A jQuery object of the toggle link. - * @param callback function Function to be called after the toggle is - * completed (including the animation) (optional) - * @return mixed Boolean visibility of the toc (true if it's visible) + * @param {jQuery} $toggleLink A jQuery object of the toggle link. + * @param {Function} [callback] Function to be called after the toggle is + * completed (including the animation). + * @return {Mixed} Boolean visibility of the toc (true if it's visible) * or Null if there was no table of contents. */ toggleToc: function ( $toggleLink, callback ) { @@ -253,12 +250,14 @@ * Grab the URL parameter value for the given parameter. * Returns null if not found. * - * @param param string The parameter name. - * @param url string URL to search through (optional) - * @return mixed Parameter value or null. + * @param {string} param The parameter name. + * @param {string} [url] URL to search through. + * @return {Mixed} Parameter value or null. */ getParamValue: function ( param, url ) { - url = url || document.location.href; + if ( url === undefined ) { + url = document.location.href; + } // Get last match, stop at hash var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ), m = re.exec( url ); @@ -271,14 +270,14 @@ }, /** - * @var string + * @property {string} * Access key prefix. Will be re-defined based on browser/operating system - * detection in mw.util.init(). + * detection in mw.util#init. */ tooltipAccessKeyPrefix: 'alt-', /** - * @var RegExp + * @property {RegExp} * Regex to match accesskey tooltips. */ tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/, @@ -289,8 +288,7 @@ * otherwise, all the nodes that will probably have accesskeys by * default are updated. * - * @param $nodes {Array|jQuery} [optional] A jQuery object, or array - * of elements to update. + * @param {Array|jQuery} [$nodes] A jQuery object, or array of nodes to update. */ updateTooltipAccessKeys: function ( $nodes ) { if ( !$nodes ) { @@ -312,9 +310,9 @@ }, /* - * @var jQuery - * A jQuery object that refers to the content area element - * Populated by init(). + * @property {jQuery} + * A jQuery object that refers to the content area element. + * Populated by #init. */ $content: null, @@ -329,28 +327,28 @@ * * By default the new link will be added to the end of the list. To * add the link before a given existing item, pass the DOM node - * (document.getElementById( 'foobar' )) or the jQuery-selector - * ( '#foobar' ) of that item. + * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector + * (e.g. `'#foobar'`) for that item. * - * @example mw.util.addPortletLink( - * 'p-tb', 'http://mediawiki.org/', - * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print' - * ) + * mw.util.addPortletLink( + * 'p-tb', 'http://mediawiki.org/', + * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print' + * ); * - * @param portlet string ID of the target portlet ( 'p-cactions' or 'p-personal' etc.) - * @param href string Link URL - * @param text string Link text - * @param id string ID of the new item, should be unique and preferably have - * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' ) - * @param tooltip string Text to show when hovering over the link, without accesskey suffix - * @param accesskey string Access key to activate this link (one character, try - * to avoid conflicts. Use $( '[accesskey=x]' ).get() in the console to - * see if 'x' is already used. - * @param nextnode mixed DOM Node or jQuery-selector string of the item that the new - * item should be added before, should be another item in the same - * list, it will be ignored otherwise + * @param {string} portlet ID of the target portlet ( 'p-cactions' or 'p-personal' etc.) + * @param {string} href Link URL + * @param {string} text Link text + * @param {string} [id] ID of the new item, should be unique and preferably have + * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' ) + * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix + * @param {string} [accesskey] Access key to activate this link (one character, try + * to avoid conflicts. Use `$( '[accesskey=x]' ).get()` in the console to + * see if 'x' is already used. + * @param {HTMLElement|jQuery|string} [nextnode] Element or jQuery-selector string to the item that + * the new item should be added before, should be another item in the same + * list, it will be ignored otherwise * - * @return mixed The DOM Node of the added item (a ListItem or Anchor element, + * @return {HTMLElement|null} The added element (a ListItem or Anchor element, * depending on the skin) or null if no element was added to the document. */ addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) { @@ -370,7 +368,6 @@ // just add it to the bottom of their 'sidebar' element as a fallback switch ( mw.config.get( 'skin' ) ) { case 'standard': - case 'cologneblue': $( '#quickbar' ).append( $link.after( '<br/>' ) ); return $link[0]; case 'nostalgia': @@ -440,7 +437,7 @@ // If the jQuery selector isn't found within the <ul>, // or if nextnode was invalid or not passed at all, - // then just append it at the end of the <ul> (this is the default behaviour) + // then just append it at the end of the <ul> (this is the default behavior) } else { $ul.append( $item ); } @@ -455,9 +452,9 @@ * something, replacing any previous message. * 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 {Mixed} message The DOM-element, jQuery object or HTML-string to be put inside the message box. * to allow CSS/JS to hide different boxes. null = no class used. - * @depreceated Use mw.notify + * @deprecated Use mw#notify */ jsMessage: function ( message ) { if ( !arguments.length || message === '' || message === null ) { @@ -475,87 +472,80 @@ * according to HTML5 specification. Please note the specification * does not validate a domain with one character. * - * @todo FIXME: should be moved to or replaced by a JavaScript validation module. + * FIXME: should be moved to or replaced by a validation module. * - * @param mailtxt string E-mail address to be validated. - * @return mixed Null if mailtxt was an empty string, otherwise true/false - * is determined by validation. + * @param {string} mailtxt E-mail address to be validated. + * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false + * as determined by validation. */ validateEmail: function ( mailtxt ) { - var rfc5322_atext, rfc1034_ldh_str, HTML5_email_regexp; + var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp; if ( mailtxt === '' ) { return null; } - /** - * HTML5 defines a string as valid e-mail address if it matches - * the ABNF: - * 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str ) - * With: - * - atext : defined in RFC 5322 section 3.2.3 - * - ldh-str : defined in RFC 1034 section 3.5 - * - * (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68): - */ - - /** - * First, define the RFC 5322 'atext' which is pretty easy: - * atext = ALPHA / DIGIT / ; Printable US-ASCII - "!" / "#" / ; characters not including - "$" / "%" / ; specials. Used for atoms. - "&" / "'" / - "*" / "+" / - "-" / "/" / - "=" / "?" / - "^" / "_" / - "`" / "{" / - "|" / "}" / - "~" - */ - rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~"; - - /** - * Next define the RFC 1034 'ldh-str' - * <domain> ::= <subdomain> | " " - * <subdomain> ::= <label> | <subdomain> "." <label> - * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] - * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> - * <let-dig-hyp> ::= <let-dig> | "-" - * <let-dig> ::= <letter> | <digit> - */ - rfc1034_ldh_str = "a-z0-9\\-"; - - HTML5_email_regexp = new RegExp( + // HTML5 defines a string as valid e-mail address if it matches + // the ABNF: + // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str ) + // With: + // - atext : defined in RFC 5322 section 3.2.3 + // - ldh-str : defined in RFC 1034 section 3.5 + // + // (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68) + // First, define the RFC 5322 'atext' which is pretty easy: + // atext = ALPHA / DIGIT / ; Printable US-ASCII + // "!" / "#" / ; characters not including + // "$" / "%" / ; specials. Used for atoms. + // "&" / "'" / + // "*" / "+" / + // "-" / "/" / + // "=" / "?" / + // "^" / "_" / + // "`" / "{" / + // "|" / "}" / + // "~" + rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~'; + + // Next define the RFC 1034 'ldh-str' + // <domain> ::= <subdomain> | " " + // <subdomain> ::= <label> | <subdomain> "." <label> + // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] + // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> + // <let-dig-hyp> ::= <let-dig> | "-" + // <let-dig> ::= <letter> | <digit> + rfc1034LdhStr = 'a-z0-9\\-'; + + html5EmailRegexp = new RegExp( // start of string '^' + // User part which is liberal :p - '[' + rfc5322_atext + '\\.]+' + '[' + rfc5322Atext + '\\.]+' + // 'at' '@' + // Domain first part - '[' + rfc1034_ldh_str + ']+' + '[' + rfc1034LdhStr + ']+' + // Optional second part and following are separated by a dot - '(?:\\.[' + rfc1034_ldh_str + ']+)*' + '(?:\\.[' + rfc1034LdhStr + ']+)*' + // End of string '$', // RegExp is case insensitive 'i' ); - return (null !== mailtxt.match( HTML5_email_regexp ) ); + return (null !== mailtxt.match( html5EmailRegexp ) ); }, /** * Note: borrows from IP::isIPv4 * - * @param address string - * @param allowBlock boolean - * @return boolean + * @param {string} address + * @param {boolean} allowBlock + * @return {boolean} */ isIPv4Address: function ( address, allowBlock ) { if ( typeof address !== 'string' ) { @@ -572,9 +562,9 @@ /** * Note: borrows from IP::isIPv6 * - * @param address string - * @param allowBlock boolean - * @return boolean + * @param {string} address + * @param {boolean} allowBlock + * @return {boolean} */ isIPv6Address: function ( address, allowBlock ) { if ( typeof address !== 'string' ) { |