(bug 36329) Fix accesskey tooltip for Firefox 14 on Mac
[mediawiki.git] / resources / mediawiki / mediawiki.util.js
blobc697692fb4e12e15a6f06fd5c8cb211d82a2f9ee
1 /**
2  * Implements mediaWiki.util library
3  */
4 ( function ( mw, $ ) {
5         'use strict';
7         // Local cache and alias
8         var hideMessageTimeout,
9                 messageBoxEvents = false,
10                 util = {
12                 /**
13                  * Initialisation
14                  * (don't call before document ready)
15                  */
16                 init: function () {
17                         var profile, $tocTitle, $tocToggleLink, hideTocCookie;
19                         /* Set up $.messageBox */
20                         $.messageBoxNew( {
21                                 id: 'mw-js-message',
22                                 parent: '#content'
23                         } );
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'
37                                                 // Chrome on Mac
38                                                 ? 'ctrl-option-'
39                                                 : profile.platform === 'win'
40                                                         // Chrome on Windows
41                                                         // (both alt- and alt-shift work, but alt-f triggers Chrome wrench menu
42                                                         // which alt-shift-f does not)
43                                                         ? 'alt-shift-'
44                                                         // Chrome on other (Ubuntu?)
45                                                         : 'alt-'
46                                 );
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-';
53                         // Firefox 14+ on Mac
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-';
69                         }
71                         /* Fill $content var */
72                         if ( $( '#bodyContent' ).length ) {
73                                 // Vector, Monobook, Chick etc.
74                                 util.$content = $( '#bodyContent' );
76                         } else if ( $( '#mw_contentholder' ).length ) {
77                                 // Modern
78                                 util.$content = $( '#mw_contentholder' );
80                         } else if ( $( '#article' ).length ) {
81                                 // Standard, CologneBlue
82                                 util.$content = $( '#article' );
84                         } else {
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' );
90                         }
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 ) {
101                                                         e.preventDefault();
102                                                         util.toggleToc( $(this) );
103                                                 } );
104                                 $tocTitle.append(
105                                         $tocToggleLink
106                                                 .wrap( '<span class="toctoggle"></span>' )
107                                                 .parent()
108                                                         .prepend( '&nbsp;[' )
109                                                         .append( ']&nbsp;' )
110                                 );
112                                 if ( hideTocCookie === '1' ) {
113                                         util.toggleToc( $tocToggleLink );
114                                 }
115                         }
116                 },
118                 /* Main body */
120                 /**
121                  * Encode the string like PHP's rawurlencode
122                  *
123                  * @param str string String to be encoded
124                  */
125                 rawurlencode: function ( str ) {
126                         str = String( 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' );
130                 },
132                 /**
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
136                  *
137                  * @param str string String to be encoded
138                  */
139                 wikiUrlencode: function ( str ) {
140                         return util.rawurlencode( str )
141                                 .replace( /%20/g, '_' ).replace( /%3A/g, ':' ).replace( /%2F/g, '/' );
142                 },
144                 /**
145                  * Get the link to a page name (relative to wgServer)
146                  *
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.
149                  */
150                 wikiGetlink: function ( str ) {
151                         return mw.config.get( 'wgArticlePath' ).replace( '$1',
152                                 util.wikiUrlencode( typeof str === 'string' ? str : mw.config.get( 'wgPageName' ) ) );
153                 },
155                 /**
156                  * Get address to a script in the wiki root.
157                  * For index.php use mw.config.get( 'wgScript' )
158                  *
159                  * @since 1.18
160                  * @param str string Name of script (eg. 'api'), defaults to 'index'
161                  * @return string Address to script (eg. '/w/api.php' )
162                  */
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' );
169                         } else {
170                                 return mw.config.get( 'wgScriptPath' ) + '/' + str +
171                                         mw.config.get( 'wgScriptExtension' );
172                         }
173                 },
175                 /**
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).
180                  * @example
181                  * <code>
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;
186                  * });
187                  * </code>
188                  *
189                  * @param text string CSS to be appended
190                  * @return CSSStyleSheet (use .ownerNode to get to the <style> element)
191                  */
192                 addCSS: function ( text ) {
193                         var s = mw.loader.addStyleTag( text );
194                         return s.sheet || s;
195                 },
197                 /**
198                  * Hide/show the table of contents element
199                  *
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.
205                  */
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, {
217                                                 expires: 30,
218                                                 path: '/'
219                                         } );
220                                         return true;
221                                 } else {
222                                         $tocList.slideUp( 'fast', callback );
223                                         $toggleLink.text( mw.msg( 'showtoc' ) );
224                                         $( '#toc' ).addClass( 'tochidden' );
225                                         $.cookie( 'mw_hidetoc', '1', {
226                                                 expires: 30,
227                                                 path: '/'
228                                         } );
229                                         return false;
230                                 }
231                         } else {
232                                 return null;
233                         }
234                 },
236                 /**
237                  * Grab the URL parameter value for the given parameter.
238                  * Returns null if not found.
239                  *
240                  * @param param string The parameter name.
241                  * @param url string URL to search through (optional)
242                  * @return mixed Parameter value or null.
243                  */
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 ) + '=([^&#]*)' ),
248                                 m = re.exec( url );
249                         if ( m ) {
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' ) );
253                         }
254                         return null;
255                 },
257                 /**
258                  * @var string
259                  * Access key prefix. Will be re-defined based on browser/operating system
260                  * detection in mw.util.init().
261                  */
262                 tooltipAccessKeyPrefix: 'alt-',
264                 /**
265                  * @var RegExp
266                  * Regex to match accesskey tooltips.
267                  */
268                 tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/,
270                 /**
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.
275                  *
276                  * @param $nodes {Array|jQuery} [optional] A jQuery object, or array
277                  * of elements to update.
278                  */
279                 updateTooltipAccessKeys: function ( $nodes ) {
280                         if ( !$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 );
287                         }
289                         $nodes.attr( 'title', function ( i, val ) {
290                                 if ( val && util.tooltipAccessKeyRegexp.exec( val ) ) {
291                                         return val.replace( util.tooltipAccessKeyRegexp,
292                                                 '[' + util.tooltipAccessKeyPrefix + '$5]' );
293                                 }
294                                 return val;
295                         } );
296                 },
298                 /*
299                  * @var jQuery
300                  * A jQuery object that refers to the page-content element
301                  * Populated by init().
302                  */
303                 $content: null,
305                 /**
306                  * Add a link to a portlet menu on the page, such as:
307                  *
308                  * p-cactions (Content actions), p-personal (Personal tools),
309                  * p-navigation (Navigation), p-tb (Toolbox)
310                  *
311                  * The first three paramters are required, the others are optional and
312                  * may be null. Though providing an id and tooltip is recommended.
313                  *
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.
318                  *
319                  * @example mw.util.addPortletLink(
320                  *       'p-tb', 'http://mediawiki.org/',
321                  *       'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print'
322                  * )
323                  *
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
336                  *
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.
339                  */
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 ) {
345                                 return null;
346                         }
347                         // Setup the anchor tag
348                         $link = $( '<a>' ).attr( 'href', href ).text( text );
349                         if ( tooltip ) {
350                                 $link.attr( 'title', tooltip );
351                         }
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' ) ) {
356                         case 'standard':
357                         case 'cologneblue':
358                                 $( '#quickbar' ).append( $link.after( '<br/>' ) );
359                                 return $link[0];
360                         case 'nostalgia':
361                                 $( '#searchform' ).before( $link ).before( ' &#124; ' );
362                                 return $link[0];
363                         default: // Skins like chick, modern, monobook, myskin, simple, vector...
365                                 // Select the specified portlet
366                                 $portlet = $( '#' + portlet );
367                                 if ( $portlet.length === 0 ) {
368                                         return null;
369                                 }
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 ) {
376                                         $ul = $( '<ul>' );
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 );
381                                         } else {
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 );
385                                         }
386                                 }
387                                 // Just in case..
388                                 if ( $ul.length === 0 ) {
389                                         return null;
390                                 }
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();
399                                 } else {
400                                         $item = $link.wrap( '<li></li>' ).parent();
401                                 }
403                                 // Implement the properties passed to the function
404                                 if ( id ) {
405                                         $item.attr( 'id', id );
406                                 }
407                                 if ( accesskey ) {
408                                         $link.attr( 'accesskey', accesskey );
409                                         tooltip += ' [' + accesskey + ']';
410                                         $link.attr( 'title', tooltip );
411                                 }
412                                 if ( accesskey && tooltip ) {
413                                         util.updateTooltipAccessKeys( $link );
414                                 }
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)
428                                 } else {
429                                         $ul.append( $item );
430                                 }
433                                 return $item[0];
434                         }
435                 },
437                 /**
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
441                  *
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.
446                  */
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
454                         } else {
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 );
462                                         } else {
463                                                 return false;
464                                         }
465                                 }
467                                 if ( !messageBoxEvents ) {
468                                         messageBoxEvents = true;
469                                         $messageDiv
470                                                 .on( {
471                                                         'mouseenter': stopHideMessageTimeout,
472                                                         'mouseleave': startHideMessageTimeout,
473                                                         'click': hideMessage
474                                                 } )
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
478                                                         e.stopPropagation();
479                                                 } );
480                                 }
482                                 if ( className ) {
483                                         $messageDiv.prop( 'className', 'mw-js-message-' + className );
484                                 }
486                                 if ( typeof message === 'object' ) {
487                                         $messageDiv.empty();
488                                         $messageDiv.append( message );
489                                 } else {
490                                         $messageDiv.html( message );
491                                 }
493                                 $messageDiv.slideDown();
494                                 startHideMessageTimeout();
495                                 return true;
496                         }
497                 },
499                 /**
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.
503                  *
504                  * @todo FIXME: should be moved to or replaced by a JavaScript validation module.
505                  *
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.
509                  */
510                 validateEmail: function ( mailtxt ) {
511                         var rfc5322_atext, rfc1034_ldh_str, HTML5_email_regexp;
513                         if ( mailtxt === '' ) {
514                                 return null;
515                         }
517                         /**
518                          * HTML5 defines a string as valid e-mail address if it matches
519                          * the ABNF:
520                          *      1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
521                          * With:
522                          * - atext      : defined in RFC 5322 section 3.2.3
523                          * - ldh-str : defined in RFC 1034 section 3.5
524                          *
525                          * (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68):
526                          */
528                         /**
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.
533                                                  "&" / "'" /
534                                                  "*" / "+" /
535                                                  "-" / "/" /
536                                                  "=" / "?" /
537                                                  "^" / "_" /
538                                                  "`" / "{" /
539                                                  "|" / "}" /
540                                                  "~"
541                         */
542                         rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~";
544                         /**
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>
552                          */
553                         rfc1034_ldh_str = "a-z0-9\\-";
555                         HTML5_email_regexp = new RegExp(
556                                 // start of string
557                                 '^'
558                                 +
559                                 // User part which is liberal :p
560                                 '[' + rfc5322_atext + '\\.]+'
561                                 +
562                                 // 'at'
563                                 '@'
564                                 +
565                                 // Domain first part
566                                 '[' + rfc1034_ldh_str + ']+'
567                                 +
568                                 // Optional second part and following are separated by a dot
569                                 '(?:\\.[' + rfc1034_ldh_str + ']+)*'
570                                 +
571                                 // End of string
572                                 '$',
573                                 // RegExp is case insensitive
574                                 'i'
575                         );
576                         return (null !== mailtxt.match( HTML5_email_regexp ) );
577                 },
579                 /**
580                  * Note: borrows from IP::isIPv4
581                  *
582                  * @param address string
583                  * @param allowBlock boolean
584                  * @return boolean
585                  */
586                 isIPv4Address: function ( address, allowBlock ) {
587                         if ( typeof address !== 'string' ) {
588                                 return false;
589                         }
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;
596                 },
598                 /**
599                  * Note: borrows from IP::isIPv6
600                  *
601                  * @param address string
602                  * @param allowBlock boolean
603                  * @return boolean
604                  */
605                 isIPv6Address: function ( address, allowBlock ) {
606                         if ( typeof address !== 'string' ) {
607                                 return false;
608                         }
610                         var     block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '',
611                                 RE_IPV6_ADD =
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}' +
618                         ')';
620                         if ( address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1 ) {
621                                 return true;
622                         }
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;
629                 }
630         };
632         // Message auto-hide helpers
633         function hideMessage() {
634                 $( '#mw-js-message' ).fadeOut( 'slow' );
635         }
636         function stopHideMessageTimeout() {
637                 clearTimeout( hideMessageTimeout );
638         }
639         function startHideMessageTimeout() {
640                 clearTimeout( hideMessageTimeout );
641                 hideMessageTimeout = setTimeout( hideMessage, 5000 );
642         }
644         mw.util = util;
646 }( mediaWiki, jQuery ) );