MediaWiki:Gadget-OralHistoryRecords-Updated.js: Difference between revisions
Appearance
No edit summary |
No edit summary |
||
| Line 4: | Line 4: | ||
'use strict'; | 'use strict'; | ||
var COL = { | var COL = { | ||
transcript: 'Transcript Identifier', | transcript: 'Transcript Identifier', | ||
| Line 20: | Line 18: | ||
}; | }; | ||
var ALL = []; | var ALL = []; | ||
var FILTER = { q: '' }; | var FILTER = { q: '' }; | ||
function $(id) { return document.getElementById(id); } | function $(id) { return document.getElementById(id); } | ||
| Line 50: | Line 46: | ||
var t; | var t; | ||
return function () { | return function () { | ||
var a = arguments | var a = arguments; | ||
clearTimeout(t); | clearTimeout(t); | ||
t = setTimeout(function () { fn.apply( | t = setTimeout(function () { fn.apply(null, a); }, ms); | ||
}; | }; | ||
} | } | ||
function getViewMode() { | function getViewMode() { | ||
var root = $('ohr-directory'); | var root = $('ohr-directory'); | ||
| Line 64: | Line 59: | ||
} | } | ||
function getCsvUrl() { | function getCsvUrl() { | ||
var root = $('ohr-directory'); | var root = $('ohr-directory'); | ||
var override = root ? (root.getAttribute('data-csv') || '').trim() : ''; | var override = root ? (root.getAttribute('data-csv') || '').trim() : ''; | ||
var base = override || ((mw.config.get('wgScript') || '/index.php') + '/Special:FilePath/OHData.csv'); | var base = override || ((mw.config.get('wgScript') || '/index.php') + '/Special:FilePath/OHData.csv'); | ||
return base + (base.indexOf('?') === -1 ? '?' : '&') + 'cb=' + Date.now(); | return base + (base.indexOf('?') === -1 ? '?' : '&') + 'cb=' + Date.now(); | ||
} | } | ||
| Line 78: | Line 68: | ||
function toYear(value) { | function toYear(value) { | ||
if (!value) return ''; | if (!value) return ''; | ||
var | var m = String(value).match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/); | ||
return m ? m[1] : ''; | return m ? m[1] : ''; | ||
} | } | ||
| Line 94: | Line 82: | ||
function splitLinksCSV(value) { | function splitLinksCSV(value) { | ||
if (!value) return []; | if (!value) return []; | ||
return String(value) | return String(value).split(',').map(function (x) { return x.trim(); }).filter(Boolean); | ||
} | } | ||
// NEW: Only valid if BOTH first and last exist | |||
function fullName(person) { | function fullName(person) { | ||
var first = (person[COL.first] || '').trim(); | var first = (person[COL.first] || '').trim(); | ||
var last = (person[COL.last] || '').trim(); | var last = (person[COL.last] || '').trim(); | ||
return | if (!first || !last) return ''; | ||
return first + ' ' + last; | |||
} | } | ||
| Line 115: | Line 101: | ||
if (!b && d) return '–' + d; | if (!b && d) return '–' + d; | ||
return ''; | 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) { | function buildAccessLabel(name, kind, idx, total) { | ||
var part = | 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) { | 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); | |||
return | |||
} | } | ||
function buildToolbar(root) { | function buildToolbar(root) { | ||
var | var bar = document.createElement('div'); | ||
bar.className = 'ohr-toolbar'; | |||
var search = document.createElement('input'); | var search = document.createElement('input'); | ||
| Line 138: | Line 128: | ||
search.type = 'search'; | search.type = 'search'; | ||
search.placeholder = 'Search names, summaries…'; | search.placeholder = 'Search names, summaries…'; | ||
var count = document.createElement('div'); | var count = document.createElement('div'); | ||
count.id = 'ohr-count'; | count.id = 'ohr-count'; | ||
count.className = 'ohr-count'; | count.className = 'ohr-count'; | ||
bar.appendChild(search); | |||
bar.appendChild(count); | |||
root.insertBefore( | root.insertBefore(bar, root.firstChild); | ||
} | } | ||
// | // ROBUST CSV PARSER | ||
function parseCSV(text) { | function parseCSV(text) { | ||
text = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | |||
text = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | |||
var rows = []; | var rows = []; | ||
var row = []; | var row = []; | ||
var field = ''; | var field = ''; | ||
var inQuotes = false; | var inQuotes = false; | ||
for (var i = 0; i < text.length; i++) { | |||
var c = text[i]; | var c = text[i]; | ||
if (inQuotes) { | if (inQuotes) { | ||
if (c === '"') { | if (c === '"') { | ||
if (text[i + 1] === '"') { | |||
if ( | |||
field += '"'; | field += '"'; | ||
i + | i++; | ||
} else { | |||
inQuotes = false; | |||
} | } | ||
} else { | |||
inQuotes = | 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); | row.push(field); | ||
if (row.length) rows.push(row); | |||
if (row.length | |||
if (!rows.length) return []; | if (!rows.length) return []; | ||
var headers = rows[0] | var headers = rows[0]; | ||
var out = []; | |||
for (var r = 1; r < rows.length; r++) { | for (var r = 1; r < rows.length; r++) { | ||
var values = rows[r]; | var values = rows[r]; | ||
var obj = {}; | var obj = {}; | ||
for (var c2 = 0; c2 < headers.length; c2++) { | for (var c2 = 0; c2 < headers.length; c2++) { | ||
obj[headers[c2]] = values[c2] || ''; | |||
} | } | ||
out.push(obj); | out.push(obj); | ||
} | } | ||
return out; | return out; | ||
} | } | ||
function render(rows) { | |||
function | |||
var container = $('ohr-list'); | var container = $('ohr-list'); | ||
if (!container) return; | if (!container) return; | ||
| Line 408: | Line 207: | ||
var mode = getViewMode(); | var mode = getViewMode(); | ||
var | 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() { | function applyFilters() { | ||
var q = norm(FILTER.q); | var q = norm(FILTER.q); | ||
var filtered = ALL.filter(function (row) { | var filtered = ALL.filter(function (row) { | ||
if (!fullName(row)) return false; // remove nameless records entirely | |||
if (!q) return true; | if (!q) return true; | ||
var hay = norm( | var hay = norm( | ||
(row[COL.first] || '') + ' ' + | (row[COL.first] || '') + ' ' + | ||
(row[COL.last] || '') + ' ' + | (row[COL.last] || '') + ' ' + | ||
(row[COL.summary | (row[COL.summary] || '') | ||
); | ); | ||
return hay.indexOf(q) !== -1; | return hay.indexOf(q) !== -1; | ||
}); | }); | ||
filtered | render(filtered); | ||
var count = $('ohr-count'); | |||
if (count) count.textContent = filtered.length + ' records'; | |||
} | } | ||
function attachHandlers() { | function attachHandlers() { | ||
var | var input = $('ohr-search'); | ||
if ( | if (input) { | ||
input.addEventListener('input', debounce(function () { | |||
FILTER.q = | FILTER.q = input.value.trim(); | ||
applyFilters(); | applyFilters(); | ||
}, 200)); | }, 200)); | ||
| Line 464: | Line 268: | ||
} | } | ||
function displayError(msg) { | |||
function displayError( | |||
var loading = $('ohr-loading'); | var loading = $('ohr-loading'); | ||
var error = $('ohr-error'); | var error = $('ohr-error'); | ||
| Line 474: | Line 277: | ||
error.innerHTML = | error.innerHTML = | ||
'<p><strong>Failed to load records.</strong></p>' + | '<p><strong>Failed to load records.</strong></p>' + | ||
'<p class="ohr-muted | '<p class="ohr-muted">' + esc(msg) + '</p>'; | ||
} | } | ||
} | } | ||
function init() { | |||
function | |||
var root = $('ohr-directory'); | var root = $('ohr-directory'); | ||
if (!root) return; | |||
if (!root | |||
buildToolbar(root); | buildToolbar(root); | ||
| Line 494: | Line 294: | ||
.then(function (text) { | .then(function (text) { | ||
ALL = parseCSV(text); | ALL = parseCSV(text); | ||
$('ohr-loading').classList.add('ohr-hidden'); | |||
attachHandlers(); | attachHandlers(); | ||
applyFilters(); | applyFilters(); | ||
}) | }) | ||
.catch(function (err) { | .catch(function (err) { | ||
displayError( | displayError(err.message); | ||
console.error(err); | console.error(err); | ||
}); | }); | ||
} | } | ||
onReady( | onReady(init); | ||
})(); | })(); | ||
Revision as of 14:27, 21 February 2026
/* ===== 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);
})();