* Consistent single quotes
[lhc/web/wiklou.git] / resources / mediawiki.util / mediawiki.util.js
1 /*
2 * Utilities
3 */
4
5 (function ($, mw) {
6
7 mw.util = {
8
9 /* Initialisation */
10 'initialised' : false,
11 'init' : function () {
12 if ( this.initialised === false ) {
13 this.initialised = true;
14
15 // Any initialisation after the DOM is ready
16 $(function () {
17
18 // Shortcut to client profile return
19 var profile = $.client.profile();
20
21 /* Set tooltipAccessKeyPrefix */
22
23 // Opera on any platform
24 if ( profile.name == 'opera' ) {
25 mw.util.tooltipAccessKeyPrefix = 'shift-esc-';
26
27 // Chrome on any platform
28 } else if ( profile.name == 'chrome' ) {
29 // Chrome on Mac or Chrome on other platform ?
30 mw.util.tooltipAccessKeyPrefix = ( profile.platform == 'mac'
31 ? 'ctrl-option-' : 'alt-' );
32
33 // Non-Windows Safari with webkit_version > 526
34 } else if ( profile.platform !== 'win'
35 && profile.name == 'safari'
36 && profile.layoutVersion > 526 ) {
37 mw.util.tooltipAccessKeyPrefix = 'ctrl-alt-';
38
39 // Safari/Konqueror on any platform, or any browser on Mac
40 // (but not Safari on Windows)
41 } else if ( !( profile.platform == 'win' && profile.name == 'safari' )
42 && ( profile.name == 'safari'
43 || profile.platform == 'mac'
44 || profile.name == 'konqueror' ) ) {
45 mw.util.tooltipAccessKeyPrefix = 'ctrl-';
46
47 // Firefox 2.x
48 } else if ( profile.name == 'firefox' && profile.versionBase == '2' ) {
49 mw.util.tooltipAccessKeyPrefix = 'alt-shift-';
50 }
51
52 /* Enable CheckboxShiftClick */
53 $( 'input[type=checkbox]:not(.noshiftselect)' ).checkboxShiftClick();
54
55 /* Emulate placeholder if not supported by browser */
56 if ( !( 'placeholder' in document.createElement( 'input' ) ) ) {
57 $( 'input[placeholder]' ).placeholder();
58 }
59
60 /* Fill $content var */
61 if ( $( '#bodyContent' ).length ) {
62 mw.util.$content = $( '#bodyContent' );
63 } else if ( $( '#article' ).length ) {
64 mw.util.$content = $( '#article' );
65 } else {
66 mw.util.$content = $( '#content' );
67 }
68
69 /* Enable makeCollapse */
70 $( '.mw-collapsible' ).makeCollapsible();
71
72 /* Table of Contents toggle */
73 var $tocContainer = $( '#toc' ),
74 $tocTitle = $( '#toctitle' ),
75 $tocToggleLink = $( '#togglelink' );
76 // Only add it if there is a TOC and there is no toggle added already
77 if ( $tocContainer.size() && $tocTitle.size() && !$tocToggleLink.size() ) {
78 var hideTocCookie = $.cookie( 'mw_hidetoc' );
79 $tocToggleLink = $( '<a href="#" class="internal" id="togglelink">' ).text( mw.msg( 'hidetoc' ) ).click( function(e){
80 e.preventDefault();
81 mw.util.toggleToc( $(this) );
82 } );
83 $tocTitle.append( $tocToggleLink.wrap( '<span class="toctoggle">' ).parent().prepend( '&nbsp;[' ).append( ']&nbsp;' ) );
84
85 if ( hideTocCookie == '1' ) {
86 // Cookie says user want toc hidden
87 $tocToggleLink.click();
88 }
89 }
90 } );
91
92 return true;
93 }
94 return false;
95 },
96
97 /* Main body */
98
99 /**
100 * Encode the string like PHP's rawurlencode
101 *
102 * @param str String to be encoded
103 */
104 'rawurlencode' : function( str ) {
105 str = ( str + '' ).toString();
106 return encodeURIComponent( str )
107 .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
108 .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' );
109 },
110
111 /**
112 * Encode page titles for use in a URL
113 * We want / and : to be included as literal characters in our title URLs
114 * as they otherwise fatally break the title
115 *
116 * @param str String to be encoded
117 */
118 'wikiUrlencode' : function( str ) {
119 return this.rawurlencode( str )
120 .replace( /%20/g, '_' ).replace( /%3A/g, ':' ).replace( /%2F/g, '/' );
121 },
122
123 /**
124 * Append a new style block to the head
125 *
126 * @param text String CSS to be appended
127 * @return CSSStyleSheet object
128 */
129 'addCSS' : function( text ) {
130 var s = document.createElement( 'style' );
131 s.type = 'text/css';
132 s.rel = 'stylesheet';
133 if ( s.styleSheet ) {
134 s.styleSheet.cssText = text; // IE
135 } else {
136 s.appendChild( document.createTextNode( text + '' ) ); // Safari sometimes borks on null
137 }
138 document.getElementsByTagName("head")[0].appendChild( s );
139 return s.sheet || s;
140 },
141
142 /**
143 * Hide/show the table of contents element
144 *
145 * @param $toggleLink jQuery object of the toggle link
146 * @return String boolean visibility of the toc (true means it's visible)
147 */
148 'toggleToc' : function( $toggleLink ) {
149 var $tocList = $( '#toc ul:first' );
150
151 // This function shouldn't be called if there's no TOC,
152 // but just in case...
153 if ( $tocList.size() ) {
154 if ( $tocList.is( ':hidden' ) ) {
155 $tocList.slideDown( 'fast' );
156 $toggleLink.text( mw.msg( 'hidetoc' ) );
157 $.cookie( 'mw_hidetoc', null, {
158 expires: 30,
159 path: '/'
160 } );
161 return true;
162 } else {
163 $tocList.slideUp( 'fast' );
164 $toggleLink.text( mw.msg( 'showtoc' ) );
165 $.cookie( 'mw_hidetoc', '1', {
166 expires: 30,
167 path: '/'
168 } );
169 return false;
170 }
171 } else {
172 return false;
173 }
174 },
175
176 /**
177 * Get the full URL to a page name
178 *
179 * @param str Page name to link to
180 */
181 'wikiGetlink' : function( str ) {
182 return wgServer + wgArticlePath.replace( '$1', this.wikiUrlencode( str ) );
183 },
184
185 /**
186 * Grab the URL parameter value for the given parameter.
187 * Returns null if not found.
188 *
189 * @param param The parameter name
190 * @param url URL to search through (optional)
191 */
192 'getParamValue' : function( param, url ) {
193 url = url ? url : document.location.href;
194 // Get last match, stop at hash
195 var re = new RegExp( '[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' );
196 var m = re.exec( url );
197 if ( m && m.length > 1 ) {
198 return decodeURIComponent( m[1] );
199 }
200 return null;
201 },
202
203 // Access key prefix.
204 // Will be re-defined based on browser/operating system detection in
205 // mw.util.init().
206 'tooltipAccessKeyPrefix' : 'alt-',
207
208 // Regex to match accesskey tooltips
209 'tooltipAccessKeyRegexp': /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/,
210
211 /**
212 * Add the appropriate prefix to the accesskey shown in the tooltip.
213 * If the nodeList parameter is given, only those nodes are updated;
214 * otherwise, all the nodes that will probably have accesskeys by
215 * default are updated.
216 *
217 * @param nodeList jQuery object, or array of elements
218 */
219 'updateTooltipAccessKeys' : function( nodeList ) {
220 var $nodes;
221 if ( nodeList instanceof jQuery ) {
222 $nodes = nodeList;
223 } else if ( nodeList ) {
224 $nodes = $( nodeList );
225 } else {
226 // Rather than scanning all links, just the elements that
227 // contain the relevant links
228 this.updateTooltipAccessKeys(
229 $( '#column-one a, #mw-head a, #mw-panel a, #p-logo a' ) );
230
231 // these are rare enough that no such optimization is needed
232 this.updateTooltipAccessKeys( $( 'input' ) );
233 this.updateTooltipAccessKeys( $( 'label' ) );
234 return;
235 }
236
237 $nodes.each( function ( i ) {
238 var tip = $(this).attr( 'title' );
239 if ( !!tip && mw.util.tooltipAccessKeyRegexp.exec( tip ) ) {
240 tip = tip.replace( mw.util.tooltipAccessKeyRegexp,
241 '[' + mw.util.tooltipAccessKeyPrefix + "$5]" );
242 $(this).attr( 'title', tip );
243 }
244 } );
245 },
246
247 // jQuery object that refers to the page-content element
248 // Populated by init()
249 '$content' : null,
250
251 /**
252 * Add a link to a portlet menu on the page, such as:
253 *
254 * p-cactions (Content actions), p-personal (Personal tools),
255 * p-navigation (Navigation), p-tb (Toolbox)
256 *
257 * The first three paramters are required, others are optionals. Though
258 * providing an id and tooltip is recommended.
259 *
260 * By default the new link will be added to the end of the list. To
261 * add the link before a given existing item, pass the DOM node
262 * (document.getElementById( 'foobar' )) or the jQuery-selector
263 * ( '#foobar' ) of that item.
264 *
265 * @example mw.util.addPortletLink(
266 * 'p-tb', 'http://mediawiki.org/',
267 * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print'
268 * )
269 *
270 * @param portlet ID of the target portlet ( 'p-cactions' or 'p-personal' etc.)
271 * @param href Link URL
272 * @param text Link text (will be automatically converted to lower
273 * case by CSS for p-cactions in Monobook)
274 * @param id ID of the new item, should be unique and preferably have
275 * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' )
276 * @param tooltip Text to show when hovering over the link, without accesskey suffix
277 * @param accesskey Access key to activate this link (one character, try
278 * to avoid conflicts. Use $( '[accesskey=x' ).get() in the console to
279 * see if 'x' is already used.
280 * @param nextnode DOM node or jQuery-selector of the item that the new
281 * item should be added before, should be another item in the same
282 * list will be ignored if not the so
283 *
284 * @return The DOM node of the new item (a LI element, or A element for
285 * older skins) or null.
286 */
287 'addPortletLink' : function( portlet, href, text, id, tooltip, accesskey, nextnode ) {
288
289 // Check if there's atleast 3 arguments to prevent a TypeError
290 if ( arguments.length < 3 ) {
291 return null;
292 }
293 // Setup the anchor tag
294 var $link = $( '<a />' ).attr( 'href', href ).text( text );
295
296 // Some skins don't have any portlets
297 // just add it to the bottom of their 'sidebar' element as a fallback
298 switch ( skin ) {
299 case 'standard' :
300 case 'cologneblue' :
301 $("#quickbar").append($link.after( '<br />' ));
302 return $link.get(0);
303 case 'nostalgia' :
304 $("#searchform").before($link).before( ' &#124; ' );
305 return $link.get(0);
306 default : // Skins like chick, modern, monobook, myskin, simple, vector...
307
308 // Select the specified portlet
309 var $portlet = $( '#' + portlet);
310 if ( $portlet.length === 0 ) {
311 return null;
312 }
313 // Select the first (most likely only) unordered list inside the portlet
314 var $ul = $portlet.find( 'ul' ).eq( 0 );
315
316 // If it didn't have an unordered list yet, create it
317 if ($ul.length === 0) {
318 // If there's no <div> inside, append it to the portlet directly
319 if ($portlet.find( 'div' ).length === 0) {
320 $portlet.append( '<ul/>' );
321 } else {
322 // otherwise if there's a div (such as div.body or div.pBody)
323 // append the <ul> to last (most likely only) div
324 $portlet.find( 'div' ).eq( -1 ).append( '<ul/>' );
325 }
326 // Select the created element
327 $ul = $portlet.find( 'ul' ).eq( 0 );
328 }
329 // Just in case..
330 if ( $ul.length === 0 ) {
331 return null;
332 }
333
334 // Unhide portlet if it was hidden before
335 $portlet.removeClass( 'emptyPortlet' );
336
337 // Wrap the anchor tag in a <span> and create a list item for it
338 // and back up the selector to the list item
339 var $item = $link.wrap( '<li><span /></li>' ).parent().parent();
340
341 // Implement the properties passed to the function
342 if ( id ) {
343 $item.attr( 'id', id );
344 }
345 if ( accesskey ) {
346 $link.attr( 'accesskey', accesskey );
347 tooltip += ' [' + accesskey + ']';
348 }
349 if ( tooltip ) {
350 $link.attr( 'title', tooltip );
351 }
352 if ( accesskey && tooltip ) {
353 this.updateTooltipAccessKeys( $link );
354 }
355
356 // Append using DOM-element passing
357 if ( nextnode && nextnode.parentNode == $ul.get( 0 ) ) {
358 $(nextnode).before( $item );
359 } else {
360 // If the jQuery selector isn't found within the <ul>, just
361 // append it at the end
362 if ( $ul.find( nextnode ).length === 0 ) {
363 $ul.append( $item );
364 } else {
365 // Append using jQuery CSS selector
366 $ul.find( nextnode ).eq( 0 ).before( $item );
367 }
368 }
369
370 return $item.get( 0 );
371 }
372 },
373
374 /**
375 * Add a little box at the top of the screen to inform the user of
376 * something, replacing any previous message.
377 *
378 * @param message mixed The DOM-element or HTML-string to be put inside the message box]
379 * Calling with no arguments, with an empty string or null will hide the message
380 * @param className string Used in adding a class; should be different for each
381 * call to allow CSS/JS to hide different boxes. null = no class used.
382 * @return Boolean True on success, false on failure
383 */
384 'jsMessage' : function( message, className ) {
385
386 if ( !arguments.length || message === '' || message === null ) {
387
388 $( '#mw-js-message' ).empty().hide();
389 return true; // Emptying and hiding message is intended behaviour, return true
390
391 } else {
392 // We special-case skin structures provided by the software. Skins that
393 // choose to abandon or significantly modify our formatting can just define
394 // an mw-js-message div to start with.
395 var $messageDiv = $( '#mw-js-message' );
396 if ( !$messageDiv.length ) {
397 $messageDiv = $( '<div id="mw-js-message">' );
398 if ( mw.util.$content.parent().length ) {
399 mw.util.$content.parent().prepend( $messageDiv );
400 } else {
401 return false;
402 }
403 }
404
405 if ( className ) {
406 $messageDiv.attr( 'class', 'mw-js-message-' + className );
407 }
408
409 if ( typeof message === 'object' ) {
410 $messageDiv.empty();
411 $messageDiv.append( message ); // Append new content
412 } else {
413 $messageDiv.html( message );
414 }
415
416 $messageDiv.slideDown();
417 return true;
418 }
419 },
420
421 /**
422 * Validate a string as representing a valid e-mail address
423 * according to HTML5 specification. Please note the specification
424 * does not validate a domain with one character.
425 *
426 * FIXME: should be moved to a JavaScript validation module.
427 */
428 'validateEmail' : function( mailtxt ) {
429 if( mailtxt === '' ) {
430 return null;
431 }
432
433 /**
434 * HTML5 defines a string as valid e-mail address if it matches
435 * the ABNF:
436 * 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
437 * With:
438 * - atext : defined in RFC 5322 section 3.2.3
439 * - ldh-str : defined in RFC 1034 section 3.5
440 *
441 * (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68):
442 */
443
444 /**
445 * First, define the RFC 5322 'atext' which is pretty easy :
446 * atext = ALPHA / DIGIT / ; Printable US-ASCII
447 "!" / "#" / ; characters not including
448 "$" / "%" / ; specials. Used for atoms.
449 "&" / "'" /
450 "*" / "+" /
451 "-" / "/" /
452 "=" / "?" /
453 "^" / "_" /
454 "`" / "{" /
455 "|" / "}" /
456 "~"
457 */
458 var rfc5322_atext = "a-z0-9!#$%&'*+-/=?^_`{|}Ñ~",
459
460 /**
461 * Next define the RFC 1034 'ldh-str'
462 * <domain> ::= <subdomain> | " "
463 * <subdomain> ::= <label> | <subdomain> "." <label>
464 * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
465 * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
466 * <let-dig-hyp> ::= <let-dig> | "-"
467 * <let-dig> ::= <letter> | <digit>
468 */
469 rfc1034_ldh_str = "a-z0-9-",
470
471 HTML5_email_regexp = new RegExp(
472 // start of string
473 '^'
474 +
475 // User part which is liberal :p
476 '[' + rfc5322_atext + '\\.' + ']' + '+'
477 +
478 // "at"
479 '@'
480 +
481 // Domain first part
482 '[' + rfc1034_ldh_str + ']+'
483 +
484 // Second part and following are separated by a dot
485 '(?:\\.[' + rfc1034_ldh_str + ']+)+'
486 +
487 // End of string
488 '$',
489 // RegExp is case insensitive
490 'i'
491 );
492 return (null !== mailtxt.match( HTML5_email_regexp ) );
493 }
494
495 };
496
497 mw.util.init();
498
499 })(jQuery, mediaWiki);