MediaWiki:Overlays.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:BuildTextTable.js'); var selectedAnnotation = 'psalmist'; var coloredWords = []; // set up color map var divColorMap = {}; //document.getElementById("color-map"); var colorPicker = null; //document.querySelector("div.color-picker"); var overlayTable = null; var colorMap = {}; var INSERT_BLANK_AFTER_SECTIONS = false; var INCLUDE_EMOTION_COLUMN = true; var SECTION_HEADING = 'Structure' var DEBUG = true; // 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' }; var sectionLevelsAdded = 0; var speakerAndAddresseeBarsAdded = false; var sectionsAdded = false; var subSectionsAdded = false; function parseJSONFromDiv(divId) { var div = document.getElementById(divId); if (!div) return null; try { return JSON.parse(div.textContent.trim()); } catch (err) { console.error("Failed to parse JSON from #" + divId + ":", err); return null; } } function setHeatmapScaleClass(overlay, scale) { if (!overlay) return; var classList = overlay.className.split(/\s+/); var newClassList = []; for (var i = 0; i < classList.length; i++) { if (classList[i].indexOf("heatmap-scale-") !== 0) { newClassList.push(classList[i]); } } newClassList.push("heatmap-scale-" + scale); overlay.className = newClassList.join(" "); } function reapplyOverlayColors(overlayId) { var overlay = document.getElementById(overlayId); if (!overlay) return; var annotationsRaw = overlay.getAttribute("data-annotations"); if (!annotationsRaw) return; var annotations; try { annotations = JSON.parse(annotationsRaw); } catch (e) { console.error("Invalid annotations JSON:", annotationsRaw); return; } // Assumes this is defined globally elsewhere if (typeof applyOverlayColors === "function") { applyOverlayColors(overlayId, annotations); } } function handleHeatmapScaleChange(scale) { var overlayId = "overlay-heatmap"; var mapId = scale === "6" ? "color-map" : scale === "10" ? "color-map-medium" : scale === "20" ? "color-map-full" : null; if (!mapId) return; var newMap = parseJSONFromDiv(mapId); if (!newMap) return; // set the global variable colorMap = newMap; var overlay = document.getElementById(overlayId); setHeatmapScaleClass(overlay, scale); reapplyOverlayColors(overlayId); console.log("Heat map scale changed to " + scale); } function highlightActiveButton(activeButton, allButtons) { for (var i = 0; i < allButtons.length; i++) { allButtons[i].className = allButtons[i].className.replace(/\bactive\b/, "").trim(); } if (activeButton.className.indexOf("active") === -1) { activeButton.className += " active"; } } function bindHeatmapButtons() { var buttons = document.querySelectorAll(".prominence-scale-link"); if (buttons) console.log("Binding prominence scale buttons"); for (var i = 0; i < buttons.length; i++) { (function (button) { button.addEventListener("click", function (e) { if (e.preventDefault) e.preventDefault(); var scale = this.id.replace("prominence-scale-", ""); handleHeatmapScaleChange(scale); highlightActiveButton(button, buttons); }); })(buttons[i]); } } function attachColorGridHandler(gridSelector) { if (!gridSelector) { console.warn("Cannot find " + gridSelector + " to attach handlers."); return; } var cells = document.querySelectorAll('.color-cell'); if (!cells.length) { //console.warn("No color cells found for handlers."); return; } cells.forEach(function (cell) { cell.style.cursor = 'pointer'; // optional UX boost cell.addEventListener('click', function () { // Clear highlight from all cells in the grid cells.forEach(function (c) { c.style.outline = 'none'; }); // Highlight selected cell cell.style.outline = '4px solid black'; // Set globals window.selectedColor = cell.dataset.color || ''; window.selectedAnnotation = cell.dataset.participant || 'unknown'; }); }); } function setupWordClickHandler(containerSelector) { var selector = containerSelector + ' span.hebrew:not(#buildTextTable span.hebrew)'; var spans = document.querySelectorAll(selector); spans.forEach(function (span) { span.style.cursor = 'pointer'; span.addEventListener('click', function (event) { event.stopPropagation(); // prevent parent line click if (!selectedColor || !selectedAnnotation) { console.warn("No color or annotation selected."); return; } 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 }); 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) { 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); } } console.log("Setting to " + selectedColor); span.style.backgroundColor = selectedColor; if (glossEl) glossEl.style.backgroundColor = selectedColor; }); }); } function setupOverlayLineClickHandler(containerSelector) { var lines = document.querySelectorAll(containerSelector + ' div.line'); console.log("Adding click handlers for " + lines.length + " lines"); lines.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(); if (event.shiftKey) { // Remove from coloredWords coloredWords = coloredWords.filter(function (w) { return !(w.id === lineId && w.annotation === selectedAnnotation && w.color === selectedColor); }); // Remove matching form instances document.querySelectorAll('#overlay-annotations .multipleTemplateInstance').forEach(function (instance) { 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; } // Add annotation if not already 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) { 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 attachExportSectionsHandler(buttonId) { var button = document.getElementById(buttonId); if (!button) return; button.addEventListener("click", function () { var overlay = document.querySelector(".overlay"); if (!overlay) return; var annotationsRaw = overlay.getAttribute("data-annotations"); if (!annotationsRaw) return; var annotations = JSON.parse(annotationsRaw.replace(/"/g, '"')); // Group by color 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)) continue; var lines = groups[color]; // Sort numerically and lexically lines.sort(function (a, b) { var re = /^(\d+)([a-z]*)$/; var ma = re.exec(a), mb = re.exec(b); var na = ma ? parseInt(ma[1], 10) : 0; var nb = mb ? parseInt(mb[1], 10) : 0; return na !== nb ? na - nb : a.localeCompare(b); }); var first = lines[0], last = lines[lines.length - 1]; // Extract level from the row var lineEl = overlay.querySelector('[data-line="' + first + '"]'); var levelClass = 'unknown'; if (lineEl) { var row = lineEl.closest("tr"); var sectionCell = row && row.querySelector('td[class^="section-"]'); if (sectionCell) { var match = sectionCell.className.match(/\bsection-\d+\b/); if (match) levelClass = match[0]; } } output += "{{Overlay Section |level=" + levelClass + " |firstLine=" + first + " |lastLine=" + last + " |color=" + color + "}},"; } var inputs = document.getElementsByName('Text Overlay[Sections]'); if (inputs.length > 0) { inputs[0].value = output.replace(/,$/, ''); } }); } 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 applyOverlayColors(containerId, annotations) { var container = document.getElementById(containerId); if (!container) return; for (var wordID in annotations) { if (!annotations.hasOwnProperty(wordID) || wordID.trim() === "") continue; 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); } if (!color) continue; // Support line-wide selectors like "id-3c-*" if (wordID.endsWith("-*")) { var prefix = wordID.slice(0, -1); // removes the * but leaves the dash var allMatching = container.querySelectorAll('[class*="' + prefix + '"]'); for (var m = 0; m < allMatching.length; m++) { var el = allMatching[m]; if (!el.style.backgroundColor) { el.style.backgroundColor = color; } } } else { // Word-level match var elements = container.querySelectorAll("." + CSS.escape(wordID)); for (var j = 0; j < elements.length; j++) { elements[j].style.backgroundColor = color; } // Line-level match var lines = container.querySelectorAll('[data-line="' + wordID + '"]'); 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; } // 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; } 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 headerCells = document.querySelectorAll('#overlay-sections thead tr th'); 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); } 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; } } } function reverseEngineerImageHTML(htmlString) { var tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlString; var img = tempDiv.querySelector('a.image > img'); if (!img) return htmlString; var src = img.getAttribute('src') || ''; var match = src.match(/\/thumb\.php\?f=([^&]+)/); if (!match) return htmlString; var filename = decodeURIComponent(match[1]); var size = img.getAttribute('width') || '50'; var wikitext = '[[File:' + filename + '|' + size + 'px|frameless|center|class=aag-icon]]'; return wikitext; } function saveRenderedTableAsSubpage(currentPage, divID) { var api = new mw.Api(); // Try to resolve the page title if (typeof currentPage !== "string" || !currentPage) { var divTrigger = document.getElementById("triggerForSavingSubpage") || document.getElementById(divID); if (divTrigger && divTrigger.textContent) { currentPage = divTrigger.textContent.trim(); } else { currentPage = mw.config.get("wgPageName"); } } var targetPage = currentPage.replace(/ /g, "_"); if (divID) { targetPage = targetPage + "/" + divID; } else { targetPage = targetPage + "/Rendered"; } // Find the relevant table var psalmTable = null; if (divID) { var container = document.getElementById(divID); if (container) { psalmTable = container.querySelector("table.psalm-table"); if (!psalmTable) { console.warn("No psalm-table found within " + divID, container); } } } if (!psalmTable) { psalmTable = document.querySelector("table.psalm-table"); } if (!psalmTable) { alert("No psalm-table found."); return; } var clonedTable = psalmTable.cloneNode(true); // deep clone for in-memory processing var centerDivs = clonedTable.querySelectorAll('td.subsection-bar .center'); for (var i = 0; i < centerDivs.length; i++) { var html = centerDivs[i].outerHTML; var wikitext = reverseEngineerImageHTML(html); if (wikitext !== html) { var placeholder = document.createElement('span'); placeholder.textContent = wikitext; centerDivs[i].replaceWith(placeholder); // modifies only clone } } var renderedHTML = clonedTable.outerHTML .replace(/psalm-table/gi, 'psalm-table pre-rendered') .replace(/<tbody>/gi, '') .replace(/<\/tbody>/gi, ''); var wrapped = '<onlyinclude>{{#tag:html|\n' + renderedHTML + '\n}}</onlyinclude>'; api.postWithEditToken({ action: "edit", title: targetPage, text: renderedHTML, summary: "Saving rendered text table", format: "json", contentmodel: "wikitext" }).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 () { // ensure the mode & css are aligned upon load (SHOULD THIS MOVE TO BUILDTABLE?!?) var overlay = document.getElementById('buildTextTable'); var modeField = document.querySelector('input[name="BuildTextTable[Mode]"]:checked'); var overlayTableDiv = document.getElementById("overlay-1"); if (overlayTableDiv){ overlayTable = overlayTableDiv.querySelector('table'); if (!overlayTable) { console.warn("No overlay table could be found."); return; } } // Attach checkbox listeners for the Overlays form to ADD columns var sectionCheckboxes = document.querySelectorAll(".pf-checkbox-input-container"); 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) { var name = checkbox.name; var match = name.match(/\[([^\]]+)\]/); var firstKey = match ? match[1] : null; // Attach listener checkbox.addEventListener("click", function () { adjustSectionColumns(firstKey, checkbox.checked); }); // Run immediately if already checked if (checkbox.checked) { //adjustSectionColumns(firstKey, true); } })(checkboxes[j]); } } } // 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()); }); // BEGIN TEXT OVERLAY CODE // === TEXT OVERLAY COLOR PICKER LOGIC === // set up color map divColorMap = document.getElementById("color-map"); 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); } } } selectedColor = ''; // Color selection via grid attachColorGridHandler('#colorPickerTab'); // handle any overlay colors at page load applyAllOverlayAnnotations(); // applySections(); this is old?!? // on click - individual words document.querySelectorAll('[id^="overlay-"]').forEach(function (overlayContainer) { var selector = "#" + overlayContainer.id; console.log("Initializing handlers for div " + selector); setupOverlayLineClickHandler(selector); setupWordClickHandler(selector); }); //document.querySelectorAll('[id^="overlay-"]').forEach(function (overlayContainer) { // setupOverlayLineClickHandler("#" + overlayContainer.id); //}); attachExportSectionsHandler("export-sections-button"); // enable a prominence scale bindHeatmapButtons(); });