MediaWiki: Common.js: Difference between revisions

From Psalms: Layer by Layer
Jump to: navigation, search
mNo edit summary
mNo edit summary
(161 intermediate revisions by 2 users not shown)
Line 1: Line 1:
importScript('MediaWiki:Overlays.js');
// importScript('MediaWiki:Lineation.js');
importScript('MediaWiki:AutoLoad.js');
importScript('MediaWiki:Compare.js');
importScript('MediaWiki:json.js');
var debug = false;
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */
function currentChapter(){
// Function to check if all <pre class="mermaid"> elements are processed
    var pageName = mw.config.get("wgPageName"); // e.g., Psalms/Psalm_18/alignment/1-20
function checkMermaidProcessed() {
 
    var mermaidPreElements = document.querySelectorAll('pre.mermaid');
    var chapter = "";
    var allProcessed = true;
    var verseRange = "";
 
    mermaidPreElements.forEach(function (element) {
    // Extract the chapter number
        if (!element.hasAttribute('data-processed') || element.getAttribute('data-processed') !== 'true') {
    var chapterMatch = pageName.match(/Psalm_(\d+)/);
            allProcessed = false;
    if (chapterMatch) {
        }
      return chapterMatch[1];
    });
    }
 
    return allProcessed;
    if (!chapter) {
}
      //console.error("Could not detect Psalm chapter from page name:", pageName);
      return;
// Function to wait until all Mermaid diagrams are processed
    }
function waitForMermaidProcessing(callback) {
}
    var interval = setInterval(function () {
 
        if (checkMermaidProcessed()) {
function setupExpandablePreviews() {
            clearInterval(interval);
  var boxes = document.querySelectorAll(".preview-box");
            callback(); // Once all elements are processed, run the callback
 
        }
  for (var i = 0; i < boxes.length; i++) {
    }, 100); // Check every 100ms
    (function(box) {
}
      box.classList.add("collapsed");
 
      var link = document.createElement("a");
      link.className = "preview-toggle-link";
      link.textContent = "Show more";
 
      link.addEventListener("click", function(e) {
        e.preventDefault();
        var isCollapsed = box.classList.contains("collapsed");
        box.classList.toggle("collapsed", !isCollapsed);
        box.classList.toggle("expanded", isCollapsed);
        link.textContent = isCollapsed ? "Show less" : "Show more";
      });
 
      // Insert the link right after the box
      if (box.nextSibling) {
        box.parentNode.insertBefore(link, box.nextSibling);
      } else {
        box.parentNode.appendChild(link);
      }
    })(boxes[i]);
  }
}
 
 
 
function toggleVisibility(containerId, className) {
function toggleVisibility(containerId, className) {
    //console.log("Toggling visibility for " + className + " in " + containerId);
    //console.log("Toggling visibility for " + className + " in " + containerId);
    var container = document.getElementById(containerId);
    var container = document.getElementById(containerId);
    var elements = null;
    if (container) {
    if (container) {
    if (className==="alternative"){
    if (className==="alternative"){
Line 33: Line 67:
// Match all elements with pinkish FILL or STROKE
// Match all elements with pinkish FILL or STROKE
var elements = container.querySelectorAll('[fill^="#f4"], [stroke^="#f4"], [fill^="#f6"], [stroke^="#f6"]');
elements = container.querySelectorAll('[fill^="#f4"], [stroke^="#f4"], [fill^="#f6"], [stroke^="#f6"]');
elements.forEach(function(el) {
elements.forEach(function(el) {
Line 48: Line 82:
    }else{
    }else{
        var elements = container.querySelectorAll("g." + className);
        elements = container.querySelectorAll("g." + className);
        for (var i = 0; i < elements.length; i++) {
        for (var i = 0; i < elements.length; i++) {
            var element = elements[i];
            var element = elements[i];
Line 71: Line 105:
    // If no toggle links are found, print a warning
    // If no toggle links are found, print a warning
    if (toggleLinks.length === 0) {
    if (toggleLinks.length === 0) {
        console.warn("No toggle links found on the page.");
        //console.warn("No toggle links found on the page.");
    }
    }
     
     
Line 87: Line 121:


// ===========================


// Mermaid code that, if stored elsewhere, doesn't load in time
 


// ===========================
// ===========================
$(document).ready(function () {
// ===========================
// Function to check if all <pre class="mermaid"> elements are processed
// Begin placeholder auto-loading
function checkMermaidProcessed() {
// ===========================
    var mermaidPreElements = document.querySelectorAll('pre.mermaid');
 
    var allProcessed = true;
 
 
    mermaidPreElements.forEach(function (element) {
var observer = new IntersectionObserver(function(entries) {
        if (!element.hasAttribute('data-processed') || element.getAttribute('data-processed') !== 'true') {
    entries.forEach(function(entry) {
            allProcessed = false;
        if (entry.isIntersecting) {
            var target = entry.target;
            if (!target.dataset.loaded) { // Don't double-load
                loadChunk(target);
            }
        }
        }
    });
    });
}, {
    rootMargin: '200px'
});
document.querySelectorAll('.placeholder').forEach(function(placeholder) {
if (!allProcessed)
    observer.observe(placeholder);
console.log("Mermaid items still remaining to be processed.");
});
    return allProcessed;
}
function loadChunk(placeholder) {
    var id = placeholder.id; // e.g., v-1


var basePage = mw.config.get('wgPageName').replace(/Test/g, "");
// Remove prefix (v, vv, verse) followed by any number of ".", "_", "-", or space
var cleanedId = id.replace(/^(vv|v|verse)[\.\_\-\s]*/i, '');
var nextPage = basePage + cleanedId;
console.log('Seeking to load ' + nextPage + ' (from original ' + id + ')');
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/w/' + nextPage + '?action=render', true);
    xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                placeholder.innerHTML = xhr.responseText;
                placeholder.dataset.loaded = 'true';


addHeadingsFromChunkToTOC(placeholder);
function createAutoDiagramLinks(){
 
// ===========================
            } else if (xhr.status === 404) {
// Auto-create links for notes such as "v. 2 preferred diagram"
                placeholder.innerHTML = '<div class="missing-chunk">This section is not yet available.</div>';
// ===========================
                placeholder.dataset.loaded = 'true'; // Prevent reloading attempts
            } else {
                placeholder.innerHTML = '<div class="load-error">Error loading section (status ' + xhr.status + ').</div>';
                placeholder.dataset.loaded = 'true';
            }
        }
 
    };
    xhr.send();
}
 
function addHeadingsFromChunkToTOC(chunkDiv) {
    if (!chunkDiv) return;
    var targetText = chunkDiv.id.replace(/_/,' ') + ' (loading...)';
    var allSpans = document.querySelectorAll('.toctext');
    var headingLinks = {};
    var parentTocSpan = null;
    for (var i = 0; i < allSpans.length; i++) {
        if (allSpans[i].textContent === targetText) {
            allSpans[i].textContent = chunkDiv.id.replace(/_/,' '); // Remove (loading...)
            parentTocSpan = allSpans[i];
            break;
        }
    }
    if (!parentTocSpan) {
if (document.getElementById("createDiagramLinks")) {
        console.warn('Parent TOC span not found for chunk:', chunkDiv.id);
    // 1. Build a mapping from "v. 2 preferred" ➔ "Preferred_2"
        return;
    var currentVerseRange = "";
    }
 
    var headings = document.querySelectorAll('h1, h2');
 
    for (var i = 0; i < headings.length; i++) {
// Clean up the corresponding H1 heading (remove " (loading...)" from text and ID)
        var heading = headings[i];
var originalHeadingId = chunkDiv.id + '_(loading...)';
        if (heading.tagName === 'H1') {
var h1span = document.getElementById(originalHeadingId);
            currentVerseRange = heading.innerText.trim().toLowerCase(); // e.g., "v. 2"
        }
        if (heading.tagName === 'H2') {
            var description = heading.innerText.trim();
            var combinedKey = (currentVerseRange + " " + description).toLowerCase(); // e.g., "v. 2 preferred"
if (h1span && h1span.classList.contains('mw-headline')) {
            // Ensure heading has a usable ID
    var cleanedId = chunkDiv.id;
            if (!heading.id) {
                heading.id = description.replace(/\s+/g, "_") + "_" + currentVerseRange.replace(/[^0-9]/g, "");
            }
    // Update ID
            headingLinks[combinedKey] = heading.id;
    h1span.id = cleanedId;
        }
    }
    // Update display text
    // 3. Apply to whole document
    h1span.textContent = cleanedId.replace(/_/g, ' ');
    linkifyTextNodes(document.body);
}
}
    var parentTocLi = parentTocSpan.closest('li');
    // 2. Search and replace text nodes
    if (!parentTocLi) {
    function linkifyTextNodes(node) {
        console.warn('Parent TOC li not found for chunk:', chunkDiv.id);
        if (node.nodeType === Node.TEXT_NODE) {
        return;
            var text = node.nodeValue;
    }
            var parent = node.parentNode;
    // Find or create a sub-UL under this parent LI
            for (var phrase in headingLinks) {
    var subUl = parentTocLi.querySelector('ul');
                var regex = new RegExp("\\b(" + phrase + ") diagram\\b", "i");
    if (!subUl) {
                var match = regex.exec(text);
        subUl = document.createElement('ul');
                if (match) {
        subUl.className = 'toc-sublist'; // optional styling class
                    var before = text.slice(0, match.index);
        parentTocLi.appendChild(subUl);
                    var after = text.slice(match.index + match[0].length);
    }
    // Now find all new headings inside the chunk
                    var anchor = document.createElement('a');
    var newHeadings = chunkDiv.querySelectorAll('h2, h3');
                    anchor.href = "#" + headingLinks[phrase];
                    anchor.textContent = match[0];
    for (var j = 0; j < newHeadings.length; j++) {
                    parent.insertBefore(document.createTextNode(before), node);
        var heading = newHeadings[j];
                    parent.insertBefore(anchor, node);
                    if (after) parent.insertBefore(document.createTextNode(after), node);
        // Ensure the heading has an id
                    parent.removeChild(node);
        if (!heading.id) {
                    break; // Stop after first match in this node
            heading.id = 'heading-' + Math.random().toString(36).substr(2, 9);
                }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'A') {
            for (var j = 0; j < node.childNodes.length; j++) {
                linkifyTextNodes(node.childNodes[j]);
            }
        }
        }
    }
}
// ===========================
// End links for notes such as "v. 2 preferred diagram"
// ===========================
        // Create new TOC <li>
function attachResizeHandler($pre, $svg) {
        var li = document.createElement('li');
var resizeHandler = function () {
        li.className = 'toclevel-' + (heading.tagName === 'H2' ? '1' : '2');
var newWidth = $pre.width();
$svg.css({
width: newWidth + 'px',
'max-width': newWidth + 'px'
// Note: do not touch height to prevent layout reflow
});
};
        var a = document.createElement('a');
$(window).on('resize', resizeHandler);
        a.href = '#' + heading.id;
        a.className = 'nav-link';
        var spanNumber = document.createElement('span');
        spanNumber.className = 'tocnumber';
        spanNumber.textContent = ''; // optional numbering if needed
        var spanText = document.createElement('span');
        spanText.className = 'toctext';
        spanText.textContent = heading.textContent;
        a.appendChild(spanNumber);
        a.appendChild(spanText);
        li.appendChild(a);
        subUl.appendChild(li);
    }
}
}
// ===========================
function initializePanZoom(container, svgElement) {
// End placeholder auto-loading
var panZoomInstance = Panzoom(svgElement, {
// ===========================
contain: 'outside',
 
minScale: 1,
maxScale: 10,
panOnlyWhenZoomed: true,
zoomSpeed: 0.040,
pinchSpeed: 1.5
});
 
container.addEventListener('wheel', function (e) {
// ===========================
e.preventDefault();
// Code to build overlays in the first place
panZoomInstance.zoomWithWheel(e, { step: 0.04 });
// ===========================
});
 
container.addEventListener('dblclick', function (e) {
var rect = container.getBoundingClientRect();
var offsetX = e.clientX - rect.left;
var offsetY = e.clientY - rect.top;
// split first column of Hebrew into spans per word and attaches handlers
if (e.shiftKey) {
document.getElementById("createSpans").onclick = function () {
panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
} else {
panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
}
});
}


  var rows = document.querySelectorAll("#buildOverlay tr");
function processMermaidContainer(container) {
var $container = $(container);
var $svg = $container.find('svg');
if ($svg.length === 0) {
console.log("Found no svg", container);
return;
}
  for (var i = 0; i < rows.length; i++) {
var $pre = $container.find('pre.mermaid');
    var row = rows[i];
var preWidth = $pre.width();
    if (!row.cells || row.cells.length < 3) continue;
var preHeight = $pre.height();
var viewBox = $svg[0].getAttribute('viewBox');
if (!viewBox) {
console.log("Found no viewBox", container);
return;
}
    var hebrewCell = row.cells[0];
var viewBoxValues = viewBox.split(' ');
    var verseCell = row.cells[1];
var viewBoxWidth = parseFloat(viewBoxValues[2]);
    var englishCell = row.cells[2];
var viewBoxHeight = parseFloat(viewBoxValues[3]);
var scaleX = preWidth / viewBoxWidth;
var scaleY = preHeight / viewBoxHeight;
var scale = Math.min(scaleX, scaleY);
    var verse = verseCell
$svg.css({
      ? verseCell.textContent.replace(/\s+/g, '').trim()
width: preWidth + 'px',
      : "v" + i;
'max-width': preWidth + 'px',
height: (viewBoxHeight * scale) + 'px',
position: 'relative',
left: '-10px'
});
    if (hebrewCell.getAttribute("data-wrapped") !== "true") {
initializePanZoom($container[0], $svg[0]);
      wrapCellWords(hebrewCell, verse, true); // true = Hebrew
attachResizeHandler($pre, $svg);
      hebrewCell.setAttribute("data-wrapped", "true");
}
    }
    if (englishCell.getAttribute("data-wrapped") !== "true") {
      wrapCellWords(englishCell, verse, false); // false = English
      englishCell.setAttribute("data-wrapped", "true");
    }
  }
};


function processAllMermaidContainers(container) {
var c = container || document;
var verseDivs = c.querySelectorAll('div[id^="verse-"]');
verseDivs.forEach(function (div) {
processMermaidContainer(div);
});
}


function wrapCellWords(cell, verse, isHebrew) {
  var text = cell.textContent || cell.innerText;
  var words = text.replace(/\s+/g, ' ').trim().split(' ');
  var lineDiv = document.createElement("div");
// Function to wait until all Mermaid diagrams are processed
  lineDiv.className = "line";
function waitForMermaidProcessing(callback) {
  lineDiv.setAttribute("data-line", verse);
    var interval = setInterval(function () {
        if (checkMermaidProcessed()) {
  for (var i = 0; i < words.length; i++) {
            clearInterval(interval);
    var span = document.createElement("span");
            callback(); // Once all elements are processed, run the callback
    var word = words[i];
    if (isHebrew) {
      var wordID = verse + "-" + (i + 1) + "-1";
      span.className = "hebrew id-" + wordID;
      //span.id = wordID;
      span.textContent = word;
      attachHebrewSplitHandler(span);
    } else {
      span.className = "english";
      span.textContent = word;
      span.style.cursor = "pointer";
      span.addEventListener("click", function () {
        var mode = document.querySelector('input[name="Overlay/Build[Mode]"]:checked');
        if (!mode || mode.value !== "Align English") return;
        if (!selectedHebrewID) return;
        // Remove previous Hebrew links (id-*) from this span
        var classes = this.className.split(" ");
        var newClasses = [];
        for (var j = 0; j < classes.length; j++) {
          if (classes[j].indexOf("id-") !== 0) {
            newClasses.push(classes[j]);
          }
        }
        }
        newClasses.push("id-" + selectedHebrewID);
    }, 100); // Check every 100ms
        this.className = newClasses.join(" ");
      });
    }
    lineDiv.appendChild(span);
    lineDiv.appendChild(document.createTextNode(" "));
  }
  cell.innerHTML = "";
  cell.appendChild(lineDiv);
}
}


// split-span-into-multiple-spans (to be used both at startup and when new spans are created)
var selectedHebrewID = null;
function hideHighlightPhrases(container) {
var collectedSpans = []; // Array of {language, word, wordID, line, index}
var c = container || document;
 
var elements = c.querySelectorAll(".highlight-phrase");
for (var i = 0; i < elements.length; i++) {
function attachHebrewSplitHandler(span) {
elements[i].style.display = "none";
 
  span.addEventListener("click", function(event) {
    event.stopPropagation();
 
var span = event.target;
    if (span.tagName !== "SPAN") return;
// Find the class starting with "id-"
var classes = span.className.split(" ");
var idClass = null;
for (var i = 0; i < classes.length; i++) {
  if (classes[i].indexOf("id-") === 0) {
    idClass = classes[i];
    break;
  }
}
}
if (!idClass) return; // No ID found, stop processing
// If you still want to extract parts like "2a", "3", etc.:
var idParts = idClass.slice(3).split("-"); // remove "id-" and split
var verse = idParts[0]; // e.g., "2a"
var wordIndex = idParts[1]; // e.g., "3"
var baseID = verse + "-" + wordIndex;
  // Check if "Split Hebrew" mode is selected
var selectedMode = document.querySelector('input[name="Overlay/Build[Mode]"]:checked');
if (selectedMode.value === "Split Hebrew") {
  // run split logic
 
    var range;
    if (document.caretRangeFromPoint) {
      range = document.caretRangeFromPoint(event.clientX, event.clientY);
    } else if (document.caretPositionFromPoint) {
      var pos = document.caretPositionFromPoint(event.clientX, event.clientY);
      if (!pos) return;
      range = document.createRange();
      range.setStart(pos.offsetNode, pos.offset);
    } else {
      return;
    }
    if (!range || !span.textContent) return;
var classMatch = span.className.match(/id-(\d+[a-z]?)-(\d+)-(\d+)/);
if (!classMatch) return;
var verse = classMatch[1];      // e.g., "3" or "3a"
var wordIndex = classMatch[2];  // e.g., "1"
var letterIndex = parseInt(classMatch[3], 10); // e.g., 1
var baseID = verse + "-" + wordIndex;
var offset = range.startOffset;
var text = span.textContent;
if (offset <= 0 || offset >= text.length) return;
var before = text.slice(0, offset);
var after = text.slice(offset);
// First span keeps same class
var span1 = document.createElement("span");
span1.className = "hebrew id-" + baseID + "-" + letterIndex;
span1.textContent = before;
span1.style.cursor = "pointer";
attachHebrewSplitHandler(span1);
// Second span gets incremented letter index
var newLetterIndex = letterIndex + before.length;
var span2 = document.createElement("span");
span2.className = "hebrew id-" + baseID + "-" + newLetterIndex;
span2.textContent = after;
span2.style.cursor = "pointer";
attachHebrewSplitHandler(span2);
// Insert into DOM
var parent = span.parentNode;
var spacer = document.createTextNode(" ");
parent.insertBefore(span1, span);
parent.insertBefore(spacer, span);
parent.insertBefore(span2, span);
parent.removeChild(span);
 
} else if (selectedMode.value === "Align English") {
  // Determine if the clicked span is Hebrew or English
  if (span.classList.contains("hebrew")) {
    // Hebrew word clicked → highlight it and clear all prior alignments
    // Remove previous Hebrew highlights
    var allHebrew = document.querySelectorAll("span.hebrew");
    for (var i = 0; i < allHebrew.length; i++) {
      allHebrew[i].classList.remove("hebrew-selected");
    }
    // Hide all previous alignments from English
    var allEnglish = document.querySelectorAll("span.gloss, span.english");
    for (var i = 0; i < allEnglish.length; i++) {
      allEnglish[i].classList.remove("aligned-english");
      /* Remove any id-XXX class
      var classes = allEnglish[i].className.split(" ");
      var newClasses = [];
      for (var j = 0; j < classes.length; j++) {
        if (classes[j].indexOf("id-") !== 0) {
          newClasses.push(classes[j]);
        }
      }
      allEnglish[i].className = newClasses.join(" "); */
    }
    // Set and highlight the selected Hebrew word
    span.classList.add("hebrew-selected");
    var idMatch = span.className.match(/id-[\w-]+/);
    selectedHebrewID = idMatch ? idMatch[0].substring(3) : null;
  } else if (span.classList.contains("gloss") || span.classList.contains("english")) {
    // English word clicked → align it to selected Hebrew (if any)
    if (!selectedHebrewID) return;
    // Remove existing id-XXX classes on this span
    var classes = span.className.split(" ");
    var newClasses = [];
    for (var j = 0; j < classes.length; j++) {
      if (classes[j].indexOf("id-") !== 0) {
        newClasses.push(classes[j]);
      }
    }
    // Add the selected Hebrew ID and highlight class
    newClasses.push("id-" + selectedHebrewID);
    newClasses.push("aligned-english");
    span.className = newClasses.join(" ");
  }
}
  });
}
}


 
function openLightbox(svgElement) {
document.getElementById("saveSpans").onclick = function () {
var lightbox = $('<div id="lightbox-overlay" class="lightbox-overlay">')
  var allSpans = document.querySelectorAll('#buildOverlay .line span');
.appendTo('body')
  var formAdder = document.querySelector('.multipleTemplateAdder a');
.css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(128, 128, 128, 0.8)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999
});
  // Clear existing form instances
var lightboxSvgContainer = $('<div class="lightbox-svg-container">')
  var allInstances = document.querySelectorAll('.multipleTemplateInstance');
.appendTo(lightbox)
  for (var i = 0; i < allInstances.length; i++) {
.css({
    allInstances[i].remove();
width: '95%',
  }
height: '95%',
overflow: 'hidden',
backgroundColor: 'rgba(255, 255, 255, 1.0)'
});
  // Process and add new entries
var lightboxSvg = $(svgElement).clone().appendTo(lightboxSvgContainer).css({
  var count = 0;
width: '100%',
  for (var i = 0; i < allSpans.length; i++) {
'max-width': '100%',
    var span = allSpans[i];
height: '100%'
    var classes = span.className.split(' ');
});
    var idClass = null;
    var language = span.classList.contains('hebrew') ? 'Hebrew' : span.classList.contains('gloss') || span.classList.contains('english') ? 'English' : null;
    if (!language) continue;
    for (var j = 0; j < classes.length; j++) {
var panZoomInstance = Panzoom(lightboxSvg[0], {
      if (classes[j].indexOf('id-') === 0) {
contain: 'outside',
        idClass = classes[j].substring(3);
minScale: 1,
        break;
maxScale: 10,
      }
panOnlyWhenZoomed: true,
    }
zoomSpeed: 0.040,
    if (!idClass) continue;
pinchSpeed: 1.5
});
    var lineDiv = span.closest('.line');
lightboxSvg[0].addEventListener('wheel', function (e) {
    var lineID = lineDiv ? lineDiv.getAttribute('data-line') : '';
e.preventDefault();
    var word = span.textContent.trim();
panZoomInstance.zoomWithWheel(e, { step: 0.04 });
});
    var index = 1;
lightboxSvg[0].addEventListener('dblclick', function (e) {
    var siblings = lineDiv ? lineDiv.querySelectorAll('span.' + language.toLowerCase()) : [];
var rect = lightboxSvg[0].getBoundingClientRect();
    for (var k = 0; k < siblings.length; k++) {
var offsetX = e.clientX - rect.left;
      if (siblings[k] === span) {
var offsetY = e.clientY - rect.top;
        index = k + 1;
        break;
      }
    }
    // Add form row
if (e.shiftKey) {
    if (formAdder) formAdder.click();
panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
} else {
panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
}
});
    (function (word, idClass, lineID, language, index) {
var closeButton = $('<button class="lightbox-close-button">Close</button>')
      setTimeout(function () {
.appendTo(lightbox)
        var latestInstances = document.querySelectorAll('.multipleTemplateInstance');
.css({
        var latest = latestInstances[latestInstances.length - 1];
position: 'absolute',
top: '10px',
right: '10px',
backgroundColor: '#fff',
color: '#000',
border: '1px solid #bbb',
borderRadius: '1rem',
padding: '10px 20px',
cursor: 'pointer',
zIndex: 10000
})
.on('click', function () {
lightbox.remove();
});
        if (latest) {
lightbox.on('click', function (e) {
          latest.querySelector('input[name$="[Language]"]').value = language;
if ($(e.target).is(lightbox)) {
          latest.querySelector('input[name$="[Word]"]').value = word;
lightbox.remove();
          latest.querySelector('input[name$="[WordID]"]').value = idClass;
}
          latest.querySelector('input[name$="[LineID]"]').value = lineID;
});
          latest.querySelector('input[name$="[Index]"]').value = index;
        }
      }, 100 * count);
    })(word, idClass, lineID, language, index);
    count++;
$(document).on('keydown.lightbox', function (e) {
  }
if (e.key === "Escape" || e.keyCode === 27) {
lightbox.remove();
  console.log("Saved " + count + " spans.");
$(document).off('keydown.lightbox');
};
}
});
}


function bindLightboxButtons() {
$('.lightbox-button').off('click').on('click', function () {
if (document.getElementById("buildOverlay")) {
var targetDivId = $(this).data('target');
  var spans = document.querySelectorAll('span.hebrew');
var parentDiv = $(targetDivId);
  for (var i = 0; i < spans.length; i++) {
var associatedSvg = parentDiv.find('svg');
    attachHebrewSplitHandler(spans[i]);
if (associatedSvg.length > 0) {
  }
openLightbox(associatedSvg[0]);
}
});
}
}
// ===========================
// Auto-create links for notes such as "v. 2 preferred diagram"
// ===========================
    var headingLinks = {};


function initializeMermaidSVGScaling(container) {
var c = container || document;
waitForMermaidProcessing(function () {
processAllMermaidContainers(c);
hideHighlightPhrases(c);
});
}


if (document.getElementById("createDiagramLinks")) {
function initializeLazyLoadedEnhancements(container) {
    // 1. Build a mapping from "v. 2 preferred" ➔ "Preferred_2"
var c = container || document;
    var currentVerseRange = "";
initializeMermaidSVGScaling(c);
bindLightboxButtons(c);
    var headings = document.querySelectorAll('h1, h2');
mw.loader.using('jquery.makeCollapsible', function () {
    for (var i = 0; i < headings.length; i++) {
$(c).find('.mw-collapsible').makeCollapsible();
        var heading = headings[i];
});
        if (heading.tagName === 'H1') {
            currentVerseRange = heading.innerText.trim().toLowerCase(); // e.g., "v. 2"
        }
        if (heading.tagName === 'H2') {
            var description = heading.innerText.trim();
            var combinedKey = (currentVerseRange + " " + description).toLowerCase(); // e.g., "v. 2 preferred"
            // Ensure heading has a usable ID
            if (!heading.id) {
                heading.id = description.replace(/\s+/g, "_") + "_" + currentVerseRange.replace(/[^0-9]/g, "");
            }
            headingLinks[combinedKey] = heading.id;
        }
    }
    // 3. Apply to whole document
    linkifyTextNodes(document.body);
}
}


    // 2. Search and replace text nodes
    function linkifyTextNodes(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            var text = node.nodeValue;
            var parent = node.parentNode;


            for (var phrase in headingLinks) {
                var regex = new RegExp("\\b(" + phrase + ") diagram\\b", "i");
                var match = regex.exec(text);
                if (match) {
                    var before = text.slice(0, match.index);
                    var after = text.slice(match.index + match[0].length);


                    var anchor = document.createElement('a');
$(document).ready(function () {
                    anchor.href = "#" + headingLinks[phrase];
                    anchor.textContent = match[0];
 
                    parent.insertBefore(document.createTextNode(before), node);
                    parent.insertBefore(anchor, node);
                    if (after) parent.insertBefore(document.createTextNode(after), node);
                    parent.removeChild(node);
                    break; // Stop after first match in this node
                }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'A') {
            for (var j = 0; j < node.childNodes.length; j++) {
                linkifyTextNodes(node.childNodes[j]);
            }
        }
    }
 
// ===========================
// End links for notes such as "v. 2 preferred diagram"
// ===========================
 
 
// BEGIN TEXT OVERLAY CODE
 
// === TEXT OVERLAY COLOR PICKER LOGIC ===
// var selectedColor = 'red';
var selectedParticipant = 'psalmist';
var coloredWords = [];
/*
document.querySelectorAll('input[name="Text Overlay[Color]"]').forEach(function (radio) {
  radio.addEventListener('change', function (e) {
    selectedColor = e.target.value;
    console.warn("Selected color:", selectedColor);
  });
});*/
// 2. Set default color based on default participant
// set up color map
setupExpandablePreviews();
    var divColorMap = document.getElementById("color-map");
 
var colorMap = {};
 
var colorPicker = document.querySelector("div.color-picker");
var allPageChapters = [];
if (divColorMap) {
    var encoded = divColorMap.textContent;
    if (encoded) {
try {
    var decoded = decodeURIComponent(encoded);
    colorMap = JSON.parse(decoded);
    console.log("Color map successfully decoded.");
} catch (e) {
    console.error("Error processing color map:", e);
}
    }
}
else{
console.log("No color map found");
}
selectedColor = '';
var buttons = document.querySelectorAll('.save-page-button');
buttons.forEach(function (button) {
  var page = button.dataset.page;
  var chapter = button.dataset.chapter;
// 3a. Setup listener for radio buttons (when using radio buttons)
  // Store the unique (page, chapter) pair
document.querySelectorAll('input[name="Text Overlay[Participant]"]').forEach(function (radio) {
  allPageChapters.push({ page: page, chapter: chapter });
    radio.addEventListener('change', function (e) {
        selectedParticipant = e.target.value;
        selectedColor = colorMap[selectedParticipant] || '';
        console.warn("Selected:", selectedParticipant, "→", selectedColor);
    });
});
  // Bind individual button click
// 3b. Setup listener for color grid (when using that)
  button.addEventListener('click', function () {
    console.log('[SaveDiv] Clicked button for page:', page, 'and chapter:', chapter);
// Color selection via grid
    copyPageContents(page, chapter);
document.querySelectorAll('.color-cell').forEach(function(cell) {
  cell.addEventListener('click', function () {
    // Remove highlight from all cells on the color grid
    document.querySelectorAll('.color-cell').forEach(function(c) {
      c.style.outline = 'none';
    });
    // Highlight this one
    cell.style.outline = '4px solid black';
    // Set selectedColor
    selectedColor = cell.dataset.color || '';
        selectedParticipant = cell.dataset.participant || 'unknown';
    console.log("Selected color from grid: " + selectedColor + " for participant: " + selectedParticipant);
  });
});
// on click - individual words
document.querySelectorAll('span.hebrew').forEach(function (span) {
  span.style.cursor = 'pointer';
//   span.addEventListener('click', function () {
 
 
  span.addEventListener('click', function (event) {
event.stopPropagation(); // prevent the line-level click from firing
 
 
    var idMatch = span.className.match(/id-[\w-]+/);
    if (!idMatch) return;
    var id = idMatch[0];
    var word = span.textContent;
    var glossEl = document.querySelector('.gloss.' + id);
    var gloss = glossEl ? glossEl.textContent : '';
    if (!coloredWords.some(function (w) { return w.id === id; })) {
      coloredWords.push({ id: id, hebrew: word, gloss: gloss, participant:selectedParticipant, color: selectedColor });
     
// Add a new form instance by clicking the "Add another" button
var addButton = document.querySelector('.multipleTemplateAdder a');
if (addButton) {
  addButton.click();
  // Slight delay to allow the DOM to update
  setTimeout(function () {
    // Get the list of all current instances
    var allInstances = document.querySelectorAll('.multipleTemplateInstance');
    var latest = allInstances[allInstances.length - 1];
    if (latest) {
      // Fill in WordID and Color
      var hebrewInput = latest.querySelector('input[name$="[Hebrew]"]');
      var wordIdInput = latest.querySelector('input[name$="[WordID]"]');
      var participantInput = latest.querySelector('input[name$="[Participant]"]');
      var colorInput = latest.querySelector('input[name$="[Color]"]');
      if (hebrewInput) hebrewInput.value = word;
      if (wordIdInput) wordIdInput.value = id;
      if (participantInput) participantInput.value = selectedParticipant;
      if (colorInput) colorInput.value = selectedColor;
    }
  }, 100); // Adjust delay if needed
}
   
    }
console.log("Setting to " + selectedColor);
    span.style.backgroundColor = selectedColor;
    if (glossEl) glossEl.style.backgroundColor = selectedColor;
  });
  });
});
});
 
//console.log("Save-page-buttons ready for saving.");
//console.log("All page/chapter combinations:", allPageChapters);
// Line-level click handling
// Optional: Set up a "copy all" button
document.querySelectorAll('div.line').forEach(function (lineDiv) {
var copyAllButton = document.querySelector('#copyAllButton');
  lineDiv.style.cursor = 'pointer';
if (copyAllButton) {
  lineDiv.addEventListener('click', function () {
  copyAllButton.addEventListener('click', function () {
    if (!selectedColor || !selectedParticipant) {
    console.log('[SaveDiv] Copying all pages...');
      console.warn("No color or participant selected.");
    allPageChapters.forEach(function (pair) {
      return;
      copyPageContents(pair.page, pair.chapter);
    }
    var lineId = lineDiv.dataset.line;
    if (!lineId) return;
    // Find all lines with the same data-line attribute
    var matchingLines = document.querySelectorAll('div.line[data-line="' + CSS.escape(lineId) + '"]');
    // Get representative text from the first match
    var lineText = matchingLines[0].textContent.trim();
    if (!coloredWords.some(function (w) { return w.id === lineId; })) {
      coloredWords.push({
        id: lineId,
        hebrew: lineText,
        gloss: '',
        participant: selectedParticipant,
        color: selectedColor
      });
      var addButton = document.querySelector('.multipleTemplateAdder a');
      if (addButton) {
        addButton.click();
        setTimeout(function () {
          var allInstances = document.querySelectorAll('.multipleTemplateInstance');
          var latest = allInstances[allInstances.length - 1];
          if (latest) {
            var hebrewInput = latest.querySelector('input[name$="[Hebrew]"]');
            var wordIdInput = latest.querySelector('input[name$="[WordID]"]');
            var participantInput = latest.querySelector('input[name$="[Participant]"]');
            var colorInput = latest.querySelector('input[name$="[Color]"]');
            if (hebrewInput) hebrewInput.value = lineText;
            if (wordIdInput) wordIdInput.value = lineId;
            if (participantInput) participantInput.value = selectedParticipant;
            if (colorInput) colorInput.value = selectedColor;
          }
        }, 100);
      }
    }
    // Apply background color to all matching line divs
    matchingLines.forEach(function (div) {
      div.style.backgroundColor = selectedColor;
    });
    });
    console.log("Highlighted all lines with data-line:", lineId);
  });
  });
});
// export a list
window.exportAnnotations = function () {
  var output = document.getElementById('annotation-output');
  if (output) {
    output.textContent = coloredWords.map(function (w) {
      return w.id + ": " + w.hebrew + " / " + w.gloss + " [" + w.color + "]";
    }).join('\n');
  } else {
    console.warn("No #annotation-output element found.");
  }
};
function applyAllOverlayAnnotations() {
    var overlays = document.querySelectorAll("div.overlay");
    // If no overlays are found, print a warning
    if (overlays.length === 0) {
        console.warn("No overlays found on the page.");
    }
    for (var i = 0; i < overlays.length; i++) {
        var overlay = overlays[i];
        var containerId = overlay.id;
        var encoded = overlay.getAttribute("data-annotations");
        if (!encoded) continue;
       
        try {
            var decoded = decodeURIComponent(encoded);
            var annotations = JSON.parse(decoded);
            applyOverlayColors(containerId, annotations);
        } catch (e) {
            console.error("Error processing annotations for", containerId, e);
        }
    }
}
}
    function applyOverlayColors(containerId, annotations) {
        var container = document.getElementById(containerId);
        if (!container) return;
        for (var wordID in annotations) {
    if (annotations.hasOwnProperty(wordID) && wordID.trim() !== "") {
            console.log("Seeking color for " + wordID + " and " + annotations[wordID]);
        var color = colorMap[annotations[wordID]];
        var elements = container.querySelectorAll("." + CSS.escape(wordID));
            console.log("Processing color " + color + " for " + elements + " elements");
        for (var j = 0; j < elements.length; j++) {
            elements[j].style.backgroundColor = color;
        }
    }
}
    }
   
   


// END TEXT OVERLAY CODE
     //console.log("Document ready. Attaching event listeners to toggle links.");
     //console.log("Document ready. Attaching event listeners to toggle links.");


Line 892: Line 486:
     attachToggleListeners();
     attachToggleListeners();
      
      
     // handle any overlay colors
     initializeLazyLoadedEnhancements(document);
    applyAllOverlayAnnotations();
 
    // Wait until all Mermaid diagrams are processed
    waitForMermaidProcessing(function () {
        //console.log("Mermaid diagrams are fully processed.");
 
        $('div[id^="verse-"]').each(function () {
            var parentDiv = $(this);
            var svg = parentDiv.find('svg');
 
            if (svg.length > 0) {
                var preElement = parentDiv.find('pre.mermaid');  // The <pre> element containing the SVG
                var preWidth = preElement.width();
                var preHeight = preElement.height();
                var viewBox = svg[0].getAttribute('viewBox');
 
                if (viewBox) {
                    var viewBoxValues = viewBox.split(' ');
                    var viewBoxWidth = parseFloat(viewBoxValues[2]);
                    var viewBoxHeight = parseFloat(viewBoxValues[3]);
                    var scaleX = preWidth / viewBoxWidth;
                    var scaleY = preHeight / viewBoxHeight;
                    var scale = Math.min(scaleX, scaleY);
 
                    svg.css({
                        'width': (preWidth) + 'px',
                        'max-width': (preWidth) + 'px',
                        'height': (viewBoxHeight * scale) + 'px',
                        'position': 'relative',  // Ensure the SVG has a positioning context
                        'left': '-10px'  // Offset the SVG to the left, because firefox and others misalign it to the right. This removes the horizontal scrollbar
                    });
 
// Initialize panzoom
var panZoomInstance = Panzoom(svg[0], {
contain: 'outside',
minScale: 1,  // default is 0.125
maxScale: 10,  // default is 4
panOnlyWhenZoomed: true, //default is false
zoomSpeed: 0.040, // default is 6.5% per mouse wheel event
pinchSpeed: 1.5 // default is zoom two times faster than the distance between fingers
});
                    parentDiv[0].addEventListener('wheel', function (e) {
e.preventDefault();
    panZoomInstance.zoomWithWheel(e, { step: 0.04 }); // custom override per event
});
                    parentDiv[0].addEventListener('dblclick', function (event) {
            var rect = parentDiv[0].getBoundingClientRect();
    var offsetX = event.clientX - rect.left;
    var offsetY = event.clientY - rect.top;
    if (event.shiftKey) {
        // Shift + Double-click → Zoom Out
        panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
    } else {
        // Regular Double-click → Zoom In
        panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
    }
            });
 
                    // Resize handler to keep SVG scaled on window resize
                    var resizeHandler = function () {
                        var newWidth = preElement.width();
 
                        svg.css({
                            'width': (newWidth) + 'px',
                            'max-width': (newWidth) + 'px'
                            // Do not change the height to avoid reflowing the html page
                        });
                    };
                   
                    // Listen for resize events
                    $(window).on('resize', resizeHandler);
                }
            }
       
           
        });
 
        // Initially hide elements with the "highlight-phrase" class
        document.querySelectorAll(".highlight-phrase").forEach(function (element) {
            element.style.display = "none"; // Hide elements initially
        });
 
 
 
        // Bind lightbox functionality
        $('.lightbox-button').on('click', function () {
            // Get the target <div> ID from the button's data-target attribute
            var targetDivId = $(this).data('target'); // e.g., '#verse-1'
            var parentDiv = $(targetDivId); // Find the corresponding <div> by ID
            var associatedSvg = parentDiv.find('svg'); // Find the SVG inside the <pre>
 
            if (associatedSvg.length > 0) {
                openLightbox(associatedSvg[0]);
            }
        });
 
        // Open the lightbox and display the SVG in full-screen
        function openLightbox(svgElement) {
            // Create lightbox container if it doesn't exist           
            var lightbox = $('<div id="lightbox-overlay" class="lightbox-overlay">')
                .appendTo('body')
                .css({
                    position: 'fixed',
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: '100%',
                    backgroundColor: 'rgba(128, 128, 128, 0.8)',
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    zIndex: 9999,
                });
 
            // Create the SVG container in the lightbox
                var lightboxSvgContainer = $('<div class="lightbox-svg-container">')
                .appendTo(lightbox)
                .css({
                    width: '95%',
                    height: '95%',
                    overflow: 'hidden',
                    backgroundColor: 'rgba(255, 255, 255, 1.0)',
                });
 
            var lightboxSvg = $(svgElement).clone().appendTo(lightboxSvgContainer);  // Clone the SVG
            // resize the svg to the available space
            lightboxSvg.css({
                'width': '100%',
                'max-width': '100%',
                'height': '100%'
            });


            // Apply panzoom to the cloned SVG in the lightbox
var panZoomInstanceLightbox = Panzoom(lightboxSvg[0], {
contain: 'outside',
minScale: 1,  // default is 0.125
maxScale: 10,  // default is 4
panOnlyWhenZoomed: true, //default is false
zoomSpeed: 0.040, // default is 6.5% per mouse wheel event
pinchSpeed: 1.5 // default is zoom two times faster than the distance between fingers
});
            lightboxSvg[0].addEventListener('wheel', function (e) {
e.preventDefault();
panZoomInstanceLightbox.zoomWithWheel(e, { step: 0.04 }); // custom override per event
});
            lightboxSvg[0].addEventListener('dblclick', function (event) {
            var rect = lightboxSvg[0].getBoundingClientRect();
    var offsetX = event.clientX - rect.left;
    var offsetY = event.clientY - rect.top;
    if (event.shiftKey) {
        // Shift + Double-click → Zoom Out
        panZoomInstanceLightbox.zoomOut({ focal: { x: offsetX, y: offsetY } });
    } else {
        // Regular Double-click → Zoom In
        panZoomInstanceLightbox.zoomIn({ focal: { x: offsetX, y: offsetY } });
    }
            });
            var closeButton = $('<button class="lightbox-close-button">Close</button>')
                .appendTo(lightbox)
                .css({
                    position: 'absolute',
                    top: '10px',
                    right: '10px',
                    backgroundColor: '#fff',
                    color: '#000',
                    border: '1px solid #bbb',
                    borderRadius: '1rem',
                    padding: '10px 20px',
                    cursor: 'pointer',
                    zIndex: 10000
                })
                .on('click', function () {
                    // Close the lightbox when the close button is clicked
                    lightbox.remove();
                });
            lightbox.on('click', function (event) {
                // Close the lightbox when clicking outside the SVG
                if ($(event.target).is(lightbox)) {
                    lightbox.remove();
                }
            });
            // Close the lightbox with the Escape key
            $(document).on('keydown', function (event) {
                if (event.key === "Escape" || event.keyCode === 27) {
                    lightbox.remove();
                    $(document).off('keydown');  // Remove the keydown listener to prevent multiple bindings
                }
            });
        }
    });


// Bidirectional hover for Hebrew and Gloss (ES5-compatible)
// Bidirectional hover for Hebrew and Gloss (ES5-compatible)
var hoverElements = document.querySelectorAll(".hebrew, .gloss");
var hoverElements = document.querySelectorAll(".hebrew, .english");


// If no toggle links are found, print a warning
// If no toggle links are found, print a warning
    if (hoverElements.length === 0) {
    if (hoverElements.length === 0) {
        console.warn("No hover elements found on the page.");
        //console.warn("No hover elements found on the page.");
    }
    }
    
    
Line 1,105: Line 507:
            for (var j = 0; j < classList.length; j++) {
            for (var j = 0; j < classList.length; j++) {
                var cls = classList[j];
                var cls = classList[j];
                if (cls.indexOf("id-") === 0) {
                if (cls.indexOf("id-") === 0 && cls !== "id-none") {
                    var matches = document.getElementsByClassName(cls);
                    var matches = document.getElementsByClassName(cls);
                    for (var k = 0; k < matches.length; k++) {
                    for (var k = 0; k < matches.length; k++) {
Line 1,132: Line 534:
}
}
//createAutoDiagramLinks();
mw.loader.using([], function () {
setTimeout(function () {
if (typeof createAutoDiagramLinks === 'function') {
createAutoDiagramLinks();
} else {
console.warn('createAutoDiagramLinks is still not available.');
}
}, 200); // or more, depending on network
});
});
// Stopping fixed elements
$(document).ready(function () {
  var $box = $('.fixed-box');
  var $wrapper = $box.parent();
  var stopY = 515;
  $(window).on('scroll', function () {
    var scrollY = window.scrollY || window.pageYOffset;
    if (scrollY >= stopY) {
      $box.css({
        position: 'absolute',
        top: stopY + 'px', // place it exactly where it was fixed
        left: 0,
        width: '100%'
      });
    } else {
      $box.css({
        position: 'fixed',
        top: '0px',
        left: 0,
        width: '100%'
      });
    }
  });
});
// Rotating caret for collapsible elements
// This ensures the code runs after the entire page (DOM) is loaded.
jQuery( function( $ ) {
    //console.log('Caret toggle JS loaded (Fresh Start Version).');
    // Select all toggle spans that have our dedicated controlling class.
    // This makes the script work for multiple toggles on the same page.
    $( '.js-toggle-caret-controller' ).each( function() {
        var $toggleSpan = $( this ); // The current toggle span in the loop
        // --- Extract the unique name for matching toggle span and div ---
        // This is crucial for matching the 'mw-customtoggle-NAME' with 'mw-customcollapsible-NAME'.
        var uniqueName = null;
        var classList = $toggleSpan.attr('class').split(' '); // Get all classes as an array


        // Loop through the classes to find the one starting with 'mw-customtoggle-'
        for (var i = 0; i < classList.length; i++) {
            // Checks if a class starts with "mw-customtoggle-" (ES5 compatible)
            if (classList[i].indexOf('mw-customtoggle-') === 0) {
                uniqueName = classList[i].replace('mw-customtoggle-', ''); // Extract the unique name (e.g., "HomeIntro")
                break; // Found it, no need to check further
            }
        }
        if (uniqueName === null) {
            console.warn('Skipping toggle: Could not find "mw-customtoggle-" class on span for:', $toggleSpan);
            return; // Skip this span if its unique name cannot be determined
        }
        // --- End unique name extraction ---
        // Construct the ID of the corresponding collapsible div (e.g., #mw-customcollapsible-HomeIntro)
        var $collapsibleDiv = $( '#mw-customcollapsible-' + uniqueName );
        // Log findings for this specific toggle pair (for debugging)
        //console.log('Processing toggle: ' + uniqueName);
        //console.log('  Collapsible div found:', $collapsibleDiv.length > 0 ? 'Yes' : 'No');
        //console.log('  Toggle span found:', $toggleSpan.length > 0 ? 'Yes' : 'No');
        // Stop processing this toggle if its corresponding div is not found
        if ($collapsibleDiv.length === 0) {
            console.warn('Skipping toggle ' + uniqueName + ': Corresponding collapsible div (#mw-customcollapsible-' + uniqueName + ') not found.');
            return;
        }
        // Function to update the 'is-expanded-toggle' class on the span
        // This class will trigger the CSS rotation.
        function updateCaretDirection() {
            // Check the div's current state: does it have the 'mw-collapsed' class?
            var isCollapsed = $collapsibleDiv.hasClass( 'mw-collapsed' );
            //console.log('  updateCaretDirection called for ' + uniqueName + '. Div has mw-collapsed:', isCollapsed);
            if ( isCollapsed ) {
                // If the DIV is collapsed, remove our custom class from the toggle SPAN
                $toggleSpan.removeClass( 'is-expanded-toggle' );
                //console.log('  Caret state for ' + uniqueName + ': collapsed (removed is-expanded-toggle)');
            } else {
                // If the DIV is expanded, add our custom class to the toggle SPAN
                $toggleSpan.addClass( 'is-expanded-toggle' );
                //console.log('  Caret state for ' + uniqueName + ': expanded (added is-expanded-toggle)');
            }
        }
        // --- Event Handling for this specific toggle ---
        // 1. Set the initial state of the caret on page load.
        // This is important if the toggle starts collapsed (which it usually does).
        updateCaretDirection();
        // 2. Attach a click listener directly to this specific toggle span.
        // When the button is clicked, we'll update the caret.
        $toggleSpan.on( 'click', function() {
            //console.log('  Toggle button CLICKED for ' + uniqueName + '!');
            // Use a small delay (50ms) to ensure MediaWiki's native toggle logic
            // has finished updating the 'mw-collapsed' class on the div *before* our function runs.
            setTimeout(updateCaretDirection, 50);
        });
    }); // End of .each() loop: This ensures the above logic runs for every toggle found.
}); // End of jQuery(function($)): This ensures the script runs when the DOM is ready.
// Re-collapse expanded elements when clicking outside the element
document.addEventListener('click', function (event) {
  var collapsibles = document.querySelectorAll('[id^="mw-customcollapsible-"]');
  for (var i = 0; i < collapsibles.length; i++) {
    var collapsible = collapsibles[i];
    // ✅ Skip if it has opt-out flag
    if (collapsible.hasAttribute('data-no-auto-collapse') || collapsible.classList.contains('no-auto-collapse')) {
      continue;
    }
    var id = collapsible.id;
    var shortId = id.replace('mw-customcollapsible-', '');
    var toggle = document.querySelector('.mw-customtoggle-' + shortId);
    if (!toggle || !collapsible) {
      continue;
    }
    var isClickInside = collapsible.contains(event.target) || toggle.contains(event.target);
    if (!isClickInside && !collapsible.classList.contains('mw-collapsed')) {
      toggle.click(); // Trigger collapse
    }
  }
});
});

Revision as of 21:38, 10 July 2025

importScript('MediaWiki:Overlays.js');
// importScript('MediaWiki:Lineation.js');
importScript('MediaWiki:AutoLoad.js');
importScript('MediaWiki:Compare.js');
importScript('MediaWiki:json.js');

var debug = false;


	/* Any JavaScript here will be loaded for all users on every page load. */
function currentChapter(){
    var pageName = mw.config.get("wgPageName"); // e.g., Psalms/Psalm_18/alignment/1-20

    var chapter = "";
    var verseRange = "";

    // Extract the chapter number
    var chapterMatch = pageName.match(/Psalm_(\d+)/);
    if (chapterMatch) {
      return chapterMatch[1];
    }

    if (!chapter) {
      //console.error("Could not detect Psalm chapter from page name:", pageName);
      return;
    }	
}

function setupExpandablePreviews() {
  var boxes = document.querySelectorAll(".preview-box");

  for (var i = 0; i < boxes.length; i++) {
    (function(box) {
      box.classList.add("collapsed");

      var link = document.createElement("a");
      link.className = "preview-toggle-link";
      link.textContent = "Show more";

      link.addEventListener("click", function(e) {
        e.preventDefault();
        var isCollapsed = box.classList.contains("collapsed");
        box.classList.toggle("collapsed", !isCollapsed);
        box.classList.toggle("expanded", isCollapsed);
        link.textContent = isCollapsed ? "Show less" : "Show more";
      });

      // Insert the link right after the box
      if (box.nextSibling) {
        box.parentNode.insertBefore(link, box.nextSibling);
      } else {
        box.parentNode.appendChild(link);
      }
    })(boxes[i]);
  }
}



	function toggleVisibility(containerId, className) {
	    //console.log("Toggling visibility for " + className + " in " + containerId);
	    var container = document.getElementById(containerId);
	    var elements = null;
	    if (container) {
	    	if (className==="alternative"){
				
	
				// Match all elements with pinkish FILL or STROKE
				elements = container.querySelectorAll('[fill^="#f4"], [stroke^="#f4"], [fill^="#f6"], [stroke^="#f6"]');
				
				elements.forEach(function(el) {
				    if (!el.hasAttribute('data-original-display')) {
				        el.setAttribute('data-original-display', el.style.display || "");
				    }
				
				    if (el.style.display === "none") {
				        el.style.display = el.getAttribute('data-original-display') || "";
				    } else {
				        el.style.display = "none";
				    }
				});
	
	    	}else{
		        elements = container.querySelectorAll("g." + className);
		        for (var i = 0; i < elements.length; i++) {
		            var element = elements[i];
		            
		            if (element.style.display === "none") {
		                element.style.display = ""; // Show element
		            } else {
		                element.style.display = "none"; // Hide element
		            }
		        }
	    	}
	    } else {
	        console.warn("Container with ID \"" + containerId + "\" not found.");
	    }
	}
	
	function attachToggleListeners() {
	    //console.log("Attaching event listeners to toggle links.");
	    var toggleLinks = document.querySelectorAll("[data-container-id][data-class]");
	    //console.log("Found " + toggleLinks.length + " links.");
	
	    // If no toggle links are found, print a warning
	    if (toggleLinks.length === 0) {
	        //console.warn("No toggle links found on the page.");
	    }
	    
	    for (var i = 0; i < toggleLinks.length; i++) {
	        (function (toggleLink) {
	            toggleLink.addEventListener("click", function (event) {
	                event.preventDefault(); // Prevent default link behavior
	                var containerId = toggleLink.getAttribute("data-container-id");
	                var className = toggleLink.getAttribute("data-class");
	                toggleVisibility(containerId, className);
	            });
	        })(toggleLinks[i]);
	    }
	}
	

// ===========================

// Mermaid code that, if stored elsewhere, doesn't load in time

// ===========================
	
	// Function to check if all <pre class="mermaid"> elements are processed
	function checkMermaidProcessed() {
	    var mermaidPreElements = document.querySelectorAll('pre.mermaid');
	    var allProcessed = true;
	
	    mermaidPreElements.forEach(function (element) {
	        if (!element.hasAttribute('data-processed') || element.getAttribute('data-processed') !== 'true') {
	            allProcessed = false;
	        }
	    });
	
		if (!allProcessed)
			console.log("Mermaid items still remaining to be processed.");
			
	    return allProcessed;
	}
	


	function createAutoDiagramLinks(){
			// ===========================
		// Auto-create links for notes such as "v. 2 preferred diagram"
		// ===========================
	
		
	    var headingLinks = {};
	
	
		if (document.getElementById("createDiagramLinks")) {
		    // 1. Build a mapping from "v. 2 preferred" ➔ "Preferred_2"
		    var currentVerseRange = "";
		
		    var headings = document.querySelectorAll('h1, h2');
		    for (var i = 0; i < headings.length; i++) {
		        var heading = headings[i];
		        if (heading.tagName === 'H1') {
		            currentVerseRange = heading.innerText.trim().toLowerCase(); // e.g., "v. 2"
		        }
		        if (heading.tagName === 'H2') {
		            var description = heading.innerText.trim();
		            var combinedKey = (currentVerseRange + " " + description).toLowerCase(); // e.g., "v. 2 preferred"
		
		            // Ensure heading has a usable ID
		            if (!heading.id) {
		                heading.id = description.replace(/\s+/g, "_") + "_" + currentVerseRange.replace(/[^0-9]/g, "");
		            }
		
		            headingLinks[combinedKey] = heading.id;
		        }
		    }
		
		    // 3. Apply to whole document
		    linkifyTextNodes(document.body);
		}
		
	
	    // 2. Search and replace text nodes
	    function linkifyTextNodes(node) {
	        if (node.nodeType === Node.TEXT_NODE) {
	            var text = node.nodeValue;
	            var parent = node.parentNode;
	
	            for (var phrase in headingLinks) {
	                var regex = new RegExp("\\b(" + phrase + ") diagram\\b", "i");
	                var match = regex.exec(text);
	                if (match) {
	                    var before = text.slice(0, match.index);
	                    var after = text.slice(match.index + match[0].length);
	
	                    var anchor = document.createElement('a');
	                    anchor.href = "#" + headingLinks[phrase];
	                    anchor.textContent = match[0];
	
	                    parent.insertBefore(document.createTextNode(before), node);
	                    parent.insertBefore(anchor, node);
	                    if (after) parent.insertBefore(document.createTextNode(after), node);
	                    parent.removeChild(node);
	                    break; // Stop after first match in this node
	                }
	            }
	        } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'A') {
	            for (var j = 0; j < node.childNodes.length; j++) {
	                linkifyTextNodes(node.childNodes[j]);
	            }
	        }
	    }
	}
		// ===========================
		// End links for notes such as "v. 2 preferred diagram"
		// ===========================
	
	function attachResizeHandler($pre, $svg) {
		var resizeHandler = function () {
			var newWidth = $pre.width();
			$svg.css({
				width: newWidth + 'px',
				'max-width': newWidth + 'px'
				// Note: do not touch height to prevent layout reflow
			});
		};
	
		$(window).on('resize', resizeHandler);
	}
	
	function initializePanZoom(container, svgElement) {
		var panZoomInstance = Panzoom(svgElement, {
			contain: 'outside',
			minScale: 1,
			maxScale: 10,
			panOnlyWhenZoomed: true,
			zoomSpeed: 0.040,
			pinchSpeed: 1.5
		});
	
		container.addEventListener('wheel', function (e) {
			e.preventDefault();
			panZoomInstance.zoomWithWheel(e, { step: 0.04 });
		});
	
		container.addEventListener('dblclick', function (e) {
			var rect = container.getBoundingClientRect();
			var offsetX = e.clientX - rect.left;
			var offsetY = e.clientY - rect.top;
	
			if (e.shiftKey) {
				panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
			} else {
				panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
			}
		});
	}

	function processMermaidContainer(container) {
		var $container = $(container);
		var $svg = $container.find('svg');
		if ($svg.length === 0) {
			console.log("Found no svg", container);
			return;
		}
	
		var $pre = $container.find('pre.mermaid');
		var preWidth = $pre.width();
		var preHeight = $pre.height();
		var viewBox = $svg[0].getAttribute('viewBox');
		if (!viewBox) {
			console.log("Found no viewBox", container);
			return;
		}
	
		var viewBoxValues = viewBox.split(' ');
		var viewBoxWidth = parseFloat(viewBoxValues[2]);
		var viewBoxHeight = parseFloat(viewBoxValues[3]);
		var scaleX = preWidth / viewBoxWidth;
		var scaleY = preHeight / viewBoxHeight;
		var scale = Math.min(scaleX, scaleY);
	
		$svg.css({
			width: preWidth + 'px',
			'max-width': preWidth + 'px',
			height: (viewBoxHeight * scale) + 'px',
			position: 'relative',
			left: '-10px'
		});
	
		initializePanZoom($container[0], $svg[0]);
		attachResizeHandler($pre, $svg);
	}

	function processAllMermaidContainers(container) {
		var c = container || document;
		var verseDivs = c.querySelectorAll('div[id^="verse-"]');
		verseDivs.forEach(function (div) {
			processMermaidContainer(div);
		});
	}

	
	// Function to wait until all Mermaid diagrams are processed
	function waitForMermaidProcessing(callback) {
	    var interval = setInterval(function () {
	        if (checkMermaidProcessed()) {
	            clearInterval(interval);
	            callback(); // Once all elements are processed, run the callback
	        }
	    }, 100); // Check every 100ms
	}

	
	function hideHighlightPhrases(container) {
		var c = container || document;
		var elements = c.querySelectorAll(".highlight-phrase");
		for (var i = 0; i < elements.length; i++) {
			elements[i].style.display = "none";
		}
	}

	function openLightbox(svgElement) {
		var lightbox = $('<div id="lightbox-overlay" class="lightbox-overlay">')
			.appendTo('body')
			.css({
				position: 'fixed',
				top: 0,
				left: 0,
				width: '100%',
				height: '100%',
				backgroundColor: 'rgba(128, 128, 128, 0.8)',
				display: 'flex',
				justifyContent: 'center',
				alignItems: 'center',
				zIndex: 9999
			});
	
		var lightboxSvgContainer = $('<div class="lightbox-svg-container">')
			.appendTo(lightbox)
			.css({
				width: '95%',
				height: '95%',
				overflow: 'hidden',
				backgroundColor: 'rgba(255, 255, 255, 1.0)'
			});
	
		var lightboxSvg = $(svgElement).clone().appendTo(lightboxSvgContainer).css({
			width: '100%',
			'max-width': '100%',
			height: '100%'
		});
	
		var panZoomInstance = Panzoom(lightboxSvg[0], {
			contain: 'outside',
			minScale: 1,
			maxScale: 10,
			panOnlyWhenZoomed: true,
			zoomSpeed: 0.040,
			pinchSpeed: 1.5
		});
	
		lightboxSvg[0].addEventListener('wheel', function (e) {
			e.preventDefault();
			panZoomInstance.zoomWithWheel(e, { step: 0.04 });
		});
	
		lightboxSvg[0].addEventListener('dblclick', function (e) {
			var rect = lightboxSvg[0].getBoundingClientRect();
			var offsetX = e.clientX - rect.left;
			var offsetY = e.clientY - rect.top;
	
			if (e.shiftKey) {
				panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
			} else {
				panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
			}
		});
	
		var closeButton = $('<button class="lightbox-close-button">Close</button>')
			.appendTo(lightbox)
			.css({
				position: 'absolute',
				top: '10px',
				right: '10px',
				backgroundColor: '#fff',
				color: '#000',
				border: '1px solid #bbb',
				borderRadius: '1rem',
				padding: '10px 20px',
				cursor: 'pointer',
				zIndex: 10000
			})
			.on('click', function () {
				lightbox.remove();
			});
	
		lightbox.on('click', function (e) {
			if ($(e.target).is(lightbox)) {
				lightbox.remove();
			}
		});
	
		$(document).on('keydown.lightbox', function (e) {
			if (e.key === "Escape" || e.keyCode === 27) {
				lightbox.remove();
				$(document).off('keydown.lightbox');
			}
		});
	}

	function bindLightboxButtons() {
		$('.lightbox-button').off('click').on('click', function () {
			var targetDivId = $(this).data('target');
			var parentDiv = $(targetDivId);
			var associatedSvg = parentDiv.find('svg');
			if (associatedSvg.length > 0) {
				openLightbox(associatedSvg[0]);
			}
		});
	}

	function initializeMermaidSVGScaling(container) {
		var c = container || document;
		waitForMermaidProcessing(function () {
			processAllMermaidContainers(c);
			hideHighlightPhrases(c);
		});
	}

	function initializeLazyLoadedEnhancements(container) {
		var c = container || document;
		initializeMermaidSVGScaling(c);
		bindLightboxButtons(c);
		mw.loader.using('jquery.makeCollapsible', function () {
			$(c).find('.mw-collapsible').makeCollapsible();
		});
	}



$(document).ready(function () {
	
	
	
	setupExpandablePreviews();


	var allPageChapters = [];
	
	var buttons = document.querySelectorAll('.save-page-button');
	buttons.forEach(function (button) {
	  var page = button.dataset.page;
	  var chapter = button.dataset.chapter;
	
	  // Store the unique (page, chapter) pair
	  allPageChapters.push({ page: page, chapter: chapter });
	
	  // Bind individual button click
	  button.addEventListener('click', function () {
	    console.log('[SaveDiv] Clicked button for page:', page, 'and chapter:', chapter);
	    copyPageContents(page, chapter);
	  });
	});
	
	//console.log("Save-page-buttons ready for saving.");
	//console.log("All page/chapter combinations:", allPageChapters);
	
	// Optional: Set up a "copy all" button
	var copyAllButton = document.querySelector('#copyAllButton');
	if (copyAllButton) {
	  copyAllButton.addEventListener('click', function () {
	    console.log('[SaveDiv] Copying all pages...');
	    allPageChapters.forEach(function (pair) {
	      copyPageContents(pair.page, pair.chapter);
	    });
	  });
	}

	
    //console.log("Document ready. Attaching event listeners to toggle links.");

    // Now attach event listeners for toggling visibility
    attachToggleListeners();
    
    initializeLazyLoadedEnhancements(document);


	// Bidirectional hover for Hebrew and Gloss (ES5-compatible)
	var hoverElements = document.querySelectorAll(".hebrew, .english");

	// If no toggle links are found, print a warning
	    if (hoverElements.length === 0) {
	        //console.warn("No hover elements found on the page.");
	    }
    	
	for (var i = 0; i < hoverElements.length; i++) {
	    (function (el) {
	        el.addEventListener("mouseenter", function () {
	            //var classList = el.className.split(" ");
	            
	            var className = (typeof el.className === 'object' && el.className.baseVal) ? el.className.baseVal : el.className;
				var classList = className.split(" ");

	            for (var j = 0; j < classList.length; j++) {
	                var cls = classList[j];
	                if (cls.indexOf("id-") === 0 && cls !== "id-none") {
	                    var matches = document.getElementsByClassName(cls);
	                    for (var k = 0; k < matches.length; k++) {
	                        matches[k].classList.add("highlighted");
	                    }
	                }
	            }
	        });
	
	        el.addEventListener("mouseleave", function () {
	            //var classList = el.className.split(" ");
	            
	            var className = (typeof el.className === 'object' && el.className.baseVal) ? el.className.baseVal : el.className;
				var classList = className.split(" ");
	            for (var j = 0; j < classList.length; j++) {
	                var cls = classList[j];
	                if (cls.indexOf("id-") === 0) {
	                    var matches = document.getElementsByClassName(cls);
	                    for (var k = 0; k < matches.length; k++) {
	                        matches[k].classList.remove("highlighted");
	                    }
	                }
	            }
	        });
	    })(hoverElements[i]);
	}
	
	
	//createAutoDiagramLinks();
	mw.loader.using([], function () {
		setTimeout(function () {
			if (typeof createAutoDiagramLinks === 'function') {
				createAutoDiagramLinks();
			} else {
				console.warn('createAutoDiagramLinks is still not available.');
			}
		}, 200); // or more, depending on network
	});




});

// Stopping fixed elements
$(document).ready(function () {
  var $box = $('.fixed-box');
  var $wrapper = $box.parent();
  var stopY = 515;

  $(window).on('scroll', function () {
    var scrollY = window.scrollY || window.pageYOffset;

    if (scrollY >= stopY) {
      $box.css({
        position: 'absolute',
        top: stopY + 'px', // place it exactly where it was fixed
        left: 0,
        width: '100%'
      });
    } else {
      $box.css({
        position: 'fixed',
        top: '0px',
        left: 0,
        width: '100%'
      });
    }
  });
});

// Rotating caret for collapsible elements
// This ensures the code runs after the entire page (DOM) is loaded.
jQuery( function( $ ) {

    //console.log('Caret toggle JS loaded (Fresh Start Version).');

    // Select all toggle spans that have our dedicated controlling class.
    // This makes the script work for multiple toggles on the same page.
    $( '.js-toggle-caret-controller' ).each( function() {
        var $toggleSpan = $( this ); // The current toggle span in the loop

        // --- Extract the unique name for matching toggle span and div ---
        // This is crucial for matching the 'mw-customtoggle-NAME' with 'mw-customcollapsible-NAME'.
        var uniqueName = null;
        var classList = $toggleSpan.attr('class').split(' '); // Get all classes as an array

        // Loop through the classes to find the one starting with 'mw-customtoggle-'
        for (var i = 0; i < classList.length; i++) {
            // Checks if a class starts with "mw-customtoggle-" (ES5 compatible)
            if (classList[i].indexOf('mw-customtoggle-') === 0) {
                uniqueName = classList[i].replace('mw-customtoggle-', ''); // Extract the unique name (e.g., "HomeIntro")
                break; // Found it, no need to check further
            }
        }

        if (uniqueName === null) {
            console.warn('Skipping toggle: Could not find "mw-customtoggle-" class on span for:', $toggleSpan);
            return; // Skip this span if its unique name cannot be determined
        }
        // --- End unique name extraction ---

        // Construct the ID of the corresponding collapsible div (e.g., #mw-customcollapsible-HomeIntro)
        var $collapsibleDiv = $( '#mw-customcollapsible-' + uniqueName );

        // Log findings for this specific toggle pair (for debugging)
        //console.log('Processing toggle: ' + uniqueName);
        //console.log('   Collapsible div found:', $collapsibleDiv.length > 0 ? 'Yes' : 'No');
        //console.log('   Toggle span found:', $toggleSpan.length > 0 ? 'Yes' : 'No');

        // Stop processing this toggle if its corresponding div is not found
        if ($collapsibleDiv.length === 0) {
            console.warn('Skipping toggle ' + uniqueName + ': Corresponding collapsible div (#mw-customcollapsible-' + uniqueName + ') not found.');
            return;
        }

        // Function to update the 'is-expanded-toggle' class on the span
        // This class will trigger the CSS rotation.
        function updateCaretDirection() {
            // Check the div's current state: does it have the 'mw-collapsed' class?
            var isCollapsed = $collapsibleDiv.hasClass( 'mw-collapsed' );
            //console.log('   updateCaretDirection called for ' + uniqueName + '. Div has mw-collapsed:', isCollapsed);

            if ( isCollapsed ) {
                // If the DIV is collapsed, remove our custom class from the toggle SPAN
                $toggleSpan.removeClass( 'is-expanded-toggle' );
                //console.log('   Caret state for ' + uniqueName + ': collapsed (removed is-expanded-toggle)');
            } else {
                // If the DIV is expanded, add our custom class to the toggle SPAN
                $toggleSpan.addClass( 'is-expanded-toggle' );
                //console.log('   Caret state for ' + uniqueName + ': expanded (added is-expanded-toggle)');
            }
        }

        // --- Event Handling for this specific toggle ---

        // 1. Set the initial state of the caret on page load.
        // This is important if the toggle starts collapsed (which it usually does).
        updateCaretDirection();

        // 2. Attach a click listener directly to this specific toggle span.
        // When the button is clicked, we'll update the caret.
        $toggleSpan.on( 'click', function() {
            //console.log('   Toggle button CLICKED for ' + uniqueName + '!');
            // Use a small delay (50ms) to ensure MediaWiki's native toggle logic
            // has finished updating the 'mw-collapsed' class on the div *before* our function runs.
            setTimeout(updateCaretDirection, 50);
        });

    }); // End of .each() loop: This ensures the above logic runs for every toggle found.

}); // End of jQuery(function($)): This ensures the script runs when the DOM is ready.

// Re-collapse expanded elements when clicking outside the element
document.addEventListener('click', function (event) {
  var collapsibles = document.querySelectorAll('[id^="mw-customcollapsible-"]');

  for (var i = 0; i < collapsibles.length; i++) {
    var collapsible = collapsibles[i];

    // ✅ Skip if it has opt-out flag
    if (collapsible.hasAttribute('data-no-auto-collapse') || collapsible.classList.contains('no-auto-collapse')) {
      continue;
    }

    var id = collapsible.id;
    var shortId = id.replace('mw-customcollapsible-', '');
    var toggle = document.querySelector('.mw-customtoggle-' + shortId);

    if (!toggle || !collapsible) {
      continue;
    }

    var isClickInside = collapsible.contains(event.target) || toggle.contains(event.target);

    if (!isClickInside && !collapsible.classList.contains('mw-collapsed')) {
      toggle.click(); // Trigger collapse
    }
  }
});