[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/resources/src/jquery/ -> jquery.tablesorter.js (source)

   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 ) );


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1