Main Page:mediawiki:common.js
Revision as of 17:35, 26 August 2025 by Jayprakash12345 (talk | contribs)
/* 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 ); } );
} )();