MediaWiki:Gadget-OralHistoryRecords-Updated.js
Appearance
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.
/* ===== OralHistoryRecords-Updated.js ===== */
/* global mw */
(function () {
'use strict';
var COL = {
transcript: 'Transcript Identifier',
audio: 'Audio Identifier',
video: 'Video Identifier',
first: 'First Name',
last: 'Last Name',
birth: 'Birth Year',
death: 'Death Year',
dateFrom: 'Date From',
dateTo: 'Date To',
summary: 'Summary',
wiki: 'Wiki Link'
};
var ALL = [];
var FILTER = { q: '' };
function $(id) { return document.getElementById(id); }
function onReady(fn) {
if (document.readyState !== 'loading') fn();
else document.addEventListener('DOMContentLoaded', fn);
}
function esc(s) {
s = (s == null ? '' : String(s));
return s.replace(/[&<>"']/g, function (c) {
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]);
});
}
function norm(s) {
return (s || '')
.toString()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
function debounce(fn, ms) {
var t;
return function () {
var a = arguments;
clearTimeout(t);
t = setTimeout(function () { fn.apply(null, a); }, ms);
};
}
function getViewMode() {
var root = $('ohr-directory');
var mode = root ? (root.getAttribute('data-view') || '').trim().toLowerCase() : '';
if (mode === 'accordion' || mode === 'chips' || mode === 'directory') return mode;
return 'directory';
}
function getCsvUrl() {
var root = $('ohr-directory');
var override = root ? (root.getAttribute('data-csv') || '').trim() : '';
var base = override || ((mw.config.get('wgScript') || '/index.php') + '/Special:FilePath/OHData.csv');
return base + (base.indexOf('?') === -1 ? '?' : '&') + 'cb=' + Date.now();
}
function toYear(value) {
if (!value) return '';
var m = String(value).match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);
return m ? m[1] : '';
}
function recordedLabel(person) {
var y1 = toYear(person[COL.dateFrom]);
var y2 = toYear(person[COL.dateTo]);
if (y1 && y2) return (y1 === y2) ? ('Recorded ' + y1) : ('Recorded ' + y1 + '–' + y2);
if (y1) return 'Recorded ' + y1;
if (y2) return 'Recorded ' + y2;
return '';
}
function splitLinksCSV(value) {
if (!value) return [];
return String(value).split(',').map(function (x) { return x.trim(); }).filter(Boolean);
}
// NEW: Only valid if BOTH first and last exist
function fullName(person) {
var first = (person[COL.first] || '').trim();
var last = (person[COL.last] || '').trim();
if (!first || !last) return '';
return first + ' ' + last;
}
function lifespan(person) {
var b = (person[COL.birth] || '').trim();
var d = (person[COL.death] || '').trim();
if (b && d) return b + '–' + d;
if (b && !d) return b + '–';
if (!b && d) return '–' + d;
return '';
}
function truncateSummary(text, limit) {
if (!text) return '';
text = String(text).trim();
if (text.length <= limit) return text;
return text.slice(0, limit).trim() + '…';
}
function buildAccessLabel(name, kind, idx, total) {
var part = total > 1 ? ' (Part ' + (idx + 1) + '/' + total + ')' : '';
return 'Access ' + name + '’s ' + kind.charAt(0).toUpperCase() + kind.slice(1) + part;
}
function buildChipText(kind, idx, total) {
if (total > 1) return kind.charAt(0).toUpperCase() + kind.slice(1) + ' ' + (idx + 1) + '/' + total;
return kind.charAt(0).toUpperCase() + kind.slice(1);
}
function buildToolbar(root) {
var bar = document.createElement('div');
bar.className = 'ohr-toolbar';
var search = document.createElement('input');
search.id = 'ohr-search';
search.type = 'search';
search.placeholder = 'Search names, summaries…';
var count = document.createElement('div');
count.id = 'ohr-count';
count.className = 'ohr-count';
bar.appendChild(search);
bar.appendChild(count);
root.insertBefore(bar, root.firstChild);
}
// ROBUST CSV PARSER
function parseCSV(text) {
text = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
var rows = [];
var row = [];
var field = '';
var inQuotes = false;
for (var i = 0; i < text.length; i++) {
var c = text[i];
if (inQuotes) {
if (c === '"') {
if (text[i + 1] === '"') {
field += '"';
i++;
} else {
inQuotes = false;
}
} else {
field += c;
}
} else {
if (c === '"') {
inQuotes = true;
} else if (c === ',') {
row.push(field);
field = '';
} else if (c === '\n') {
row.push(field);
rows.push(row);
row = [];
field = '';
} else {
field += c;
}
}
}
row.push(field);
if (row.length) rows.push(row);
if (!rows.length) return [];
var headers = rows[0];
var out = [];
for (var r = 1; r < rows.length; r++) {
var values = rows[r];
var obj = {};
for (var c2 = 0; c2 < headers.length; c2++) {
obj[headers[c2]] = values[c2] || '';
}
out.push(obj);
}
return out;
}
function render(rows) {
var container = $('ohr-list');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<p class="ohr-muted" style="text-align:center">No matching records.</p>';
return;
}
var mode = getViewMode();
var html = '';
rows.forEach(function (p) {
var name = fullName(p);
if (!name) return; // skip invalid records
var summary = truncateSummary(p[COL.summary], 300);
html += '<div class="ohr-item">';
html += '<h3 class="ohr-name">' + esc(name) + '</h3>';
var life = lifespan(p);
var rec = recordedLabel(p);
if (life || rec) {
html += '<div class="ohr-meta">';
if (life) html += esc(life);
if (life && rec) html += ' · ';
if (rec) html += esc(rec);
html += '</div>';
}
if (summary) {
html += '<p class="ohr-bio">' + esc(summary) + '</p>';
}
html += '</div>';
});
container.innerHTML = html;
}
function applyFilters() {
var q = norm(FILTER.q);
var filtered = ALL.filter(function (row) {
if (!fullName(row)) return false; // remove nameless records entirely
if (!q) return true;
var hay = norm(
(row[COL.first] || '') + ' ' +
(row[COL.last] || '') + ' ' +
(row[COL.summary] || '')
);
return hay.indexOf(q) !== -1;
});
render(filtered);
var count = $('ohr-count');
if (count) count.textContent = filtered.length + ' records';
}
function attachHandlers() {
var input = $('ohr-search');
if (input) {
input.addEventListener('input', debounce(function () {
FILTER.q = input.value.trim();
applyFilters();
}, 200));
}
}
function displayError(msg) {
var loading = $('ohr-loading');
var error = $('ohr-error');
if (loading) loading.classList.add('ohr-hidden');
if (error) {
error.classList.remove('ohr-hidden');
error.classList.add('ohr-error');
error.innerHTML =
'<p><strong>Failed to load records.</strong></p>' +
'<p class="ohr-muted">' + esc(msg) + '</p>';
}
}
function init() {
var root = $('ohr-directory');
if (!root) return;
buildToolbar(root);
fetch(getCsvUrl(), { credentials: 'include', cache: 'no-store' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
})
.then(function (text) {
ALL = parseCSV(text);
$('ohr-loading').classList.add('ohr-hidden');
attachHandlers();
applyFilters();
})
.catch(function (err) {
displayError(err.message);
console.error(err);
});
}
onReady(init);
})();