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';
// ====== COLUMN CONFIG ======
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'
};
// ====== STATE ======
var ALL = [];
var FILTER = { q: '' };
// ====== HELPERS ======
function $(id) { return document.getElementById(id); }
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 args = arguments;
clearTimeout(t);
t = setTimeout(function () { fn.apply(null, args); }, 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';
}
// ====== CSV URL (SAFE + FLEXIBLE) ======
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();
}
// ====== DATA FORMATTERS ======
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 splitLinks(value) {
if (!value) return [];
return String(value).split(',').map(function (x) { return x.trim(); }).filter(Boolean);
}
function fullName(p) {
return ((p[COL.first] || '') + ' ' + (p[COL.last] || '')).trim() || 'Untitled';
}
function lifespan(p) {
var b = p[COL.birth] || '';
var d = p[COL.death] || '';
if (b && d) return b + '–' + d;
if (b && !d) return b + '–';
if (!b && d) return '–' + d;
return '';
}
function accessLabel(name, type, i, total) {
var part = total > 1 ? ' (Part ' + (i + 1) + '/' + total + ')' : '';
return 'Access ' + name + '’s ' + type.charAt(0).toUpperCase() + type.slice(1) + part;
}
function chipText(type, i, total) {
if (total > 1) return type.charAt(0).toUpperCase() + type.slice(1) + ' ' + (i + 1) + '/' + total;
return type.charAt(0).toUpperCase() + type.slice(1);
}
// ====== UI ======
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);
}
function parseCSV(text) {
var rows = [];
var pattern = /(?:^|,|\r?\n)(?:"([^"]*(?:""[^"]*)*)"|([^",\r\n]*))/g;
var matches = [];
var match;
while ((match = pattern.exec(text)) !== null) {
matches.push(match[1] ? match[1].replace(/""/g, '"') : match[2]);
}
if (!matches.length) return [];
var headers = matches.slice(0, text.indexOf('\n') > -1 ? text.split('\n')[0].split(',').length : matches.length);
var data = matches.slice(headers.length);
var colCount = headers.length;
for (var i = 0; i < data.length; i += colCount) {
var row = {};
for (var j = 0; j < colCount; j++) {
row[headers[j]] = data[i + j] || '';
}
rows.push(row);
}
return rows;
}
function buildHeader(p) {
var meta = [];
var life = lifespan(p);
var rec = recordedLabel(p);
if (life) meta.push('<span>' + esc(life) + '</span>');
if (rec) meta.push('<span>' + esc(rec) + '</span>');
var metaHTML = meta.join('<span class="ohr-dot">·</span>');
return (
'<div class="ohr-head">' +
'<div class="ohr-titleblock">' +
'<h3 class="ohr-name">' + esc(fullName(p)) + '</h3>' +
(metaHTML ? '<div class="ohr-meta">' + metaHTML + '</div>' : '') +
'</div>' +
'</div>'
);
}
function buildLinks(p, style) {
var name = fullName(p);
var out = [];
function add(type, urls) {
urls.forEach(function (url, i) {
var label = accessLabel(name, type, i, urls.length);
if (style === 'chips') {
out.push(
'<a class="ohr-chip" target="_blank" rel="noopener" href="' + esc(url) +
'" title="' + esc(label) + '">' +
esc(chipText(type, i, urls.length)) +
'</a>'
);
} else {
out.push(
'<a class="ohr-link" target="_blank" rel="noopener" href="' + esc(url) + '">' +
esc(label) +
'</a>'
);
}
});
}
var wiki = (p[COL.wiki] || '').trim();
if (wiki) {
out.push(
'<a class="' + (style === 'chips' ? 'ohr-chip' : 'ohr-link') +
'" target="_blank" rel="noopener" href="' + esc(wiki) + '">Read Wiki Page</a>'
);
}
add('video', splitLinks(p[COL.video]));
add('audio', splitLinks(p[COL.audio]));
add('transcript', splitLinks(p[COL.transcript]));
if (!out.length) return '';
return '<div class="' + (style === 'chips' ? 'ohr-chips' : 'ohr-links') + '">' + out.join('') + '</div>';
}
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.map(function (p, i) {
var summary = (p[COL.summary] || '').trim();
var base =
buildHeader(p) +
(summary ? '<p class="ohr-bio">' + esc(summary) + '</p>' : '') +
buildLinks(p, mode === 'chips' ? 'chips' : 'links');
return '<div class="ohr-item">' + base + '</div>';
}).join('');
container.innerHTML = html;
}
function applyFilters() {
var q = norm(FILTER.q);
var filtered = ALL.filter(function (row) {
if (!q) return true;
var hay = norm(
(row[COL.first] || '') + ' ' +
(row[COL.last] || '') + ' ' +
(row[COL.summary] || '')
);
return hay.indexOf(q) !== -1;
});
filtered.sort(function (a, b) {
return norm(a[COL.last]).localeCompare(norm(b[COL.last]));
});
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);
});
}
if (document.readyState !== 'loading') init();
else document.addEventListener('DOMContentLoaded', init);
})();