MediaWiki:Gadget-OralHistoryRecords-Updated.js: Difference between revisions
Appearance
No edit summary |
No edit summary |
||
| Line 4: | Line 4: | ||
'use strict'; | 'use strict'; | ||
// ====== CONFIG ====== | // ====== COLUMN CONFIG ====== | ||
var COL = { | var COL = { | ||
transcript: 'Transcript Identifier', | transcript: 'Transcript Identifier', | ||
| Line 24: | Line 23: | ||
var FILTER = { q: '' }; | var FILTER = { q: '' }; | ||
// ====== | // ====== HELPERS ====== | ||
function $(id) { return document.getElementById(id); } | function $(id) { return document.getElementById(id); } | ||
function esc(s) { | function esc(s) { | ||
s = (s == null ? '' : String(s)); | s = (s == null ? '' : String(s)); | ||
| Line 36: | Line 32: | ||
}); | }); | ||
} | } | ||
function norm(s) { | function norm(s) { | ||
return (s || '') | return (s || '') | ||
| Line 43: | Line 40: | ||
.toLowerCase(); | .toLowerCase(); | ||
} | } | ||
function debounce(fn, ms) { | function debounce(fn, ms) { | ||
var t; | var t; | ||
return function () { | return function () { | ||
var | var args = arguments; | ||
clearTimeout(t); | clearTimeout(t); | ||
t = setTimeout(function () { fn.apply( | t = setTimeout(function () { fn.apply(null, args); }, ms); | ||
}; | }; | ||
} | } | ||
function getViewMode() { | function getViewMode() { | ||
var root = $('ohr-directory'); | var root = $('ohr-directory'); | ||
var mode = ( | var mode = root ? (root.getAttribute('data-view') || '').trim().toLowerCase() : ''; | ||
if (mode === 'accordion' || mode === 'chips' || mode === 'directory') return mode; | if (mode === 'accordion' || mode === 'chips' || mode === 'directory') return mode; | ||
return 'directory'; | return 'directory'; | ||
} | } | ||
// | // ====== CSV URL (SAFE + FLEXIBLE) ====== | ||
function getCsvUrl() { | 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) { | 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 82: | Line 86: | ||
} | } | ||
function | function splitLinks(value) { | ||
if (!value) return []; | if (!value) return []; | ||
return String(value) | return String(value).split(',').map(function (x) { return x.trim(); }).filter(Boolean); | ||
} | } | ||
function fullName( | function fullName(p) { | ||
return ((p[COL.first] || '') + ' ' + (p[COL.last] || '')).trim() || 'Untitled'; | |||
} | } | ||
function lifespan( | function lifespan(p) { | ||
var b = | var b = p[COL.birth] || ''; | ||
var d = | var d = p[COL.death] || ''; | ||
if (b && d) return b + '–' + d; | if (b && d) return b + '–' + d; | ||
if (b && !d) return b + '–'; | if (b && !d) return b + '–'; | ||
| Line 106: | Line 104: | ||
} | } | ||
function | function accessLabel(name, type, i, total) { | ||
var part = | var part = total > 1 ? ' (Part ' + (i + 1) + '/' + total + ')' : ''; | ||
return 'Access ' + name + '’s ' + type.charAt(0).toUpperCase() + type.slice(1) + part; | |||
} | } | ||
function | 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); | |||
return | |||
} | } | ||
// ====== UI ====== | // ====== UI ====== | ||
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 127: | Line 123: | ||
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); | ||
} | } | ||
function parseCSV(text) { | |||
function parseCSV( | var lines = text.trim().split(/\r?\n/); | ||
var lines = | |||
var headers = lines[0].split(',').map(function (h) { return h.trim(); }); | var headers = lines[0].split(',').map(function (h) { return h.trim(); }); | ||
return lines.slice(1).map(function (line) { | return lines.slice(1).map(function (line) { | ||
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 156: | Line 147: | ||
} | } | ||
function buildHeader(p) { | |||
function | var meta = []; | ||
var | var life = lifespan(p); | ||
var life = lifespan( | var rec = recordedLabel(p); | ||
var rec = recordedLabel( | 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>'); | |||
if (life) | |||
if (rec) | |||
var meta | |||
return ( | return ( | ||
'<div class="ohr-head">' + | '<div class="ohr-head">' + | ||
'<div class="ohr-titleblock">' + | '<div class="ohr-titleblock">' + | ||
'<h3 class="ohr-name">' + esc( | '<h3 class="ohr-name">' + esc(fullName(p)) + '</h3>' + | ||
( | (metaHTML ? '<div class="ohr-meta">' + metaHTML + '</div>' : '') + | ||
'</div>' + | '</div>' + | ||
'</div>' | '</div>' | ||
| Line 178: | Line 165: | ||
} | } | ||
function | function buildLinks(p, style) { | ||
var name = fullName(p); | |||
var name = fullName( | |||
var out = []; | var out = []; | ||
function add( | function add(type, urls) { | ||
urls.forEach(function (url, i) { | |||
var label = | var label = accessLabel(name, type, i, urls.length); | ||
if (style === 'chips') { | if (style === 'chips') { | ||
out.push( | out.push( | ||
'<a class="ohr-chip" target="_blank" rel="noopener" href="' + esc( | '<a class="ohr-chip" target="_blank" rel="noopener" href="' + esc(url) + | ||
'" title="' + esc(label) + '">' + | |||
esc(chipText(type, i, urls.length)) + | |||
'</a>' | |||
); | ); | ||
} else { | } else { | ||
out.push( | out.push( | ||
'<a class="ohr-link" target="_blank" rel="noopener" href="' + esc( | '<a class="ohr-link" target="_blank" rel="noopener" href="' + esc(url) + '">' + | ||
esc(label) + | |||
'</a>' | '</a>' | ||
); | ); | ||
} | } | ||
} | }); | ||
} | } | ||
var wiki = (p[COL.wiki] || '').trim(); | |||
if (style === 'chips' | 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 ''; | if (!out.length) return ''; | ||
return '<div class="' + (style === 'chips' ? 'ohr-chips' : 'ohr-links') + '">' + out.join('') + '</div>'; | |||
} | } | ||
function | function render(rows) { | ||
var container = $('ohr-list'); | var container = $('ohr-list'); | ||
if (!container) return; | if (!container) return; | ||
| Line 323: | Line 215: | ||
var mode = getViewMode(); | var mode = getViewMode(); | ||
var | 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 = | 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 (!q) return true; | if (!q) return true; | ||
| Line 349: | Line 234: | ||
(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; | ||
| Line 359: | Line 240: | ||
filtered.sort(function (a, b) { | 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() { | 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 378: | Line 258: | ||
} | } | ||
function displayError(msg) { | |||
function displayError( | |||
var loading = $('ohr-loading'); | var loading = $('ohr-loading'); | ||
var error = $('ohr-error'); | var error = $('ohr-error'); | ||
| Line 388: | Line 267: | ||
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 408: | Line 284: | ||
.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); | ||
}); | }); | ||
} | } | ||
if (document.readyState !== 'loading') init(); | |||
else document.addEventListener('DOMContentLoaded', init); | |||
})(); | })(); | ||
Revision as of 14:11, 21 February 2026
/* ===== 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 lines = text.trim().split(/\r?\n/);
var headers = lines[0].split(',').map(function (h) { return h.trim(); });
return lines.slice(1).map(function (line) {
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);
}
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);
})();