MediaWiki:Gadget-OralHistoryRecords-Updated.js: Difference between revisions
Appearance
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* ===== | /* ===== OralHistoryRecords-Updated.js ===== */ | ||
/* global mw */ | /* global mw */ | ||
(function () { | (function () { | ||
| Line 5: | Line 5: | ||
// ====== CONFIG ====== | // ====== CONFIG ====== | ||
// Spreadsheet column names (exactly as provided) | // Spreadsheet column names (exactly as provided) | ||
var COL = { | var COL = { | ||
| Line 61: | Line 58: | ||
if (mode === 'accordion' || mode === 'chips' || mode === 'directory') return mode; | if (mode === 'accordion' || mode === 'chips' || mode === 'directory') return mode; | ||
return 'directory'; | return 'directory'; | ||
} | |||
// Always resolve to the latest uploaded version of File:OHData.csv | |||
// IMPORTANT: use /index.php/Special:FilePath/... form (works on your site) | |||
function getCsvUrl() { | |||
return mw.config.get('wgScriptPath') + '/Special:FilePath/OHData.csv?cb=' + Date.now(); | |||
} | } | ||
| Line 104: | Line 107: | ||
function buildAccessLabel(name, kind, idx, total) { | function buildAccessLabel(name, kind, idx, total) { | ||
var part = (total > 1) ? (' (Part ' + (idx + 1) + '/' + total + ')') : ''; | var part = (total > 1) ? (' (Part ' + (idx + 1) + '/' + total + ')') : ''; | ||
var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1); | var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1); | ||
| Line 144: | Line 146: | ||
return lines.slice(1).map(function (line) { | return lines.slice(1).map(function (line) { | ||
// | // Lightweight parser; can break on very complex quoted CSV. | ||
var values = line.match(/(?:[^,"]+|"[^"]*")+/g) || []; | var values = line.match(/(?:[^,"]+|"[^"]*")+/g) || []; | ||
values = values.map(function (v) { return v.replace(/^"|"$/g, '').trim(); }); | values = values.map(function (v) { return v.replace(/^"|"$/g, '').trim(); }); | ||
| Line 179: | Line 181: | ||
var wiki = (person[COL.wiki] || '').trim(); | var wiki = (person[COL.wiki] || '').trim(); | ||
if (!wiki) return ''; | if (!wiki) return ''; | ||
return '<a class="' + cls + '" target="_blank" rel="noopener" href="' + esc(wiki) + '">Read Wiki Page</a>'; | return '<a class="' + cls + '" target="_blank" rel="noopener" href="' + esc(wiki) + '">Read Wiki Page</a>'; | ||
} | } | ||
| Line 400: | Line 401: | ||
buildToolbar(root); | buildToolbar(root); | ||
fetch( | fetch(getCsvUrl(), { credentials: 'include', cache: 'no-store' }) | ||
.then(function (res) { | .then(function (res) { | ||
if (!res.ok) throw new Error('HTTP ' + res.status); | if (!res.ok) throw new Error('HTTP ' + res.status); | ||
Revision as of 14:04, 21 February 2026
/* ===== OralHistoryRecords-Updated.js ===== */
/* global mw */
(function () {
'use strict';
// ====== CONFIG ======
// Spreadsheet column names (exactly as provided)
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: '' };
// ====== UTILS ======
function onReady(fn) {
if (document.readyState !== 'loading') fn();
else document.addEventListener('DOMContentLoaded', fn);
}
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 a = arguments, self = this;
clearTimeout(t);
t = setTimeout(function () { fn.apply(self, a); }, ms);
};
}
// Pattern 1: page controls view via <div id="ohr-directory" data-view="...">
function getViewMode() {
var root = $('ohr-directory');
var mode = (root && root.getAttribute('data-view')) ? root.getAttribute('data-view').trim().toLowerCase() : '';
if (mode === 'accordion' || mode === 'chips' || mode === 'directory') return mode;
return 'directory';
}
// Always resolve to the latest uploaded version of File:OHData.csv
// IMPORTANT: use /index.php/Special:FilePath/... form (works on your site)
function getCsvUrl() {
return mw.config.get('wgScriptPath') + '/Special:FilePath/OHData.csv?cb=' + Date.now();
}
function toYear(value) {
if (!value) return '';
var s = String(value).trim();
var m = s.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) {
// Multiple URLs in a cell are comma-separated
if (!value) return [];
return String(value)
.split(',')
.map(function (x) { return x.trim(); })
.filter(Boolean);
}
function fullName(person) {
var first = (person[COL.first] || '').trim();
var last = (person[COL.last] || '').trim();
return (first + ' ' + last).trim() || 'Untitled';
}
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 buildAccessLabel(name, kind, idx, total) {
var part = (total > 1) ? (' (Part ' + (idx + 1) + '/' + total + ')') : '';
var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1);
return 'Access ' + name + '’s ' + kindTitle + part;
}
function buildChipText(kind, idx, total) {
var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1);
if (total > 1) return kindTitle + ' ' + (idx + 1) + '/' + total;
return kindTitle;
}
// ====== UI ======
function buildToolbar(root) {
var toolbar = document.createElement('div');
toolbar.className = 'ohr-toolbar';
var search = document.createElement('input');
search.id = 'ohr-search';
search.type = 'search';
search.placeholder = 'Search names, summaries…';
search.setAttribute('aria-label', 'Search oral history records');
var count = document.createElement('div');
count.id = 'ohr-count';
count.className = 'ohr-count';
count.setAttribute('aria-live', 'polite');
toolbar.appendChild(search);
toolbar.appendChild(count);
root.insertBefore(toolbar, root.firstChild);
}
// ====== CSV parsing ======
function parseCSV(csvText) {
var lines = csvText.trim().split(/\r?\n/);
if (!lines.length) return [];
var headers = lines[0].split(',').map(function (h) { return h.trim(); });
return lines.slice(1).map(function (line) {
// Lightweight parser; can break on very complex quoted CSV.
var values = line.match(/(?:[^,"]+|"[^"]*")+/g) || [];
values = values.map(function (v) { return v.replace(/^"|"$/g, '').trim(); });
if (values.length !== headers.length) return null;
var row = {};
headers.forEach(function (h, i) { row[h] = values[i]; });
return row;
}).filter(Boolean);
}
// ====== Rendering building blocks ======
function buildHeaderHTML(person) {
var name = fullName(person);
var life = lifespan(person);
var rec = recordedLabel(person);
var metaBits = [];
if (life) metaBits.push('<span>' + esc(life) + '</span>');
if (rec) metaBits.push('<span>' + esc(rec) + '</span>');
var meta = metaBits.join('<span class="ohr-dot">·</span>');
return (
'<div class="ohr-head">' +
'<div class="ohr-titleblock">' +
'<h3 class="ohr-name">' + esc(name) + '</h3>' +
(meta ? '<div class="ohr-meta">' + meta + '</div>' : '') +
'</div>' +
'</div>'
);
}
function buildWikiLinkHTML(person, cls) {
var wiki = (person[COL.wiki] || '').trim();
if (!wiki) return '';
return '<a class="' + cls + '" target="_blank" rel="noopener" href="' + esc(wiki) + '">Read Wiki Page</a>';
}
function buildLinksHTML(person, style) {
// style: 'links' (Directory & Accordion) or 'chips' (Chips view)
var name = fullName(person);
var video = splitLinksCSV(person[COL.video]);
var audio = splitLinksCSV(person[COL.audio]);
var transcript = splitLinksCSV(person[COL.transcript]);
var out = [];
function add(kind, urls) {
for (var i = 0; i < urls.length; i++) {
var label = buildAccessLabel(name, kind, i, urls.length);
if (style === 'chips') {
var chipText = buildChipText(kind, i, urls.length);
out.push(
'<a class="ohr-chip" target="_blank" rel="noopener" href="' + esc(urls[i]) + '"' +
' title="' + esc(label) + '"' +
' aria-label="' + esc(label) + '"' +
'>' + esc(chipText) + '</a>'
);
} else {
out.push(
'<a class="ohr-link" target="_blank" rel="noopener" href="' + esc(urls[i]) + '">' +
esc(label) +
'</a>'
);
}
}
}
// Wiki link first (if present)
if (style === 'chips') {
var wikiChip = buildWikiLinkHTML(person, 'ohr-chip');
if (wikiChip) out.push(wikiChip);
} else {
var wikiLink = buildWikiLinkHTML(person, 'ohr-link');
if (wikiLink) out.push(wikiLink);
}
// Then identifiers (only render what exists)
if (video.length) add('video', video);
if (audio.length) add('audio', audio);
if (transcript.length) add('transcript', transcript);
if (!out.length) return '';
if (style === 'chips') return '<div class="ohr-chips">' + out.join('') + '</div>';
return '<div class="ohr-links">' + out.join('') + '</div>';
}
// ====== Views ======
function makeDirectoryItem(person) {
var summary = (person[COL.summary] || '').trim();
var div = document.createElement('div');
div.className = 'ohr-item';
div.innerHTML =
buildHeaderHTML(person) +
(summary ? '<p class="ohr-bio">' + esc(summary) + '</p>' : '') +
buildLinksHTML(person, 'links');
return div;
}
function makeAccordionItem(person, idx) {
var summary = (person[COL.summary] || '').trim();
var div = document.createElement('div');
div.className = 'ohr-item';
var panelId = 'ohr-panel-' + idx;
var btnId = 'ohr-btn-' + idx;
div.innerHTML =
'<div class="ohr-head">' +
'<div class="ohr-titleblock">' +
(function () {
var name = fullName(person);
var life = lifespan(person);
var rec = recordedLabel(person);
var metaBits = [];
if (life) metaBits.push('<span>' + esc(life) + '</span>');
if (rec) metaBits.push('<span>' + esc(rec) + '</span>');
var meta = metaBits.join('<span class="ohr-dot">·</span>');
return (
'<h3 class="ohr-name">' + esc(name) + '</h3>' +
(meta ? '<div class="ohr-meta">' + meta + '</div>' : '')
);
})() +
'</div>' +
'<button class="ohr-acc-btn" id="' + btnId + '" type="button" aria-expanded="false" aria-controls="' + panelId + '">Details</button>' +
'</div>' +
'<div class="ohr-acc-panel ohr-hidden" id="' + panelId + '">' +
(summary ? '<div class="ohr-divider"><p class="ohr-bio" style="margin:0">' + esc(summary) + '</p></div>' : '') +
'<div class="ohr-divider">' + buildLinksHTML(person, 'links') + '</div>' +
'</div>';
var btn = div.querySelector('#' + btnId);
var panel = div.querySelector('#' + panelId);
if (btn && panel) {
btn.addEventListener('click', function () {
var open = !panel.classList.contains('ohr-hidden');
if (open) {
panel.classList.add('ohr-hidden');
btn.setAttribute('aria-expanded', 'false');
btn.textContent = 'Details';
} else {
panel.classList.remove('ohr-hidden');
btn.setAttribute('aria-expanded', 'true');
btn.textContent = 'Hide';
}
});
}
return div;
}
function makeChipsItem(person) {
var summary = (person[COL.summary] || '').trim();
var div = document.createElement('div');
div.className = 'ohr-item';
div.innerHTML =
buildHeaderHTML(person) +
(summary ? '<p class="ohr-bio">' + esc(summary) + '</p>' : '') +
buildLinksHTML(person, 'chips');
return div;
}
function renderList(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 frag = document.createDocumentFragment();
for (var i = 0; i < rows.length; i++) {
if (mode === 'accordion') frag.appendChild(makeAccordionItem(rows[i], i));
else if (mode === 'chips') frag.appendChild(makeChipsItem(rows[i]));
else frag.appendChild(makeDirectoryItem(rows[i]));
}
container.innerHTML = '';
container.appendChild(frag);
}
function updateCount(n, total) {
var el = $('ohr-count'); if (!el) return;
el.textContent = (n === total) ? (total + ' records') : (n + ' of ' + total + ' records');
}
// ====== Filtering ======
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] || '') + ' ' +
(row[COL.birth] || '') + ' ' +
(row[COL.death] || '') + ' ' +
(row[COL.dateFrom] || '') + ' ' +
(row[COL.dateTo] || '')
);
return hay.indexOf(q) !== -1;
});
filtered.sort(function (a, b) {
var A = norm((a[COL.last] || '') + ' ' + (a[COL.first] || ''));
var B = norm((b[COL.last] || '') + ' ' + (b[COL.first] || ''));
return A.localeCompare(B);
});
renderList(filtered);
updateCount(filtered.length, ALL.length);
}
function attachHandlers() {
var qInput = $('ohr-search');
if (qInput) {
qInput.addEventListener('input', debounce(function () {
FILTER.q = qInput.value.trim();
applyFilters();
}, 200));
}
}
// ====== Errors ======
function displayError(message) {
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" style="margin-top:0.5rem">' + esc(message) + '</p>';
}
}
// ====== Main ======
function fetchAndRender() {
var root = $('ohr-directory');
var loading = $('ohr-loading');
var container = $('ohr-list');
if (!root || !container) 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);
if (loading) loading.classList.add('ohr-hidden');
attachHandlers();
applyFilters();
})
.catch(function (err) {
displayError('There was an error accessing the records. Details: ' + err.message);
console.error(err);
});
}
onReady(function () {
if ($('ohr-directory')) {
mw.loader.using([]).then(fetchAndRender);
}
});
})();