Expand a few comments. One as per Nikerabbit's comment on r59695.
[mediawiki.git] / skins / common / wikibits.js
blobf2505312a01a60e2315c9e87cc49930f78374474
1 // MediaWiki JavaScript support functions
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]);
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 );
29 // Global external objects used by this script.
30 /*extern ta, stylepath, skin */
32 // add any onload functions in this hook (please don't hard-code any events in the xhtml source)
33 var doneOnloadHook;
35 if (!window.onloadFuncts) {
36         var onloadFuncts = [];
39 function addOnloadHook(hookFunct) {
40         // Allows add-on scripts to add onload functions
41         if(!doneOnloadHook) {
42                 onloadFuncts[onloadFuncts.length] = hookFunct;
43         } else {
44                 hookFunct();  // bug in MSIE script loading
45         }
49 function hookEvent(hookName, hookFunct) {
50         addHandler(window, hookName, hookFunct);
53 function importScript(page) {
54         // TODO: might want to introduce a utility function to match wfUrlencode() in PHP
55         var uri = wgScript + '?title=' +
56                 encodeURIComponent(page.replace(/ /g,'_')).replace(/%2F/ig,'/').replace(/%3A/ig,':') +
57                 '&action=raw&ctype=text/javascript';
58         return importScriptURI(uri);
61 var loadedScripts = {}; // included-scripts tracker
62 function importScriptURI(url) {
63         if (loadedScripts[url]) {
64                 return null;
65         }
66         loadedScripts[url] = true;
67         var s = document.createElement('script');
68         s.setAttribute('src',url);
69         s.setAttribute('type','text/javascript');
70         document.getElementsByTagName('head')[0].appendChild(s);
71         return s;
74 function importStylesheet(page) {
75         return importStylesheetURI(wgScript + '?action=raw&ctype=text/css&title=' + encodeURIComponent(page.replace(/ /g,'_')));
78 function importStylesheetURI(url,media) {
79         var l = document.createElement('link');
80         l.type = 'text/css';
81         l.rel = 'stylesheet';
82         l.href = url;
83         if(media) l.media = media
84         document.getElementsByTagName('head')[0].appendChild(l);
85         return l;
88 function appendCSS(text) {
89         var s = document.createElement('style');
90         s.type = 'text/css';
91         s.rel = 'stylesheet';
92         if (s.styleSheet) s.styleSheet.cssText = text //IE
93         else s.appendChild(document.createTextNode(text + '')) //Safari sometimes borks on null
94         document.getElementsByTagName('head')[0].appendChild(s);
95         return s;
98 // special stylesheet links
99 if (typeof stylepath != 'undefined' && typeof skin != 'undefined') {
100         // FIXME: This tries to load the stylesheets even for skins where they
101         // don't exist, i.e., everything but Monobook.
102         if (opera6_bugs) {
103                 importStylesheetURI(stylepath+'/'+skin+'/Opera6Fixes.css');
104         } else if (opera7_bugs) {
105                 importStylesheetURI(stylepath+'/'+skin+'/Opera7Fixes.css');
106         } else if (opera95_bugs) {
107                 importStylesheetURI(stylepath+'/'+skin+'/Opera9Fixes.css');
108         } else if (ff2_bugs) {
109                 importStylesheetURI(stylepath+'/'+skin+'/FF2Fixes.css');
110         }
114 if (wgBreakFrames) {
115         // Un-trap us from framesets
116         if (window.top != window) {
117                 window.top.location = window.location;
118         }
121 function showTocToggle() {
122         if (document.createTextNode) {
123                 // Uses DOM calls to avoid document.write + XHTML issues
125                 var linkHolder = document.getElementById('toctitle');
126                 var existingLink = document.getElementById('togglelink');
127                 if (!linkHolder || existingLink) {
128                         // Don't add the toggle link twice
129                         return;
130                 }
132                 var outerSpan = document.createElement('span');
133                 outerSpan.className = 'toctoggle';
135                 var toggleLink = document.createElement('a');
136                 toggleLink.id = 'togglelink';
137                 toggleLink.className = 'internal';
138                 toggleLink.href = 'javascript:toggleToc()';
139                 toggleLink.appendChild(document.createTextNode(tocHideText));
141                 outerSpan.appendChild(document.createTextNode('['));
142                 outerSpan.appendChild(toggleLink);
143                 outerSpan.appendChild(document.createTextNode(']'));
145                 linkHolder.appendChild(document.createTextNode(' '));
146                 linkHolder.appendChild(outerSpan);
148                 var cookiePos = document.cookie.indexOf("hidetoc=");
149                 if (cookiePos > -1 && document.cookie.charAt(cookiePos + 8) == 1) {
150                         toggleToc();
151                 }
152         }
155 function changeText(el, newText) {
156         // Safari work around
157         if (el.innerText) {
158                 el.innerText = newText;
159         } else if (el.firstChild && el.firstChild.nodeValue) {
160                 el.firstChild.nodeValue = newText;
161         }
164 function toggleToc() {
165         var tocmain = document.getElementById('toc');
166         var toc = document.getElementById('toc').getElementsByTagName('ul')[0];
167         var toggleLink = document.getElementById('togglelink');
169         if (toc && toggleLink && toc.style.display == 'none') {
170                 changeText(toggleLink, tocHideText);
171                 toc.style.display = 'block';
172                 document.cookie = "hidetoc=0";
173                 tocmain.className = 'toc';
174         } else {
175                 changeText(toggleLink, tocShowText);
176                 toc.style.display = 'none';
177                 document.cookie = "hidetoc=1";
178                 tocmain.className = 'toc tochidden';
179         }
182 var mwEditButtons = [];
183 var mwCustomEditButtons = []; // eg to add in MediaWiki:Common.js
185 function escapeQuotes(text) {
186         var re = new RegExp("'","g");
187         text = text.replace(re,"\\'");
188         re = new RegExp("\\n","g");
189         text = text.replace(re,"\\n");
190         return escapeQuotesHTML(text);
193 function escapeQuotesHTML(text) {
194         var re = new RegExp('&',"g");
195         text = text.replace(re,"&");
196         re = new RegExp('"',"g");
197         text = text.replace(re,""");
198         re = new RegExp('<',"g");
199         text = text.replace(re,"&lt;");
200         re = new RegExp('>',"g");
201         text = text.replace(re,"&gt;");
202         return text;
207  * Set the accesskey prefix based on browser detection.
208  */
209 var tooltipAccessKeyPrefix = 'alt-';
210 if (is_opera) {
211         tooltipAccessKeyPrefix = 'shift-esc-';
212 } else if (!is_safari_win && is_safari && webkit_version > 526) {
213         tooltipAccessKeyPrefix = 'ctrl-alt-';
214 } else if (!is_safari_win && (is_safari
215                 || clientPC.indexOf('mac') != -1
216                 || clientPC.indexOf('konqueror') != -1 )) {
217         tooltipAccessKeyPrefix = 'ctrl-';
218 } else if (is_ff2) {
219         tooltipAccessKeyPrefix = 'alt-shift-';
221 var tooltipAccessKeyRegexp = /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/;
224  * Add the appropriate prefix to the accesskey shown in the tooltip.
225  * If the nodeList parameter is given, only those nodes are updated;
226  * otherwise, all the nodes that will probably have accesskeys by
227  * default are updated.
229  * @param Array nodeList -- list of elements to update
230  */
231 function updateTooltipAccessKeys( nodeList ) {
232         if ( !nodeList ) {
233                 // Rather than scan all links on the whole page, we can just scan these
234                 // containers which contain the relevant links. This is really just an
235                 // optimization technique.
236                 var linkContainers = [
237                         "column-one", // Monobook and Modern
238                         "head", "panel", "p-logo" // Vector
239                 ];
240                 for ( var i in linkContainers ) {
241                         var linkContainer = document.getElementById( linkContainers[i] );
242                         if ( linkContainer ) {
243                                 updateTooltipAccessKeys( linkContainer.getElementsByTagName("a") );
244                         }
245                 }
246                 // these are rare enough that no such optimization is needed
247                 updateTooltipAccessKeys( document.getElementsByTagName("input") );
248                 updateTooltipAccessKeys( document.getElementsByTagName("label") );
249                 return;
250         }
252         for ( var i = 0; i < nodeList.length; i++ ) {
253                 var element = nodeList[i];
254                 var tip = element.getAttribute("title");
255                 if ( tip && tooltipAccessKeyRegexp.exec(tip) ) {
256                         tip = tip.replace(tooltipAccessKeyRegexp,
257                                           "["+tooltipAccessKeyPrefix+"$5]");
258                         element.setAttribute("title", tip );
259                 }
260         }
264  * Add a link to one of the portlet menus on the page, including:
266  * p-cactions: Content actions (shown as tabs above the main content in Monobook)
267  * p-personal: Personal tools (shown at the top right of the page in Monobook)
268  * p-navigation: Navigation
269  * p-tb: Toolbox
271  * This function exists for the convenience of custom JS authors.  All
272  * but the first three parameters are optional, though providing at
273  * least an id and a tooltip is recommended.
275  * By default the new link will be added to the end of the list.  To
276  * add the link before a given existing item, pass the DOM node of
277  * that item (easily obtained with document.getElementById()) as the
278  * nextnode parameter; to add the link _after_ an existing item, pass
279  * the node's nextSibling instead.
281  * @param String portlet -- id of the target portlet ("p-cactions", "p-personal", "p-navigation" or "p-tb")
282  * @param String href -- link URL
283  * @param String text -- link text (will be automatically lowercased by CSS for p-cactions in Monobook)
284  * @param String id -- id of the new item, should be unique and preferably have the appropriate prefix ("ca-", "pt-", "n-" or "t-")
285  * @param String tooltip -- text to show when hovering over the link, without accesskey suffix
286  * @param String accesskey -- accesskey to activate this link (one character, try to avoid conflicts)
287  * @param Node nextnode -- the DOM node before which the new item should be added, should be another item in the same list
289  * @return Node -- the DOM node of the new item (an LI element) or null
290  */
291 function addPortletLink(portlet, href, text, id, tooltip, accesskey, nextnode) {
292         var root = document.getElementById(portlet);
293         if ( !root ) return null;
294         var node = root.getElementsByTagName( "ul" )[0];
295         if ( !node ) return null;
297         // unhide portlet if it was hidden before
298         root.className = root.className.replace( /(^| )emptyPortlet( |$)/, "$2" );
300         var span = document.createElement( "span" );
301         span.appendChild( document.createTextNode( text ) );
303         var link = document.createElement( "a" );
304         link.appendChild( span );
305         link.href = href;
307         var item = document.createElement( "li" );
308         item.appendChild( link );
309         if ( id ) item.id = id;
311         if ( accesskey ) {
312                 link.setAttribute( "accesskey", accesskey );
313                 tooltip += " ["+accesskey+"]";
314         }
315         if ( tooltip ) {
316                 link.setAttribute( "title", tooltip );
317         }
318         if ( accesskey && tooltip ) {
319                 updateTooltipAccessKeys( new Array( link ) );
320         }
322         if ( nextnode && nextnode.parentNode == node )
323                 node.insertBefore( item, nextnode );
324         else
325                 node.appendChild( item );  // IE compatibility (?)
327         return item;
330 function getInnerText(el) {
331         if (typeof el == "string") return el;
332         if (typeof el == "undefined") { return el };
333         if (el.textContent) return el.textContent; // not needed but it is faster
334         if (el.innerText) return el.innerText;     // IE doesn't have textContent
335         var str = "";
337         var cs = el.childNodes;
338         var l = cs.length;
339         for (var i = 0; i < l; i++) {
340                 switch (cs[i].nodeType) {
341                         case 1: //ELEMENT_NODE
342                                 str += ts_getInnerText(cs[i]);
343                                 break;
344                         case 3: //TEXT_NODE
345                                 str += cs[i].nodeValue;
346                                 break;
347                 }
348         }
349         return str;
352 /* Dummy for deprecated function */
353 function akeytt( doId ) {
356 var checkboxes;
357 var lastCheckbox;
359 function setupCheckboxShiftClick() {
360         checkboxes = [];
361         lastCheckbox = null;
362         var inputs = document.getElementsByTagName('input');
363         addCheckboxClickHandlers(inputs);
366 function addCheckboxClickHandlers(inputs, start) {
367         if ( !start) start = 0;
369         var finish = start + 250;
370         if ( finish > inputs.length )
371                 finish = inputs.length;
373         for ( var i = start; i < finish; i++ ) {
374                 var cb = inputs[i];
375                 if ( !cb.type || cb.type.toLowerCase() != 'checkbox' )
376                         continue;
377                 var end = checkboxes.length;
378                 checkboxes[end] = cb;
379                 cb.index = end;
380                 cb.onclick = checkboxClickHandler;
381         }
383         if ( finish < inputs.length ) {
384                 setTimeout( function () {
385                         addCheckboxClickHandlers(inputs, finish);
386                 }, 200 );
387         }
390 function checkboxClickHandler(e) {
391         if (typeof e == 'undefined') {
392                 e = window.event;
393         }
394         if ( !e.shiftKey || lastCheckbox === null ) {
395                 lastCheckbox = this.index;
396                 return true;
397         }
398         var endState = this.checked;
399         var start, finish;
400         if ( this.index < lastCheckbox ) {
401                 start = this.index + 1;
402                 finish = lastCheckbox;
403         } else {
404                 start = lastCheckbox;
405                 finish = this.index - 1;
406         }
407         for (var i = start; i <= finish; ++i ) {
408                 checkboxes[i].checked = endState;
409                 if( i > start && typeof checkboxes[i].onchange == 'function' )
410                         checkboxes[i].onchange(); // fire triggers
411         }
412         lastCheckbox = this.index;
413         return true;
418         Written by Jonathan Snook, http://www.snook.ca/jonathan
419         Add-ons by Robert Nyman, http://www.robertnyman.com
420         Author says "The credit comment is all it takes, no license. Go crazy with it!:-)"
421         From http://www.robertnyman.com/2005/11/07/the-ultimate-getelementsbyclassname/
423 function getElementsByClassName(oElm, strTagName, oClassNames){
424         var arrReturnElements = new Array();
425         if ( typeof( oElm.getElementsByClassName ) == "function" ) {
426                 /* Use a native implementation where possible FF3, Saf3.2, Opera 9.5 */
427                 var arrNativeReturn = oElm.getElementsByClassName( oClassNames );
428                 if ( strTagName == "*" )
429                         return arrNativeReturn;
430                 for ( var h=0; h < arrNativeReturn.length; h++ ) {
431                         if( arrNativeReturn[h].tagName.toLowerCase() == strTagName.toLowerCase() )
432                                 arrReturnElements[arrReturnElements.length] = arrNativeReturn[h];
433                 }
434                 return arrReturnElements;
435         }
436         var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
437         var arrRegExpClassNames = new Array();
438         if(typeof oClassNames == "object"){
439                 for(var i=0; i<oClassNames.length; i++){
440                         arrRegExpClassNames[arrRegExpClassNames.length] =
441                                 new RegExp("(^|\\s)" + oClassNames[i].replace(/\-/g, "\\-") + "(\\s|$)");
442                 }
443         }
444         else{
445                 arrRegExpClassNames[arrRegExpClassNames.length] =
446                         new RegExp("(^|\\s)" + oClassNames.replace(/\-/g, "\\-") + "(\\s|$)");
447         }
448         var oElement;
449         var bMatchesAll;
450         for(var j=0; j<arrElements.length; j++){
451                 oElement = arrElements[j];
452                 bMatchesAll = true;
453                 for(var k=0; k<arrRegExpClassNames.length; k++){
454                         if(!arrRegExpClassNames[k].test(oElement.className)){
455                                 bMatchesAll = false;
456                                 break;
457                         }
458                 }
459                 if(bMatchesAll){
460                         arrReturnElements[arrReturnElements.length] = oElement;
461                 }
462         }
463         return (arrReturnElements)
466 function redirectToFragment(fragment) {
467         var match = navigator.userAgent.match(/AppleWebKit\/(\d+)/);
468         if (match) {
469                 var webKitVersion = parseInt(match[1]);
470                 if (webKitVersion < 420) {
471                         // Released Safari w/ WebKit 418.9.1 messes up horribly
472                         // Nightlies of 420+ are ok
473                         return;
474                 }
475         }
476         if (is_gecko) {
477                 // Mozilla needs to wait until after load, otherwise the window doesn't scroll
478                 addOnloadHook(function () {
479                         if (window.location.hash == "")
480                                 window.location.hash = fragment;
481                 });
482         } else {
483                 if (window.location.hash == "")
484                         window.location.hash = fragment;
485         }
489  * Table sorting script based on one (c) 1997-2006 Stuart Langridge and Joost
490  * de Valk:
491  * http://www.joostdevalk.nl/code/sortable-table/
492  * http://www.kryogenix.org/code/browser/sorttable/
494  * @todo don't break on colspans/rowspans (bug 8028)
495  * @todo language-specific digit grouping/decimals (bug 8063)
496  * @todo support all accepted date formats (bug 8226)
497  */
499 var ts_image_path = stylepath+"/common/images/";
500 var ts_image_up = "sort_up.gif";
501 var ts_image_down = "sort_down.gif";
502 var ts_image_none = "sort_none.gif";
503 var ts_europeandate = wgContentLanguage != "en"; // The non-American-inclined can change to "true"
504 var ts_alternate_row_colors = false;
505 var ts_number_transform_table = null;
506 var ts_number_regex = null;
508 function sortables_init() {
509         var idnum = 0;
510         // Find all tables with class sortable and make them sortable
511         var tables = getElementsByClassName(document, "table", "sortable");
512         for (var ti = 0; ti < tables.length ; ti++) {
513                 if (!tables[ti].id) {
514                         tables[ti].setAttribute('id','sortable_table_id_'+idnum);
515                         ++idnum;
516                 }
517                 ts_makeSortable(tables[ti]);
518         }
521 function ts_makeSortable(table) {
522         var firstRow;
523         if (table.rows && table.rows.length > 0) {
524                 if (table.tHead && table.tHead.rows.length > 0) {
525                         firstRow = table.tHead.rows[table.tHead.rows.length-1];
526                 } else {
527                         firstRow = table.rows[0];
528                 }
529         }
530         if (!firstRow) return;
532         // We have a first row: assume it's the header, and make its contents clickable links
533         for (var i = 0; i < firstRow.cells.length; i++) {
534                 var cell = firstRow.cells[i];
535                 if ((" "+cell.className+" ").indexOf(" unsortable ") == -1) {
536                         cell.innerHTML += '<a href="#" class="sortheader" '
537                                 + 'onclick="ts_resortTable(this);return false;">'
538                                 + '<span class="sortarrow">'
539                                 + '<img src="'
540                                 + ts_image_path
541                                 + ts_image_none
542                                 + '" alt="&darr;"/></span></a>';
543                 }
544         }
545         if (ts_alternate_row_colors) {
546                 ts_alternate(table);
547         }
550 function ts_getInnerText(el) {
551         return getInnerText( el );
554 function ts_resortTable(lnk) {
555         // get the span
556         var span = lnk.getElementsByTagName('span')[0];
558         var td = lnk.parentNode;
559         var tr = td.parentNode;
560         var column = td.cellIndex;
562         var table = tr.parentNode;
563         while (table && !(table.tagName && table.tagName.toLowerCase() == 'table'))
564                 table = table.parentNode;
565         if (!table) return;
567         if (table.rows.length <= 1) return;
569         // Generate the number transform table if it's not done already
570         if (ts_number_transform_table == null) {
571                 ts_initTransformTable();
572         }
574         // Work out a type for the column
575         // Skip the first row if that's where the headings are
576         var rowStart = (table.tHead && table.tHead.rows.length > 0 ? 0 : 1);
578         var itm = "";
579         for (var i = rowStart; i < table.rows.length; i++) {
580                 if (table.rows[i].cells.length > column) {
581                         itm = ts_getInnerText(table.rows[i].cells[column]);
582                         itm = itm.replace(/^[\s\xa0]+/, "").replace(/[\s\xa0]+$/, "");
583                         if (itm != "") break;
584                 }
585         }
587         // TODO: bug 8226, localised date formats
588         var sortfn = ts_sort_generic;
589         var preprocessor = ts_toLowerCase;
590         if (/^\d\d[\/. -][a-zA-Z]{3}[\/. -]\d\d\d\d$/.test(itm)) {
591                 preprocessor = ts_dateToSortKey;
592         } else if (/^\d\d[\/.-]\d\d[\/.-]\d\d\d\d$/.test(itm)) {
593                 preprocessor = ts_dateToSortKey;
594         } else if (/^\d\d[\/.-]\d\d[\/.-]\d\d$/.test(itm)) {
595                 preprocessor = ts_dateToSortKey;
596         // pound dollar euro yen currency cents
597         } else if (/(^[\u00a3$\u20ac\u00a4\u00a5]|\u00a2$)/.test(itm)) {
598                 preprocessor = ts_currencyToSortKey;
599         } else if (ts_number_regex.test(itm)) {
600                 preprocessor = ts_parseFloat;
601         }
603         var reverse = (span.getAttribute("sortdir") == 'down');
605         var newRows = new Array();
606         var staticRows = new Array();
607         for (var j = rowStart; j < table.rows.length; j++) {
608                 var row = table.rows[j];
609                 if((" "+row.className+" ").indexOf(" unsortable ") < 0) {
610                         var keyText = ts_getInnerText(row.cells[column]);
611                         if(keyText == undefined) {
612                                 keyText = ""; 
613                         }
614                         var oldIndex = (reverse ? -j : j);
615                         var preprocessed = preprocessor( keyText.replace(/^[\s\xa0]+/, "").replace(/[\s\xa0]+$/, "") );
617                         newRows[newRows.length] = new Array(row, preprocessed, oldIndex);
618                 } else staticRows[staticRows.length] = new Array(row, false, j-rowStart);
619         }
621         newRows.sort(sortfn);
623         var arrowHTML;
624         if (reverse) {
625                 arrowHTML = '<img src="'+ ts_image_path + ts_image_down + '" alt="&darr;"/>';
626                 newRows.reverse();
627                 span.setAttribute('sortdir','up');
628         } else {
629                 arrowHTML = '<img src="'+ ts_image_path + ts_image_up + '" alt="&uarr;"/>';
630                 span.setAttribute('sortdir','down');
631         }
633         for (var i = 0; i < staticRows.length; i++) {
634                 var row = staticRows[i];
635                 newRows.splice(row[2], 0, row);
636         }
638         // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
639         // don't do sortbottom rows
640         for (var i = 0; i < newRows.length; i++) {
641                 if ((" "+newRows[i][0].className+" ").indexOf(" sortbottom ") == -1)
642                         table.tBodies[0].appendChild(newRows[i][0]);
643         }
644         // do sortbottom rows only
645         for (var i = 0; i < newRows.length; i++) {
646                 if ((" "+newRows[i][0].className+" ").indexOf(" sortbottom ") != -1)
647                         table.tBodies[0].appendChild(newRows[i][0]);
648         }
650         // Delete any other arrows there may be showing
651         var spans = getElementsByClassName(tr, "span", "sortarrow");
652         for (var i = 0; i < spans.length; i++) {
653                 spans[i].innerHTML = '<img src="'+ ts_image_path + ts_image_none + '" alt="&darr;"/>';
654         }
655         span.innerHTML = arrowHTML;
657         if (ts_alternate_row_colors) {
658                 ts_alternate(table);
659         }
662 function ts_initTransformTable() {
663         if ( typeof wgSeparatorTransformTable == "undefined"
664                         || ( wgSeparatorTransformTable[0] == '' && wgDigitTransformTable[2] == '' ) )
665         {
666                 digitClass = "[0-9,.]";
667                 ts_number_transform_table = false;
668         } else {
669                 ts_number_transform_table = {};
670                 // Unpack the transform table
671                 // Separators
672                 ascii = wgSeparatorTransformTable[0].split("\t");
673                 localised = wgSeparatorTransformTable[1].split("\t");
674                 for ( var i = 0; i < ascii.length; i++ ) {
675                         ts_number_transform_table[localised[i]] = ascii[i];
676                 }
677                 // Digits
678                 ascii = wgDigitTransformTable[0].split("\t");
679                 localised = wgDigitTransformTable[1].split("\t");
680                 for ( var i = 0; i < ascii.length; i++ ) {
681                         ts_number_transform_table[localised[i]] = ascii[i];
682                 }
684                 // Construct regex for number identification
685                 digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '\\.'];
686                 maxDigitLength = 1;
687                 for ( var digit in ts_number_transform_table ) {
688                         // Escape regex metacharacters
689                         digits.push(
690                                 digit.replace( /[\\\\$\*\+\?\.\(\)\|\{\}\[\]\-]/,
691                                         function( s ) { return '\\' + s; } )
692                         );
693                         if (digit.length > maxDigitLength) {
694                                 maxDigitLength = digit.length;
695                         }
696                 }
697                 if ( maxDigitLength > 1 ) {
698                         digitClass = '[' + digits.join( '', digits ) + ']';
699                 } else {
700                         digitClass = '(' + digits.join( '|', digits ) + ')';
701                 }
702         }
704         // We allow a trailing percent sign, which we just strip.  This works fine
705         // if percents and regular numbers aren't being mixed.
706         ts_number_regex = new RegExp(
707                 "^(" +
708                         "[+-]?[0-9][0-9,]*(\\.[0-9,]*)?(E[+-]?[0-9][0-9,]*)?" + // Fortran-style scientific
709                         "|" +
710                         "[+-]?" + digitClass + "+%?" + // Generic localised
711                 ")$", "i"
712         );
715 function ts_toLowerCase( s ) {
716         return s.toLowerCase();
719 function ts_dateToSortKey(date) {
720         // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
721         if (date.length == 11) {
722                 switch (date.substr(3,3).toLowerCase()) {
723                         case "jan": var month = "01"; break;
724                         case "feb": var month = "02"; break;
725                         case "mar": var month = "03"; break;
726                         case "apr": var month = "04"; break;
727                         case "may": var month = "05"; break;
728                         case "jun": var month = "06"; break;
729                         case "jul": var month = "07"; break;
730                         case "aug": var month = "08"; break;
731                         case "sep": var month = "09"; break;
732                         case "oct": var month = "10"; break;
733                         case "nov": var month = "11"; break;
734                         case "dec": var month = "12"; break;
735                         // default: var month = "00";
736                 }
737                 return date.substr(7,4)+month+date.substr(0,2);
738         } else if (date.length == 10) {
739                 if (ts_europeandate == false) {
740                         return date.substr(6,4)+date.substr(0,2)+date.substr(3,2);
741                 } else {
742                         return date.substr(6,4)+date.substr(3,2)+date.substr(0,2);
743                 }
744         } else if (date.length == 8) {
745                 yr = date.substr(6,2);
746                 if (parseInt(yr) < 50) {
747                         yr = '20'+yr;
748                 } else {
749                         yr = '19'+yr;
750                 }
751                 if (ts_europeandate == true) {
752                         return yr+date.substr(3,2)+date.substr(0,2);
753                 } else {
754                         return yr+date.substr(0,2)+date.substr(3,2);
755                 }
756         }
757         return "00000000";
760 function ts_parseFloat( s ) {
761         if ( !s ) {
762                 return 0;
763         }
764         if (ts_number_transform_table != false) {
765                 var newNum = '', c;
767                 for ( var p = 0; p < s.length; p++ ) {
768                         c = s.charAt( p );
769                         if (c in ts_number_transform_table) {
770                                 newNum += ts_number_transform_table[c];
771                         } else {
772                                 newNum += c;
773                         }
774                 }
775                 s = newNum;
776         }
778         num = parseFloat(s.replace(/,/g, ""));
779         return (isNaN(num) ? 0 : num);
782 function ts_currencyToSortKey( s ) {
783         return ts_parseFloat(s.replace(/[^0-9.,]/g,''));
786 function ts_sort_generic(a, b) {
787         return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[2] - b[2];
790 function ts_alternate(table) {
791         // Take object table and get all it's tbodies.
792         var tableBodies = table.getElementsByTagName("tbody");
793         // Loop through these tbodies
794         for (var i = 0; i < tableBodies.length; i++) {
795                 // Take the tbody, and get all it's rows
796                 var tableRows = tableBodies[i].getElementsByTagName("tr");
797                 // Loop through these rows
798                 // Start at 1 because we want to leave the heading row untouched
799                 for (var j = 0; j < tableRows.length; j++) {
800                         // Check if j is even, and apply classes for both possible results
801                         var oldClasses = tableRows[j].className.split(" ");
802                         var newClassName = "";
803                         for (var k = 0; k < oldClasses.length; k++) {
804                                 if (oldClasses[k] != "" && oldClasses[k] != "even" && oldClasses[k] != "odd")
805                                         newClassName += oldClasses[k] + " ";
806                         }
807                         tableRows[j].className = newClassName + (j % 2 == 0 ? "even" : "odd");
808                 }
809         }
813  * End of table sorting code
814  */
818  * Add a cute little box at the top of the screen to inform the user of
819  * something, replacing any preexisting message.
821  * @param String -or- Dom Object message HTML to be put inside the right div
822  * @param String className   Used in adding a class; should be different for each
823  *   call to allow CSS/JS to hide different boxes.  null = no class used.
824  * @return Boolean       True on success, false on failure
825  */
826 function jsMsg( message, className ) {
827         if ( !document.getElementById ) {
828                 return false;
829         }
830         // We special-case skin structures provided by the software.  Skins that
831         // choose to abandon or significantly modify our formatting can just define
832         // an mw-js-message div to start with.
833         var messageDiv = document.getElementById( 'mw-js-message' );
834         if ( !messageDiv ) {
835                 messageDiv = document.createElement( 'div' );
836                 if ( document.getElementById( 'column-content' )
837                 && document.getElementById( 'content' ) ) {
838                         // MonoBook, presumably
839                         document.getElementById( 'content' ).insertBefore(
840                                 messageDiv,
841                                 document.getElementById( 'content' ).firstChild
842                         );
843                 } else if ( document.getElementById('content')
844                 && document.getElementById( 'article' ) ) {
845                         // Non-Monobook but still recognizable (old-style)
846                         document.getElementById( 'article').insertBefore(
847                                 messageDiv,
848                                 document.getElementById( 'article' ).firstChild
849                         );
850                 } else {
851                         return false;
852                 }
853         }
855         messageDiv.setAttribute( 'id', 'mw-js-message' );
856         messageDiv.style.display = 'block';
857         if( className ) {
858                 messageDiv.setAttribute( 'class', 'mw-js-message-'+className );
859         }
861         if (typeof message === 'object') {
862                 while (messageDiv.hasChildNodes()) // Remove old content
863                         messageDiv.removeChild(messageDiv.firstChild);
864                 messageDiv.appendChild (message); // Append new content
865         }
866         else {
867                 messageDiv.innerHTML = message;
868         }
869         return true;
873  * Inject a cute little progress spinner after the specified element
875  * @param element Element to inject after
876  * @param id Identifier string (for use with removeSpinner(), below)
877  */
878 function injectSpinner( element, id ) {
879         var spinner = document.createElement( "img" );
880         spinner.id = "mw-spinner-" + id;
881         spinner.src = stylepath + "/common/images/spinner.gif";
882         spinner.alt = spinner.title = "...";
883         if( element.nextSibling ) {
884                 element.parentNode.insertBefore( spinner, element.nextSibling );
885         } else {
886                 element.parentNode.appendChild( spinner );
887         }
891  * Remove a progress spinner added with injectSpinner()
893  * @param id Identifier string
894  */
895 function removeSpinner( id ) {
896         var spinner = document.getElementById( "mw-spinner-" + id );
897         if( spinner ) {
898                 spinner.parentNode.removeChild( spinner );
899         }
902 function runOnloadHook() {
903         // don't run anything below this for non-dom browsers
904         if (doneOnloadHook || !(document.getElementById && document.getElementsByTagName)) {
905                 return;
906         }
908         // set this before running any hooks, since any errors below
909         // might cause the function to terminate prematurely
910         doneOnloadHook = true;
912         updateTooltipAccessKeys( null );
913         setupCheckboxShiftClick();
914         sortables_init();
916         // Run any added-on functions
917         for (var i = 0; i < onloadFuncts.length; i++) {
918                 onloadFuncts[i]();
919         }
923  * Add an event handler to an element
925  * @param Element element Element to add handler to
926  * @param String attach Event to attach to
927  * @param callable handler Event handler callback
928  */
929 function addHandler( element, attach, handler ) {
930         if( window.addEventListener ) {
931                 element.addEventListener( attach, handler, false );
932         } else if( window.attachEvent ) {
933                 element.attachEvent( 'on' + attach, handler );
934         }
938  * Add a click event handler to an element
940  * @param Element element Element to add handler to
941  * @param callable handler Event handler callback
942  */
943 function addClickHandler( element, handler ) {
944         addHandler( element, 'click', handler );
948  * Removes an event handler from an element
950  * @param Element element Element to remove handler from
951  * @param String remove Event to remove
952  * @param callable handler Event handler callback to remove
953  */
954 function removeHandler( element, remove, handler ) {
955         if( window.removeEventListener ) {
956                 element.removeEventListener( remove, handler, false );
957         } else if( window.detachEvent ) {
958                 element.detachEvent( 'on' + remove, handler );
959         }
961 //note: all skins should call runOnloadHook() at the end of html output,
962 //      so the below should be redundant. It's there just in case.
963 hookEvent("load", runOnloadHook);