JavaScript cleanup: add spacing, missing braces, use single quotes instead of double...
[lhc/web/wiklou.git] / skins / common / wikibits.js
1 // MediaWiki JavaScript support functions
2
3 var clientPC = navigator.userAgent.toLowerCase(); // Get client info
4 var is_gecko = /gecko/.test( clientPC ) &&
5 !/khtml|spoofer|netscape\/7\.0/.test(clientPC);
6 var webkit_match = clientPC.match(/applewebkit\/(\d+)/);
7 if (webkit_match) {
8 var is_safari = clientPC.indexOf('applewebkit') != -1 &&
9 clientPC.indexOf('spoofer') == -1;
10 var is_safari_win = is_safari && clientPC.indexOf('windows') != -1;
11 var webkit_version = parseInt(webkit_match[1]);
12 }
13 // For accesskeys; note that FF3+ is included here!
14 var is_ff2 = /firefox\/[2-9]|minefield\/3/.test( clientPC );
15 var ff2_bugs = /firefox\/2/.test( clientPC );
16 // These aren't used here, but some custom scripts rely on them
17 var is_ff2_win = is_ff2 && clientPC.indexOf('windows') != -1;
18 var is_ff2_x11 = is_ff2 && clientPC.indexOf('x11') != -1;
19 if (clientPC.indexOf('opera') != -1) {
20 var is_opera = true;
21 var is_opera_preseven = window.opera && !document.childNodes;
22 var is_opera_seven = window.opera && document.childNodes;
23 var is_opera_95 = /opera\/(9\.[5-9]|[1-9][0-9])/.test( clientPC );
24 var opera6_bugs = is_opera_preseven;
25 var opera7_bugs = is_opera_seven && !is_opera_95;
26 var opera95_bugs = /opera\/(9\.5)/.test( clientPC );
27 }
28 // Start at 4 to minimize the chance of breaking on IE10 :)
29 var ie6_bugs = /msie [4-6]/.test( clientPC );
30
31 // Global external objects used by this script.
32 /*extern ta, stylepath, skin */
33
34 // add any onload functions in this hook (please don't hard-code any events in the xhtml source)
35 var doneOnloadHook;
36
37 if (!window.onloadFuncts) {
38 var onloadFuncts = [];
39 }
40
41 function addOnloadHook( hookFunct ) {
42 // Allows add-on scripts to add onload functions
43 if( !doneOnloadHook ) {
44 onloadFuncts[onloadFuncts.length] = hookFunct;
45 } else {
46 hookFunct(); // bug in MSIE script loading
47 }
48 }
49
50 function hookEvent( hookName, hookFunct ) {
51 addHandler( window, hookName, hookFunct );
52 }
53
54 function importScript( page ) {
55 // TODO: might want to introduce a utility function to match wfUrlencode() in PHP
56 var uri = wgScript + '?title=' +
57 encodeURIComponent(page.replace(/ /g,'_')).replace(/%2F/ig,'/').replace(/%3A/ig,':') +
58 '&action=raw&ctype=text/javascript';
59 return importScriptURI( uri );
60 }
61
62 var loadedScripts = {}; // included-scripts tracker
63 function importScriptURI( url ) {
64 if ( loadedScripts[url] ) {
65 return null;
66 }
67 loadedScripts[url] = true;
68 var s = document.createElement( 'script' );
69 s.setAttribute( 'src', url );
70 s.setAttribute( 'type', 'text/javascript' );
71 document.getElementsByTagName('head')[0].appendChild( s );
72 return s;
73 }
74
75 function importStylesheet( page ) {
76 return importStylesheetURI( wgScript + '?action=raw&ctype=text/css&title=' + encodeURIComponent( page.replace(/ /g,'_') ) );
77 }
78
79 function importStylesheetURI( url, media ) {
80 var l = document.createElement( 'link' );
81 l.type = 'text/css';
82 l.rel = 'stylesheet';
83 l.href = url;
84 if( media ) {
85 l.media = media;
86 }
87 document.getElementsByTagName('head')[0].appendChild( l );
88 return l;
89 }
90
91 function appendCSS( text ) {
92 var s = document.createElement( 'style' );
93 s.type = 'text/css';
94 s.rel = 'stylesheet';
95 if ( s.styleSheet ) {
96 s.styleSheet.cssText = text; // IE
97 } else {
98 s.appendChild( document.createTextNode( text + '' ) ); // Safari sometimes borks on null
99 }
100 document.getElementsByTagName('head')[0].appendChild( s );
101 return s;
102 }
103
104 // special stylesheet links
105 if ( typeof stylepath != 'undefined' && typeof skin != 'undefined' ) {
106 // FIXME: This tries to load the stylesheets even for skins where they
107 // don't exist, i.e., everything but Monobook.
108 if ( opera6_bugs ) {
109 importStylesheetURI( stylepath + '/' + skin + '/Opera6Fixes.css' );
110 } else if ( opera7_bugs ) {
111 importStylesheetURI( stylepath + '/' + skin + '/Opera7Fixes.css' );
112 } else if ( opera95_bugs ) {
113 importStylesheetURI( stylepath + '/' + skin + '/Opera9Fixes.css' );
114 } else if ( ff2_bugs ) {
115 importStylesheetURI( stylepath + '/' + skin + '/FF2Fixes.css' );
116 }
117 }
118
119
120 if ( wgBreakFrames ) {
121 // Un-trap us from framesets
122 if ( window.top != window ) {
123 window.top.location = window.location;
124 }
125 }
126
127 function showTocToggle() {
128 if ( document.createTextNode ) {
129 // Uses DOM calls to avoid document.write + XHTML issues
130
131 var linkHolder = document.getElementById( 'toctitle' );
132 var existingLink = document.getElementById( 'togglelink' );
133 if ( !linkHolder || existingLink ) {
134 // Don't add the toggle link twice
135 return;
136 }
137
138 var outerSpan = document.createElement( 'span' );
139 outerSpan.className = 'toctoggle';
140
141 var toggleLink = document.createElement( 'a' );
142 toggleLink.id = 'togglelink';
143 toggleLink.className = 'internal';
144 toggleLink.href = 'javascript:toggleToc()';
145 toggleLink.appendChild( document.createTextNode( tocHideText ) );
146
147 outerSpan.appendChild( document.createTextNode( '[' ) );
148 outerSpan.appendChild( toggleLink );
149 outerSpan.appendChild( document.createTextNode( ']' ) );
150
151 linkHolder.appendChild( document.createTextNode( ' ' ) );
152 linkHolder.appendChild( outerSpan );
153
154 var cookiePos = document.cookie.indexOf( "hidetoc=" );
155 if ( cookiePos > -1 && document.cookie.charAt( cookiePos + 8 ) == 1 ) {
156 toggleToc();
157 }
158 }
159 }
160
161 function changeText( el, newText ) {
162 // Safari work around
163 if ( el.innerText ) {
164 el.innerText = newText;
165 } else if ( el.firstChild && el.firstChild.nodeValue ) {
166 el.firstChild.nodeValue = newText;
167 }
168 }
169
170 function toggleToc() {
171 var tocmain = document.getElementById( 'toc' );
172 var toc = document.getElementById('toc').getElementsByTagName('ul')[0];
173 var toggleLink = document.getElementById( 'togglelink' );
174
175 if ( toc && toggleLink && toc.style.display == 'none' ) {
176 changeText( toggleLink, tocHideText );
177 toc.style.display = 'block';
178 document.cookie = "hidetoc=0";
179 tocmain.className = 'toc';
180 } else {
181 changeText( toggleLink, tocShowText );
182 toc.style.display = 'none';
183 document.cookie = "hidetoc=1";
184 tocmain.className = 'toc tochidden';
185 }
186 }
187
188 var mwEditButtons = [];
189 var mwCustomEditButtons = []; // eg to add in MediaWiki:Common.js
190
191 function escapeQuotes( text ) {
192 var re = new RegExp( "'", "g" );
193 text = text.replace( re, "\\'" );
194 re = new RegExp( "\\n", "g" );
195 text = text.replace( re, "\\n" );
196 return escapeQuotesHTML( text );
197 }
198
199 function escapeQuotesHTML( text ) {
200 var re = new RegExp( '&', "g" );
201 text = text.replace( re, "&" );
202 re = new RegExp( '"', "g" );
203 text = text.replace( re, """ );
204 re = new RegExp( '<', "g" );
205 text = text.replace( re, "&lt;" );
206 re = new RegExp( '>', "g" );
207 text = text.replace( re, "&gt;" );
208 return text;
209 }
210
211 /**
212 * Set the accesskey prefix based on browser detection.
213 */
214 var tooltipAccessKeyPrefix = 'alt-';
215 if ( is_opera ) {
216 tooltipAccessKeyPrefix = 'shift-esc-';
217 } else if ( !is_safari_win && is_safari && webkit_version > 526 ) {
218 tooltipAccessKeyPrefix = 'ctrl-alt-';
219 } else if ( !is_safari_win && ( is_safari
220 || clientPC.indexOf('mac') != -1
221 || clientPC.indexOf('konqueror') != -1 ) ) {
222 tooltipAccessKeyPrefix = 'ctrl-';
223 } else if ( is_ff2 ) {
224 tooltipAccessKeyPrefix = 'alt-shift-';
225 }
226 var tooltipAccessKeyRegexp = /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/;
227
228 /**
229 * Add the appropriate prefix to the accesskey shown in the tooltip.
230 * If the nodeList parameter is given, only those nodes are updated;
231 * otherwise, all the nodes that will probably have accesskeys by
232 * default are updated.
233 *
234 * @param Array nodeList -- list of elements to update
235 */
236 function updateTooltipAccessKeys( nodeList ) {
237 if ( !nodeList ) {
238 // Rather than scan all links on the whole page, we can just scan these
239 // containers which contain the relevant links. This is really just an
240 // optimization technique.
241 var linkContainers = [
242 'column-one', // Monobook and Modern
243 'head', 'panel', 'p-logo' // Vector
244 ];
245 for ( var i in linkContainers ) {
246 var linkContainer = document.getElementById( linkContainers[i] );
247 if ( linkContainer ) {
248 updateTooltipAccessKeys( linkContainer.getElementsByTagName( 'a' ) );
249 }
250 }
251 // these are rare enough that no such optimization is needed
252 updateTooltipAccessKeys( document.getElementsByTagName( 'input' ) );
253 updateTooltipAccessKeys( document.getElementsByTagName( 'label' ) );
254 return;
255 }
256
257 for ( var i = 0; i < nodeList.length; i++ ) {
258 var element = nodeList[i];
259 var tip = element.getAttribute( 'title' );
260 if ( tip && tooltipAccessKeyRegexp.exec( tip ) ) {
261 tip = tip.replace(tooltipAccessKeyRegexp,
262 '[' + tooltipAccessKeyPrefix + "$5]");
263 element.setAttribute( 'title', tip );
264 }
265 }
266 }
267
268 /**
269 * Add a link to one of the portlet menus on the page, including:
270 *
271 * p-cactions: Content actions (shown as tabs above the main content in Monobook)
272 * p-personal: Personal tools (shown at the top right of the page in Monobook)
273 * p-navigation: Navigation
274 * p-tb: Toolbox
275 *
276 * This function exists for the convenience of custom JS authors. All
277 * but the first three parameters are optional, though providing at
278 * least an id and a tooltip is recommended.
279 *
280 * By default the new link will be added to the end of the list. To
281 * add the link before a given existing item, pass the DOM node of
282 * that item (easily obtained with document.getElementById()) as the
283 * nextnode parameter; to add the link _after_ an existing item, pass
284 * the node's nextSibling instead.
285 *
286 * @param String portlet -- id of the target portlet ("p-cactions", "p-personal", "p-navigation" or "p-tb")
287 * @param String href -- link URL
288 * @param String text -- link text (will be automatically lowercased by CSS for p-cactions in Monobook)
289 * @param String id -- id of the new item, should be unique and preferably have the appropriate prefix ("ca-", "pt-", "n-" or "t-")
290 * @param String tooltip -- text to show when hovering over the link, without accesskey suffix
291 * @param String accesskey -- accesskey to activate this link (one character, try to avoid conflicts)
292 * @param Node nextnode -- the DOM node before which the new item should be added, should be another item in the same list
293 *
294 * @return Node -- the DOM node of the new item (an LI element) or null
295 */
296 function addPortletLink( portlet, href, text, id, tooltip, accesskey, nextnode ) {
297 var root = document.getElementById( portlet );
298 if ( !root ) {
299 return null;
300 }
301 var node = root.getElementsByTagName( 'ul' )[0];
302 if ( !node ) {
303 return null;
304 }
305
306 // unhide portlet if it was hidden before
307 root.className = root.className.replace( /(^| )emptyPortlet( |$)/, "$2" );
308
309 var span = document.createElement( 'span' );
310 span.appendChild( document.createTextNode( text ) );
311
312 var link = document.createElement( 'a' );
313 link.appendChild( span );
314 link.href = href;
315
316 var item = document.createElement( 'li' );
317 item.appendChild( link );
318 if ( id ) {
319 item.id = id;
320 }
321
322 if ( accesskey ) {
323 link.setAttribute( 'accesskey', accesskey );
324 tooltip += ' [' + accesskey + ']';
325 }
326 if ( tooltip ) {
327 link.setAttribute( 'title', tooltip );
328 }
329 if ( accesskey && tooltip ) {
330 updateTooltipAccessKeys( new Array( link ) );
331 }
332
333 if ( nextnode && nextnode.parentNode == node ) {
334 node.insertBefore( item, nextnode );
335 } else {
336 node.appendChild( item ); // IE compatibility (?)
337 }
338
339 return item;
340 }
341
342 function getInnerText( el ) {
343 if ( typeof el == 'string' ) {
344 return el;
345 }
346 if ( typeof el == 'undefined' ) {
347 return el;
348 }
349 if ( el.textContent ) {
350 return el.textContent; // not needed but it is faster
351 }
352 if ( el.innerText ) {
353 return el.innerText; // IE doesn't have textContent
354 }
355 var str = '';
356
357 var cs = el.childNodes;
358 var l = cs.length;
359 for ( var i = 0; i < l; i++ ) {
360 switch ( cs[i].nodeType ) {
361 case 1: // ELEMENT_NODE
362 str += ts_getInnerText( cs[i] );
363 break;
364 case 3: // TEXT_NODE
365 str += cs[i].nodeValue;
366 break;
367 }
368 }
369 return str;
370 }
371
372 /* Dummy for deprecated function */
373 function akeytt( doId ) {
374 }
375
376 var checkboxes;
377 var lastCheckbox;
378
379 function setupCheckboxShiftClick() {
380 checkboxes = [];
381 lastCheckbox = null;
382 var inputs = document.getElementsByTagName( 'input' );
383 addCheckboxClickHandlers( inputs );
384 }
385
386 function addCheckboxClickHandlers( inputs, start ) {
387 if ( !start ) {
388 start = 0;
389 }
390
391 var finish = start + 250;
392 if ( finish > inputs.length ) {
393 finish = inputs.length;
394 }
395
396 for ( var i = start; i < finish; i++ ) {
397 var cb = inputs[i];
398 if ( !cb.type || cb.type.toLowerCase() != 'checkbox' ) {
399 continue;
400 }
401 var end = checkboxes.length;
402 checkboxes[end] = cb;
403 cb.index = end;
404 cb.onclick = checkboxClickHandler;
405 }
406
407 if ( finish < inputs.length ) {
408 setTimeout( function() {
409 addCheckboxClickHandlers( inputs, finish );
410 }, 200 );
411 }
412 }
413
414 function checkboxClickHandler( e ) {
415 if ( typeof e == 'undefined' ) {
416 e = window.event;
417 }
418 if ( !e.shiftKey || lastCheckbox === null ) {
419 lastCheckbox = this.index;
420 return true;
421 }
422 var endState = this.checked;
423 var start, finish;
424 if ( this.index < lastCheckbox ) {
425 start = this.index + 1;
426 finish = lastCheckbox;
427 } else {
428 start = lastCheckbox;
429 finish = this.index - 1;
430 }
431 for ( var i = start; i <= finish; ++i ) {
432 checkboxes[i].checked = endState;
433 if( i > start && typeof checkboxes[i].onchange == 'function' ) {
434 checkboxes[i].onchange(); // fire triggers
435 }
436 }
437 lastCheckbox = this.index;
438 return true;
439 }
440
441
442 /*
443 Written by Jonathan Snook, http://www.snook.ca/jonathan
444 Add-ons by Robert Nyman, http://www.robertnyman.com
445 Author says "The credit comment is all it takes, no license. Go crazy with it!:-)"
446 From http://www.robertnyman.com/2005/11/07/the-ultimate-getelementsbyclassname/
447 */
448 function getElementsByClassName( oElm, strTagName, oClassNames ) {
449 var arrReturnElements = new Array();
450 if ( typeof( oElm.getElementsByClassName ) == 'function' ) {
451 /* Use a native implementation where possible FF3, Saf3.2, Opera 9.5 */
452 var arrNativeReturn = oElm.getElementsByClassName( oClassNames );
453 if ( strTagName == '*' ) {
454 return arrNativeReturn;
455 }
456 for ( var h = 0; h < arrNativeReturn.length; h++ ) {
457 if( arrNativeReturn[h].tagName.toLowerCase() == strTagName.toLowerCase() ) {
458 arrReturnElements[arrReturnElements.length] = arrNativeReturn[h];
459 }
460 }
461 return arrReturnElements;
462 }
463 var arrElements = ( strTagName == '*' && oElm.all ) ? oElm.all : oElm.getElementsByTagName( strTagName );
464 var arrRegExpClassNames = new Array();
465 if( typeof oClassNames == 'object' ) {
466 for( var i = 0; i < oClassNames.length; i++ ) {
467 arrRegExpClassNames[arrRegExpClassNames.length] =
468 new RegExp("(^|\\s)" + oClassNames[i].replace(/\-/g, "\\-") + "(\\s|$)");
469 }
470 } else {
471 arrRegExpClassNames[arrRegExpClassNames.length] =
472 new RegExp("(^|\\s)" + oClassNames.replace(/\-/g, "\\-") + "(\\s|$)");
473 }
474 var oElement;
475 var bMatchesAll;
476 for( var j = 0; j < arrElements.length; j++ ) {
477 oElement = arrElements[j];
478 bMatchesAll = true;
479 for( var k = 0; k < arrRegExpClassNames.length; k++ ) {
480 if( !arrRegExpClassNames[k].test( oElement.className ) ) {
481 bMatchesAll = false;
482 break;
483 }
484 }
485 if( bMatchesAll ) {
486 arrReturnElements[arrReturnElements.length] = oElement;
487 }
488 }
489 return ( arrReturnElements );
490 }
491
492 function redirectToFragment( fragment ) {
493 var match = navigator.userAgent.match(/AppleWebKit\/(\d+)/);
494 if ( match ) {
495 var webKitVersion = parseInt( match[1] );
496 if ( webKitVersion < 420 ) {
497 // Released Safari w/ WebKit 418.9.1 messes up horribly
498 // Nightlies of 420+ are ok
499 return;
500 }
501 }
502 if ( is_gecko ) {
503 // Mozilla needs to wait until after load, otherwise the window doesn't scroll
504 addOnloadHook(function() {
505 if ( window.location.hash == '' ) {
506 window.location.hash = fragment;
507 }
508 });
509 } else {
510 if ( window.location.hash == '' ) {
511 window.location.hash = fragment;
512 }
513 }
514 }
515
516 /*
517 * Table sorting script based on one (c) 1997-2006 Stuart Langridge and Joost
518 * de Valk:
519 * http://www.joostdevalk.nl/code/sortable-table/
520 * http://www.kryogenix.org/code/browser/sorttable/
521 *
522 * @todo don't break on colspans/rowspans (bug 8028)
523 * @todo language-specific digit grouping/decimals (bug 8063)
524 * @todo support all accepted date formats (bug 8226)
525 */
526
527 var ts_image_path = stylepath + '/common/images/';
528 var ts_image_up = 'sort_up.gif';
529 var ts_image_down = 'sort_down.gif';
530 var ts_image_none = 'sort_none.gif';
531 var ts_europeandate = wgContentLanguage != 'en'; // The non-American-inclined can change to "true"
532 var ts_alternate_row_colors = false;
533 var ts_number_transform_table = null;
534 var ts_number_regex = null;
535
536 function sortables_init() {
537 var idnum = 0;
538 // Find all tables with class sortable and make them sortable
539 var tables = getElementsByClassName( document, 'table', 'sortable' );
540 for ( var ti = 0; ti < tables.length ; ti++ ) {
541 if ( !tables[ti].id ) {
542 tables[ti].setAttribute( 'id', 'sortable_table_id_' + idnum );
543 ++idnum;
544 }
545 ts_makeSortable( tables[ti] );
546 }
547 }
548
549 function ts_makeSortable( table ) {
550 var firstRow;
551 if ( table.rows && table.rows.length > 0 ) {
552 if ( table.tHead && table.tHead.rows.length > 0 ) {
553 firstRow = table.tHead.rows[table.tHead.rows.length-1];
554 } else {
555 firstRow = table.rows[0];
556 }
557 }
558 if ( !firstRow ) {
559 return;
560 }
561
562 // We have a first row: assume it's the header, and make its contents clickable links
563 for ( var i = 0; i < firstRow.cells.length; i++ ) {
564 var cell = firstRow.cells[i];
565 if ( (' ' + cell.className + ' ').indexOf(' unsortable ') == -1 ) {
566 cell.innerHTML += '<a href="#" class="sortheader" '
567 + 'onclick="ts_resortTable(this);return false;">'
568 + '<span class="sortarrow">'
569 + '<img src="'
570 + ts_image_path
571 + ts_image_none
572 + '" alt="&darr;"/></span></a>';
573 }
574 }
575 if ( ts_alternate_row_colors ) {
576 ts_alternate( table );
577 }
578 }
579
580 function ts_getInnerText( el ) {
581 return getInnerText( el );
582 }
583
584 function ts_resortTable( lnk ) {
585 // get the span
586 var span = lnk.getElementsByTagName('span')[0];
587
588 var td = lnk.parentNode;
589 var tr = td.parentNode;
590 var column = td.cellIndex;
591
592 var table = tr.parentNode;
593 while ( table && !( table.tagName && table.tagName.toLowerCase() == 'table' ) ) {
594 table = table.parentNode;
595 }
596 if ( !table ) {
597 return;
598 }
599
600 if ( table.rows.length <= 1 ) {
601 return;
602 }
603
604 // Generate the number transform table if it's not done already
605 if ( ts_number_transform_table === null ) {
606 ts_initTransformTable();
607 }
608
609 // Work out a type for the column
610 // Skip the first row if that's where the headings are
611 var rowStart = ( table.tHead && table.tHead.rows.length > 0 ? 0 : 1 );
612
613 var itm = '';
614 for ( var i = rowStart; i < table.rows.length; i++ ) {
615 if ( table.rows[i].cells.length > column ) {
616 itm = ts_getInnerText(table.rows[i].cells[column]);
617 itm = itm.replace(/^[\s\xa0]+/, '').replace(/[\s\xa0]+$/, '');
618 if ( itm != '' ) {
619 break;
620 }
621 }
622 }
623
624 // TODO: bug 8226, localised date formats
625 var sortfn = ts_sort_generic;
626 var preprocessor = ts_toLowerCase;
627 if ( /^\d\d[\/. -][a-zA-Z]{3}[\/. -]\d\d\d\d$/.test( itm ) ) {
628 preprocessor = ts_dateToSortKey;
629 } else if ( /^\d\d[\/.-]\d\d[\/.-]\d\d\d\d$/.test( itm ) ) {
630 preprocessor = ts_dateToSortKey;
631 } else if ( /^\d\d[\/.-]\d\d[\/.-]\d\d$/.test( itm ) ) {
632 preprocessor = ts_dateToSortKey;
633 // (minus sign)([pound dollar euro yen currency]|cents)
634 } else if ( /(^([-\u2212] *)?[\u00a3$\u20ac\u00a4\u00a5]|\u00a2$)/.test( itm ) ) {
635 preprocessor = ts_currencyToSortKey;
636 } else if ( ts_number_regex.test( itm ) ) {
637 preprocessor = ts_parseFloat;
638 }
639
640 var reverse = ( span.getAttribute( 'sortdir' ) == 'down' );
641
642 var newRows = new Array();
643 var staticRows = new Array();
644 for ( var j = rowStart; j < table.rows.length; j++ ) {
645 var row = table.rows[j];
646 if( (' ' + row.className + ' ').indexOf(' unsortable ') < 0 ) {
647 var keyText = ts_getInnerText( row.cells[column] );
648 if( keyText === undefined ) {
649 keyText = '';
650 }
651 var oldIndex = ( reverse ? -j : j );
652 var preprocessed = preprocessor( keyText.replace(/^[\s\xa0]+/, '').replace(/[\s\xa0]+$/, '') );
653
654 newRows[newRows.length] = new Array( row, preprocessed, oldIndex );
655 } else {
656 staticRows[staticRows.length] = new Array( row, false, j-rowStart );
657 }
658 }
659
660 newRows.sort( sortfn );
661
662 var arrowHTML;
663 if ( reverse ) {
664 arrowHTML = '<img src="' + ts_image_path + ts_image_down + '" alt="&darr;"/>';
665 newRows.reverse();
666 span.setAttribute( 'sortdir', 'up' );
667 } else {
668 arrowHTML = '<img src="' + ts_image_path + ts_image_up + '" alt="&uarr;"/>';
669 span.setAttribute( 'sortdir', 'down' );
670 }
671
672 for ( var i = 0; i < staticRows.length; i++ ) {
673 var row = staticRows[i];
674 newRows.splice( row[2], 0, row );
675 }
676
677 // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
678 // don't do sortbottom rows
679 for ( var i = 0; i < newRows.length; i++ ) {
680 if ( ( ' ' + newRows[i][0].className + ' ').indexOf(' sortbottom ') == -1 ) {
681 table.tBodies[0].appendChild( newRows[i][0] );
682 }
683 }
684 // do sortbottom rows only
685 for ( var i = 0; i < newRows.length; i++ ) {
686 if ( ( ' ' + newRows[i][0].className + ' ').indexOf(' sortbottom ') != -1 ) {
687 table.tBodies[0].appendChild( newRows[i][0] );
688 }
689 }
690
691 // Delete any other arrows there may be showing
692 var spans = getElementsByClassName( tr, 'span', 'sortarrow' );
693 for ( var i = 0; i < spans.length; i++ ) {
694 spans[i].innerHTML = '<img src="' + ts_image_path + ts_image_none + '" alt="&darr;"/>';
695 }
696 span.innerHTML = arrowHTML;
697
698 if ( ts_alternate_row_colors ) {
699 ts_alternate( table );
700 }
701 }
702
703 function ts_initTransformTable() {
704 if ( typeof wgSeparatorTransformTable == 'undefined'
705 || ( wgSeparatorTransformTable[0] == '' && wgDigitTransformTable[2] == '' ) )
706 {
707 digitClass = "[0-9,.]";
708 ts_number_transform_table = false;
709 } else {
710 ts_number_transform_table = {};
711 // Unpack the transform table
712 // Separators
713 ascii = wgSeparatorTransformTable[0].split("\t");
714 localised = wgSeparatorTransformTable[1].split("\t");
715 for ( var i = 0; i < ascii.length; i++ ) {
716 ts_number_transform_table[localised[i]] = ascii[i];
717 }
718 // Digits
719 ascii = wgDigitTransformTable[0].split("\t");
720 localised = wgDigitTransformTable[1].split("\t");
721 for ( var i = 0; i < ascii.length; i++ ) {
722 ts_number_transform_table[localised[i]] = ascii[i];
723 }
724
725 // Construct regex for number identification
726 digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '\\.'];
727 maxDigitLength = 1;
728 for ( var digit in ts_number_transform_table ) {
729 // Escape regex metacharacters
730 digits.push(
731 digit.replace( /[\\\\$\*\+\?\.\(\)\|\{\}\[\]\-]/,
732 function( s ) { return '\\' + s; } )
733 );
734 if ( digit.length > maxDigitLength ) {
735 maxDigitLength = digit.length;
736 }
737 }
738 if ( maxDigitLength > 1 ) {
739 digitClass = '[' + digits.join( '', digits ) + ']';
740 } else {
741 digitClass = '(' + digits.join( '|', digits ) + ')';
742 }
743 }
744
745 // We allow a trailing percent sign, which we just strip. This works fine
746 // if percents and regular numbers aren't being mixed.
747 ts_number_regex = new RegExp(
748 "^(" +
749 "[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?" + // Fortran-style scientific
750 "|" +
751 "[-+\u2212]?" + digitClass + "+%?" + // Generic localised
752 ")$", "i"
753 );
754 }
755
756 function ts_toLowerCase( s ) {
757 return s.toLowerCase();
758 }
759
760 function ts_dateToSortKey( date ) {
761 // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
762 if ( date.length == 11 ) {
763 switch ( date.substr( 3, 3 ).toLowerCase() ) {
764 case 'jan':
765 var month = '01';
766 break;
767 case 'feb':
768 var month = '02';
769 break;
770 case 'mar':
771 var month = '03';
772 break;
773 case 'apr':
774 var month = '04';
775 break;
776 case 'may':
777 var month = '05';
778 break;
779 case 'jun':
780 var month = '06';
781 break;
782 case 'jul':
783 var month = '07';
784 break;
785 case 'aug':
786 var month = '08';
787 break;
788 case 'sep':
789 var month = '09';
790 break;
791 case 'oct':
792 var month = '10';
793 break;
794 case 'nov':
795 var month = '11';
796 break;
797 case 'dec':
798 var month = '12';
799 break;
800 // default: var month = '00';
801 }
802 return date.substr( 7, 4 ) + month + date.substr( 0, 2 );
803 } else if ( date.length == 10 ) {
804 if ( ts_europeandate == false ) {
805 return date.substr( 6, 4 ) + date.substr( 0, 2 ) + date.substr( 3, 2 );
806 } else {
807 return date.substr( 6, 4 ) + date.substr( 3, 2 ) + date.substr( 0, 2 );
808 }
809 } else if ( date.length == 8 ) {
810 yr = date.substr( 6, 2 );
811 if ( parseInt( yr ) < 50 ) {
812 yr = '20' + yr;
813 } else {
814 yr = '19' + yr;
815 }
816 if ( ts_europeandate == true ) {
817 return yr + date.substr( 3, 2 ) + date.substr( 0, 2 );
818 } else {
819 return yr + date.substr( 0, 2 ) + date.substr( 3, 2 );
820 }
821 }
822 return '00000000';
823 }
824
825 function ts_parseFloat( s ) {
826 if ( !s ) {
827 return 0;
828 }
829 if ( ts_number_transform_table != false ) {
830 var newNum = '', c;
831
832 for ( var p = 0; p < s.length; p++ ) {
833 c = s.charAt( p );
834 if ( c in ts_number_transform_table ) {
835 newNum += ts_number_transform_table[c];
836 } else {
837 newNum += c;
838 }
839 }
840 s = newNum;
841 }
842 num = parseFloat( s.replace(/[, ]/g, '').replace("\u2212", '-') );
843 return ( isNaN( num ) ? -Infinity : num );
844 }
845
846 function ts_currencyToSortKey( s ) {
847 return ts_parseFloat(s.replace(/[^-\u22120-9.,]/g,''));
848 }
849
850 function ts_sort_generic( a, b ) {
851 return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[2] - b[2];
852 }
853
854 function ts_alternate( table ) {
855 // Take object table and get all it's tbodies.
856 var tableBodies = table.getElementsByTagName( 'tbody' );
857 // Loop through these tbodies
858 for ( var i = 0; i < tableBodies.length; i++ ) {
859 // Take the tbody, and get all it's rows
860 var tableRows = tableBodies[i].getElementsByTagName( 'tr' );
861 // Loop through these rows
862 // Start at 1 because we want to leave the heading row untouched
863 for ( var j = 0; j < tableRows.length; j++ ) {
864 // Check if j is even, and apply classes for both possible results
865 var oldClasses = tableRows[j].className.split(' ');
866 var newClassName = '';
867 for ( var k = 0; k < oldClasses.length; k++ ) {
868 if ( oldClasses[k] != '' && oldClasses[k] != 'even' && oldClasses[k] != 'odd' ) {
869 newClassName += oldClasses[k] + ' ';
870 }
871 }
872 tableRows[j].className = newClassName + ( j % 2 == 0 ? 'even' : 'odd' );
873 }
874 }
875 }
876
877 /*
878 * End of table sorting code
879 */
880
881
882 /**
883 * Add a cute little box at the top of the screen to inform the user of
884 * something, replacing any preexisting message.
885 *
886 * @param String -or- Dom Object message HTML to be put inside the right div
887 * @param String className Used in adding a class; should be different for each
888 * call to allow CSS/JS to hide different boxes. null = no class used.
889 * @return Boolean True on success, false on failure
890 */
891 function jsMsg( message, className ) {
892 if ( !document.getElementById ) {
893 return false;
894 }
895 // We special-case skin structures provided by the software. Skins that
896 // choose to abandon or significantly modify our formatting can just define
897 // an mw-js-message div to start with.
898 var messageDiv = document.getElementById( 'mw-js-message' );
899 if ( !messageDiv ) {
900 messageDiv = document.createElement( 'div' );
901 if ( document.getElementById( 'column-content' )
902 && document.getElementById( 'content' ) ) {
903 // MonoBook, presumably
904 document.getElementById( 'content' ).insertBefore(
905 messageDiv,
906 document.getElementById( 'content' ).firstChild
907 );
908 } else if ( document.getElementById( 'content' )
909 && document.getElementById( 'article' ) ) {
910 // Non-Monobook but still recognizable (old-style)
911 document.getElementById( 'article').insertBefore(
912 messageDiv,
913 document.getElementById( 'article' ).firstChild
914 );
915 } else {
916 return false;
917 }
918 }
919
920 messageDiv.setAttribute( 'id', 'mw-js-message' );
921 messageDiv.style.display = 'block';
922 if( className ) {
923 messageDiv.setAttribute( 'class', 'mw-js-message-' + className );
924 }
925
926 if ( typeof message === 'object' ) {
927 while ( messageDiv.hasChildNodes() ) { // Remove old content
928 messageDiv.removeChild( messageDiv.firstChild );
929 }
930 messageDiv.appendChild( message ); // Append new content
931 } else {
932 messageDiv.innerHTML = message;
933 }
934 return true;
935 }
936
937 /**
938 * Inject a cute little progress spinner after the specified element
939 *
940 * @param element Element to inject after
941 * @param id Identifier string (for use with removeSpinner(), below)
942 */
943 function injectSpinner( element, id ) {
944 var spinner = document.createElement( 'img' );
945 spinner.id = 'mw-spinner-' + id;
946 spinner.src = stylepath + '/common/images/spinner.gif';
947 spinner.alt = spinner.title = '...';
948 if( element.nextSibling ) {
949 element.parentNode.insertBefore( spinner, element.nextSibling );
950 } else {
951 element.parentNode.appendChild( spinner );
952 }
953 }
954
955 /**
956 * Remove a progress spinner added with injectSpinner()
957 *
958 * @param id Identifier string
959 */
960 function removeSpinner( id ) {
961 var spinner = document.getElementById( 'mw-spinner-' + id );
962 if( spinner ) {
963 spinner.parentNode.removeChild( spinner );
964 }
965 }
966
967 function runOnloadHook() {
968 // don't run anything below this for non-dom browsers
969 if ( doneOnloadHook || !( document.getElementById && document.getElementsByTagName ) ) {
970 return;
971 }
972
973 // set this before running any hooks, since any errors below
974 // might cause the function to terminate prematurely
975 doneOnloadHook = true;
976
977 updateTooltipAccessKeys( null );
978 setupCheckboxShiftClick();
979 sortables_init();
980
981 // Run any added-on functions
982 for ( var i = 0; i < onloadFuncts.length; i++ ) {
983 onloadFuncts[i]();
984 }
985 }
986
987 /**
988 * Add an event handler to an element
989 *
990 * @param Element element Element to add handler to
991 * @param String attach Event to attach to
992 * @param callable handler Event handler callback
993 */
994 function addHandler( element, attach, handler ) {
995 if( window.addEventListener ) {
996 element.addEventListener( attach, handler, false );
997 } else if( window.attachEvent ) {
998 element.attachEvent( 'on' + attach, handler );
999 }
1000 }
1001
1002 /**
1003 * Add a click event handler to an element
1004 *
1005 * @param Element element Element to add handler to
1006 * @param callable handler Event handler callback
1007 */
1008 function addClickHandler( element, handler ) {
1009 addHandler( element, 'click', handler );
1010 }
1011
1012 /**
1013 * Removes an event handler from an element
1014 *
1015 * @param Element element Element to remove handler from
1016 * @param String remove Event to remove
1017 * @param callable handler Event handler callback to remove
1018 */
1019 function removeHandler( element, remove, handler ) {
1020 if( window.removeEventListener ) {
1021 element.removeEventListener( remove, handler, false );
1022 } else if( window.detachEvent ) {
1023 element.detachEvent( 'on' + remove, handler );
1024 }
1025 }
1026 // note: all skins should call runOnloadHook() at the end of html output,
1027 // so the below should be redundant. It's there just in case.
1028 hookEvent( 'load', runOnloadHook );
1029
1030 if ( ie6_bugs ) {
1031 var isMSIE55 = ( window.showModalDialog && window.clipboardData && window.createPopup );
1032 var doneIETransform;
1033 var doneIEAlphaFix;
1034
1035 if ( document.attachEvent ) {
1036 document.attachEvent( 'onreadystatechange', hookit );
1037 }
1038
1039 function hookit() {
1040 if ( !doneIETransform && document.getElementById && document.getElementById( 'bodyContent' ) ) {
1041 doneIETransform = true;
1042 relativeforfloats();
1043 fixalpha();
1044 }
1045 }
1046
1047 // png alpha transparency fixes
1048 function fixalpha( logoId ) {
1049 // bg
1050 if ( isMSIE55 && !doneIEAlphaFix ) {
1051 var plogo = document.getElementById( logoId || 'p-logo' );
1052 if ( !plogo ) {
1053 return;
1054 }
1055
1056 var logoa = plogo.getElementsByTagName('a')[0];
1057 if ( !logoa ) {
1058 return;
1059 }
1060
1061 var bg = logoa.currentStyle.backgroundImage;
1062 var imageUrl = bg.substring( 5, bg.length - 2 );
1063
1064 doneIEAlphaFix = true;
1065
1066 if ( imageUrl.substr( imageUrl.length - 4 ).toLowerCase() == '.png' ) {
1067 var logospan = logoa.appendChild( document.createElement( 'span' ) );
1068
1069 logoa.style.backgroundImage = 'none';
1070 logospan.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=' + imageUrl + ')';
1071 logospan.style.height = '100%';
1072 logospan.style.position = 'absolute';
1073 logospan.style.width = logoa.currentStyle.width;
1074 logospan.style.cursor = 'hand';
1075 // Center image with hack for IE5.5
1076 if ( document.documentElement.dir == 'rtl' ) {
1077 logospan.style.right = '50%';
1078 logospan.style.setExpression( 'marginRight', '"-" + (this.offsetWidth / 2) + "px"' );
1079 } else {
1080 logospan.style.left = '50%';
1081 logospan.style.setExpression( 'marginLeft', '"-" + (this.offsetWidth / 2) + "px"' );
1082 }
1083 logospan.style.top = '50%';
1084 logospan.style.setExpression( 'marginTop', '"-" + (this.offsetHeight / 2) + "px"' );
1085
1086 var linkFix = logoa.appendChild( logoa.cloneNode() );
1087 linkFix.style.position = 'absolute';
1088 linkFix.style.height = '100%';
1089 linkFix.style.width = '100%';
1090 }
1091 }
1092 }
1093
1094 // fix ie6 disappering float bug
1095 function relativeforfloats() {
1096 var bc = document.getElementById( 'bodyContent' );
1097 if ( bc ) {
1098 var tables = bc.getElementsByTagName( 'table' );
1099 var divs = bc.getElementsByTagName( 'div' );
1100 }
1101 setrelative( tables );
1102 setrelative( divs );
1103 }
1104 function setrelative( nodes ) {
1105 var i = 0;
1106 while ( i < nodes.length ) {
1107 if( ( ( nodes[i].style.float && nodes[i].style.float != ( 'none' ) ||
1108 ( nodes[i].align && nodes[i].align != ( 'none' ) ) ) &&
1109 ( !nodes[i].style.position || nodes[i].style.position != 'relative' ) ) )
1110 {
1111 nodes[i].style.position = 'relative';
1112 }
1113 i++;
1114 }
1115 }
1116
1117 // Expand links for printing
1118 String.prototype.hasClass = function( classWanted ) {
1119 var classArr = this.split(/\s/);
1120 for ( var i = 0; i < classArr.length; i++ ) {
1121 if ( classArr[i].toLowerCase() == classWanted.toLowerCase() ) {
1122 return true;
1123 }
1124 }
1125 return false;
1126 }
1127
1128 var expandedURLs;
1129
1130 onbeforeprint = function() {
1131 expandedURLs = [];
1132
1133 var contentEl = document.getElementById( 'content' );
1134
1135 if ( contentEl ) {
1136 var allLinks = contentEl.getElementsByTagName( 'a' );
1137
1138 for ( var i = 0; i < allLinks.length; i++ ) {
1139 if ( allLinks[i].className.hasClass( 'external' ) && !allLinks[i].className.hasClass( 'free' ) ) {
1140 var expandedLink = document.createElement( 'span' );
1141 var expandedText = document.createTextNode( ' (' + allLinks[i].href + ')' );
1142 expandedLink.appendChild( expandedText );
1143 allLinks[i].parentNode.insertBefore( expandedLink, allLinks[i].nextSibling );
1144 expandedURLs[i] = expandedLink;
1145 }
1146 }
1147 }
1148 }
1149
1150 onafterprint = function() {
1151 for ( var i = 0; i < expandedURLs.length; i++ ) {
1152 if ( expandedURLs[i] ) {
1153 expandedURLs[i].removeNode( true );
1154 }
1155 }
1156 }
1157 }