MediaWiki:Common.js

From Encyclopedium Universum
Revision as of 09:32, 24 May 2026 by Yehudhah (talk | contribs) (Finalize claim citations and grammar-entry popups)

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */
(function(){
  // Tanakh verse counts
  const verseCounts = {
    "Genesis":     [31,25,24,26,32,22,24,22,29,32,32,20,18,24,21,16,27,33,38,18,34,24,20,67,34,35,46,22,35,43,55,32,20,31,29,43,36,30,23,23,57,38,34,34,31,22,33,26,22,25],
    "Exodus":      [22,25,22,31,23,30,25,32,35,29,10,51,22,31,27,36,16,27,25,26,37,30,33,18,40,37,21,43,46,38,18,35,23,35,35,38,29,31,43,38],
    "Leviticus":   [17,16,17,35,19,30,38,36,24,20,47,8,59,57,33,34,16,30,37,27,24,33,44,23,55,46,34],
    "Numbers":     [54,34,51,49,31,27,89,26,23,36,35,16,33,45,41,50,13,32,22,29,35,41,30,25,18,65,23,31,40,16,54,42,56,29,34,13],
    "Deuteronomy": [46,37,29,49,33,25,26,20,29,22,32,32,18,29,23,22,20,22,21,20,23,30,25,22,19,19,26,68,29,20,30,52,29,12],
    // Add rest of Tanakh...
  };

  const title = mw.config.get('wgTitle'); // current wiki page title
  const match = title.match(/^(.*?) \((.*?)\)(?: (\d+))?$/);

  if (!match) return; // doesn't match expected format

  const translitName = match[1];
  const englishName  = match[2];
  const chapterNum   = match[3] ? parseInt(match[3], 10) : null;

  if (!verseCounts[englishName]) return; // no data for this book

  const container = document.getElementById('tanakh-selector');
  if (!container) return;

  const chapters = verseCounts[englishName];

  // Title
  const h2 = document.createElement('h2');
  h2.textContent = `${translitName} (${englishName}) — Select Chapter & Verse`;
  container.appendChild(h2);

  // Chapter buttons grid
  const chaptersDiv = document.createElement('div');
  chaptersDiv.style.display = 'grid';
  chaptersDiv.style.gridTemplateColumns = 'repeat(auto-fit, minmax(72px, 1fr))';
  chaptersDiv.style.gap = '8px';
  container.appendChild(chaptersDiv);

  // Verse panel
  const versesPanel = document.createElement('div');
  versesPanel.style.display = 'none';
  versesPanel.style.marginTop = '12px';
  container.appendChild(versesPanel);

  const versesHeader = document.createElement('div');
  versesHeader.style.display = 'flex';
  versesHeader.style.justifyContent = 'space-between';
  versesHeader.style.alignItems = 'center';
  versesPanel.appendChild(versesHeader);

  const currentChLabel = document.createElement('strong');
  versesHeader.appendChild(currentChLabel);

  const closeBtn = document.createElement('button');
  closeBtn.textContent = 'Close';
  closeBtn.style.cssText = 'background:#eee;border:1px solid #ccc;padding:4px 8px;border-radius:6px;cursor:pointer;';
  versesHeader.appendChild(closeBtn);

  const versesDiv = document.createElement('div');
  versesDiv.style.display = 'flex';
  versesDiv.style.flexWrap = 'wrap';
  versesDiv.style.gap = '6px';
  versesDiv.style.marginTop = '8px';
  versesPanel.appendChild(versesDiv);

  // Function to open chapter
  function openChapter(chNum) {
    const verseCount = chapters[chNum - 1];
    currentChLabel.textContent = `Chapter ${chNum}${verseCount} verses`;
    versesDiv.innerHTML = '';

    for (let v = 1; v <= verseCount; v++) {
      const a = document.createElement('a');
      const pageName = `${translitName} (${englishName}) ${chNum}`.replace(/ /g, '_');
      a.href = `/wiki/${pageName}#v${v}`;
      a.textContent = v;
      a.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;padding:6px 8px;border-radius:6px;border:1px solid #e0e0e0;text-decoration:none;min-width:36px;text-align:center;';
      versesDiv.appendChild(a);
    }

    versesPanel.style.display = 'block';
    versesPanel.scrollIntoView({behavior:'smooth', block:'nearest'});
  }

  // Build chapter buttons
  chapters.forEach((verseCount, i) => {
    const chNum = i + 1;
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = chNum;
    btn.style.cssText = 'padding:10px 8px;border-radius:8px;border:1px solid #d0d0d0;background:#fff;cursor:pointer;';
    btn.addEventListener('click', () => openChapter(chNum));
    chaptersDiv.appendChild(btn);
  });

  closeBtn.addEventListener('click', () => {
    versesPanel.style.display = 'none';
  });

  // Auto-open chapter if we are on a chapter page
  if (chapterNum) {
    openChapter(chapterNum);
  }

})();

/* UKOY Torah citation popups: Chicago-note references backed by Pealim Tanakh.v2++ and KJV++ exports. */
(function () {
  const DATA_URL = "/resources/ukoy/verse-citations.json";
  let dataPromise = null;
  let modal = null;

  function fetchData() {
    if (!dataPromise) {
      dataPromise = fetch(DATA_URL, { credentials: "same-origin" }).then(function (response) {
        if (!response.ok) throw new Error("Unable to load citation data");
        return response.json();
      });
    }
    return dataPromise;
  }

  function ensureModal() {
    if (modal) return modal;
    modal = document.createElement("div");
    modal.className = "ukoy-verse-modal";
    modal.innerHTML = "<div class='ukoy-verse-modal-card' role='dialog' aria-modal='true' aria-label='Torah citation'><button type='button' class='ukoy-verse-modal-close' aria-label='Close'>×</button><div class='ukoy-verse-modal-body'></div></div>";
    document.body.appendChild(modal);
    modal.addEventListener("click", function (event) {
      if (event.target === modal || event.target.classList.contains("ukoy-verse-modal-close")) closeModal();
    });
    document.addEventListener("keydown", function (event) { if (event.key === "Escape") closeModal(); });
    return modal;
  }

  function closeModal() { if (modal) modal.classList.remove("is-open"); }

  function escapeHtml(value) {
    return String(value || "").replace(/[&<>'"]/g, function (char) {
      return { "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" }[char];
    });
  }

  function showModal(html) {
    const m = ensureModal();
    m.querySelector(".ukoy-verse-modal-body").innerHTML = html;
    m.classList.add("is-open");
  }

  function renderCitation(entry) {
    if (!entry) return "<div class='ukoy-verse-error'>Citation data not found.</div>";
    const verses = entry.verses || [];
    return "<div class='ukoy-verse-heading'>" + escapeHtml(entry.label) + "</div>" +
      verses.map(function (verse) {
        return "<section class='ukoy-verse-entry'>" +
          "<div class='ukoy-verse-ref'>" + escapeHtml(verse.reference) + "</div>" +
          "<div class='ukoy-verse-label'>Tanakh.v2++ interlinear</div>" +
          (verse.tanakhInterlinearHtml || "<div class='ukoy-missing'>Tanakh.v2++ verse not available.</div>") +
          "<div class='ukoy-verse-label'>KJV++ plain English rendering</div>" +
          "<div class='ukoy-kjv-text'>" + escapeHtml(verse.kjvText || "") + "</div>" +
        "</section>";
      }).join("");
  }

  function renderEntry(kind, key, entry) {
    const label = kind === "strong" ? "Strong's / BDB Lexicon" : "Morphology";
    if (!entry) {
      return "<div class='ukoy-verse-heading'>" + escapeHtml(label + " " + key) + "</div><div class='ukoy-verse-error'>Entry not found in the loaded Pealim data.</div>";
    }
    let body = entry.html || entry.definitionHtml || entry.content || "";
    body = body.replace(/<style[\s\S]*?<\/style>/gi, "");
    return "<div class='ukoy-verse-heading'>" + escapeHtml(label + " " + key) + "</div>" +
      "<section class='ukoy-verse-entry ukoy-entry-popup'><div class='ukoy-entry-html'>" + body + "</div></section>";
  }

  function openCitation(ref) {
    showModal("<div class='ukoy-loading'>Loading citation…</div>");
    fetchData()
      .then(function (data) {
        const citations = data.citations || data;
        showModal(renderCitation(citations[ref]));
      })
      .catch(function (error) { showModal("<div class='ukoy-verse-error'>" + escapeHtml(error.message) + "</div>"); });
  }

  function openEntry(kind, key) {
    showModal("<div class='ukoy-loading'>Loading entry…</div>");
    fetchData()
      .then(function (data) {
        const entries = (data.entries && data.entries[kind]) || {};
        showModal(renderEntry(kind, key, entries[key]));
      })
      .catch(function (error) { showModal("<div class='ukoy-verse-error'>" + escapeHtml(error.message) + "</div>"); });
  }

  document.addEventListener("click", function (event) {
    const citation = event.target.closest(".ukoy-cite");
    if (citation) {
      event.preventDefault();
      openCitation(citation.getAttribute("data-ref"));
      return;
    }
    const strong = event.target.closest(".ukoy-token-link.ukoy-strong");
    if (strong) {
      event.preventDefault();
      event.stopPropagation();
      openEntry("strong", strong.getAttribute("data-key"));
      return;
    }
    const morph = event.target.closest(".ukoy-token-link.ukoy-morph");
    if (morph) {
      event.preventDefault();
      event.stopPropagation();
      openEntry("morph", morph.getAttribute("data-key"));
    }
  });
})();


/* UKOY final citation + grammar override: data-ref superscripts, stacked popups, grammar-book links. */
(function () {
  const VERSE_URL = "/resources/ukoy/verse-citations.json";
  const GRAMMAR_URL = "/resources/ukoy/grammar-entries.json";
  let verseDataPromise = null;
  let grammarDataPromise = null;
  let overlay = null;
  const stack = [];

  function esc(v) {
    return String(v || "").replace(/[&<>"']/g, function (c) {
      return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
    });
  }
  function verseData() {
    if (!verseDataPromise) verseDataPromise = fetch(VERSE_URL, { credentials: "same-origin" }).then(function (r) { if (!r.ok) throw new Error("Unable to load verse data"); return r.json(); });
    return verseDataPromise;
  }
  function grammarData() {
    if (!grammarDataPromise) grammarDataPromise = fetch(GRAMMAR_URL, { credentials: "same-origin" }).then(function (r) { if (!r.ok) throw new Error("Unable to load grammar data"); return r.json(); });
    return grammarDataPromise;
  }
  function ensureOverlay() {
    if (overlay) return overlay;
    overlay = document.createElement("div");
    overlay.className = "ukoy-modal-stack-overlay";
    document.body.appendChild(overlay);
    overlay.addEventListener("click", function (e) { if (e.target === overlay) closeTop(); });
    document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeTop(); });
    return overlay;
  }
  function lockScroll() { document.documentElement.classList.add("ukoy-modal-open"); document.body.classList.add("ukoy-modal-open"); }
  function unlockIfEmpty() { if (!stack.length) { document.documentElement.classList.remove("ukoy-modal-open"); document.body.classList.remove("ukoy-modal-open"); } }
  function closeTop() {
    const top = stack.pop();
    if (top && top.parentNode) top.parentNode.removeChild(top);
    if (stack.length) stack[stack.length - 1].classList.add("is-top");
    else if (overlay) overlay.classList.remove("is-open");
    unlockIfEmpty();
  }
  function openLayer(html) {
    const ov = ensureOverlay();
    stack.forEach(function (c) { c.classList.remove("is-top"); });
    const card = document.createElement("div");
    card.className = "ukoy-verse-modal-card is-top";
    card.style.zIndex = String(100000 + stack.length + 1);
    card.innerHTML = "<button type='button' class='ukoy-verse-modal-close' aria-label='Close'>×</button><div class='ukoy-verse-modal-body'></div>";
    card.querySelector(".ukoy-verse-modal-body").innerHTML = html;
    card.querySelector(".ukoy-verse-modal-close").addEventListener("click", function (e) { e.preventDefault(); closeTop(); });
    ov.appendChild(card);
    ov.classList.add("is-open");
    stack.push(card);
    lockScroll();
    return card;
  }
  function replaceTop(html) { if (!stack.length) return openLayer(html); stack[stack.length - 1].querySelector(".ukoy-verse-modal-body").innerHTML = html; }
  function stripOccurrences(html) {
    html = String(html || "").replace(/<style[\s\S]*?<\/style>/gi, "");
    return html.replace(/<p><strong>Total KJV Occurrences:[\s\S]*$/i, "").replace(/<strong>Total KJV Occurrences:[\s\S]*$/i, "").replace(/Total KJV Occurrences:[\s\S]*$/i, "");
  }
  function renderCitation(entry) {
    if (!entry) return "<div class='ukoy-verse-error'>Citation data not found.</div>";
    return "<div class='ukoy-verse-heading'>" + esc(entry.label) + "</div>" + (entry.verses || []).map(function (verse) {
      return "<section class='ukoy-verse-entry'><div class='ukoy-verse-ref'>" + esc(verse.reference) + "</div>" +
        "<div class='ukoy-verse-label'>Tanakh.v2++ interlinear</div>" + (verse.tanakhInterlinearHtml || "") +
        "<div class='ukoy-verse-label'>KJV++ plain English rendering</div><div class='ukoy-kjv-text'>" + esc(verse.kjvText || "") + "</div></section>";
    }).join("");
  }
  function refsFromNote(note) {
    if (note.dataset.refs) return note.dataset.refs.split(";").filter(Boolean);
    const n = (note.id || "").replace("ukoy-note-ref-", "");
    const box = document.getElementById("ukoy-note-" + n);
    return box ? Array.from(box.querySelectorAll(".ukoy-cite")).map(function (x) { return x.getAttribute("data-ref"); }).filter(Boolean) : [];
  }
  function openRefs(refs) {
    openLayer("<div class='ukoy-loading'>Loading citation…</div>");
    verseData().then(function (data) {
      const citations = data.citations || data;
      replaceTop(refs.map(function (r) { return renderCitation(citations[r]); }).join("<hr class='ukoy-citation-separator'>"));
    }).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); });
  }
  function tokenContext(button) {
    const token = button.closest(".ukoy-vtok");
    const verse = button.closest(".ukoy-verse-entry");
    return {
      surface: token && token.querySelector(".ukoy-vtok-surface") ? token.querySelector(".ukoy-vtok-surface").textContent : "",
      strong: token && token.querySelector(".ukoy-strong") ? token.querySelector(".ukoy-strong").getAttribute("data-key") : "",
      morph: Array.from(token ? token.querySelectorAll(".ukoy-morph") : []).map(function (m) { return m.textContent; }).join(" "),
      ref: verse && verse.querySelector(".ukoy-verse-ref") ? verse.querySelector(".ukoy-verse-ref").textContent : ""
    };
  }
  function renderLex(kind, key, entry, ctx) {
    const title = kind === "strong" ? "Strong's / BDB Lexicon" : "Morphology";
    const context = "<div class='ukoy-token-context'>" +
      (ctx.surface ? "<div><span>Word</span> <b class='ukoy-context-surface'>" + esc(ctx.surface) + "</b></div>" : "") +
      (ctx.ref ? "<div><span>Verse</span> " + esc(ctx.ref) + "</div>" : "") +
      (ctx.strong ? "<div><span>Strong</span> " + esc(ctx.strong) + "</div>" : "") +
      (ctx.morph ? "<div><span>Morphology</span> " + esc(ctx.morph) + "</div>" : "") + "</div>";
    if (!entry) return "<div class='ukoy-verse-heading'>" + esc(title + " " + key) + "</div>" + context + "<div class='ukoy-verse-error'>Entry not found.</div>";
    const raw = entry.html || entry.definitionHtml || entry.content || "";
    const trimmed = stripOccurrences(raw);
    return "<div class='ukoy-verse-heading'>" + esc(title + " " + key) + "</div>" + context + "<section class='ukoy-verse-entry ukoy-entry-popup'><div class='ukoy-entry-html'>" + trimmed + "</div></section>" + (raw !== trimmed ? "<details class='ukoy-see-all-entry'><summary>See full lexicon detail</summary><div class='ukoy-entry-html'>" + raw + "</div></details>" : "");
  }
  function openLex(kind, key, ctx) {
    openLayer("<div class='ukoy-loading'>Loading entry…</div>");
    verseData().then(function (data) {
      const entries = (data.entries && data.entries[kind]) || {};
      replaceTop(renderLex(kind, key, entries[key], ctx || {}));
    }).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); });
  }
  function grammarKeys(href, label) {
    let key = decodeURIComponent(String(href || "").replace(/_/g, " ").trim());
    const txt = String(label || "").trim();
    const title = key.indexOf("k-HebrewGrammar ") === 0 ? key.replace("k-HebrewGrammar ", "") : (key.indexOf("k ") === 0 ? key.slice(2) : txt);
    const cap = title.replace(/\b\w/g, function (m) { return m.toUpperCase(); });
    return [key, key.toLowerCase(), "k " + title.toLowerCase(), "k-HebrewGrammar " + cap, cap, txt];
  }
  function openGrammar(href, label) {
    const keys = grammarKeys(href, label);
    openLayer("<div class='ukoy-loading'>Loading grammar entry…</div>");
    grammarData().then(function (data) {
      const entries = data.entries || {};
      let entry = null;
      for (const k of keys) { if (entries[k]) { entry = entries[k]; break; } }
      if (!entry) return replaceTop("<div class='ukoy-verse-heading'>Grammar Entry</div><div class='ukoy-verse-error'>No grammar entry found for " + esc(keys[0]) + ".</div>");
      replaceTop("<div class='ukoy-verse-heading'>" + esc(entry.title) + "</div><section class='ukoy-verse-entry ukoy-entry-popup ukoy-grammar-entry'><div class='ukoy-entry-html'>" + (entry.html || "") + "</div></section>");
    }).catch(function (e) { replaceTop("<div class='ukoy-verse-error'>" + esc(e.message) + "</div>"); });
  }

  document.addEventListener("click", function (event) {
    const grammar = event.target.closest(".ukoy-entry-html a[href^='k'], .ukoy-entry-html a[href^='k-HebrewGrammar']");
    if (grammar) { event.preventDefault(); event.stopImmediatePropagation(); openGrammar(grammar.getAttribute("href"), grammar.textContent); return; }
    const note = event.target.closest(".ukoy-cite-note");
    if (note) { event.preventDefault(); event.stopImmediatePropagation(); const refs = refsFromNote(note); if (refs.length) openRefs(refs); return; }
    const citation = event.target.closest(".ukoy-cite");
    if (citation) { event.preventDefault(); event.stopImmediatePropagation(); openRefs([citation.getAttribute("data-ref")]); return; }
    const strong = event.target.closest(".ukoy-token-link.ukoy-strong");
    if (strong) { event.preventDefault(); event.stopImmediatePropagation(); openLex("strong", strong.getAttribute("data-key"), tokenContext(strong)); return; }
    const morph = event.target.closest(".ukoy-token-link.ukoy-morph");
    if (morph) { event.preventDefault(); event.stopImmediatePropagation(); openLex("morph", morph.getAttribute("data-key"), tokenContext(morph)); }
  }, true);
})();