Tabledemo:MediaWiki:Common.js

/* MediaWiki:Common.js

  Lightweight, mobile-friendly table sortable + responsive + collapsible
  Works on Vector (desktop) and Minerva (mobile) without depending on core tablesorter
  • /

( function () {

 /* --- Wrap tables for responsive scrolling --- */
 function wrapTables( $root ) {
   var $scope = $root.find( '.mw-parser-output' ).addBack( '.mw-parser-output' );
   if ( !$scope.length ) $scope = $root;
   $scope.find( 'table' ).each( function () {
     var $table = $( this );
     // Skip if already wrapped, or marked no-responsive
     if ( $table.closest( '.table-responsive' ).length ) return;
     if ( $table.hasClass( 'no-responsive' ) ) return;
     // Skip obvious layout/meta tables
     if ( $table.is( '.infobox, .navbox, .toc, .metadata' ) ) return;
     // Accessible label from caption
     var label = 'Scrollable table';
     var $cap = $table.children( 'caption' ).first();
     if ( $cap.length ) {
       var capText = ( $cap.text() ||  ).trim();
       if ( capText ) label = 'Scrollable table: ' + capText;
     }

var $wrapper = $( '

', {
       'class': 'table-responsive',
       'role': 'region',
       'aria-label': label,
       'tabindex': 0
     } );
     $table.before( $wrapper );
     $wrapper.append( $table );
   } );
 }
 /* --- Init collapsible --- */
 function initCollapsible( $root ) {
   // Try to use core collapsible module if available
   if ( mw.loader.getState( 'jquery.makeCollapsible' ) === 'ready' ) {
     $root.find( '.mw-collapsible' ).each( function () {
       var $c = $( this );
       if ( !$c.data( 'collapsible-initialized' ) ) {
         try { $c.makeCollapsible(); } catch (e) { /* ignore */ }
         $c.data( 'collapsible-initialized', true );
       }
     } );
   } else {
     // lazy-load if needed
     mw.loader.using( 'jquery.makeCollapsible' ).then( function () {
       $root.find( '.mw-collapsible' ).each( function () {
         var $c = $( this );
         if ( !$c.data( 'collapsible-initialized' ) ) {
           try { $c.makeCollapsible(); } catch (e) { /* ignore */ }
           $c.data( 'collapsible-initialized', true );
         }
       } );
     } );
   }
 }
 /* --- Lightweight, dependency-free sortable implementation --- */
 function initSortable( $root ) {
   $root.find( 'table.sortable' ).each( function () {
     var $table = $( this );
     if ( $table.data( 'sortable-init' ) ) return;
     $table.data( 'sortable-init', true );
     // find headers - prefer THEAD
     var $headers = $table.find( 'thead th' );
     if ( !$headers.length ) {
       // fallback to first row's cells
       $headers = $table.find( 'tr' ).first().find( 'th,td' );
     }
     $headers.each( function ( colIndex ) {
       var $th = $( this );
       if ( $th.hasClass( 'unsortable' ) ) return;
       // make touch/click-friendly
       $th.attr( 'role', 'button' ).css( 'cursor', 'pointer' );
       // prevent double-firing (touch + click)
       var lastTap = 0;
       $th.on( 'click touchstart', function ( e ) {
         var now = Date.now();
         if ( e.type === 'touchstart' && now - lastTap < 400 ) return; // ignore
         lastTap = now;
         e.preventDefault();
         e.stopPropagation();
         sortTableByColumn( $table, colIndex, $th );
       } );
     } );
   } );
 }
 /* Sort a single table's tbody rows by column (simple numeric/date/string heuristics) */
 function sortTableByColumn( $table, colIndex, $th ) {
   // toggle order
   var current = $th.data( 'sort-order' ) || 'none';
   var order = ( current === 'asc' ) ? 'desc' : 'asc';
   $th.closest( 'tr' ).find( 'th' ).removeClass( 'headerSortUp headerSortDown' );
   $th.addClass( order === 'asc' ? 'headerSortUp' : 'headerSortDown' );
   $th.data( 'sort-order', order );
   // process each TBODY independently (keeps logical groups)
   $table.find( 'tbody' ).each( function () {
     var tbody = this;
     var $tbody = $( tbody );
     var rows = $tbody.children( 'tr' ).toArray();
     // build keys (stable sort)
     var items = rows.map( function ( row, idx ) {
       return {
         row: row,
         idx: idx,
         key: extractCellValue( row, colIndex )
       };
     } );
     // detect numeric-like keys
     var numeric = items.every( function ( it ) {
       return it.key === null ? true : ( typeof it.key === 'number' );
     } );
     items.sort( function ( a, b ) {
       var va = a.key, vb = b.key;
       // treat nulls as last
       if ( va === null && vb === null ) return a.idx - b.idx;
       if ( va === null ) return 1;
       if ( vb === null ) return -1;
       if ( numeric ) {
         if ( va === vb ) return a.idx - b.idx;
         return order === 'asc' ? ( va - vb ) : ( vb - va );
       } else {
         va = String( va ).toLowerCase();
         vb = String( vb ).toLowerCase();
         if ( va === vb ) return a.idx - b.idx;
         return order === 'asc' ? va.localeCompare( vb ) : vb.localeCompare( va );
       }
     } );
     // append in new order
     var frag = document.createDocumentFragment();
     items.forEach( function ( it ) { frag.appendChild( it.row ); } );
     tbody.appendChild( frag );
   } );
 }
 /* Extract value for a given column index from a row (handles colspans) */
 function extractCellValue( row, colIndex ) {
   var $cells = $( row ).children( 'th,td' );
   var col = 0;
   for ( var i = 0; i < $cells.length; i++ ) {
     var cell = $cells[ i ];
     var colspan = parseInt( cell.getAttribute( 'colspan' ) || 1, 10 );
     if ( col + colspan - 1 >= colIndex ) {
       var txt = $( cell ).text().trim();
       if ( txt ===  ) return null;
       // numeric? strip currency symbols and commas
       var numTxt = txt.replace( /[,\s₹$€£¥]/g,  );
       var num = parseFloat( numTxt );
       if ( !isNaN( num ) && numTxt !==  ) return num;
       // ISO date (YYYY-MM-DD)
       if ( /^\d{4}-\d{2}-\d{2}$/.test( txt ) ) {
         var t = Date.parse( txt );
         if ( !isNaN( t ) ) return t;
       }
       return txt;
     }
     col += colspan;
   }
   return null;
 }

// Fix table sorting on mobile (Minerva skin) if ( mw.config.get( 'skin' ) === 'minerva' ) {

   mw.hook( 'wikipage.content' ).add( function ( $content ) {
       // Load tablesorter for sortable tables
       if ( $content.find( '.sortable' ).length ) {
           mw.loader.using( 'jquery.tablesorter' ).done( function () {
               $content.find( '.sortable' ).tablesorter();
           });
       }
   });

}

 /* --- Enhance function runs everything --- */
 function enhance( $content ) {
   wrapTables( $content );
   initCollapsible( $content );
   initSortable( $content );
 }
 /* Initial run and re-run on dynamic updates */
 $( function () { enhance( $( document ) ); } );
 mw.hook( 'wikipage.content' ).add( function ( $content ) { enhance( $content ); } );


} )();