2 * Implements mediaWiki.util library
7 // Local cache and alias
8 var hideMessageTimeout,
9 messageBoxEvents = false,
14 * (don't call before document ready)
17 var profile, $tocTitle, $tocToggleLink, hideTocCookie;
19 /* Set up $.messageBox */
25 /* Set tooltipAccessKeyPrefix */
26 profile = $.client.profile();
28 // Opera on any platform
29 if ( profile.name === 'opera' ) {
30 util.tooltipAccessKeyPrefix = 'shift-esc-';
32 // Chrome on any platform
33 } else if ( profile.name === 'chrome' ) {
35 util.tooltipAccessKeyPrefix = (
36 profile.platform === 'mac'
39 : profile.platform === 'win'
41 // (both alt- and alt-shift work, but alt-f triggers Chrome wrench menu
42 // which alt-shift-f does not)
44 // Chrome on other (Ubuntu?)
48 // Non-Windows Safari with webkit_version > 526
49 } else if ( profile.platform !== 'win'
50 && profile.name === 'safari'
51 && profile.layoutVersion > 526 ) {
52 util.tooltipAccessKeyPrefix = 'ctrl-alt-';
54 } else if ( profile.platform === 'mac'
55 && profile.name === 'firefox'
56 && profile.versionNumber >= 14 ) {
57 util.tooltipAccessKeyPrefix = 'ctrl-option-';
58 // Safari/Konqueror on any platform, or any browser on Mac
59 // (but not Safari on Windows)
60 } else if ( !( profile.platform === 'win' && profile.name === 'safari' )
61 && ( profile.name === 'safari'
62 || profile.platform === 'mac'
63 || profile.name === 'konqueror' ) ) {
64 util.tooltipAccessKeyPrefix = 'ctrl-';
66 // Firefox 2.x and later
67 } else if ( profile.name === 'firefox' && profile.versionBase > '1' ) {
68 util.tooltipAccessKeyPrefix = 'alt-shift-';
71 /* Fill $content var */
72 if ( $( '#bodyContent' ).length ) {
73 // Vector, Monobook, Chick etc.
74 util.$content = $( '#bodyContent' );
76 } else if ( $( '#mw_contentholder' ).length ) {
78 util.$content = $( '#mw_contentholder' );
80 } else if ( $( '#article' ).length ) {
81 // Standard, CologneBlue
82 util.$content = $( '#article' );
85 // #content is present on almost all if not all skins. Most skins (the above cases)
86 // have #content too, but as an outer wrapper instead of the article text container.
87 // The skins that don't have an outer wrapper do have #content for everything
88 // so it's a good fallback
89 util.$content = $( '#content' );
92 // Table of contents toggle
93 $tocTitle = $( '#toctitle' );
94 $tocToggleLink = $( '#togglelink' );
95 // Only add it if there is a TOC and there is no toggle added already
96 if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) {
97 hideTocCookie = $.cookie( 'mw_hidetoc' );
98 $tocToggleLink = $( '<a href="#" class="internal" id="togglelink"></a>' )
99 .text( mw.msg( 'hidetoc' ) )
100 .click( function ( e ) {
102 util.toggleToc( $(this) );
106 .wrap( '<span class="toctoggle"></span>' )
108 .prepend( ' [' )
112 if ( hideTocCookie === '1' ) {
113 util.toggleToc( $tocToggleLink );
121 * Encode the string like PHP's rawurlencode
123 * @param str string String to be encoded
125 rawurlencode: function ( str ) {
127 return encodeURIComponent( str )
128 .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
129 .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' );
133 * Encode page titles for use in a URL
134 * We want / and : to be included as literal characters in our title URLs
135 * as they otherwise fatally break the title
137 * @param str string String to be encoded
139 wikiUrlencode: function ( str ) {
140 return util.rawurlencode( str )
141 .replace( /%20/g, '_' ).replace( /%3A/g, ':' ).replace( /%2F/g, '/' );
145 * Get the link to a page name (relative to wgServer)
147 * @param str String: Page name to get the link for.
148 * @return String: Location for a page with name of 'str' or boolean false on error.
150 wikiGetlink: function ( str ) {
151 return mw.config.get( 'wgArticlePath' ).replace( '$1',
152 util.wikiUrlencode( typeof str === 'string' ? str : mw.config.get( 'wgPageName' ) ) );
156 * Get address to a script in the wiki root.
157 * For index.php use mw.config.get( 'wgScript' )
160 * @param str string Name of script (eg. 'api'), defaults to 'index'
161 * @return string Address to script (eg. '/w/api.php' )
163 wikiScript: function ( str ) {
164 str = str || 'index';
165 if ( str === 'index' ) {
166 return mw.config.get( 'wgScript' );
167 } else if ( str === 'load' ) {
168 return mw.config.get( 'wgLoadScript' );
170 return mw.config.get( 'wgScriptPath' ) + '/' + str +
171 mw.config.get( 'wgScriptExtension' );
176 * Append a new style block to the head and return the CSSStyleSheet object.
177 * Use .ownerNode to access the <style> element, or use mw.loader.addStyleTag.
178 * This function returns the styleSheet object for convience (due to cross-browsers
179 * difference as to where it is located).
182 * var sheet = mw.util.addCSS('.foobar { display: none; }');
183 * $(foo).click(function () {
184 * // Toggle the sheet on and off
185 * sheet.disabled = !sheet.disabled;
189 * @param text string CSS to be appended
190 * @return CSSStyleSheet (use .ownerNode to get to the <style> element)
192 addCSS: function ( text ) {
193 var s = mw.loader.addStyleTag( text );
198 * Hide/show the table of contents element
200 * @param $toggleLink jQuery A jQuery object of the toggle link.
201 * @param callback function Function to be called after the toggle is
202 * completed (including the animation) (optional)
203 * @return mixed Boolean visibility of the toc (true if it's visible)
204 * or Null if there was no table of contents.
206 toggleToc: function ( $toggleLink, callback ) {
207 var $tocList = $( '#toc ul:first' );
209 // This function shouldn't be called if there's no TOC,
210 // but just in case...
211 if ( $tocList.length ) {
212 if ( $tocList.is( ':hidden' ) ) {
213 $tocList.slideDown( 'fast', callback );
214 $toggleLink.text( mw.msg( 'hidetoc' ) );
215 $( '#toc' ).removeClass( 'tochidden' );
216 $.cookie( 'mw_hidetoc', null, {
222 $tocList.slideUp( 'fast', callback );
223 $toggleLink.text( mw.msg( 'showtoc' ) );
224 $( '#toc' ).addClass( 'tochidden' );
225 $.cookie( 'mw_hidetoc', '1', {
237 * Grab the URL parameter value for the given parameter.
238 * Returns null if not found.
240 * @param param string The parameter name.
241 * @param url string URL to search through (optional)
242 * @return mixed Parameter value or null.
244 getParamValue: function ( param, url ) {
245 url = url || document.location.href;
246 // Get last match, stop at hash
247 var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ),
250 // Beware that decodeURIComponent is not required to understand '+'
251 // by spec, as encodeURIComponent does not produce it.
252 return decodeURIComponent( m[1].replace( /\+/g, '%20' ) );
259 * Access key prefix. Will be re-defined based on browser/operating system
260 * detection in mw.util.init().
262 tooltipAccessKeyPrefix: 'alt-',
266 * Regex to match accesskey tooltips.
268 tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/,
271 * Add the appropriate prefix to the accesskey shown in the tooltip.
272 * If the nodeList parameter is given, only those nodes are updated;
273 * otherwise, all the nodes that will probably have accesskeys by
274 * default are updated.
276 * @param $nodes {Array|jQuery} [optional] A jQuery object, or array
277 * of elements to update.
279 updateTooltipAccessKeys: function ( $nodes ) {
281 // Rather than going into a loop of all anchor tags, limit to few elements that
282 // contain the relevant anchor tags.
283 // Input and label are rare enough that no such optimization is needed
284 $nodes = $( '#column-one a, #mw-head a, #mw-panel a, #p-logo a, input, label' );
285 } else if ( !( $nodes instanceof $ ) ) {
286 $nodes = $( $nodes );
289 $nodes.attr( 'title', function ( i, val ) {
290 if ( val && util.tooltipAccessKeyRegexp.exec( val ) ) {
291 return val.replace( util.tooltipAccessKeyRegexp,
292 '[' + util.tooltipAccessKeyPrefix + '$5]' );
300 * A jQuery object that refers to the page-content element
301 * Populated by init().
306 * Add a link to a portlet menu on the page, such as:
308 * p-cactions (Content actions), p-personal (Personal tools),
309 * p-navigation (Navigation), p-tb (Toolbox)
311 * The first three paramters are required, the others are optional and
312 * may be null. Though providing an id and tooltip is recommended.
314 * By default the new link will be added to the end of the list. To
315 * add the link before a given existing item, pass the DOM node
316 * (document.getElementById( 'foobar' )) or the jQuery-selector
317 * ( '#foobar' ) of that item.
319 * @example mw.util.addPortletLink(
320 * 'p-tb', 'http://mediawiki.org/',
321 * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print'
324 * @param portlet string ID of the target portlet ( 'p-cactions' or 'p-personal' etc.)
325 * @param href string Link URL
326 * @param text string Link text
327 * @param id string ID of the new item, should be unique and preferably have
328 * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' )
329 * @param tooltip string Text to show when hovering over the link, without accesskey suffix
330 * @param accesskey string Access key to activate this link (one character, try
331 * to avoid conflicts. Use $( '[accesskey=x]' ).get() in the console to
332 * see if 'x' is already used.
333 * @param nextnode mixed DOM Node or jQuery-selector string of the item that the new
334 * item should be added before, should be another item in the same
335 * list, it will be ignored otherwise
337 * @return mixed The DOM Node of the added item (a ListItem or Anchor element,
338 * depending on the skin) or null if no element was added to the document.
340 addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) {
341 var $item, $link, $portlet, $ul;
343 // Check if there's atleast 3 arguments to prevent a TypeError
344 if ( arguments.length < 3 ) {
347 // Setup the anchor tag
348 $link = $( '<a>' ).attr( 'href', href ).text( text );
350 $link.attr( 'title', tooltip );
353 // Some skins don't have any portlets
354 // just add it to the bottom of their 'sidebar' element as a fallback
355 switch ( mw.config.get( 'skin' ) ) {
358 $( '#quickbar' ).append( $link.after( '<br/>' ) );
361 $( '#searchform' ).before( $link ).before( ' | ' );
363 default: // Skins like chick, modern, monobook, myskin, simple, vector...
365 // Select the specified portlet
366 $portlet = $( '#' + portlet );
367 if ( $portlet.length === 0 ) {
370 // Select the first (most likely only) unordered list inside the portlet
371 $ul = $portlet.find( 'ul' ).eq( 0 );
373 // If it didn't have an unordered list yet, create it
374 if ( $ul.length === 0 ) {
378 // If there's no <div> inside, append it to the portlet directly
379 if ( $portlet.find( 'div:first' ).length === 0 ) {
380 $portlet.append( $ul );
382 // otherwise if there's a div (such as div.body or div.pBody)
383 // append the <ul> to last (most likely only) div
384 $portlet.find( 'div' ).eq( -1 ).append( $ul );
388 if ( $ul.length === 0 ) {
392 // Unhide portlet if it was hidden before
393 $portlet.removeClass( 'emptyPortlet' );
395 // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab)
396 // and back up the selector to the list item
397 if ( $portlet.hasClass( 'vectorTabs' ) ) {
398 $item = $link.wrap( '<li><span></span></li>' ).parent().parent();
400 $item = $link.wrap( '<li></li>' ).parent();
403 // Implement the properties passed to the function
405 $item.attr( 'id', id );
408 $link.attr( 'accesskey', accesskey );
409 tooltip += ' [' + accesskey + ']';
410 $link.attr( 'title', tooltip );
412 if ( accesskey && tooltip ) {
413 util.updateTooltipAccessKeys( $link );
416 // Where to put our node ?
417 // - nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
418 if ( nextnode && nextnode.parentNode === $ul[0] ) {
419 $(nextnode).before( $item );
421 // - nextnode is a CSS selector for jQuery
422 } else if ( typeof nextnode === 'string' && $ul.find( nextnode ).length !== 0 ) {
423 $ul.find( nextnode ).eq( 0 ).before( $item );
425 // If the jQuery selector isn't found within the <ul>,
426 // or if nextnode was invalid or not passed at all,
427 // then just append it at the end of the <ul> (this is the default behaviour)
438 * Add a little box at the top of the screen to inform the user of
439 * something, replacing any previous message.
440 * Calling with no arguments, with an empty string or null will hide the message
442 * @param message {mixed} The DOM-element, jQuery object or HTML-string to be put inside the message box.
443 * @param className {String} Used in adding a class; should be different for each call
444 * to allow CSS/JS to hide different boxes. null = no class used.
445 * @return {Boolean} True on success, false on failure.
447 jsMessage: function ( message, className ) {
448 var $messageDiv = $( '#mw-js-message' );
450 if ( !arguments.length || message === '' || message === null ) {
451 $messageDiv.empty().hide();
452 stopHideMessageTimeout();
453 return true; // Emptying and hiding message is intended behaviour, return true
455 // We special-case skin structures provided by the software. Skins that
456 // choose to abandon or significantly modify our formatting can just define
457 // an mw-js-message div to start with.
458 if ( !$messageDiv.length ) {
459 $messageDiv = $( '<div id="mw-js-message"></div>' );
460 if ( util.$content.parent().length ) {
461 util.$content.parent().prepend( $messageDiv );
467 if ( !messageBoxEvents ) {
468 messageBoxEvents = true;
471 'mouseenter': stopHideMessageTimeout,
472 'mouseleave': startHideMessageTimeout,
475 .on( 'click', 'a', function ( e ) {
476 // Prevent links, even those that don't exist yet, from causing the
477 // message box to close when clicked
483 $messageDiv.prop( 'className', 'mw-js-message-' + className );
486 if ( typeof message === 'object' ) {
488 $messageDiv.append( message );
490 $messageDiv.html( message );
493 $messageDiv.slideDown();
494 startHideMessageTimeout();
500 * Validate a string as representing a valid e-mail address
501 * according to HTML5 specification. Please note the specification
502 * does not validate a domain with one character.
504 * @todo FIXME: should be moved to or replaced by a JavaScript validation module.
506 * @param mailtxt string E-mail address to be validated.
507 * @return mixed Null if mailtxt was an empty string, otherwise true/false
508 * is determined by validation.
510 validateEmail: function ( mailtxt ) {
511 var rfc5322_atext, rfc1034_ldh_str, HTML5_email_regexp;
513 if ( mailtxt === '' ) {
518 * HTML5 defines a string as valid e-mail address if it matches
520 * 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
522 * - atext : defined in RFC 5322 section 3.2.3
523 * - ldh-str : defined in RFC 1034 section 3.5
525 * (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68):
529 * First, define the RFC 5322 'atext' which is pretty easy:
530 * atext = ALPHA / DIGIT / ; Printable US-ASCII
531 "!" / "#" / ; characters not including
532 "$" / "%" / ; specials. Used for atoms.
542 rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~";
545 * Next define the RFC 1034 'ldh-str'
546 * <domain> ::= <subdomain> | " "
547 * <subdomain> ::= <label> | <subdomain> "." <label>
548 * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
549 * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
550 * <let-dig-hyp> ::= <let-dig> | "-"
551 * <let-dig> ::= <letter> | <digit>
553 rfc1034_ldh_str = "a-z0-9\\-";
555 HTML5_email_regexp = new RegExp(
559 // User part which is liberal :p
560 '[' + rfc5322_atext + '\\.]+'
566 '[' + rfc1034_ldh_str + ']+'
568 // Optional second part and following are separated by a dot
569 '(?:\\.[' + rfc1034_ldh_str + ']+)*'
573 // RegExp is case insensitive
576 return (null !== mailtxt.match( HTML5_email_regexp ) );
580 * Note: borrows from IP::isIPv4
582 * @param address string
583 * @param allowBlock boolean
586 isIPv4Address: function ( address, allowBlock ) {
587 if ( typeof address !== 'string' ) {
591 var block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '',
592 RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])',
593 RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
595 return address.search( new RegExp( '^' + RE_IP_ADD + block + '$' ) ) !== -1;
599 * Note: borrows from IP::isIPv6
601 * @param address string
602 * @param allowBlock boolean
605 isIPv6Address: function ( address, allowBlock ) {
606 if ( typeof address !== 'string' ) {
610 var block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '',
612 '(?:' + // starts with "::" (including "::")
613 ':(?::|(?::' + '[0-9A-Fa-f]{1,4}' + '){1,7})' +
614 '|' + // ends with "::" (except "::")
615 '[0-9A-Fa-f]{1,4}' + '(?::' + '[0-9A-Fa-f]{1,4}' + '){0,6}::' +
616 '|' + // contains no "::"
617 '[0-9A-Fa-f]{1,4}' + '(?::' + '[0-9A-Fa-f]{1,4}' + '){7}' +
620 if ( address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1 ) {
624 RE_IPV6_ADD = // contains one "::" in the middle (single '::' check below)
625 '[0-9A-Fa-f]{1,4}' + '(?:::?' + '[0-9A-Fa-f]{1,4}' + '){1,6}';
627 return address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1
628 && address.search( /::/ ) !== -1 && address.search( /::.*::/ ) === -1;
632 // Message auto-hide helpers
633 function hideMessage() {
634 $( '#mw-js-message' ).fadeOut( 'slow' );
636 function stopHideMessageTimeout() {
637 clearTimeout( hideMessageTimeout );
639 function startHideMessageTimeout() {
640 clearTimeout( hideMessageTimeout );
641 hideMessageTimeout = setTimeout( hideMessage, 5000 );
646 }( mediaWiki, jQuery ) );