Main Page:mediawiki:common.js: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* | /* 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 --- */ | /* --- 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 | // Skip obvious layout/meta tables | ||
if ($table.is('.infobox, .navbox, .toc, .metadata')) return; | if ( $table.is( '.infobox, .navbox, .toc, .metadata' ) ) return; | ||
// | // 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; | ||
} | } | ||
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 ); | ||
}); | } ); | ||
} | } | ||
/* --- | /* --- Init collapsible --- */ | ||
function initCollapsible($root) { | function initCollapsible( $root ) { | ||
mw.loader. | // 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 ); | |||
} ); | |||
} ); | |||
} ); | |||
} | } | ||
/* Sort a single table's tbody rows by column (simple numeric/date/string heuristics) */ | |||
function | 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' ); | |||
var | $th.addClass( order === 'asc' ? 'headerSortUp' : 'headerSortDown' ); | ||
if ( | $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(); | |||
var $ | items.forEach( function ( it ) { frag.appendChild( it.row ); } ); | ||
if (!$ | 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 | /* --- Enhance function runs everything --- */ | ||
function enhance($content) { | function enhance( $content ) { | ||
wrapTables($content); | wrapTables( $content ); | ||
initCollapsible($content); | initCollapsible( $content ); | ||
initSortable($content); | initSortable( $content ); | ||
} | } | ||
/* | /* Initial run and re-run on dynamic updates */ | ||
$(function () { enhance($(document)); }); | $( function () { enhance( $( document ) ); } ); | ||
mw.hook( 'wikipage.content' ).add( function ( $content ) { enhance( $content ); } ); | |||
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 ); } );
} )();