jquery.tablesorter: Remove unused config variables
[lhc/web/wiklou.git] / resources / src / jquery / jquery.tablesorter.js
1 /**
2 * TableSorter for MediaWiki
3 *
4 * Written 2011 Leo Koppelkamm
5 * Based on tablesorter.com plugin, written (c) 2007 Christian Bach.
6 *
7 * Dual licensed under the MIT and GPL licenses:
8 * http://www.opensource.org/licenses/mit-license.php
9 * http://www.gnu.org/licenses/gpl.html
10 *
11 * Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgContentLanguage)
12 * and mw.language.months.
13 *
14 * Uses 'tableSorterCollation' in mw.config (if available)
15 */
16 /**
17 *
18 * @description Create a sortable table with multi-column sorting capabilitys
19 *
20 * @example $( 'table' ).tablesorter();
21 * @desc Create a simple tablesorter interface.
22 *
23 * @example $( 'table' ).tablesorter( { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } );
24 * @desc Create a tablesorter interface initially sorting on the first and second column.
25 *
26 * @option String cssHeader ( optional ) A string of the class name to be appended
27 * to sortable tr elements in the thead of the table. Default value:
28 * "header"
29 *
30 * @option String cssAsc ( optional ) A string of the class name to be appended to
31 * sortable tr elements in the thead on a ascending sort. Default value:
32 * "headerSortUp"
33 *
34 * @option String cssDesc ( optional ) A string of the class name to be appended
35 * to sortable tr elements in the thead on a descending sort. Default
36 * value: "headerSortDown"
37 *
38 * @option String sortMultisortKey ( optional ) A string of the multi-column sort
39 * key. Default value: "shiftKey"
40 *
41 * @option Boolean cancelSelection ( optional ) Boolean flag indicating if
42 * tablesorter should cancel selection of the table headers text.
43 * Default value: true
44 *
45 * @option Array sortList ( optional ) An array containing objects specifying sorting.
46 * By passing more than one object, multi-sorting will be applied. Object structure:
47 * { <Integer column index>: <String 'asc' or 'desc'> }
48 * Default value: []
49 *
50 * @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied.
51 *
52 * @type jQuery
53 *
54 * @name tablesorter
55 *
56 * @cat Plugins/Tablesorter
57 *
58 * @author Christian Bach/christian.bach@polyester.se
59 */
60
61 ( function ( $, mw ) {
62 /* Local scope */
63
64 var ts,
65 parsers = [];
66
67 /* Parser utility functions */
68
69 function getParserById( name ) {
70 var i,
71 len = parsers.length;
72 for ( i = 0; i < len; i++ ) {
73 if ( parsers[i].id.toLowerCase() === name.toLowerCase() ) {
74 return parsers[i];
75 }
76 }
77 return false;
78 }
79
80 function getElementSortKey( node ) {
81 var $node = $( node ),
82 // Use data-sort-value attribute.
83 // Use data() instead of attr() so that live value changes
84 // are processed as well (bug 38152).
85 data = $node.data( 'sortValue' );
86
87 if ( data !== null && data !== undefined ) {
88 // Cast any numbers or other stuff to a string, methods
89 // like charAt, toLowerCase and split are expected.
90 return String( data );
91 } else {
92 if ( !node ) {
93 return $node.text();
94 } else if ( node.tagName.toLowerCase() === 'img' ) {
95 return $node.attr( 'alt' ) || ''; // handle undefined alt
96 } else {
97 return $.map( $.makeArray( node.childNodes ), function ( elem ) {
98 // 1 is for document.ELEMENT_NODE (the constant is undefined on old browsers)
99 if ( elem.nodeType === 1 ) {
100 return getElementSortKey( elem );
101 } else {
102 return $.text( elem );
103 }
104 } ).join( '' );
105 }
106 }
107 }
108
109 function detectParserForColumn( table, rows, cellIndex ) {
110 var l = parsers.length,
111 nodeValue,
112 // Start with 1 because 0 is the fallback parser
113 i = 1,
114 rowIndex = 0,
115 concurrent = 0,
116 needed = ( rows.length > 4 ) ? 5 : rows.length;
117
118 while ( i < l ) {
119 if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) {
120 nodeValue = $.trim( getElementSortKey( rows[rowIndex].cells[cellIndex] ) );
121 } else {
122 nodeValue = '';
123 }
124
125 if ( nodeValue !== '' ) {
126 if ( parsers[i].is( nodeValue, table ) ) {
127 concurrent++;
128 rowIndex++;
129 if ( concurrent >= needed ) {
130 // Confirmed the parser for multiple cells, let's return it
131 return parsers[i];
132 }
133 } else {
134 // Check next parser, reset rows
135 i++;
136 rowIndex = 0;
137 concurrent = 0;
138 }
139 } else {
140 // Empty cell
141 rowIndex++;
142 if ( rowIndex > rows.length ) {
143 rowIndex = 0;
144 i++;
145 }
146 }
147 }
148
149 // 0 is always the generic parser (text)
150 return parsers[0];
151 }
152
153 function buildParserCache( table, $headers ) {
154 var sortType, cells, len, i, parser,
155 rows = table.tBodies[0].rows,
156 parsers = [];
157
158 if ( rows[0] ) {
159
160 cells = rows[0].cells;
161 len = cells.length;
162
163 for ( i = 0; i < len; i++ ) {
164 parser = false;
165 sortType = $headers.eq( i ).data( 'sortType' );
166 if ( sortType !== undefined ) {
167 parser = getParserById( sortType );
168 }
169
170 if ( parser === false ) {
171 parser = detectParserForColumn( table, rows, i );
172 }
173
174 parsers.push( parser );
175 }
176 }
177 return parsers;
178 }
179
180 /* Other utility functions */
181
182 function buildCache( table ) {
183 var i, j, $row, cols,
184 totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0,
185 totalCells = ( table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length ) || 0,
186 parsers = table.config.parsers,
187 cache = {
188 row: [],
189 normalized: []
190 };
191
192 for ( i = 0; i < totalRows; ++i ) {
193
194 // Add the table data to main data array
195 $row = $( table.tBodies[0].rows[i] );
196 cols = [];
197
198 // if this is a child row, add it to the last row's children and
199 // continue to the next row
200 if ( $row.hasClass( table.config.cssChildRow ) ) {
201 cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add( $row );
202 // go to the next for loop
203 continue;
204 }
205
206 cache.row.push( $row );
207
208 for ( j = 0; j < totalCells; ++j ) {
209 cols.push( parsers[j].format( getElementSortKey( $row[0].cells[j] ), table, $row[0].cells[j] ) );
210 }
211
212 cols.push( cache.normalized.length ); // add position for rowCache
213 cache.normalized.push( cols );
214 cols = null;
215 }
216
217 return cache;
218 }
219
220 function appendToTable( table, cache ) {
221 var i, pos, l, j,
222 row = cache.row,
223 normalized = cache.normalized,
224 totalRows = normalized.length,
225 checkCell = ( normalized[0].length - 1 ),
226 fragment = document.createDocumentFragment();
227
228 for ( i = 0; i < totalRows; i++ ) {
229 pos = normalized[i][checkCell];
230
231 l = row[pos].length;
232
233 for ( j = 0; j < l; j++ ) {
234 fragment.appendChild( row[pos][j] );
235 }
236
237 }
238 table.tBodies[0].appendChild( fragment );
239
240 $( table ).trigger( 'sortEnd.tablesorter' );
241 }
242
243 /**
244 * Find all header rows in a thead-less table and put them in a <thead> tag.
245 * This only treats a row as a header row if it contains only <th>s (no <td>s)
246 * and if it is preceded entirely by header rows. The algorithm stops when
247 * it encounters the first non-header row.
248 *
249 * After this, it will look at all rows at the bottom for footer rows
250 * And place these in a tfoot using similar rules.
251 * @param $table jQuery object for a <table>
252 */
253 function emulateTHeadAndFoot( $table ) {
254 var $thead, $tfoot, i, len,
255 $rows = $table.find( '> tbody > tr' );
256 if ( !$table.get( 0 ).tHead ) {
257 $thead = $( '<thead>' );
258 $rows.each( function () {
259 if ( $( this ).children( 'td' ).length ) {
260 // This row contains a <td>, so it's not a header row
261 // Stop here
262 return false;
263 }
264 $thead.append( this );
265 } );
266 $table.find( ' > tbody:first' ).before( $thead );
267 }
268 if ( !$table.get( 0 ).tFoot ) {
269 $tfoot = $( '<tfoot>' );
270 len = $rows.length;
271 for ( i = len - 1; i >= 0; i-- ) {
272 if ( $( $rows[i] ).children( 'td' ).length ) {
273 break;
274 }
275 $tfoot.prepend( $( $rows[i] ) );
276 }
277 $table.append( $tfoot );
278 }
279 }
280
281 function buildHeaders( table, msg ) {
282 var maxSeen = 0,
283 colspanOffset = 0,
284 columns,
285 i,
286 rowspan,
287 colspan,
288 headerCount,
289 longestTR,
290 exploded,
291 $tableHeaders = $( [] ),
292 $tableRows = $( 'thead:eq(0) > tr', table );
293 if ( $tableRows.length <= 1 ) {
294 $tableHeaders = $tableRows.children( 'th' );
295 } else {
296 exploded = [];
297
298 // Loop through all the dom cells of the thead
299 $tableRows.each( function ( rowIndex, row ) {
300 $.each( row.cells, function ( columnIndex, cell ) {
301 var matrixRowIndex,
302 matrixColumnIndex;
303
304 rowspan = Number( cell.rowSpan );
305 colspan = Number( cell.colSpan );
306
307 // Skip the spots in the exploded matrix that are already filled
308 while ( exploded[rowIndex] && exploded[rowIndex][columnIndex] !== undefined ) {
309 ++columnIndex;
310 }
311
312 // Find the actual dimensions of the thead, by placing each cell
313 // in the exploded matrix rowspan times colspan times, with the proper offsets
314 for ( matrixColumnIndex = columnIndex; matrixColumnIndex < columnIndex + colspan; ++matrixColumnIndex ) {
315 for ( matrixRowIndex = rowIndex; matrixRowIndex < rowIndex + rowspan; ++matrixRowIndex ) {
316 if ( !exploded[matrixRowIndex] ) {
317 exploded[matrixRowIndex] = [];
318 }
319 exploded[matrixRowIndex][matrixColumnIndex] = cell;
320 }
321 }
322 } );
323 } );
324 // We want to find the row that has the most columns (ignoring colspan)
325 $.each( exploded, function ( index, cellArray ) {
326 headerCount = $( uniqueElements( cellArray ) ).filter( 'th' ).length;
327 if ( headerCount >= maxSeen ) {
328 maxSeen = headerCount;
329 longestTR = index;
330 }
331 } );
332 // We cannot use $.unique() here because it sorts into dom order, which is undesirable
333 $tableHeaders = $( uniqueElements( exploded[longestTR] ) ).filter( 'th' );
334 }
335
336 // as each header can span over multiple columns (using colspan=N),
337 // we have to bidirectionally map headers to their columns and columns to their headers
338 table.headerToColumns = [];
339 table.columnToHeader = [];
340
341 $tableHeaders.each( function ( headerIndex ) {
342 columns = [];
343 for ( i = 0; i < this.colSpan; i++ ) {
344 table.columnToHeader[ colspanOffset + i ] = headerIndex;
345 columns.push( colspanOffset + i );
346 }
347
348 table.headerToColumns[ headerIndex ] = columns;
349 colspanOffset += this.colSpan;
350
351 this.headerIndex = headerIndex;
352 this.order = 0;
353 this.count = 0;
354
355 if ( $( this ).hasClass( table.config.unsortableClass ) ) {
356 this.sortDisabled = true;
357 }
358
359 if ( !this.sortDisabled ) {
360 $( this )
361 .addClass( table.config.cssHeader )
362 .prop( 'tabIndex', 0 )
363 .attr( {
364 role: 'columnheader button',
365 title: msg[1]
366 } );
367 }
368
369 // add cell to headerList
370 table.config.headerList[headerIndex] = this;
371 } );
372
373 return $tableHeaders;
374
375 }
376
377 /**
378 * Sets the sort count of the columns that are not affected by the sorting to have them sorted
379 * in default (ascending) order when their header cell is clicked the next time.
380 *
381 * @param {jQuery} $headers
382 * @param {Number[][]} sortList
383 * @param {Number[][]} headerToColumns
384 */
385 function setHeadersOrder( $headers, sortList, headerToColumns ) {
386 // Loop through all headers to retrieve the indices of the columns the header spans across:
387 $.each( headerToColumns, function ( headerIndex, columns ) {
388
389 $.each( columns, function ( i, columnIndex ) {
390 var header = $headers[headerIndex];
391
392 if ( !isValueInArray( columnIndex, sortList ) ) {
393 // Column shall not be sorted: Reset header count and order.
394 header.order = 0;
395 header.count = 0;
396 } else {
397 // Column shall be sorted: Apply designated count and order.
398 $.each( sortList, function ( j, sortColumn ) {
399 if ( sortColumn[0] === i ) {
400 header.order = sortColumn[1];
401 header.count = sortColumn[1] + 1;
402 return false;
403 }
404 } );
405 }
406 } );
407
408 } );
409 }
410
411 function isValueInArray( v, a ) {
412 var i,
413 len = a.length;
414 for ( i = 0; i < len; i++ ) {
415 if ( a[i][0] === v ) {
416 return true;
417 }
418 }
419 return false;
420 }
421
422 function uniqueElements( array ) {
423 var uniques = [];
424 $.each( array, function ( index, elem ) {
425 if ( elem !== undefined && $.inArray( elem, uniques ) === -1 ) {
426 uniques.push( elem );
427 }
428 } );
429 return uniques;
430 }
431
432 function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) {
433 // Remove all header information and reset titles to default message
434 $headers.removeClass( css[0] ).removeClass( css[1] ).attr( 'title', msg[1] );
435
436 for ( var i = 0; i < list.length; i++ ) {
437 $headers.eq( columnToHeader[ list[i][0] ] )
438 .addClass( css[ list[i][1] ] )
439 .attr( 'title', msg[ list[i][1] ] );
440 }
441 }
442
443 function sortText( a, b ) {
444 return ( ( a < b ) ? -1 : ( ( a > b ) ? 1 : 0 ) );
445 }
446
447 function sortTextDesc( a, b ) {
448 return ( ( b < a ) ? -1 : ( ( b > a ) ? 1 : 0 ) );
449 }
450
451 function multisort( table, sortList, cache ) {
452 var i,
453 sortFn = [],
454 len = sortList.length;
455 for ( i = 0; i < len; i++ ) {
456 sortFn[i] = ( sortList[i][1] ) ? sortTextDesc : sortText;
457 }
458 cache.normalized.sort( function ( array1, array2 ) {
459 var i, col, ret;
460 for ( i = 0; i < len; i++ ) {
461 col = sortList[i][0];
462 ret = sortFn[i].call( this, array1[col], array2[col] );
463 if ( ret !== 0 ) {
464 return ret;
465 }
466 }
467 // Fall back to index number column to ensure stable sort
468 return sortText.call( this, array1[array1.length - 1], array2[array2.length - 1] );
469 } );
470 return cache;
471 }
472
473 function buildTransformTable() {
474 var ascii, localised, i, digitClass,
475 digits = '0123456789,.'.split( '' ),
476 separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ),
477 digitTransformTable = mw.config.get( 'wgDigitTransformTable' );
478
479 if ( separatorTransformTable === null || ( separatorTransformTable[0] === '' && digitTransformTable[2] === '' ) ) {
480 ts.transformTable = false;
481 } else {
482 ts.transformTable = {};
483
484 // Unpack the transform table
485 ascii = separatorTransformTable[0].split( '\t' ).concat( digitTransformTable[0].split( '\t' ) );
486 localised = separatorTransformTable[1].split( '\t' ).concat( digitTransformTable[1].split( '\t' ) );
487
488 // Construct regex for number identification
489 for ( i = 0; i < ascii.length; i++ ) {
490 ts.transformTable[localised[i]] = ascii[i];
491 digits.push( $.escapeRE( localised[i] ) );
492 }
493 }
494 digitClass = '[' + digits.join( '', digits ) + ']';
495
496 // We allow a trailing percent sign, which we just strip. This works fine
497 // if percents and regular numbers aren't being mixed.
498 ts.numberRegex = new RegExp( '^(' + '[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?' + // Fortran-style scientific
499 '|' + '[-+\u2212]?' + digitClass + '+[\\s\\xa0]*%?' + // Generic localised
500 ')$', 'i' );
501 }
502
503 function buildDateTable() {
504 var i, name,
505 regex = [];
506
507 ts.monthNames = {};
508
509 for ( i = 0; i < 12; i++ ) {
510 name = mw.language.months.names[i].toLowerCase();
511 ts.monthNames[name] = i + 1;
512 regex.push( $.escapeRE( name ) );
513 name = mw.language.months.genitive[i].toLowerCase();
514 ts.monthNames[name] = i + 1;
515 regex.push( $.escapeRE( name ) );
516 name = mw.language.months.abbrev[i].toLowerCase().replace( '.', '' );
517 ts.monthNames[name] = i + 1;
518 regex.push( $.escapeRE( name ) );
519 }
520
521 // Build piped string
522 regex = regex.join( '|' );
523
524 // Build RegEx
525 // Any date formated with . , ' - or /
526 ts.dateRegex[0] = new RegExp( /^\s*(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{2,4})\s*?/i );
527
528 // Written Month name, dmy
529 ts.dateRegex[1] = new RegExp( '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
530
531 // Written Month name, mdy
532 ts.dateRegex[2] = new RegExp( '^\\s*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
533
534 }
535
536 /**
537 * Replace all rowspanned cells in the body with clones in each row, so sorting
538 * need not worry about them.
539 *
540 * @param $table jQuery object for a <table>
541 */
542 function explodeRowspans( $table ) {
543 var spanningRealCellIndex, rowSpan, colSpan,
544 cell, i, $tds, $clone, $nextRows,
545 rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get();
546
547 // Short circuit
548 if ( !rowspanCells.length ) {
549 return;
550 }
551
552 // First, we need to make a property like cellIndex but taking into
553 // account colspans. We also cache the rowIndex to avoid having to take
554 // cell.parentNode.rowIndex in the sorting function below.
555 $table.find( '> tbody > tr' ).each( function () {
556 var i,
557 col = 0,
558 l = this.cells.length;
559 for ( i = 0; i < l; i++ ) {
560 this.cells[i].realCellIndex = col;
561 this.cells[i].realRowIndex = this.rowIndex;
562 col += this.cells[i].colSpan;
563 }
564 } );
565
566 // Split multi row cells into multiple cells with the same content.
567 // Sort by column then row index to avoid problems with odd table structures.
568 // Re-sort whenever a rowspanned cell's realCellIndex is changed, because it
569 // might change the sort order.
570 function resortCells() {
571 rowspanCells = rowspanCells.sort( function ( a, b ) {
572 var ret = a.realCellIndex - b.realCellIndex;
573 if ( !ret ) {
574 ret = a.realRowIndex - b.realRowIndex;
575 }
576 return ret;
577 } );
578 $.each( rowspanCells, function () {
579 this.needResort = false;
580 } );
581 }
582 resortCells();
583
584 function filterfunc() {
585 return this.realCellIndex >= spanningRealCellIndex;
586 }
587
588 function fixTdCellIndex() {
589 this.realCellIndex += colSpan;
590 if ( this.rowSpan > 1 ) {
591 this.needResort = true;
592 }
593 }
594
595 while ( rowspanCells.length ) {
596 if ( rowspanCells[0].needResort ) {
597 resortCells();
598 }
599
600 cell = rowspanCells.shift();
601 rowSpan = cell.rowSpan;
602 colSpan = cell.colSpan;
603 spanningRealCellIndex = cell.realCellIndex;
604 cell.rowSpan = 1;
605 $nextRows = $( cell ).parent().nextAll();
606 for ( i = 0; i < rowSpan - 1; i++ ) {
607 $tds = $( $nextRows[i].cells ).filter( filterfunc );
608 $clone = $( cell ).clone();
609 $clone[0].realCellIndex = spanningRealCellIndex;
610 if ( $tds.length ) {
611 $tds.each( fixTdCellIndex );
612 $tds.first().before( $clone );
613 } else {
614 $nextRows.eq( i ).append( $clone );
615 }
616 }
617 }
618 }
619
620 function buildCollationTable() {
621 ts.collationTable = mw.config.get( 'tableSorterCollation' );
622 ts.collationRegex = null;
623 if ( ts.collationTable ) {
624 var key,
625 keys = [];
626
627 // Build array of key names
628 for ( key in ts.collationTable ) {
629 // Check hasOwn to be safe
630 if ( ts.collationTable.hasOwnProperty( key ) ) {
631 keys.push( key );
632 }
633 }
634 if ( keys.length ) {
635 ts.collationRegex = new RegExp( '[' + keys.join( '' ) + ']', 'ig' );
636 }
637 }
638 }
639
640 function cacheRegexs() {
641 if ( ts.rgx ) {
642 return;
643 }
644 ts.rgx = {
645 IPAddress: [
646 new RegExp( /^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/ )
647 ],
648 currency: [
649 new RegExp( /(^[£$€¥]|[£$€¥]$)/ ),
650 new RegExp( /[£$€¥]/g )
651 ],
652 url: [
653 new RegExp( /^(https?|ftp|file):\/\/$/ ),
654 new RegExp( /(https?|ftp|file):\/\// )
655 ],
656 isoDate: [
657 new RegExp( /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/ )
658 ],
659 usLongDate: [
660 new RegExp( /^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/ )
661 ],
662 time: [
663 new RegExp( /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/ )
664 ]
665 };
666 }
667
668 /**
669 * Converts sort objects [ { Integer: String }, ... ] to the internally used nested array
670 * structure [ [ Integer , Integer ], ... ]
671 *
672 * @param sortObjects {Array} List of sort objects.
673 * @return {Array} List of internal sort definitions.
674 */
675
676 function convertSortList( sortObjects ) {
677 var sortList = [];
678 $.each( sortObjects, function ( i, sortObject ) {
679 $.each( sortObject, function ( columnIndex, order ) {
680 var orderIndex = ( order === 'desc' ) ? 1 : 0;
681 sortList.push( [parseInt( columnIndex, 10 ), orderIndex] );
682 } );
683 } );
684 return sortList;
685 }
686
687 /* Public scope */
688
689 $.tablesorter = {
690
691 defaultOptions: {
692 cssHeader: 'headerSort',
693 cssAsc: 'headerSortUp',
694 cssDesc: 'headerSortDown',
695 cssChildRow: 'expand-child',
696 sortMultiSortKey: 'shiftKey',
697 unsortableClass: 'unsortable',
698 parsers: {},
699 cancelSelection: true,
700 sortList: [],
701 headerList: []
702 },
703
704 dateRegex: [],
705 monthNames: {},
706
707 /**
708 * @param $tables {jQuery}
709 * @param settings {Object} (optional)
710 */
711 construct: function ( $tables, settings ) {
712 return $tables.each( function ( i, table ) {
713 // Declare and cache.
714 var $headers, cache, config, sortCSS, sortMsg,
715 $table = $( table ),
716 firstTime = true;
717
718 // Quit if no tbody
719 if ( !table.tBodies ) {
720 return;
721 }
722 if ( !table.tHead ) {
723 // No thead found. Look for rows with <th>s and
724 // move them into a <thead> tag or a <tfoot> tag
725 emulateTHeadAndFoot( $table );
726
727 // Still no thead? Then quit
728 if ( !table.tHead ) {
729 return;
730 }
731 }
732 $table.addClass( 'jquery-tablesorter' );
733
734 // FIXME config should probably not be stored in the plain table node
735 // New config object.
736 table.config = {};
737
738 // Merge and extend.
739 config = $.extend( table.config, $.tablesorter.defaultOptions, settings );
740
741 // Save the settings where they read
742 $.data( table, 'tablesorter', { config: config } );
743
744 // Get the CSS class names, could be done else where.
745 sortCSS = [ config.cssDesc, config.cssAsc ];
746 sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-ascending' ) ];
747
748 // Build headers
749 $headers = buildHeaders( table, sortMsg );
750
751 // Grab and process locale settings.
752 buildTransformTable();
753 buildDateTable();
754
755 // Precaching regexps can bring 10 fold
756 // performance improvements in some browsers.
757 cacheRegexs();
758
759 function setupForFirstSort() {
760 firstTime = false;
761
762 // Defer buildCollationTable to first sort. As user and site scripts
763 // may customize tableSorterCollation but load after $.ready(), other
764 // scripts may call .tablesorter() before they have done the
765 // tableSorterCollation customizations.
766 buildCollationTable();
767
768 // Legacy fix of .sortbottoms
769 // Wrap them inside inside a tfoot (because that's what they actually want to be) &
770 // and put the <tfoot> at the end of the <table>
771 var $tfoot,
772 $sortbottoms = $table.find( '> tbody > tr.sortbottom' );
773 if ( $sortbottoms.length ) {
774 $tfoot = $table.children( 'tfoot' );
775 if ( $tfoot.length ) {
776 $tfoot.eq( 0 ).prepend( $sortbottoms );
777 } else {
778 $table.append( $( '<tfoot>' ).append( $sortbottoms ) );
779 }
780 }
781
782 explodeRowspans( $table );
783
784 // try to auto detect column type, and store in tables config
785 table.config.parsers = buildParserCache( table, $headers );
786 }
787
788 // Apply event handling to headers
789 // this is too big, perhaps break it out?
790 $headers.not( '.' + table.config.unsortableClass ).on( 'keypress click', function ( e ) {
791 var cell, columns, newSortList, i,
792 totalRows,
793 j, s, o;
794
795 if ( e.type === 'click' && e.target.nodeName.toLowerCase() === 'a' ) {
796 // The user clicked on a link inside a table header.
797 // Do nothing and let the default link click action continue.
798 return true;
799 }
800
801 if ( e.type === 'keypress' && e.which !== 13 ) {
802 // Only handle keypresses on the "Enter" key.
803 return true;
804 }
805
806 if ( firstTime ) {
807 setupForFirstSort();
808 }
809
810 // Build the cache for the tbody cells
811 // to share between calculations for this sort action.
812 // Re-calculated each time a sort action is performed due to possiblity
813 // that sort values change. Shouldn't be too expensive, but if it becomes
814 // too slow an event based system should be implemented somehow where
815 // cells get event .change() and bubbles up to the <table> here
816 cache = buildCache( table );
817
818 totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0;
819 if ( !table.sortDisabled && totalRows > 0 ) {
820 // Get current column sort order
821 this.order = this.count % 2;
822 this.count++;
823
824 cell = this;
825 // Get current column index
826 columns = table.headerToColumns[ this.headerIndex ];
827 newSortList = $.map( columns, function ( c ) {
828 // jQuery "helpfully" flattens the arrays...
829 return [[c, cell.order]];
830 } );
831 // Index of first column belonging to this header
832 i = columns[0];
833
834 if ( !e[config.sortMultiSortKey] ) {
835 // User only wants to sort on one column set
836 // Flush the sort list and add new columns
837 config.sortList = newSortList;
838 } else {
839 // Multi column sorting
840 // It is not possible for one column to belong to multiple headers,
841 // so this is okay - we don't need to check for every value in the columns array
842 if ( isValueInArray( i, config.sortList ) ) {
843 // The user has clicked on an already sorted column.
844 // Reverse the sorting direction for all tables.
845 for ( j = 0; j < config.sortList.length; j++ ) {
846 s = config.sortList[j];
847 o = config.headerList[s[0]];
848 if ( isValueInArray( s[0], newSortList ) ) {
849 o.count = s[1];
850 o.count++;
851 s[1] = o.count % 2;
852 }
853 }
854 } else {
855 // Add columns to sort list array
856 config.sortList = config.sortList.concat( newSortList );
857 }
858 }
859
860 // Reset order/counts of cells not affected by sorting
861 setHeadersOrder( $headers, config.sortList, table.headerToColumns );
862
863 // Set CSS for headers
864 setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, table.columnToHeader );
865 appendToTable(
866 $table[0], multisort( $table[0], config.sortList, cache )
867 );
868
869 // Stop normal event by returning false
870 return false;
871 }
872
873 // Cancel selection
874 } ).mousedown( function () {
875 if ( config.cancelSelection ) {
876 this.onselectstart = function () {
877 return false;
878 };
879 return false;
880 }
881 } );
882
883 /**
884 * Sorts the table. If no sorting is specified by passing a list of sort
885 * objects, the table is sorted according to the initial sorting order.
886 * Passing an empty array will reset sorting (basically just reset the headers
887 * making the table appear unsorted).
888 *
889 * @param sortList {Array} (optional) List of sort objects.
890 */
891 $table.data( 'tablesorter' ).sort = function ( sortList ) {
892
893 if ( firstTime ) {
894 setupForFirstSort();
895 }
896
897 if ( sortList === undefined ) {
898 sortList = config.sortList;
899 } else if ( sortList.length > 0 ) {
900 sortList = convertSortList( sortList );
901 }
902
903 // Set each column's sort count to be able to determine the correct sort
904 // order when clicking on a header cell the next time
905 setHeadersOrder( $headers, sortList, table.headerToColumns );
906
907 // re-build the cache for the tbody cells
908 cache = buildCache( table );
909
910 // set css for headers
911 setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, table.columnToHeader );
912
913 // sort the table and append it to the dom
914 appendToTable( table, multisort( table, sortList, cache ) );
915 };
916
917 // sort initially
918 if ( config.sortList.length > 0 ) {
919 setupForFirstSort();
920 config.sortList = convertSortList( config.sortList );
921 $table.data( 'tablesorter' ).sort();
922 }
923
924 } );
925 },
926
927 addParser: function ( parser ) {
928 var i,
929 len = parsers.length,
930 a = true;
931 for ( i = 0; i < len; i++ ) {
932 if ( parsers[i].id.toLowerCase() === parser.id.toLowerCase() ) {
933 a = false;
934 }
935 }
936 if ( a ) {
937 parsers.push( parser );
938 }
939 },
940
941 formatDigit: function ( s ) {
942 var out, c, p, i;
943 if ( ts.transformTable !== false ) {
944 out = '';
945 for ( p = 0; p < s.length; p++ ) {
946 c = s.charAt( p );
947 if ( c in ts.transformTable ) {
948 out += ts.transformTable[c];
949 } else {
950 out += c;
951 }
952 }
953 s = out;
954 }
955 i = parseFloat( s.replace( /[, ]/g, '' ).replace( '\u2212', '-' ) );
956 return isNaN( i ) ? 0 : i;
957 },
958
959 formatFloat: function ( s ) {
960 var i = parseFloat( s );
961 return isNaN( i ) ? 0 : i;
962 },
963
964 formatInt: function ( s ) {
965 var i = parseInt( s, 10 );
966 return isNaN( i ) ? 0 : i;
967 },
968
969 clearTableBody: function ( table ) {
970 $( table.tBodies[0] ).empty();
971 }
972 };
973
974 // Shortcut
975 ts = $.tablesorter;
976
977 // Register as jQuery prototype method
978 $.fn.tablesorter = function ( settings ) {
979 return ts.construct( this, settings );
980 };
981
982 // Add default parsers
983 ts.addParser( {
984 id: 'text',
985 is: function () {
986 return true;
987 },
988 format: function ( s ) {
989 s = $.trim( s.toLowerCase() );
990 if ( ts.collationRegex ) {
991 var tsc = ts.collationTable;
992 s = s.replace( ts.collationRegex, function ( match ) {
993 var r = tsc[match] ? tsc[match] : tsc[match.toUpperCase()];
994 return r.toLowerCase();
995 } );
996 }
997 return s;
998 },
999 type: 'text'
1000 } );
1001
1002 ts.addParser( {
1003 id: 'IPAddress',
1004 is: function ( s ) {
1005 return ts.rgx.IPAddress[0].test( s );
1006 },
1007 format: function ( s ) {
1008 var i, item,
1009 a = s.split( '.' ),
1010 r = '',
1011 len = a.length;
1012 for ( i = 0; i < len; i++ ) {
1013 item = a[i];
1014 if ( item.length === 1 ) {
1015 r += '00' + item;
1016 } else if ( item.length === 2 ) {
1017 r += '0' + item;
1018 } else {
1019 r += item;
1020 }
1021 }
1022 return $.tablesorter.formatFloat( r );
1023 },
1024 type: 'numeric'
1025 } );
1026
1027 ts.addParser( {
1028 id: 'currency',
1029 is: function ( s ) {
1030 return ts.rgx.currency[0].test( s );
1031 },
1032 format: function ( s ) {
1033 return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[1], '' ) );
1034 },
1035 type: 'numeric'
1036 } );
1037
1038 ts.addParser( {
1039 id: 'url',
1040 is: function ( s ) {
1041 return ts.rgx.url[0].test( s );
1042 },
1043 format: function ( s ) {
1044 return $.trim( s.replace( ts.rgx.url[1], '' ) );
1045 },
1046 type: 'text'
1047 } );
1048
1049 ts.addParser( {
1050 id: 'isoDate',
1051 is: function ( s ) {
1052 return ts.rgx.isoDate[0].test( s );
1053 },
1054 format: function ( s ) {
1055 return $.tablesorter.formatFloat( ( s !== '' ) ? new Date( s.replace(
1056 new RegExp( /-/g ), '/' ) ).getTime() : '0' );
1057 },
1058 type: 'numeric'
1059 } );
1060
1061 ts.addParser( {
1062 id: 'usLongDate',
1063 is: function ( s ) {
1064 return ts.rgx.usLongDate[0].test( s );
1065 },
1066 format: function ( s ) {
1067 return $.tablesorter.formatFloat( new Date( s ).getTime() );
1068 },
1069 type: 'numeric'
1070 } );
1071
1072 ts.addParser( {
1073 id: 'date',
1074 is: function ( s ) {
1075 return ( ts.dateRegex[0].test( s ) || ts.dateRegex[1].test( s ) || ts.dateRegex[2].test( s ) );
1076 },
1077 format: function ( s ) {
1078 var match, y;
1079 s = $.trim( s.toLowerCase() );
1080
1081 if ( ( match = s.match( ts.dateRegex[0] ) ) !== null ) {
1082 if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgContentLanguage' ) === 'en' ) {
1083 s = [ match[3], match[1], match[2] ];
1084 } else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
1085 s = [ match[3], match[2], match[1] ];
1086 } else {
1087 // If we get here, we don't know which order the dd-dd-dddd
1088 // date is in. So return something not entirely invalid.
1089 return '99999999';
1090 }
1091 } else if ( ( match = s.match( ts.dateRegex[1] ) ) !== null ) {
1092 s = [ match[3], '' + ts.monthNames[match[2]], match[1] ];
1093 } else if ( ( match = s.match( ts.dateRegex[2] ) ) !== null ) {
1094 s = [ match[3], '' + ts.monthNames[match[1]], match[2] ];
1095 } else {
1096 // Should never get here
1097 return '99999999';
1098 }
1099
1100 // Pad Month and Day
1101 if ( s[1].length === 1 ) {
1102 s[1] = '0' + s[1];
1103 }
1104 if ( s[2].length === 1 ) {
1105 s[2] = '0' + s[2];
1106 }
1107
1108 if ( ( y = parseInt( s[0], 10 ) ) < 100 ) {
1109 // Guestimate years without centuries
1110 if ( y < 30 ) {
1111 s[0] = 2000 + y;
1112 } else {
1113 s[0] = 1900 + y;
1114 }
1115 }
1116 while ( s[0].length < 4 ) {
1117 s[0] = '0' + s[0];
1118 }
1119 return parseInt( s.join( '' ), 10 );
1120 },
1121 type: 'numeric'
1122 } );
1123
1124 ts.addParser( {
1125 id: 'time',
1126 is: function ( s ) {
1127 return ts.rgx.time[0].test( s );
1128 },
1129 format: function ( s ) {
1130 return $.tablesorter.formatFloat( new Date( '2000/01/01 ' + s ).getTime() );
1131 },
1132 type: 'numeric'
1133 } );
1134
1135 ts.addParser( {
1136 id: 'number',
1137 is: function ( s ) {
1138 return $.tablesorter.numberRegex.test( $.trim( s ) );
1139 },
1140 format: function ( s ) {
1141 return $.tablesorter.formatDigit( s );
1142 },
1143 type: 'numeric'
1144 } );
1145
1146 }( jQuery, mediaWiki ) );