Main Page:mediawiki:common.js: Difference between revisions

From Ekatra Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
Line 1: Line 1:
/* ==========================================================
/* MediaWiki:Common.js
  MediaWiki:Common.js
   Lightweight, mobile-friendly table sortable + responsive + collapsible
   Purpose: Ensure tables are responsive, sortable, and collapsible
  Works on Vector (desktop) and Minerva (mobile) without depending on core tablesorter
            across desktop (Vector) and mobile (Minerva).
*/
  ========================================================== */
 
(function () {


( function () {
   /* --- Wrap tables for responsive scrolling --- */
   /* --- Wrap tables for responsive scrolling --- */
   function wrapTables($root) {
   function wrapTables( $root ) {
     var $scope = $root.find('.mw-parser-output').addBack('.mw-parser-output');
     var $scope = $root.find( '.mw-parser-output' ).addBack( '.mw-parser-output' );
     if (!$scope.length) $scope = $root;
     if ( !$scope.length ) $scope = $root;


     $scope.find('table').each(function () {
     $scope.find( 'table' ).each( function () {
       var $table = $(this);
       var $table = $( this );


       // Skip if already wrapped, or marked no-responsive
       // Skip if already wrapped, or marked no-responsive
       if ($table.closest('.table-responsive').length) return;
       if ( $table.closest( '.table-responsive' ).length ) return;
       if ($table.hasClass('no-responsive')) return;
       if ( $table.hasClass( 'no-responsive' ) ) return;


       // Skip common layout/meta tables
       // Skip obvious layout/meta tables
       if ($table.is('.infobox, .navbox, .toc, .metadata')) return;
       if ( $table.is( '.infobox, .navbox, .toc, .metadata' ) ) return;


       // Use caption as label if present
       // Accessible label from caption
       var label = 'Scrollable table';
       var label = 'Scrollable table';
       var $cap = $table.children('caption').first();
       var $cap = $table.children( 'caption' ).first();
       if ($cap.length) {
       if ( $cap.length ) {
         var capText = ($cap.text() || '').trim();
         var capText = ( $cap.text() || '' ).trim();
         if (capText) label = 'Scrollable table: ' + capText;
         if ( capText ) label = 'Scrollable table: ' + capText;
       }
       }


      // Wrap with scrollable div
       var $wrapper = $( '<div>', {
       var $wrapper = $('<div>', {
         'class': 'table-responsive',
         'class': 'table-responsive',
         'role': 'region',
         'role': 'region',
         'aria-label': label,
         'aria-label': label,
         'tabindex': 0
         'tabindex': 0
       });
       } );


       $table.before($wrapper);
       $table.before( $wrapper );
       $wrapper.append($table);
       $wrapper.append( $table );
     });
     } );
   }
   }


   /* --- Initialize collapsible tables --- */
   /* --- Init collapsible --- */
   function initCollapsible($root) {
   function initCollapsible( $root ) {
     mw.loader.using('jquery.makeCollapsible').then(function () {
     // Try to use core collapsible module if available
       $root.find('.mw-collapsible').each(function () {
    if ( mw.loader.getState( 'jquery.makeCollapsible' ) === 'ready' ) {
         var $c = $(this);
       $root.find( '.mw-collapsible' ).each( function () {
         if (!$c.data('collapsible-initialized')) {
         var $c = $( this );
           $c.makeCollapsible();
         if ( !$c.data( 'collapsible-initialized' ) ) {
           $c.data('collapsible-initialized', true);
           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 );
        } );
       } );
     } );
   }
   }


/* --- Initialize sortable tables (desktop + mobile) --- */
  /* Sort a single table's tbody rows by column (simple numeric/date/string heuristics) */
function initSortable($root) {
  function sortTableByColumn( $table, colIndex, $th ) {
  mw.loader.using(['jquery.tablesorter', 'jquery.tablesorter.styles']).then(function () {
    // toggle order
     // Use MediaWiki's built-in sortTables() if available
    var current = $th.data( 'sort-order' ) || 'none';
     if (typeof window.sortTables === 'function') {
    var order = ( current === 'asc' ) ? 'desc' : 'asc';
       $root.find('table.sortable').each(function () {
    $th.closest( 'tr' ).find( 'th' ).removeClass( 'headerSortUp headerSortDown' );
         var $t = $(this);
    $th.addClass( order === 'asc' ? 'headerSortUp' : 'headerSortDown' );
         if (!$t.hasClass('sortable-initialized')) {
    $th.data( 'sort-order', order );
           window.sortTables(this);
 
           $t.addClass('sortable-initialized');
     // 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 );
         }
         }
       });
       } );
     } else {
 
      // Fallback: tablesorter
      // append in new order
      $root.find('table.sortable').each(function () {
      var frag = document.createDocumentFragment();
         var $t = $(this);
      items.forEach( function ( it ) { frag.appendChild( it.row ); } );
         if (!$t.hasClass('tablesorter-processed')) {
      tbody.appendChild( frag );
           $t.tablesorter();
     } );
  }
 
  /* 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;
}
  }


   /* --- Enhance page content --- */
   /* --- Enhance function runs everything --- */
   function enhance($content) {
   function enhance( $content ) {
     wrapTables($content);
     wrapTables( $content );
     initCollapsible($content);
     initCollapsible( $content );
     initSortable($content);
     initSortable( $content );
   }
   }


   /* Run on initial load */
   /* Initial run and re-run on dynamic updates */
   $(function () { enhance($(document)); });
   $( function () { enhance( $( document ) ); } );
 
   mw.hook( 'wikipage.content' ).add( function ( $content ) { enhance( $content ); } );
  /* Run again when new content is injected (e.g., VisualEditor, AJAX) */
   mw.hook('wikipage.content').add(function ($content) { enhance($content); });


})();
} )();

Latest revision as of 17:35, 26 August 2025

/* 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;
 }
 /* --- 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 ); } );

} )();