Tip of the Day
Skill levels exceeding an activity's main skill requirement gives +1.25% extra Work efficiency for what you're doing, up to 20 levels.
MediaWiki:Gadget-TableChecklist.js
From Walkscape Walkthrough
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Gadget: Table Checklist
* Multi-select highlight for tables with class "mw-lighttable".
* Optional footer (counter + Clear All) when table also has class "mw-lighttablefooter".
* Persists by stable row IDs via localStorage (fallback to mw.storage).
*/
mw.loader.using(['jquery', 'mediawiki.storage']).then(function () {
var DEBUG = false; // flip to true for console logs
// ---------- Config ----------
var TABLE_CLASS_OPTIN = 'mw-lighttablefooter'; // add this class to tables that should get a footer
var TABLE_KEY_ATTR = 'data-storage-key'; // per-table storage key attribute
var ROW_ID_ATTRS = ['data-achievement-id', 'data-row-id']; // attributes that carry a stable ID
// ---------- Storage (localStorage with MW fallback) ----------
function canUseLocalStorage() {
try {
var k = '__lt_test__' + Math.random();
localStorage.setItem(k, '1');
localStorage.removeItem(k);
return true;
} catch (e) { if (DEBUG) console.warn('localStorage unavailable:', e && e.name); return false; }
}
var storageImpl = canUseLocalStorage()
? { get: k => localStorage.getItem(k), set: (k,v) => localStorage.setItem(k,v), remove: k => localStorage.removeItem(k) }
: { get: k => mw.storage.get(k), set: (k,v) => mw.storage.set(k,v), remove: k => mw.storage.remove(k) };
function readMap(key){ try{ var raw=storageImpl.get(key); return raw ? JSON.parse(raw) : {}; }catch(e){ if(DEBUG)console.warn('readMap',key,e); return {}; } }
function writeMap(key,map){ try{ storageImpl.set(key, JSON.stringify(map)); if(DEBUG)console.log('writeMap',key,map); }catch(e){ console.error('writeMap failed',key,e); } }
// ---------- Helpers ----------
function defaultKeyForTable($table){
var page = mw.config.get('wgPageName') || 'UnknownPage';
var idx = $('table.mw-lighttable').index($table);
return 'rs:lightTable:' + page + ':' + idx;
}
function getStorageKeyForTable($table){
var key = $table.attr(TABLE_KEY_ATTR) || defaultKeyForTable($table);
if (DEBUG) console.log('[lt] key:', key);
return key;
}
function getRowId($tr){
for (var i=0;i<ROW_ID_ATTRS.length;i++){ var v=$tr.attr(ROW_ID_ATTRS[i]); if(v) return v; }
// fallback: if someone put the id on the first cell
var $cell = $tr.children('td['+ROW_ID_ATTRS[0]+'],th['+ROW_ID_ATTRS[0]+'],td['+ROW_ID_ATTRS[1]+'],th['+ROW_ID_ATTRS[1]+']').first();
if ($cell.length) return $cell.attr(ROW_ID_ATTRS[0]) || $cell.attr(ROW_ID_ATTRS[1]) || '';
return '';
}
function tableColumnCount($table) {
function sumCols($row) {
var cols = 0;
$row.children('th,td').each(function () {
var span = parseInt($(this).attr('colspan') || '1', 10);
cols += (isNaN(span) ? 1 : span);
});
return cols;
}
var maxCols = 0;
// Prefer THEAD: handle multi-row headers and colspans correctly
var $thead = $table.children('thead');
if ($thead.length) {
$thead.children('tr').each(function () {
maxCols = Math.max(maxCols, sumCols($(this)));
});
}
// Fallback / cross-check with TBODY (in case no THEAD or unusual markup)
if (maxCols === 0) {
$table.children('tbody').children('tr').each(function () {
maxCols = Math.max(maxCols, sumCols($(this)));
});
}
return Math.max(maxCols, 1);
}
// ---------- Footer (counter + clear-all-by-ID) ----------
function countSelectable($table){
var seen = Object.create(null), selectedById = Object.create(null);
$table.find('tbody tr').each(function(){
var id = getRowId($(this)); if (!id) return;
seen[id] = true; if ($(this).hasClass('highlight-on')) selectedById[id] = true;
});
var total=0, selected=0; for (var k in seen) total++; for (var k2 in selectedById) selected++;
return {selected, total};
}
function ensureFooter($table){
if (!$table.hasClass(TABLE_CLASS_OPTIN)) return null; // opt-in only
var $tfoot = $table.children('tfoot'); if (!$tfoot.length) $tfoot = $('<tfoot>').appendTo($table);
var $row = $tfoot.children('tr.lt-footer-row');
if (!$row.length) {
$row = $('<tr>').addClass('lt-footer-row').appendTo($tfoot);
$('<th>').attr('colspan', tableColumnCount($table)).addClass('lt-footer-cell').appendTo($row);
} else {
$row.find('th.lt-footer-cell').attr('colspan', tableColumnCount($table));
}
var $cell = $row.find('th.lt-footer-cell');
if (!$cell.find('.lt-clear-btn').length) {
var $counter = $('<span class="lt-footer-counter"></span>');
var $btn = $('<button type="button" class="lt-clear-btn">Clear All</button>')
.css({ marginLeft: '1em' })
.on('click', function(){
// collect unique IDs present
var ids = Object.create(null);
$table.find('tbody tr').each(function(){ var id=getRowId($(this)); if (id) ids[id]=true; });
// remove highlight from all rows with those IDs
$table.find('tbody tr').each(function(){ var $tr=$(this), id=getRowId($tr); if (ids[id]) $tr.removeClass('highlight-on').attr('aria-selected','false'); });
// clear in storage by ID
var key = getStorageKeyForTable($table);
var map = readMap(key);
Object.keys(ids).forEach(function(id){ map[id]=0; });
writeMap(key, map);
updateFooter($table);
});
$cell.empty().append($counter).append($btn);
}
return $cell.find('.lt-footer-counter');
}
function updateFooter($table){
var $counter = ensureFooter($table); if (!$counter) return;
var c = countSelectable($table);
$counter.text('Entries Completed: (' + c.selected + ' / ' + c.total + ')');
}
// ---------- Apply & Bind ----------
function applyFromStorage($root){
$root.find('table.mw-lighttable').each(function(){
var $table = $(this), key = getStorageKeyForTable($table), map = readMap(key);
$table.find('tbody tr').each(function(){
var $tr=$(this), id=getRowId($tr); if(!id){ if(DEBUG)console.warn('[lt] row missing ID',this); return; }
var on = map[id] === 1 || map[id] === '1';
$tr.toggleClass('highlight-on', on).attr('aria-selected', on ? 'true' : 'false');
if (!$tr.attr('tabindex')) $tr.attr('tabindex','0');
});
updateFooter($table);
});
}
function bindHandlers($root){
$root.find('table.mw-lighttable').each(function(){
var $table = $(this), key = getStorageKeyForTable($table);
$table.off('.mwLighttable');
// Don't toggle on link clicks (and stop bubbling)
$table.on('click.mwLighttable','a',function(e){ e.stopPropagation(); });
// Toggle on non-link clicks; group by ID
$table.on('click.mwLighttable','tbody tr',function(e){
if ($(e.target).closest('a, button, input, label, select, textarea').length) return;
var $tr=$(this), id=getRowId($tr); if(!id) return;
var on = !$tr.hasClass('highlight-on');
var $group = $table.find('tbody tr').filter(function(){ return getRowId($(this)) === id; });
$group.toggleClass('highlight-on', on).attr('aria-selected', on ? 'true' : 'false');
var map = readMap(key); map[id] = on ? 1 : 0; writeMap(key, map);
updateFooter($table);
});
// Keyboard support
$table.on('keydown.mwLighttable','tbody tr',function(e){
if (e.key === ' ' || e.key === 'Enter'){ e.preventDefault(); $(this).trigger('click'); }
});
});
}
// ---------- Init ----------
$(function(){ applyFromStorage($(document)); bindHandlers($(document)); });
mw.hook('wikipage.content').add(function($c){ applyFromStorage($c); bindHandlers($c); });
// ---------- Optional debug helpers ----------
window.ltDebug = {
dump: function(){ $('table.mw-lighttable').each(function(){ var key=getStorageKeyForTable($(this)); console.log('[lt] DUMP', key, readMap(key)); }); },
clearAll: function(){ $('table.mw-lighttable').each(function(){ var key=getStorageKeyForTable($(this)); storageImpl.remove(key); console.log('[lt] CLEARED', key); }); }
};
});
