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

(Created page with "→‎Auto-wrap nearly all content tables for horizontal scrolling on small screens. Excludes common layout/meta tables and those explicitly marked .no-responsive. Also (re)initializes sorting and collapsible behavior on both desktop & mobile.: (function () { // Initialize features (sortable + collapsible) in a safe, mobile-friendly way function initTableFeatures($root) { mw.loader.using(['jquery.tablesorter', 'jquery.makeCollapsible']).then(function () {...")
 
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* Auto-wrap nearly all content tables for horizontal scrolling on small screens.
/* MediaWiki:Common.js
   Excludes common layout/meta tables and those explicitly marked .no-responsive.
   Lightweight, mobile-friendly table sortable + responsive + collapsible
   Also (re)initializes sorting and collapsible behavior on both desktop & mobile. */
   Works on Vector (desktop) and Minerva (mobile) without depending on core tablesorter
*/


(function () {
( function () {
   // Initialize features (sortable + collapsible) in a safe, mobile-friendly way
   /* --- Wrap tables for responsive scrolling --- */
  function initTableFeatures($root) {
   function wrapTables( $root ) {
    mw.loader.using(['jquery.tablesorter', 'jquery.makeCollapsible']).then(function () {
     var $scope = $root.find( '.mw-parser-output' ).addBack( '.mw-parser-output' );
      $root.find('table.sortable').each(function () {
     if ( !$scope.length ) $scope = $root;
        var $t = $(this);
        if (!$t.data('tablesorter-initialized')) {
          $t.tablesorter();
          $t.data('tablesorter-initialized', true);
        }
      });
      $root.find('.mw-collapsible').each(function () {
        var $c = $(this);
        if (!$c.data('collapsible-initialized')) {
          $c.makeCollapsible();
          $c.data('collapsible-initialized', true);
        }
      });
    });
  }
 
  // Wrap tables with a11y-friendly scroll container
   function wrapTables($root) {
    // Limit to page content; safer across skins
     var $scope = $root.find('.mw-parser-output').addBack('.mw-parser-output');
     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 explicitly opted out
       // 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 to avoid weird visuals
       // Skip obvious layout/meta tables
       if ($table.is('.infobox, .navbox, .toc, .metadata')) return;
       if ( $table.is( '.infobox, .navbox, .toc, .metadata' ) ) return;


       // Build accessible label from caption 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;
       }
       }


      // Create wrapper and move table inside
       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 );
      $wrapper.append( $table );
    } );
  }


       $table.before($wrapper);
  /* --- Init collapsible --- */
       $wrapper.append($table);
  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;
   }
   }


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


   // Run on initial page 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 on content updates (VisualEditor, previews, AJAX, etc.)
} )();
  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 ); } );

} )();