MediaWiki:Common.js
From Psalms: Layer by Layer
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.
// importScript('MediaWiki:Overlays.js'); //importScript('MediaWiki:Insertions.js'); //importScript('MediaWiki:Lineation.js'); /* Any JavaScript here will be loaded for all users on every page load. */ // 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; } }); return allProcessed; } // 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 toggleVisibility(containerId, className) { //console.log("Toggling visibility for " + className + " in " + containerId); var container = document.getElementById(containerId); if (container) { if (className==="alternative"){ // Match all elements with pinkish FILL or STROKE var 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{ var 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]); } } // =========================== $(document).ready(function () { // =========================== // Begin placeholder auto-loading // =========================== var observer = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { 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) { observer.observe(placeholder); }); 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); } else if (xhr.status === 404) { 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 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) { console.warn('Parent TOC span not found for chunk:', chunkDiv.id); return; } // Clean up the corresponding H1 heading (remove " (loading...)" from text and ID) var originalHeadingId = chunkDiv.id + '_(loading...)'; var h1span = document.getElementById(originalHeadingId); if (h1span && h1span.classList.contains('mw-headline')) { var cleanedId = chunkDiv.id; // Update ID h1span.id = cleanedId; // Update display text h1span.textContent = cleanedId.replace(/_/g, ' '); } var parentTocLi = parentTocSpan.closest('li'); if (!parentTocLi) { console.warn('Parent TOC li not found for chunk:', chunkDiv.id); return; } // Find or create a sub-UL under this parent LI var subUl = parentTocLi.querySelector('ul'); if (!subUl) { subUl = document.createElement('ul'); subUl.className = 'toc-sublist'; // optional styling class parentTocLi.appendChild(subUl); } // Now find all new headings inside the chunk var newHeadings = chunkDiv.querySelectorAll('h2, h3'); for (var j = 0; j < newHeadings.length; j++) { var heading = newHeadings[j]; // Ensure the heading has an id if (!heading.id) { heading.id = 'heading-' + Math.random().toString(36).substr(2, 9); } // Create new TOC <li> var li = document.createElement('li'); li.className = 'toclevel-' + (heading.tagName === 'H2' ? '1' : '2'); var a = document.createElement('a'); 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); } } // =========================== // End placeholder auto-loading // =========================== // =========================== // 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" // =========================== //console.log("Document ready. Attaching event listeners to toggle links."); // Now attach event listeners for toggling visibility attachToggleListeners(); // 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) var hoverElements = document.querySelectorAll(".hebrew, .gloss"); // 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) { 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]); } }); // 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%' }); } }); }); /* ====================================== BuildTextTable js ======================================*/ var selectedHebrewID = null; var selectedHebrewSpan = null; var overlay = document.getElementById('buildOverlay'); var modeField = document.querySelector('input[name="BuildTextTable[Mode]"]:checked'); // Split vs Align overlay mode switching function updateOverlayModeClass() { var modeInput = document.querySelector('input[name="BuildTextTable[Mode]"]:checked'); var overlay = document.getElementById("buildOverlay"); if (!modeInput || !overlay) return; overlay.classList.remove("mode-split", "mode-align"); if (modeInput.value === "Split Words") { overlay.classList.add("mode-split"); } else if (modeInput.value === "Align English") { overlay.classList.add("mode-align"); var englishSpans = overlay.querySelectorAll("td.cbc-cell span"); console.log("Aligning English: " + englishSpans.length); for (var i = 0; i < englishSpans.length; i++) { var engSpan = englishSpans[i]; var classMatch = engSpan.className.match(/id-[\w-]+/); if (!classMatch) continue; var idClass = classMatch[0]; var hebrewSpan = overlay.querySelector("td.hebrew-cell span." + idClass); if (!hebrewSpan) continue; var computedStyle = window.getComputedStyle(hebrewSpan); engSpan.style.color = computedStyle.color; } } } function splitSpan(span, event) { var classMatch = span.className.match(/id-(\d+[a-z]?)-(\d+)-(\d+)/); var isIDClass = !!classMatch; // Disallow splitting unless class is 'english' or has an id-* if (!isIDClass && !span.classList.contains("english")) { console.warn("Cannot split span " + span.textContent + " with class " + span.className); return; } console.log("Splitting span for span " + span.textContent + " and class " + span.className); // get range / position to know exactly where we are 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; } 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); var span1 = document.createElement("span"); span1.textContent = before.trim(); var span2 = document.createElement("span"); span2.textContent = after.trim(); var td = span.closest("td"); if (td && td.classList.contains("hebrew-cell")) { // parse class var verse = classMatch[1]; var wordIndex = classMatch[2]; var letterIndex = parseInt(classMatch[3], 10); var baseID = verse + "-" + wordIndex; var newLetterIndex = letterIndex + before.length; /* // check for merging instead of splitting if (event.shiftKey) { mergeSplitSpans(baseID); return; } */ // update Hebrew spans with a newLetterIndex span1.className = "hebrew id-" + baseID + "-" + letterIndex; span2.className = "hebrew id-" + baseID + "-" + newLetterIndex; }else if (td && td.classList.contains("cbc-cell")){ // don't change the class list for the CBC cells span1.className = span.className; span2.className = span.className; } else { // Fallback span1.className = span.className; span2.className = span.className; } attachSpanClickHandler(span1); attachSpanClickHandler(span2); var parent = span.parentNode; if (parent) { parent.insertBefore(span1, span); //parent.insertBefore(document.createTextNode(" "), span); var interveningSpace = document.createElement("span"); interveningSpace.className = "gap"; interveningSpace.textContent = " "; interveningSpace.addEventListener("click", function (event) { attemptGapMerge(interveningSpace, event); }); parent.insertBefore(interveningSpace, span); parent.insertBefore(span2, span); parent.removeChild(span); } } function attachSpanClickHandler(span) { span.addEventListener("click", function (event) { event.stopPropagation(); var selectedMode = document.querySelector('input[name="BuildTextTable[Mode]"]:checked'); if (!selectedMode) return; var mode = selectedMode.value; var isHebrew = span.classList.contains("hebrew"); var isEnglish = span.classList.contains("english") || span.classList.contains("gloss"); console.log("Clicked span:", span.innerText, "| Mode:", mode, "| Shift:", event.shiftKey); if (mode === "Split Words") { splitSpan(span, event); } else if (mode === "Align English") { if (event.shiftKey && isEnglish) { // ✅ SHIFT-CLICK ENGLISH → unalign it span.className = span.className .split(/\s+/) .filter(function (cls) { return cls.indexOf("id-") !== 0 && cls !== "aligned-english"; }) .join(" "); span.style.color = ""; // optional: reset color return; } if (event.shiftKey && isHebrew) { // ✅ SHIFT-CLICK HEBREW → deselect it document.querySelectorAll("span.hebrew").forEach(function (el) { el.classList.remove("hebrew-selected"); }); return; } if (isHebrew) { document.querySelectorAll("span.hebrew").forEach(function (el) { el.classList.remove("hebrew-selected"); }); span.classList.add("hebrew-selected"); var idMatch = span.className.match(/id-[\w-]+/); selectedHebrewID = idMatch ? idMatch[0].substring(3) : null; selectedHebrewSpan = span; } else if (isEnglish && selectedHebrewID) { // Align to selected Hebrew var newClasses = span.className.split(" ").filter(function (cls) { return cls.indexOf("id-") !== 0; }); newClasses.push("id-" + selectedHebrewID); newClasses.push("aligned-english"); span.className = newClasses.join(" "); if (selectedHebrewSpan) { var color = window.getComputedStyle(selectedHebrewSpan).color; span.style.color = color; } } } }); } /* function mergeSplitSpans(baseID) { var all = document.querySelectorAll('span[class*="id-' + baseID + '-"]'); if (all.length <= 1) return; var mergedText = ''; for (var i = 0; i < all.length; i++) { mergedText += all[i].textContent; } var parent = all[0].parentNode; var firstSpan = all[0]; for (var i = 1; i < all.length; i++) { parent.removeChild(all[i]); } var newClassName = firstSpan.className.split(" ").map(function (cls) { return cls.indexOf("id-") === 0 ? "id-" + baseID + "-1" : cls; }).join(" "); firstSpan.className = newClassName; firstSpan.textContent = mergedText; attachSpanClickHandler(firstSpan); } */ function mergeTwoSpans(spanA, spanB) { if (!spanA || !spanB || spanA.tagName !== "SPAN" || spanB.tagName !== "SPAN") return; if (spanA.parentNode !== spanB.parentNode) return; var parent = spanA.parentNode; // Merge text var mergedText = spanA.textContent + spanB.textContent; // Determine baseID if present var baseID = null; var langClass = null; var classesA = spanA.className.split(/\s+/); var newClasses = []; for (var i = 0; i < classesA.length; i++) { var cls = classesA[i]; if (cls.indexOf("id-") === 0) { var idParts = cls.split("-"); if (idParts.length >= 3) { baseID = idParts[1] + "-" + idParts[2]; // e.g., "2a-3" } } else { if (cls === "hebrew" || cls === "english") langClass = cls; newClasses.push(cls); // Preserve other classes } } // If a baseID was found, add new id-class as id-XXX-1 if (baseID) { newClasses = newClasses.filter(function(cls) { return cls.indexOf("id-") !== 0; }); newClasses.push("id-" + baseID + "-1"); } // Update spanA spanA.className = newClasses.join(" "); spanA.textContent = mergedText; attachSpanClickHandler(spanA); // Remove spanB parent.removeChild(spanB); } function attemptGapMerge(gapSpan, event) { var isGap = gapSpan.classList.contains("gap"); if (!isGap && !event.shiftKey) { return; // require Shift unless it's a "gap" span } var prev = gapSpan.previousElementSibling; var next = gapSpan.nextElementSibling; if (prev && next && prev.tagName === "SPAN" && next.tagName === "SPAN") { mergeTwoSpans(prev, next); gapSpan.parentNode.removeChild(gapSpan); } } function wrapCellWords(cell, verse, isHebrew) { if (!cell) return; var text = cell.textContent; if (typeof text !== "string") return; var pieces = text.match(/\S+|\s+/g); // Match non-space chunks or space chunks if (!pieces) return; // Nothing to wrap var lineDiv = document.createElement("div"); lineDiv.className = "line"; lineDiv.setAttribute("data-line", verse); var wordCount = 0; for (var i = 0; i < pieces.length; i++) { var piece = pieces[i]; var span = document.createElement("span"); span.textContent = piece; var isSpace = /^\s+$/.test(piece); if (isSpace) { span.className = "gap"; span.addEventListener("click", function (event) { attemptGapMerge(span, event); }); } else { wordCount++; if (isHebrew) { var wordID = verse + "-" + wordCount + "-1"; span.className = "hebrew id-" + wordID; } else { span.className = "english"; } attachSpanClickHandler(span); } lineDiv.appendChild(span); } cell.innerHTML = ""; cell.appendChild(lineDiv); } function wrapGapsBetweenSpans() { var lines = document.querySelectorAll(".psalm-table .line"); for (var i = 0; i < lines.length; i++) { var line = lines[i]; var spans = Array.from(line.querySelectorAll("span")); // PART 1: Split "english id-none" spans that contain whitespace for (var j = spans.length - 1; j >= 0; j--) { var span = spans[j]; if ( span.classList.contains("english") && span.classList.contains("id-none") && span.textContent.match(/\s/) ) { var words = span.textContent.match(/\S+|\s+/g); if (!words) { console.warn("⚠️ Could not match any words in:", span); continue; } for (var k = words.length - 1; k >= 0; k--) { var part = words[k]; var newSpan = document.createElement("span"); if (/^\s+$/.test(part)) { newSpan.className = "gap"; newSpan.textContent = part; newSpan.addEventListener("click", function (event) { attemptGapMerge(this, event); }); } else { newSpan.className = "english id-none"; newSpan.textContent = part; attachSpanClickHandler(newSpan); } line.insertBefore(newSpan, span); } line.removeChild(span); } } // PART 2: Insert gap spans between adjacent spans (if no element exists between) var updatedSpans = Array.from(line.querySelectorAll("span")); for (var j = updatedSpans.length - 1; j > 0; j--) { var prev = updatedSpans[j - 1]; var next = updatedSpans[j]; if (prev.nextElementSibling !== next) { continue; } var gapSpan = document.createElement("span"); gapSpan.className = "gap"; gapSpan.textContent = " "; gapSpan.addEventListener("click", function (event) { attemptGapMerge(this, event); }); line.insertBefore(gapSpan, next); console.log(" ➕ Inserted inter-word gap span between:", prev, next); } } } function createSpans() { var rows = document.querySelectorAll("#buildOverlay tr"); for (var i = 1; i < rows.length; i++) { var row = rows[i]; if (!row.cells || row.cells.length < 3) continue; var hebrewCell = row.cells[0]; var verseCell = row.cells[1]; var englishCell = row.cells[2]; var verse = verseCell ? verseCell.textContent.replace(/\s+/g, '').trim() : i; if (hebrewCell.getAttribute("data-wrapped") !== "true") { wrapCellWords(hebrewCell, verse, true); hebrewCell.setAttribute("data-wrapped", "true"); } if (englishCell.getAttribute("data-wrapped") !== "true") { wrapCellWords(englishCell, verse, false); englishCell.setAttribute("data-wrapped", "true"); } } } function saveSpansToDocDebug() { var overlay = document.getElementById("buildOverlay"); if (!overlay) { console.warn("No #buildOverlay found in the document."); return; } var allSpans = overlay.querySelectorAll(".line span"); console.log("Saving spans to doc... found " + allSpans.length + " spans."); for (var i = 0; i < allSpans.length; i++) { console.log("Span:", allSpans[i].outerHTML); } } function saveSpansToDoc() { var allSpans = document.querySelectorAll('#buildOverlay .line span'); var output = []; console.log("Saving spans to doc... at most " + allSpans.length); for (var i = 0; i < allSpans.length; i++) { var span = allSpans[i]; console.log("Testing span " + span.innerContent + " with class " + span.className); // Skip gaps and non-hebrew/english spans if (span.classList.contains("gap")) continue; if (!span.classList.contains("hebrew") && !span.classList.contains("english")) continue; var classes = span.className.split(/\s+/); var idMatch = null; var lang = span.classList.contains("hebrew") ? "Hebrew" : "English"; // Extract WordID (if any) for (var j = 0; j < classes.length; j++) { if (classes[j].indexOf('id-') === 0) { idMatch = classes[j].substring(3); break; } } if (!idMatch) idMatch = "none"; // fallback if no ID // Clean up word var word = span.textContent.trim(); // Get enclosing line var lineDiv = span.closest('.line'); var lineID = lineDiv ? lineDiv.getAttribute('data-line') : ''; // Compute position among siblings of the same language var siblings = lineDiv ? lineDiv.querySelectorAll('span.' + lang.toLowerCase()) : []; var index = Array.prototype.indexOf.call(siblings, span) + 1; // Compose the output line output.push( '{{Overlay/Span' + '|Language=' + lang + '|WordID=' + idMatch + '|Word=' + word + '|LineID=' + lineID + '|Index=' + index + '}}' ); } // Output to textarea var textarea = document.querySelector('textarea[name$="[SpanText]"]'); if (textarea) { textarea.value = output.join("\n"); console.log("Saved " + output.length + " spans."); } else { console.warn("No output textarea found."); } } function skipToNextHebrewWord (event) { // Only act on Tab without Shift, and if in "Align English" mode if (event.key === "Tab" && !event.shiftKey) { var modeInput = document.querySelector('input[name="BuildTextTable[Mode]"]:checked'); if (!modeInput || modeInput.value !== "Align English") return; var allHebrew = Array.from(document.querySelectorAll("span.hebrew")); var selected = document.querySelector("span.hebrew.hebrew-selected"); if (!selected) return; event.preventDefault(); // prevent default tab behavior (focus move) var index = allHebrew.indexOf(selected); if (index >= 0 && index < allHebrew.length - 1) { var next = allHebrew[index + 1]; if (next) next.click(); // simulate clicking next Hebrew word } } } $(document).ready(function () { // Set "Split Words" radio button as default checked var defaultRadio = document.querySelector('input[name="BuildTextTable[Mode]"][value="Split Words"]'); if (defaultRadio) { defaultRadio.checked = true; } updateOverlayModeClass(); document.querySelectorAll('input[name="BuildTextTable[Mode]"]').forEach(function (radio) { radio.addEventListener("change", updateOverlayModeClass); }); var createBtn = document.getElementById("createSpans"); if (createBtn) { createBtn.onclick = createSpans; } var wrapSpaces = document.getElementById("wrapSpaces"); if (wrapSpaces) { wrapSpaces.onclick = wrapGapsBetweenSpans; } var divWrap = document.getElementById("divWrapSpaces"); if (divWrap){ var isCollapsed = divWrap.classList.contains("mw-collapsed"); if (isCollapsed) { // need to create spans in the first place createBtn.click(); } else { wrapSpaces.click(); divWrap.textContent = "Spaces now wrapped."; } } var saveBtn = document.getElementById("saveSpans"); if (saveBtn) { saveBtn.onclick = saveSpansToDoc; } var spans = document.querySelectorAll('span.hebrew, span.english'); for (var i = 0; i < spans.length; i++) { attachSpanClickHandler(spans[i]); } document.querySelectorAll('input[name="BuildTextTable[Mode]"]').forEach(function (radio) { radio.addEventListener("change", function (event) { var selectedMode = event.target.value; console.log("Mode changed to: " + selectedMode); updateOverlayModeClass(); }); }); var overlay = document.getElementById("buildOverlay"); if (overlay) { document.addEventListener("keydown", skipToNextHebrewWord); } }); /* =============================== Overlays js ==============================*/ importScript('MediaWiki:BuildTextTable.js'); function saveRenderedTableAsSubpage(currentPage) { var api = new mw.Api(); // If currentPage is not a string, get the title from MediaWiki if (typeof currentPage !== "string") { var divTrigger = document.getElementById("triggerForSavingSubpage"); if (divTrigger) currentPage = divTrigger.textContent; } if (typeof currentPage !== "string") { currentPage = mw.config.get("wgPageName"); } var targetPage = currentPage.replace(/ /g, "_") + "/Rendered"; var psalmTable = document.querySelector("table.psalm-table"); if (!psalmTable) { alert("No psalm-table found."); return; } var renderedWikitext = psalmTable.outerHTML .replace(/psalm-table/gi, 'psalm-table pre-rendered') .replace(/<tbody>/gi, '') .replace(/<\/tbody>/gi, ''); console.log("API endpoint is:", mw.util.wikiScript('api')); api.postWithEditToken({ action: "edit", title: targetPage, text: renderedWikitext, summary: "Programmatically saving rendered Psalm table", format: "json", contentmodel: "wikitext", contentformat: "text/x-wiki" }).then(function (data) { if (data && data.edit && data.edit.result === "Success") { alert("Successfully saved to: " + targetPage); } else { alert("Edit failed. See console."); console.error(data); } }).catch(function (err) { alert("API call failed."); console.error(err); }); } $(document).ready(function () { /* if (document.querySelector("#triggerForSavingSubpage")) { var saveButton = document.querySelector("#wpSave"); if (saveButton) { saveButton.addEventListener("click", saveRenderedTableAsSubpage); } } */ if (document.getElementById("overlay-1")){ // set section colored boxes to their actual colors $('input[name*="[sectionColor]"]').each(function () { $(this).css('background-color', $(this).val()); }); $('input[name*="[sectionColor]"]').on('input', function () { $(this).css('background-color', $(this).val()); }); } var overlayTableDiv = document.getElementById("overlay-1"); var overlayTable = null; if (overlayTableDiv){ overlayTable = overlayTableDiv.querySelector('table'); } var INSERT_BLANK_AFTER_SECTIONS = false; var INCLUDE_EMOTION_COLUMN = true; var SECTION_HEADING = 'Structure' var DEBUG = true; function mergeCellsInRange(headerText, heading) { if (!overlayTable) return; var cells = overlayTable.querySelectorAll( '[section-type="' + headerText + '"][section-heading="' + heading + '"]' ); if (!cells || cells.length === 0) return; var firstCell = cells[0]; firstCell.rowSpan = cells.length; var imageRegex = /^File:([^ ]+\.(jpg|jpeg|png|gif|svg))$/i; var match = heading.match(imageRegex); if (match) { var filename = match[1]; var encodedFilename = encodeURIComponent(filename); var thumbBase = '/mediawiki/thumb.php?f=' + encodedFilename; var src = thumbBase + '&width=50'; var srcset = thumbBase + '&width=75 1.5x, ' + thumbBase + '&width=100 2x'; var linkHref = '/w/File:' + encodedFilename; firstCell.innerHTML = '<div class="center"><div class="floatnone">' + '<a href="' + linkHref + '" class="image">' + '<img alt="' + filename + '" src="' + src + '" decoding="async" width="50" height="50" class="aag-icon" ' + 'srcset="' + srcset + '" data-file-width="1200" data-file-height="1200">' + '</a></div></div>'; } else { firstCell.textContent = heading; } for (var k = 1; k < cells.length; k++) { if (cells[k] && cells[k].parentNode) { cells[k].parentNode.removeChild(cells[k]); } } } function applySections(){ 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-sections"); if (encoded) console.log("Applying sections:" + encoded); if (!encoded) continue; try { var decoded = decodeURIComponent(encoded); var sections = JSON.parse(decoded); var sectionInsertions = []; // store all 'Section' entries var speakerAndAddresseeBarsAdded = false; var sectionsAdded = false; var subSectionsAdded = false; // first, cycle through to BUILD all the sections for (var i = 0; i < sections.length; i++) { var section = sections[i]; if (section.Level === 'Speaker' && !speakerAndAddresseeBarsAdded) { adjustSectionColumns("includeSpeakerBars", true); speakerAndAddresseeBarsAdded = true; } else if (section.Level === 'Section') { if (!sectionsAdded) { adjustSectionColumns("includeSections", true); sectionsAdded = true; } if (INSERT_BLANK_AFTER_SECTIONS) { sectionInsertions.push(section); } } else if (section.Level === 'Subsection') { if (!subSectionsAdded) { adjustSectionColumns("includeSubsections", true); subSectionsAdded = true; } } } // then, cycle through to HIGHLIGHT for (i = 0; i < sections.length; i++) { var section = sections[i]; highlightCellsInLineRange( section.Level, section.Heading, section.FirstLine, section.LastLine, section.Color); if (section.Emotion) { // handle emotions as well highlightCellsInLineRange( "E", section.Emotion, section.FirstLine, section.LastLine, section.Color); } } // merge // then, cycle through to HIGHLIGHT for (i = 0; i < sections.length; i++) { var section = sections[i]; //console.log("Seeking to merge " + section.Level + " " + section.Heading + " with first " + section.FirstLine + " and last " + section.LastLine); mergeCellsInRange(section.Level, section.Heading, section.FirstLine, section.LastLine); if (section.Emotion) { // handle emotions as well mergeCellsInRange( "E", section.Emotion, section.FirstLine, section.LastLine); } } if (INSERT_BLANK_AFTER_SECTIONS) { // Now insert all the blank rows AFTER all highlighting is done for (var j = 0; j < sectionInsertions.length; j++) { var lastRow = getRowFromLineId(sectionInsertions[j].LastLine); var allRows = overlayTable.getElementsByTagName('tr'); // only if NOT the last row of the table if (lastRow ===! allRows[allRows.length-1]) insertBlankRowAfter(lastRow); } // add empty columns insertBlankColumnAt(1); insertBlankColumnAt(3); insertBlankColumnAt(8); } } catch (e) { console.error("Error processing sections for", containerId, e); } } } function highlightCellsInLineRange(headerText, heading, firstLine, lastLine, highlightColor) { if (!overlayTable) return; var rows = overlayTable.querySelectorAll('tr'); if (rows.length === 0) return; var headerRow = overlayTable.querySelector('thead tr') || rows[0]; var headers = headerRow.getElementsByTagName('th'); // Find the target column var targetColIndex = -1; for (var i = 0; i < headers.length; i++) { if (headers[i].textContent.trim() === headerText.trim() || (headerText === "Section" && headers[i].textContent === SECTION_HEADING)) { //console.log("Found " + headers[i].textContent + " at column " + i); targetColIndex = i; break; } } if (targetColIndex === -1) { console.warn("Header not found: " + headerText); return; } // collect all the affected cells for (var r = 1; r < rows.length; r++) { var row = rows[r]; var lineDiv = row.querySelector('div.line[data-line]'); if (!lineDiv) continue; var lineId = lineDiv.getAttribute('data-line'); if (!lineId || typeof lineId !== 'string') continue; if (isLineInRange(lineId, firstLine, lastLine)) { // found an affected cell! var cells = row.getElementsByTagName('td'); if (targetColIndex < cells.length) { cells[targetColIndex].style.backgroundColor = highlightColor; cells[targetColIndex].setAttribute('section-type',headerText); cells[targetColIndex].setAttribute('section-heading', heading); } } } } // constants for adding sections var SECTION_LEVEL = 1; var SECTION_HEADER = 2; var SECTION_BEGIN = 3; var SECTION_END = 4; var SECTION_COLOR =5; var SECTION_EMOTION = 6; function parseLineId(lineStr) { if (typeof lineStr !== "string") return null; lineStr = lineStr.trim(); // Match line IDs like "1", "1a", "12b", etc. var match = /^(\d+)([a-z]?)$/i.exec(lineStr); if (!match) return null; var base = parseInt(match[1], 10); var letter = match[2] ? match[2].toLowerCase() : ""; return { base: base, offset: letter ? (letter.charCodeAt(0) - 96) : 0 // a = 1, b = 2, ... }; } function toSortableValue(parsed) { if (!parsed) return -1; return parsed.base + parsed.offset / 100; } function rangesOverlap(startA, endA, startB, endB) { var aStart = toSortableValue(parseLineId(startA)); var aEnd = toSortableValue(parseLineId(endA)); var bStart = toSortableValue(parseLineId(startB)); var bEnd = toSortableValue(parseLineId(endB)); return aStart <= bEnd && bStart <= aEnd; } // used on Overlays when adding sections function addSectionToSpreadsheetOld(cell, colIndex) { var addButton = document.querySelector('#overlay-sections a.oo-ui-buttonElement-button'); var rows = document.querySelectorAll('#overlay-sections .multipleTemplateInstance tr'); var sectionRow = null; var sectionColumn = ''; var upperLimit = findUpperSectionLimit(cell).getAttribute('data-line'); var lowerLimit = findLowerSectionLimit(cell).getAttribute('data-line'); var classList = cell.className.split(/\s+/); var matchedBar = null; for (var i = 0; i < classList.length; i++) { if (/-bar$/.test(classList[i])) { matchedBar = classList[i]; break; } } if (matchedBar) { // Capitalize first letter, lower case the rest var prefix = matchedBar.replace(/-bar$/, ''); sectionColumn = prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase(); } else if (colIndex < headerCells.length) { sectionColumn = headerCells[colIndex].textContent; } if (DEBUG) console.log("Considering adding section to spreadsheet from cell " + cell.getAttribute('data-line') + " with upper limit " + upperLimit + " and lower limit " + lowerLimit); // hunt for a matching row for (var i = 0; i < rows.length; i++) { var cells = rows[i].cells; var container = rows[i].querySelector('td.instanceMain'); if (!container) { if (DEBUG) console.warn("Row " + i + " has no instanceMain cell."); continue; } var beginField = container.querySelector('input[origname="Overlay Section[firstLine]"]'); var endField = container.querySelector('input[origname="Overlay Section[lastLine]"]'); if (DEBUG) { var beginVal = beginField ? beginField.value : "(missing)"; var endVal = endField ? endField.value : "(missing)"; console.log("Looking at row " + i + " with limits " + beginVal + " and " + endVal); } var colorField = container.querySelector('input[origname="Overlay Section[sectionColor]"]'); if ( colorField && beginField && endField && colorField.value === selectedColor && rangesOverlap(beginField.value, endField.value, upperLimit, lowerLimit) ) { sectionRow = rows[i]; break; } } if (!sectionRow && addButton) { if (DEBUG) console.log("Adding section to spreadsheet"); // If no matching row, click Add and fill the new one addButton.click(); setTimeout(function () { var sectionsGrid = document.querySelector('#overlay-sections table.multipleTemplateInstanceTable'); if (sectionsGrid.length === 0) { console.warn("Could not find sections grid."); return; } var instances = document.querySelectorAll('#overlay-sections .multipleTemplateInstance'); if (instances.length === 0) { console.warn("No new section instance found."); return; } var lastInstance = instances[instances.length - 1]; var container = lastInstance.querySelector('td.instanceMain'); if (!container) { console.warn("Could not find input container in last instance."); return; } if (DEBUG) { console.log("Filling in values: " + sectionColumn + ", " + upperLimit + ", " + lowerLimit + ", " + selectedColor); } // Helper to set input/select/textarea values safely function setFieldValue(selector, value) { var field = container.querySelector(selector); if (field) { field.value = value; } else if (DEBUG) { console.warn("Missing field for selector: " + selector); } } setFieldValue('select[origname="Overlay Section[sectionLevel]"]', sectionColumn); //setFieldValue('textarea[origname="Overlay Section[sectionName]"]', sectionHeader); setFieldValue('input[origname="Overlay Section[firstLine]"]', upperLimit); setFieldValue('input[origname="Overlay Section[lastLine]"]', lowerLimit); setFieldValue('input[origname="Overlay Section[sectionColor]"]', selectedColor); setFieldValue('input[origname="Overlay Section[sectionAnnotation]"]', selectedAnnotation); //setFieldValue('input[origname="Overlay Section[emotionLabel]"]', ''); // Optional: add default emotion if (DEBUG) console.log("Added a new row for " + sectionColumn + " color " + selectedColor); }, 150); // allow enough time for row to be added (used to be 150) } else if (sectionRow) { // Row already exists — update it var container = sectionRow.querySelector('td.instanceMain'); if (!container) { console.warn("Could not find input container in matched row."); return; } // Helper to set input/select/textarea values safely function setFieldValue(selector, value) { var field = container.querySelector(selector); if (field) { field.value = value; } else if (DEBUG) { console.warn("Missing field for selector: " + selector); } } setFieldValue('select[origname="Overlay Section[sectionLevel]"]', sectionColumn); //setFieldValue('textarea[origname="Overlay Section[sectionName]"]', sectionHeader); setFieldValue('input[origname="Overlay Section[firstLine]"]', upperLimit); setFieldValue('input[origname="Overlay Section[lastLine]"]', lowerLimit); setFieldValue('input[origname="Overlay Section[sectionColor]"]', selectedColor); setFieldValue('input[origname="Overlay Section[sectionAnnotation]"]', selectedAnnotation); //setFieldValue('input[origname="Overlay Section[emotionLabel]"]', ''); // Or your desired default if (DEBUG) { console.log("Modified existing row for " + sectionColumn + " with color " + selectedColor); } } else { console.warn("No add button found or spreadsheet table missing."); } } // Compare line + letter pairs, treating "1" as 1.0, "1a" as 1.1, "1b" as 1.2, etc. function isLineInRange(line, firstLine, lastLine) { function parse(lineStr) { if (typeof lineStr !== "string") return null; lineStr = lineStr.trim(); // Match "1", "1a", "12b", etc. var match = /^(\d+)([a-z]?)$/i.exec(lineStr); if (!match) return null; var number = parseInt(match[1], 10); var letter = match[2] ? match[2].toLowerCase() : null; return { base: number, offset: letter ? (letter.charCodeAt(0) - 96) : 0 // 'a' = 1, 'b' = 2, ..., no letter = 0 }; } var lineVal = parse(line); var firstVal = parse(firstLine); var lastVal = parse(lastLine); if (!lineVal || !firstVal || !lastVal) { console.warn("Invalid line format:", line, firstLine, lastLine); return false; } var value = toSortableValue(lineVal); var first = toSortableValue(firstVal); var last = toSortableValue(lastVal); return value >= first && value <= last; } // compare line + letter pairs, seeing if a given line is between another first + last function isLineInRangeStrict(line, firstLine, lastLine) { function parse(lineStr) { lineStr = lineStr.trim(); // Remove any leading/trailing spaces var match = /^(\d+)([a-z])$/i.exec(lineStr); if (!match) return null; return { number: parseInt(match[1], 10), letter: match[2].toLowerCase() }; } var lineVal = parse(line); var firstVal = parse(firstLine); var lastVal = parse(lastLine); if (!lineVal || !firstVal || !lastVal) { console.warn("Invalid line format:", line, firstLine, lastLine); return false; } function compare(a, b) { if (a.number !== b.number) { return a.number - b.number; } return a.letter.charCodeAt(0) - b.letter.charCodeAt(0); } return compare(firstVal, lineVal) <= 0 && compare(lineVal, lastVal) <= 0; } // when building sections, handle colors & large sections function handleSectionCellClick (event) { var cell = event.currentTarget; var selectedBg = selectedColor; //var selectedBg = window.selectedBg || '#f8c936'; cell.style.backgroundColor = selectedBg; var row = cell.parentNode; var table = row; while (table && table.tagName !== 'TABLE') { table = table.parentNode; } if (!table) return null; var rows = table.getElementsByTagName('tr'); // Determine current row and column index var rowIndex = -1; var colIndex = -1; for (var i = 0; i < rows.length; i++) { if (rows[i] === row) { rowIndex = i; break; } } if (rowIndex === -1) return null; var cells = rows[rowIndex].cells; for (var j = 0, col = 0; j < cells.length; j++) { if (cells[j] === cell) { colIndex = col; break; } col += cells[j].colSpan || 1; } //var aboveCell = findCellAbove(cell, rows, rowIndex, colIndex, false); //if (aboveCell) aboveCell.style.backgroundColor='blue'; var aboveMatchingCell = findCellAbove(cell, rows, rowIndex, colIndex, true); if (aboveMatchingCell){ //aboveMatchingCell.style.backgroundColor='orange'; highlightCellsVertically(aboveMatchingCell, cell); } var belowMatchingCell = findCellBelow(cell, rows, rowIndex, colIndex, true); if (belowMatchingCell){ //aboveMatchingCell.style.backgroundColor='orange'; highlightCellsVertically(cell, belowMatchingCell); } // create section addSectionToSpreadsheetOld(cell, colIndex); } var sectionLevelsAdded = 0; var speakerAndAddresseeBarsAdded = false; var sectionsAdded = false; var subSectionsAdded = false; var sectionCheckboxes = document.querySelectorAll(".pf-checkbox-input-container"); // Attach checkbox listeners for the Overlays form to ADD columns if (sectionCheckboxes){ for (var i = 0; i < sectionCheckboxes.length; i++) { var checkboxes = sectionCheckboxes[i].querySelectorAll('input[type="checkbox"]'); for (var j = 0; j < checkboxes.length; j++) { (function (checkbox) { checkbox.addEventListener("click", function () { var name = checkbox.name; var match = name.match(/\[([^\]]+)\]/); var firstKey = match ? match[1] : null; adjustSectionColumns(firstKey, checkbox.checked); }); })(checkboxes[j]); } } } // standard styling for section headers and cells var stylePropsTh = { padding: '1rem', textAlign: 'center', verticalAlign: 'middle' }; var stylePropsTd = { minWidth: '1rem', border: '1px solid #ccc', backgroundColor: 'white' }; function adjustSectionColumns(sectionFlag, checked) { var allRows = overlayTable.querySelectorAll('tr'); var headerRow = overlayTable.querySelector('thead tr') || allRows[0]; var bodyRows = overlayTable.querySelectorAll('tbody tr'); // Speaker + Addressee if (sectionFlag === "includeSpeakerBars") { if (checked && !speakerAndAddresseeBarsAdded) { var newTh1 = newElement('th', 'Speaker', stylePropsTh, 'speaker'); var newTh2 = newElement('th', 'Addressee', stylePropsTh, 'addressee'); headerRow.insertBefore(newTh1, headerRow.firstChild); headerRow.appendChild(newTh2); for (var k = 1; k < bodyRows.length; k++) { var row = bodyRows[k]; var speakerTd = newElement('td', '', stylePropsTd, 'speaker-bar'); speakerTd.setAttribute('data-line', getLineIdFromRow(row)); speakerTd.textContent = getLineIdFromRow(row); speakerTd.style.color = 'lightgray'; speakerTd.onclick = handleSectionCellClick; row.insertBefore(speakerTd, row.firstChild); var addresseeTd = newElement('td', '', stylePropsTd, 'addressee-bar'); addresseeTd.onclick = handleSectionCellClick; addresseeTd.setAttribute('data-line', getLineIdFromRow(row)); addresseeTd.textContent = getLineIdFromRow(row); addresseeTd.style.color = 'lightgray'; row.appendChild(addresseeTd); } speakerAndAddresseeBarsAdded = true; } else if (!checked && speakerAndAddresseeBarsAdded) { removeColumn('Addressee', headerRow, bodyRows); removeColumn('Speaker', headerRow, bodyRows); speakerAndAddresseeBarsAdded = false; } } // Section if (sectionFlag === "includeSections") { if (checked && !sectionsAdded) { var sectionHeader = newElement('th', SECTION_HEADING, stylePropsTh, 'section'); var insertIndex = 0; //getInsertIndex(headerRow, 'Speaker', 0); if (speakerAndAddresseeBarsAdded) insertIndex = 1; headerRow.insertBefore(sectionHeader, headerRow.children[insertIndex]); //console.log("Inserted " + sectionFlag + " at index " + insertIndex); for (var m = 1; m < bodyRows.length; m++) { var row = bodyRows[m]; var sectionTd = newElement('td', '', stylePropsTd, 'section'); sectionTd.setAttribute('data-line', getLineIdFromRow(row)); sectionTd.textContent = getLineIdFromRow(row); sectionTd.style.color = 'lightgray'; sectionTd.onclick = handleSectionCellClick; row.insertBefore(sectionTd, row.children[insertIndex]); } if (INCLUDE_EMOTION_COLUMN){ sectionHeader = newElement('th', 'E', stylePropsTh, 'emotion'); insertIndex = 4; //getInsertIndex(headerRow, 'Speaker', 0); if (speakerAndAddresseeBarsAdded) insertIndex++; headerRow.insertBefore(sectionHeader, headerRow.children[insertIndex]); //console.log("Inserted emotion column at index " + insertIndex); for (m = 1; m < bodyRows.length; m++) { row = bodyRows[m]; sectionTd = newElement('td', '', stylePropsTd, 'emotion'); sectionTd.setAttribute('data-line', getLineIdFromRow(row)); sectionTd.textContent = getLineIdFromRow(row); sectionTd.onclick = handleSectionCellClick; row.insertBefore(sectionTd, row.children[insertIndex]); } } sectionsAdded = true; } else if (!checked && sectionsAdded) { removeColumn(SECTION_HEADING, headerRow, bodyRows); sectionsAdded = false; } } // Subsection if (sectionFlag === "includeSubsections") { if (checked && !subSectionsAdded) { var subsectionHeader = newElement('th', 'Subsection', stylePropsTh, 'subsection'); var subInsertIndex = 1; //getInsertIndex(headerRow, 'Speaker', 0); if (speakerAndAddresseeBarsAdded) subInsertIndex = 2; if (!sectionsAdded) subInsertIndex --; //var subInsertIndex = getInsertIndex(headerRow, 'Section', -1); headerRow.insertBefore(subsectionHeader, headerRow.children[subInsertIndex]); for (var n = 1; n < bodyRows.length; n++) { var row = bodyRows[n]; var subsectionTd = newElement('td', '', stylePropsTd, 'subsection'); subsectionTd.setAttribute('data-line', getLineIdFromRow(row)); subsectionTd.textContent = getLineIdFromRow(row); subsectionTd.onclick = handleSectionCellClick; row.insertBefore(subsectionTd, row.children[subInsertIndex]); } subSectionsAdded = true; } else if (!checked && subSectionsAdded) { removeColumn('Subsection', headerRow, bodyRows); subSectionsAdded = false; } } } // ensure the mode & css are aligned upon load var overlay = document.getElementById('buildOverlay'); var modeField = document.querySelector('input[name="BuildTextTable[Mode]"]:checked'); /* if (overlay && modeField) { if (modeField.value.includes("Align")) { overlay.classList.add("mode-align"); } else { overlay.classList.add("mode-split"); } } */ // BEGIN TEXT OVERLAY CODE // === TEXT OVERLAY COLOR PICKER LOGIC === // var selectedColor = 'red'; var selectedAnnotation = '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 var divColorMap = document.getElementById("color-map"); var colorMap = {}; var colorPicker = document.querySelector("div.color-picker"); 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 = ''; /* // 3a. Setup listener for radio buttons (when using radio buttons) document.querySelectorAll('input[name="Text Overlay[Participant]"]').forEach(function (radio) { radio.addEventListener('change', function (e) { selectedAnnotation = e.target.value; selectedColor = colorMap[selectedAnnotation] || ''; console.warn("Selected:", selectedAnnotation, "→", selectedColor); }); });*/ // 3b. Setup listener for color grid (when using that) // Color selection via grid 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 || ''; selectedAnnotation = cell.dataset.participant || 'unknown'; // console.log("Selected color from grid: " + selectedColor + " for annotation: " + selectedAnnotation); }); }); // handle any overlay colors at page load applyAllOverlayAnnotations(); applySections(); // on click - individual words document.querySelectorAll('div.overlay span.hebrew').forEach(function (span) { span.style.cursor = 'pointer'; span.addEventListener('click', function (event) { event.stopPropagation(); // prevent the line-level click from firing console.log("In click handler to highlight a word"); 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, annotation:selectedAnnotation, color: selectedColor }); // Add a new form instance by clicking the "Add another" button var addButton = document.querySelector('#overlay-annotations .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('#overlay-annotations .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 annotationInput = latest.querySelector('input[name$="[Participant]"]'); var colorInput = latest.querySelector('input[name$="[Color]"]'); if (hebrewInput) hebrewInput.value = word; if (wordIdInput) wordIdInput.value = id; if (annotationInput) annotationInput.value = selectedAnnotation; if (colorInput) { colorInput.value = selectedColor; colorInput.style.backgroundColor = selectedColor; } } }, 100); // Adjust delay if needed } } console.log("Setting to " + selectedColor); span.style.backgroundColor = selectedColor; if (glossEl) glossEl.style.backgroundColor = selectedColor; }); }); document.querySelectorAll('#overlay-1 div.overlay div.line').forEach(function (lineDiv) { lineDiv.style.cursor = 'pointer'; lineDiv.addEventListener('click', function (event) { if (!selectedColor || !selectedAnnotation) { console.warn("No color or annotation selected."); return; } var lineId = lineDiv.dataset.line; if (!lineId) return; var matchingLines = document.querySelectorAll('div.line[data-line="' + CSS.escape(lineId) + '"]'); var lineText = matchingLines[0].textContent.trim(); // Shift-click: remove annotation and matching form row if (event.shiftKey) { coloredWords = coloredWords.filter(function (w) { return !(w.id === lineId && w.annotation === selectedAnnotation && w.color === selectedColor); }); // Search Overlay Section entries and remove matching ones document.querySelectorAll('#overlay-annotations .multipleTemplateInstance').forEach(function (instance) { var levelInput = instance.querySelector('select[name$="[sectionLevel]"]'); var firstLineInput = instance.querySelector('input[name$="[firstLine]"]'); var lastLineInput = instance.querySelector('input[name$="[lastLine]"]'); var colorInput = instance.querySelector('input[name$="[sectionColor]"]'); if ( firstLineInput && firstLineInput.value === lineId && lastLineInput && lastLineInput.value === lineId && colorInput && colorInput.value === selectedColor ) { instance.remove(); } }); matchingLines.forEach(function (div) { div.style.backgroundColor = ''; }); console.log("Removed annotation for line:", lineId); return; } // Normal click: add annotation if not present if (!coloredWords.some(function (w) { return w.id === lineId; })) { coloredWords.push({ id: lineId, hebrew: lineText, gloss: '', annotation: selectedAnnotation, color: selectedColor }); var addButton = document.querySelector('#overlay-annotations .multipleTemplateAdder a'); if (addButton) { addButton.click(); setTimeout(function () { var allInstances = document.querySelectorAll('#overlay-annotations .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 annotationInput = latest.querySelector('input[name$="[Participant]"]'); var colorInput = latest.querySelector('input[name$="[Color]"]'); if (hebrewInput) hebrewInput.value = lineText; if (wordIdInput) wordIdInput.value = lineId; if (annotationInput) annotationInput.value = selectedAnnotation; if (colorInput) { colorInput.value = selectedColor; colorInput.style.backgroundColor = selectedColor; } } }, 200); } } matchingLines.forEach(function (div) { div.style.backgroundColor = selectedColor; }); console.log("Highlighted all lines with data-line:", lineId); }); }); // export a list to be saved... since this is NOT using PageForms 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 applyOverlayColors(containerId, annotations) { var container = document.getElementById(containerId); if (!container) return; for (var wordID in annotations) { if (annotations.hasOwnProperty(wordID) && wordID.trim() !== "") { var rawKey = annotations[wordID] || ""; var normalizedKey = rawKey.toLowerCase(); // Try direct match var color = colorMap[normalizedKey]; // Fallback: append ".1" if no color found and key lacks suffix if (!color && normalizedKey.indexOf('.') === -1) { normalizedKey = normalizedKey + ".1"; color = colorMap[normalizedKey]; console.log("Fallback to \"" + normalizedKey + "\" for " + wordID); } console.log("Seeking color for \"" + wordID + "\" using key \"" + normalizedKey + "\":", color); if (!color) continue; var elements = container.querySelectorAll("." + CSS.escape(wordID)); var lines = container.querySelectorAll('[data-line="' + wordID + '"]'); console.log("Applying color " + color + " to " + elements.length + " elements and " + lines.length + " lines"); for (var j = 0; j < elements.length; j++) { elements[j].style.backgroundColor = color; } for (var k = 0; k < lines.length; k++) { lines[k].style.backgroundColor = color; } } } } function applyAllOverlayAnnotations() { var overlays = document.querySelectorAll("div.overlay"); // If no overlays are found, print a warning if (overlays.length === 0) { console.warn("No overlay container 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); console.log("Found annotations: " + JSON.stringify(annotations, null, 2)); applyOverlayColors(containerId, annotations); } catch (e) { console.error("Error processing annotations for", containerId, e); } /* encoded = overlay.getAttribute("data-sections"); if (!encoded) continue; try { decoded = decodeURIComponent(encoded); annotations = JSON.parse(decoded); applyOverlaySections(containerId, annotations); } catch (e) { console.error("Error processing sections for", containerId, e); } */ } } function hexToRgb(hex) { // Remove leading '#' if present hex = hex.replace(/^#/, ''); if (hex.length === 3) { // Convert shorthand (#f00) to full form (#ff0000) hex = hex.replace(/(.)/g, '$1$1'); } var bigint = parseInt(hex, 16); var r = (bigint >> 16) & 255; var g = (bigint >> 8) & 255; var b = bigint & 255; return 'rgb(' + r + ', ' + g + ', ' + b + ')'; } function findUpperSectionLimit(currentCell) { var table = currentCell.closest('table'); if (!table) return currentCell; var rows = table.getElementsByTagName('tr'); var row = currentCell.parentNode; var rowIndex = -1; for (var i = 0; i < rows.length; i++) { if (rows[i] === row) { rowIndex = i; break; } } if (rowIndex === -1) return currentCell; var cells = row.cells; var colIndex = -1; for (var j = 0; j < cells.length; j++) { if (cells[j] === currentCell) { colIndex = j; break; } } if (colIndex === -1) return currentCell; var lastMatch = currentCell; var targetColor = hexToRgb(selectedColor); for (var r = rowIndex - 1; r >= 0; r--) { var aboveRow = rows[r]; var cell = aboveRow.cells[colIndex]; if (!cell) break; var cellColor = window.getComputedStyle(cell).backgroundColor; if (cellColor === targetColor) { lastMatch = cell; } else { break; } } return lastMatch; } function findLowerSectionLimit(currentCell) { var table = currentCell.closest('table'); if (!table) return currentCell; var rows = table.getElementsByTagName('tr'); var row = currentCell.parentNode; var rowIndex = -1; for (var i = 0; i < rows.length; i++) { if (rows[i] === row) { rowIndex = i; break; } } if (rowIndex === -1) return currentCell; var cells = row.cells; var colIndex = -1; for (var j = 0; j < cells.length; j++) { if (cells[j] === currentCell) { colIndex = j; break; } } if (colIndex === -1) return currentCell; var lastMatch = currentCell; var targetColor = hexToRgb(selectedColor); for (var r = rowIndex + 1; r < rows.length; r++) { var belowRow = rows[r]; var cell = belowRow.cells[colIndex]; if (!cell) break; var cellColor = window.getComputedStyle(cell).backgroundColor; if (cellColor === targetColor) { lastMatch = cell; } else { break; } } return lastMatch; } function findCellAbove(currentCell, rows, rowIndex, colIndex, matchColor) { //console.log("Looking for cell above row " + rowIndex + " and " + colIndex ); // Search upwards for the cell in the same visual column var runningRow = rowIndex - 1; while (runningRow >= 0) { //console.log("Checking row " + runningRow); var aboveRow = rows[runningRow]; var aboveCells = aboveRow.cells; var colPos = 0; for (var k = 0; k < aboveCells.length; k++) { var cell = aboveCells[k]; var span = cell.colSpan || 1; //console.log("Looking at colPos " + colPos + " with " + cell.colSpan); if (colPos <= colIndex && colIndex < colPos + span) { if (matchColor) { var cellColor = window.getComputedStyle(cell).backgroundColor; var targetColor = hexToRgb(selectedColor); if (cellColor === targetColor) { return cell; } else if (cellColor !== 'rgb(255, 255, 255)') { //console.log("Found interrupting cell with color " + cellColor); return null; // cell has a DIFFERENT color } } else{ return cell; } /* var rowSpan = cell.rowSpan || 1; var cellBottomRow = runningRow + rowSpan - 1; if (cellBottomRow >= rowIndex) { console.log("Found cell"); return cell; }*/ } colPos += span; } runningRow--; } return null; } if (document.getElementById("export-sections-button")) { document.getElementById("export-sections-button").addEventListener("click", function () { var overlay = document.querySelector(".overlay"); if (!overlay) return; var annotationsRaw = overlay.getAttribute("data-annotations"); var annotations = JSON.parse(annotationsRaw.replace(/"/g, '"')); // build groups from the color map list var groups = {}; for (var lineID in annotations) { if (annotations.hasOwnProperty(lineID)) { var color = annotations[lineID]; if (!groups[color]) { groups[color] = []; } groups[color].push(lineID); } } var output = ""; for (var color in groups) { if (groups.hasOwnProperty(color)) { var lines = groups[color]; // Sort lines numerically where possible lines.sort(function (a, b) { var re = /^(\d+)([a-z]*)$/; var ma = re.exec(a); var mb = re.exec(b); var na = ma ? parseInt(ma[1], 10) : 0; var nb = mb ? parseInt(mb[1], 10) : 0; if (na !== nb) return na - nb; return a.localeCompare(b); }); var first = lines[0]; var last = lines[lines.length - 1]; // Find the line element var lineEl = overlay.querySelector('[data-line="' + first + '"]'); var levelClass = 'unknown'; if (lineEl) { var row = lineEl.closest("tr"); if (row) { var sectionCell = row.querySelector('td[class^="section-"]'); if (sectionCell) { var classList = sectionCell.className.split(/\s+/); for (var i = 0; i < classList.length; i++) { if (classList[i].indexOf("section-") === 0) { levelClass = classList[i]; // e.g. "section-2" break; } } } } } output += "{{Overlay Section |level=" + levelClass + " |firstLine=" + first + " |lastLine=" + last + " |color=" + color + "}},"; } } // Set value into input box by name var inputs = document.getElementsByName('Text Overlay[Sections]'); if (inputs.length > 0) { inputs[0].value = output.replace(/,$/, ''); } }); } // Helper functions function compareLineIds(a, b) { var re = /^(\d+)([a-z]*)$/i; var ma = re.exec(a); var mb = re.exec(b); if (!ma || !mb) return 0; var na = parseInt(ma[1], 10); var nb = parseInt(mb[1], 10); if (na !== nb) return na - nb; // Compare letters if numbers match var sa = ma[2]; var sb = mb[2]; if (sa < sb) return -1; if (sa > sb) return 1; return 0; } function getColorForOverlay(name) { var colors = { "warm.1": "#f8c936", "cool.2": "#5bc0de", "vibrant.2": "#d9534f" // Add more mappings as needed }; return colors[name] || name; // fallback to raw value } function newElement(tag, content, styleProps, className) { var el = document.createElement(tag); el.textContent = content; setStyle(el, styleProps); if (className) el.className = className; return el; } function setStyle(element, styleProps) { for (var prop in styleProps) { element.style[prop] = styleProps[prop]; } } function removeColumn(label, headerRow, bodyRows) { var index = -1; for (var i = 0; i < headerRow.children.length; i++) { if (headerRow.children[i].textContent === label) { index = i; headerRow.removeChild(headerRow.children[i]); break; } } if (index !== -1) { for (var j = 1; j < bodyRows.length; j++) { if (bodyRows[j].children[index]) { bodyRows[j].removeChild(bodyRows[j].children[index]); } } } } // Finds where to insert a new column *after* the given label function getInsertIndex(headerRow, afterLabel, defaultIndex) { for (var i = 0; i < headerRow.children.length; i++) { if (headerRow.children[i].textContent === afterLabel) { //console.log("Found " + headerRow + " at index " + i); return i + 1; } } if (defaultIndex > -1) return defaultIndex; console.warn("Did not find " + afterLabel); return headerRow.children.length; // default: append } function getLineIdFromRow(row) { if (!row){ console.warn("Attempting to get line from a null row"); return null; } if (!row || row.tagName !== 'TR') { console.warn("Attempting to get line from a non-row"); return null; } var divs = row.getElementsByTagName('div'); for (var i = 0; i < divs.length; i++) { if (divs[i].className.indexOf('line') !== -1 && divs[i].getAttribute('data-line')) { return divs[i].getAttribute('data-line'); } } console.warn("No matching data-line attribute found"); return null; // No matching <div class="line" data-line=...> found } function getRowFromLineId(lineId) { if (!lineId || !overlayTable) return null; var rows = overlayTable.getElementsByTagName('tr'); for (var i = 0; i < rows.length; i++) { var divs = rows[i].getElementsByTagName('div'); for (var j = 0; j < divs.length; j++) { if (divs[j].className.indexOf('line') !== -1 && divs[j].getAttribute('data-line') === lineId) { return rows[i]; } } } return null; // No matching row found } function insertBlankRowAfter(row) { if (!row || row.tagName !== 'TR') return; var newRow = document.createElement('tr'); var newCell = document.createElement('td'); newCell.colSpan = row.cells.length; // or a fixed number like 7 newCell.style.backgroundColor = 'black'; newCell.innerHTML = ' '; newRow.appendChild(newCell); if (row.nextSibling) { row.parentNode.insertBefore(newRow, row.nextSibling); } else { row.parentNode.appendChild(newRow); } return newRow; } function insertBlankColumnAt(colIndex) { if (!overlayTable) return; var rows = overlayTable.getElementsByTagName('tr'); if (rows.length < 2) return; // skip if there's no body var newCell = document.createElement('td'); newCell.style.backgroundColor = 'black'; newCell.innerHTML = ' '; newCell.rowSpan = rows.length; // insert into first row var firstRow = rows[0]; if (colIndex >= 0 && colIndex <= firstRow.children.length) { firstRow.insertBefore(newCell, firstRow.children[colIndex]); } else { firstRow.appendChild(newCell); } } function findCellBelow(currentCell, rows, rowIndex, colIndex, matchColor) { //console.log("Looking for cell below row " + rowIndex + " and " + colIndex ); // Search downwards for the cell in the same visual column var runningRow = rowIndex + 1; while (runningRow < rows.length) { //console.log("Checking row " + runningRow); var belowRow = rows[runningRow]; var belowCells = belowRow.cells; var colPos = 0; for (var k = 0; k < belowCells.length; k++) { var cell = belowCells[k]; var span = cell.colSpan || 1; //console.log("Looking at colPos " + colPos + " with " + cell.colSpan); if (colPos <= colIndex && colIndex < colPos + span) { if (matchColor) { var cellColor = window.getComputedStyle(cell).backgroundColor; var targetColor = hexToRgb(selectedColor); if (cellColor === targetColor) { return cell; } else if (cellColor !== 'rgb(255, 255, 255)') { return null; // cell has a DIFFERENT color } } else{ //console.log("colPos looks good"); return cell; } /* var rowSpan = cell.rowSpan || 1; var cellBottomRow = runningRow + rowSpan - 1; if (cellBottomRow >= rowIndex) { console.log("Found cell"); return cell; }*/ } colPos += span; } runningRow++; } return null; } function highlightCellsVertically(cellA, cellB) { if (!cellA || !cellB) return; var table = cellA.closest('table'); if (!table) return; var rows = Array.from(table.rows); var rowIndexA = rows.indexOf(cellA.parentNode); var rowIndexB = rows.indexOf(cellB.parentNode); if (rowIndexB <= rowIndexA) return; // Calculate visual column index of cellA var colIndex = -1; var headerRow = rows[rowIndexA]; var colPos = 0; for (var j = 0; j < headerRow.cells.length; j++) { var c = headerRow.cells[j]; var colspan = c.colSpan || 1; if (c === cellA) { colIndex = colPos; break; } colPos += colspan; } if (colIndex === -1) return; // Get section-* class from cellA var sectionClassA = null; var classListA = cellA.className.split(/\s+/); for (var s = 0; s < classListA.length; s++) { if ( classListA[s].indexOf("speaker") === 0 || classListA[s].indexOf("addressee") === 0 || classListA[s].indexOf("section") === 0 || classListA[s].indexOf("subsection") === 0 ) { sectionClassA = classListA[s]; break; } } if (!sectionClassA) return; // Loop through the rows and highlight matching cells for (var i = rowIndexA + 1; i <= rowIndexB; i++) { var row = rows[i]; var colPos = 0; for (var j = 0; j < row.cells.length; j++) { var c = row.cells[j]; var colspan = c.colSpan || 1; if (colPos <= colIndex && colIndex < colPos + colspan) { if (!c.querySelector('div.line')) { var classListC = c.className.split(/\s+/); for (var k = 0; k < classListC.length; k++) { if (classListC[k] === sectionClassA) { c.style.backgroundColor = selectedColor; break; } } } break; // Only highlight one cell per row in the target column } colPos += colspan; } } // Optionally set rowspan — commented out // cellA.rowSpan = rowIndexB - rowIndexA + 1; } }); /* ====================================== Insertions js ======================================*/ var debug = 1; var lastClickedInsertionPoint = null; function createInsertionSpan(parentLine, refNode, options) { var insert = document.createElement("span"); insert.className = "insertion"; insert.textContent = options.text || "⟪insert⟫"; //insert.setAttribute("data-before", options.before || ""); //insert.setAttribute("data-after", options.after || ""); insert.setAttribute("data-new", options.isNew ? "true" : "false"); //if (options.isSplit) insert.setAttribute("data-split", "true"); /*// Count non-insertion spans before this position var children = Array.from(parentLine.children); var insertBeforeIndex = refNode ? children.indexOf(refNode.nextSibling) : children.length; var textBefore = ""; for (var i = 0; i < insertBeforeIndex; i++) { var el = children[i]; if (el.tagName === "SPAN" && !el.classList.contains("insertion")) { textBefore += (el.textContent || "") + " "; } } textBefore = textBefore.trim(); if (options.beginning) textBefore = ""; var wordCount = textBefore === "" ? 0 : textBefore.split(/\s+/).length; insert.setAttribute("data-index", wordCount); insert.setAttribute("data-beforetext", textBefore); //insert.setAttribute("data-refnode", refNode && refNode.innerText); */ insert.addEventListener("click", insertionClickHandler); return insert; } function showInsertInput(span) { var input = document.getElementById("input_1"); var modal = document.getElementById("enter-insertion"); var table = document.querySelector("#text-insertion table"); var line = span.closest(".line"); if (!line || !modal || !input || !table) return; // Clear previous highlights document.querySelectorAll(".insertion.active").forEach(el => el.classList.remove("active")); // Highlight the current one span.classList.add("active"); input.value = span.textContent.replace(/⟪insert⟫/, "").trim(); lastClickedInsertionPoint = span; requestAnimationFrame(function () { var rect = line.getBoundingClientRect(); var tableRect = table.getBoundingClientRect(); modal.style.position = "absolute"; modal.style.left = tableRect.left + window.scrollX + "px"; modal.style.top = rect.bottom + window.scrollY + "px"; modal.style.width = tableRect.width + "px"; modal.style.removeProperty("display"); modal.style.display = "block"; input.focus(); }); } function insertBetweenSpans(target, event) { if (debug) { console.log("[insertBetweenSpans] Target element:", target); console.log("[insertBetweenSpans] Target tag:", target.tagName, "class:", target.className); console.log("[insertBetweenSpans] Target outerHTML:", target.outerHTML); } var line = target.closest ? target.closest(".line") : (typeof closestPolyfill === "function" ? closestPolyfill(target, ".line") : null); if (!line) { console.warn("[insertBetweenSpans] Could not find .line container for target."); if (target.parentNode) { console.warn("[insertBetweenSpans] Parent node tag/class:", target.parentNode.tagName, target.parentNode.className); console.warn("[insertBetweenSpans] Parent outerHTML:", target.parentNode.outerHTML); } var ancestry = []; var curr = target; while (curr && curr !== document) { ancestry.push(curr.tagName + (curr.className ? `.${curr.className}` : "")); curr = curr.parentNode; } console.warn("[insertBetweenSpans] DOM ancestry (bottom-up):", ancestry.join(" ← ")); return; } var insert = createInsertionSpan(line, refNode, { isNew: true, after: refNode && refNode.innerText, before: refNode && refNode.nextSibling && refNode.nextSibling.innerText }); var range = document.caretRangeFromPoint ? document.caretRangeFromPoint(event.clientX, event.clientY) : (document.caretPositionFromPoint && document.caretPositionFromPoint(event.clientX, event.clientY)); var node = range && range.startContainer; var refNode = node && node.nodeType === 3 ? node.parentNode : node; if (debug) { console.log("[insertBetweenSpans] Insert range object:", range); console.log("[insertBetweenSpans] Resolved refNode:", refNode); if (refNode) console.log("[insertBetweenSpans] refNode.outerHTML:", refNode.outerHTML); } if (refNode && refNode.parentNode === line) { line.insertBefore(insert, refNode.nextSibling); insert.addEventListener("click", insertionClickHandler); } else { line.appendChild(insert); insert.addEventListener("click", insertionClickHandler); } showInsertInput(insert); } function insertIntoSpan(span, event) { if (debug) console.groupCollapsed("[insertIntoSpan] Splitting span"); var clickX = event.clientX; var clickY = event.clientY; var range = document.caretRangeFromPoint ? document.caretRangeFromPoint(clickX, clickY) : document.caretPositionFromPoint && document.caretPositionFromPoint(clickX, clickY); if (!range) { console.warn("[insertIntoSpan] No range found for click"); return; } var fullText = span.textContent; var offset = range.startOffset || 0; if (offset < 0 || offset > fullText.length) offset = fullText.length; if (debug) console.log(`[insertIntoSpan] Splitting at offset ${offset} of text: "${fullText}"`); var parent = span.parentNode; // Insertion at beginning — don’t create empty 'before' if (offset === 0) { var insert = createInsertionSpan(parent, span, { beginning: true, isNew: true, isSplit: true }); parent.insertBefore(insert, span); // Reattach event handlers span.addEventListener("click", insertionClickHandler); insert.addEventListener("click", insertionClickHandler); showInsertInput(insert); console.groupEnd(); return; } // Insertion at end — don’t create empty 'after' if (offset === fullText.length) { var insert = createInsertionSpan(parent, span, { after: span.textContent || "", isNew: true, isSplit: true }); parent.insertBefore(insert, span.nextSibling); // Reattach event handlers span.addEventListener("click", insertionClickHandler); insert.addEventListener("click", insertionClickHandler); showInsertInput(insert); console.groupEnd(); return; } // Normal mid-span insertion var before = document.createElement("span"); before.className = span.className; before.textContent = fullText.slice(0, offset); var insert = createInsertionSpan(parent, span, { before: span.className || "", after: span.className || "", isNew: true, isSplit: true }); var after = document.createElement("span"); after.className = span.className; after.textContent = fullText.slice(offset); parent.replaceChild(after, span); parent.insertBefore(insert, after); parent.insertBefore(before, insert); before.addEventListener("click", insertionClickHandler); after.addEventListener("click", insertionClickHandler); insert.addEventListener("click", insertionClickHandler); showInsertInput(insert); console.groupEnd(); } function saveInsertions() { var outputTextarea = document.getElementById('input_2'); var insertions = []; document.querySelectorAll('span.insertion, span.insertion-point').forEach(function (span) { var text = span.textContent.trim(); var parent = span.closest('.line'); var children = Array.from(parent.children); var countBefore = 0; var textBefore = ""; for (var i = 0; i < children.length; i++) { var el = children[i]; if (el === span) break; if (el.tagName === "SPAN" && !el.classList.contains("insertion") && !el.classList.contains("insertion-point")) { countBefore++; textBefore += (el.textContent || "") + " "; } } textBefore = textBefore.trim(); var wordsBefore = textBefore === "" ? [] : textBefore.split(/\s+/); var wordCountBefore = wordsBefore.length; if (debug) { console.log("[saveInsertions] Found insertion:", { text: text, index: countBefore, textBefore: textBefore, wordCountBefore: wordCountBefore }); } if (!text) return; insertions.push({ text: text, lineID: parent.getAttribute("data-line"), index: countBefore, textBefore: textBefore, wordsBefore: wordCountBefore }); }); var result = JSON.stringify(insertions, null, 2); outputTextarea.value = result; if (debug) console.log("[saveInsertions] Final output JSON:", result); } function insertionClickHandler(e) { var span = e.currentTarget; if (debug) console.groupCollapsed("[insertionClickHandler] for span " + span.innerText); if (span.className && span.className.indexOf("insertion") !== -1) { if (debug) console.log("[span click] Clicked existing insertion point"); showInsertInput(span); // ✅ fills input with textContent if (debug) console.groupEnd(); return; } // clicked somewhere else: new insertion into a span insertIntoSpan(span, e); if (debug) console.groupEnd(); } $(document).ready(function () { if (document.getElementById('apply-insertion')) { if (debug) console.log("[ready] Binding click to #apply-insertion"); document.getElementById('apply-insertion').addEventListener('click', function () { var textarea = document.getElementById('input_1'); var text = textarea.value; var modal = document.getElementById('enter-insertion'); if (debug) console.log("[apply-insertion] Clicked. Text to insert:", text); if (lastClickedInsertionPoint && typeof text === 'string') { if (debug) console.log("[apply-insertion] Inserting into:", lastClickedInsertionPoint); lastClickedInsertionPoint.textContent = text; lastClickedInsertionPoint.removeAttribute("data-new"); textarea.value = ""; modal.style.display = "none"; // ✅ hide the box } else { console.warn("[apply-insertion] No insertion point selected or input missing."); } }); } function cancelInsertion() { var modal = document.getElementById('enter-insertion'); modal.style.display = "none"; document.getElementById('input_1').value = ""; if (lastClickedInsertionPoint) { lastClickedInsertionPoint.classList.remove("active"); // ✅ Only remove if it's marked as a new insertion if (lastClickedInsertionPoint.dataset.new === "true") { if (debug) console.log("[cancelInsertion] Removing new insertion span"); lastClickedInsertionPoint.remove(); } } lastClickedInsertionPoint = null; } if (document.getElementById('cancel-insertion')) { document.getElementById('cancel-insertion').addEventListener('click', function () { if (debug) console.log("[cancel-insertion] Clicked"); cancelInsertion(true); // true = remove the span }); } document.addEventListener('keydown', function (e) { var modal = document.getElementById('enter-insertion'); // ESC = cancel if (e.key === 'Escape') { if (modal && modal.style.display === 'block') { if (debug) console.log("[Esc] Cancel via Esc key"); cancelInsertion(false); // false = keep the span, just hide the box } } // ENTER/RETURN = apply if (e.key === 'Enter') { if (modal && modal.style.display === 'block') { if (debug) console.log("[Enter] Apply via Enter key"); var applyBtn = document.getElementById('apply-insertion'); if (applyBtn) applyBtn.click(); } } }); if (document.getElementById("text-insertion")) { if (debug) console.log("[ready] Attaching individual listeners to spans inside #text-insertion"); var container = document.getElementById("text-insertion"); var spans = container.querySelectorAll(".line span"); spans.forEach(function (span) { span.addEventListener("click", insertionClickHandler); }); var table = container.querySelector("table.psalm-table"); if (!table) { console.warn("Could not find psalm table"); return; } // Fallback: click outside of spans (e.g. empty part of line) table.addEventListener("click", function (e) { var target = e.target; if (debug) console.log("[container click] Fallback click handler target:", target); // Ignore clicks inside buttons, modals, inputs, etc. if (target.closest("button, input, textarea, #enter-insertion")) { if (debug) console.log("[container click] Ignored: clicked inside UI element"); return; } // Only handle if not a <span>, or if not already handled if (target === container || target.tagName !== "SPAN") { var line = target.closest(".line"); if (line) { if (debug) console.log("[container click] Inserting between spans inside line:", line); insertBetweenSpans(target, e); } else { if (debug) console.log("[container click] Clicked outside any .line — no insertion"); } } }); } if (document.getElementById('saveInsertions')) { if (debug) console.log("[ready] Binding click to #saveInsertions"); document.getElementById('saveInsertions').addEventListener('click', saveInsertions); } }); /* ====================================== Lineation js ======================================*/ var debug = 1; function insertHebrewGaps() { var lines = document.querySelectorAll('.lineation .psalm-table .hebrew-cell .line'); for (var i = 0; i < lines.length; i++) { var line = lines[i]; // Clean out text nodes (e.g. spaces) between spans var child = line.firstChild; while (child) { var next = child.nextSibling; if (child.nodeType === 3 && /^\s*$/.test(child.nodeValue)) { line.removeChild(child); } child = next; } // Collect spans after cleanup var spans = line.querySelectorAll('span.hebrew'); if (debug === 3) console.log('[insertHebrewGaps] Processing line:', line, spans.length, 'spans'); for (var j = spans.length - 1; j > 0; j--) { var idA = getHebrewSpanIDParts(spans[j - 1]); var idB = getHebrewSpanIDParts(spans[j]); if (!idA || !idB || idA.line !== idB.line || idA.word !== idB.word) { var gap = document.createElement('span'); gap.className = 'hebrew-gap'; gap.innerHTML = ' '; gap.style.cursor = 'col-resize'; gap.addEventListener('click', handleHebrewGapClick); line.insertBefore(gap, spans[j]); if (debug === 3) { console.log('[insertHebrewGaps] Inserted gap between:', spans[j - 1].className, 'and', spans[j].className); } } else if (debug === 3) { console.log('[insertHebrewGaps] Skipped gap between same word parts:', idA.word); } } // Add merge gap after Hebrew span loop var merge = document.createElement('span'); merge.className = 'hebrew-gap merge-gap'; merge.innerHTML = ' '; merge.style.cursor = 'ns-resize'; merge.title = 'Shift-click to merge with next line'; merge.addEventListener('click', handleHebrewGapClick); line.appendChild(merge); } } function getHebrewSpanIDParts(span) { var match = span.className.match(/id-(\d+[a-z]*)-(\d+)-(\d+)/); if (match) { return { line: match[1], // e.g. "1a" word: match[2], // e.g. "1" lexeme: match[3] // e.g. "3" }; } return null; } function insertHebrewGapsOnLine(line) { if (!line) return; // 1. Remove old gaps and text nodes var oldGaps = line.querySelectorAll('span.hebrew-gap'); for (var i = 0; i < oldGaps.length; i++) { oldGaps[i].parentNode.removeChild(oldGaps[i]); } var child = line.firstChild; while (child) { var next = child.nextSibling; if (child.nodeType === 3 && /^\s*$/.test(child.nodeValue)) { line.removeChild(child); } child = next; } // 2. Insert new word-splitting gaps var spans = line.querySelectorAll('span.hebrew'); for (var j = spans.length - 1; j > 0; j--) { var idA = getHebrewSpanIDParts(spans[j - 1]); var idB = getHebrewSpanIDParts(spans[j]); if (!idA || !idB || idA.line !== idB.line || idA.word !== idB.word) { var gap = document.createElement('span'); gap.className = 'hebrew-gap'; gap.innerHTML = ' '; gap.style.cursor = 'col-resize'; gap.addEventListener('click', handleHebrewGapClick); line.insertBefore(gap, spans[j]); } } // 3. Always add merge gap at end var merge = document.createElement('span'); merge.className = 'hebrew-gap merge-gap'; merge.innerHTML = ' '; merge.style.cursor = 'ns-resize'; merge.title = 'Shift-click to merge with next line'; merge.addEventListener('click', handleHebrewGapClick); line.appendChild(merge); if (debug === 3) console.log('[insertHebrewGapsOnLine] Added merge gap at end of line.'); } function getNextElementSibling(el) { var next = el.nextSibling; while (next && next.nodeType !== 1) { // 1 = ELEMENT_NODE next = next.nextSibling; } return next; } function handleMergeGapClick(event) { var gap = event.currentTarget; var lineDiv = closestByClass(gap, 'line'); var lineID = lineDiv.getAttribute('data-line'); var tr = closestByTag(gap, 'tr'); var nextTr = getNextElementSibling(tr); if (!nextTr) { if (debug === 2) console.warn('[merge] No line below to merge with.'); return; } var nextHebrewLine = nextTr.querySelector('.hebrew-cell .line'); var nextEnglishLine = nextTr.querySelector('.cbc-cell .line'); if (!nextHebrewLine || !nextEnglishLine) { if (debug === 1) console.warn('[merge] Incomplete next row.'); return; }else{ if (debug === 1) console.warn('[merge] Merging with ' + nextHebrewLine); } // Merge Hebrew var currentHebrew = lineDiv.innerHTML; var mergedHebrew = currentHebrew + nextHebrewLine.innerHTML; lineDiv.innerHTML = mergedHebrew; // Merge English var currentEnglish = tr.querySelector('.cbc-cell .line').innerHTML; var nextEnglish = nextEnglishLine.innerHTML; tr.querySelector('.cbc-cell .line').innerHTML = currentEnglish + ' ' + nextEnglish; // Remove next row nextTr.parentNode.removeChild(nextTr); // Rebuild gaps insertHebrewGapsOnLine(lineDiv); if (debug === 3) { console.log('[merge] Merged line', lineID, 'with next line.'); } } function handleHebrewGapClick(event) { var gap = event.currentTarget; if (gap.classList.contains('merge-gap')) { if (debug === 1) console.log('[gapClick] Merging line with next'); handleMergeGapClick(event); return; } // Otherwise, normal split handleSplitGapClick(event); } function handleSplitGapClick(event) { var gap = event.currentTarget; var lineDiv = closestByClass(gap, 'line'); var lineID = lineDiv.getAttribute('data-line'); var tr = closestByTag(gap, 'tr'); var tbody = tr.parentNode; var rows = Array.prototype.slice.call(tbody.children); var rowIndex = rows.indexOf(tr); var allHebrew = lineDiv.querySelectorAll('span.hebrew, span.hebrew-gap'); var allHebrewArr = Array.prototype.slice.call(allHebrew); var gapIndex = allHebrewArr.indexOf(gap); var beforeHebrew = allHebrewArr.slice(0, gapIndex); var afterHebrew = allHebrewArr.slice(gapIndex + 1); var firstHebrewAfter = null; for (var i = 0; i < afterHebrew.length; i++) { if (afterHebrew[i].className.indexOf('hebrew') !== -1) { firstHebrewAfter = afterHebrew[i]; break; } } var targetID = null; if (firstHebrewAfter) { var match = firstHebrewAfter.className.match(/id-[\w\-]+/); if (match) targetID = match[0]; } var englishLine = tr.querySelector('.cbc-cell .line'); var englishSpans = englishLine.querySelectorAll('span.english'); var englishArr = Array.prototype.slice.call(englishSpans); var splitIndex = 0; if (targetID) { for (var i = 0; i < englishArr.length; i++) { if (englishArr[i].className.indexOf(targetID) !== -1) { splitIndex = i; break; } } } else { splitIndex = englishArr.length; } var beforeEnglish = englishArr.slice(0, splitIndex); var afterEnglish = englishArr.slice(splitIndex); // Update current row lineDiv.innerHTML = joinOuterHTML(beforeHebrew); englishLine.innerHTML = joinOuterHTML(beforeEnglish, ' '); // Rebuild gaps for the original line (adds merge-gap to end) insertHebrewGapsOnLine(lineDiv); // Create new row var newTr = document.createElement('tr'); newTr.innerHTML = '<td dir="rtl" class="hebrew-cell"><div class="line" data-line="' + lineID + '">' + joinOuterHTML(afterHebrew) + '</div></td>' + '<td class="verse-cell">' + lineID + '</td>' + '<td class="cbc-cell"><div class="line" data-line="' + lineID + '">' + joinOuterHTML(afterEnglish, ' ') + '</div></td>'; tbody.insertBefore(newTr, tr.nextSibling); insertHebrewGapsOnLine(newTr.querySelector('.line')); if (debug === 1) { console.log('[split] Created new row beneath line:', lineID); } } function closestByClass(el, className) { while (el && el !== document) { if (el.classList && el.classList.contains(className)) return el; el = el.parentNode; } return null; } function closestByTag(el, tagName) { tagName = tagName.toUpperCase(); while (el && el !== document) { if (el.tagName === tagName) return el; el = el.parentNode; } return null; } function joinOuterHTML(nodes, separator) { var html = []; for (var i = 0; i < nodes.length; i++) { html.push(nodes[i].outerHTML || ''); } return html.join(typeof separator === 'string' ? separator : ''); } $(document).ready(function () { insertHebrewGaps(); if (debug === 3) console.log('[ready] Hebrew gaps initialized.'); var buttonSave = document.getElementById('saveLineation'); if (buttonSave) { document.getElementById('saveLineation').addEventListener('click', function () { var lines = document.querySelectorAll('.psalm-table .hebrew-cell .line'); var output = []; var currentVerse = null; var lineCounter = 1; for (var i = 0; i < lines.length; i++) { var line = lines[i]; var firstHebrew = line.querySelector('span.hebrew'); if (!firstHebrew) continue; var classes = firstHebrew.className.split(/\s+/); var wordID = null; var verseID = null; for (var j = 0; j < classes.length; j++) { if (classes[j].indexOf('id-') === 0) { wordID = classes[j].substring(3); // remove "id-" verseID = wordID.split('-')[0]; break; } } if (!wordID || !verseID) continue; if (verseID !== currentVerse) { currentVerse = verseID; lineCounter = 1; } output.push({ verse: verseID, line: lineCounter, wordID: wordID }); lineCounter++; } var textarea = document.getElementById('input_2'); textarea.value = JSON.stringify(output, null, 2); }); } });