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