Jump to content

MediaWiki:Gadget-OralHistoryRecords-Updated.js

Revision as of 14:27, 21 February 2026 by Hthach (talk | contribs)

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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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);

})();