|
|
(42 intermediate revisions by 2 users not shown) |
Line 2: |
Line 2: |
| importScript('MediaWiki:Insertions.js'); | | importScript('MediaWiki:Insertions.js'); |
| importScript('MediaWiki:Lineation.js'); | | importScript('MediaWiki:Lineation.js'); |
| | importScript('MediaWiki:AutoLoad.js'); |
| | importScript('MediaWiki:Compare.js'); |
|
| |
|
|
| |
|
| /* Any JavaScript here will be loaded for all users on every page load. */ | | /* Any JavaScript here will be loaded for all users on every page load. */ |
| | |
| | function copyPageContents(sourcePage, chapter) { |
| | var api = new mw.Api(); |
| | |
| | console.log("[CopyPage] Starting with source:", sourcePage); |
| | //console.log("[CopyPage] Replacing '200' with chapter:", chapter); |
| | |
| | // Replace "200" with chapter in the sourcePage string |
| | var targetPage = sourcePage.replace("200", chapter); |
| | console.log("[CopyPage] Target page:", targetPage); |
| | |
| | // Step 1: Get source page content |
| | api.get({ |
| | action: "query", |
| | prop: "revisions", |
| | titles: sourcePage, |
| | rvslots: "main", |
| | rvprop: "content", |
| | formatversion: 2 |
| | }).then(function (data) { |
| | var pages = data.query.pages; |
| | if (!pages || !pages.length || !pages[0].revisions) { |
| | alert("Could not find source page or no content."); |
| | console.error("[CopyPage] Invalid API response:", data); |
| | return; |
| | } |
| | |
| | var content = pages[0].revisions[0].slots.main.content; |
| | //console.log("[CopyPage] Retrieved content. Length:", content.length); |
| | |
| | // Step 2: Save to target page |
| | return api.postWithEditToken({ |
| | action: "edit", |
| | title: targetPage, |
| | text: content, |
| | summary: "Copied from " + sourcePage + " with chapter " + chapter, |
| | contentmodel: "wikitext" |
| | }); |
| | }).then(function (result) { |
| | if (result && result.edit && result.edit.result === "Success") { |
| | //alert("Successfully copied to: " + targetPage); |
| | console.log("[CopyPage] ✅ Success:", result); |
| | } else { |
| | //alert("Edit failed. See console."); |
| | console.error("[CopyPage] ❌ Edit failed:", result); |
| | } |
| | }).catch(function (err) { |
| | //alert("API call failed. See console."); |
| | console.error("[CopyPage] ❌ API error:", err); |
| | }); |
| | } |
| | |
| | |
| | |
| | | |
| // Function to check if all <pre class="mermaid"> elements are processed | | // Function to check if all <pre class="mermaid"> elements are processed |
Line 101: |
Line 157: |
| $(document).ready(function () { | | $(document).ready(function () { |
|
| |
|
| | var allPageChapters = []; |
| | | |
| // ===========================
| | var buttons = document.querySelectorAll('.save-page-button'); |
| // Begin placeholder auto-loading
| | buttons.forEach(function (button) { |
| // ===========================
| | var page = button.dataset.page; |
| | | var chapter = button.dataset.chapter; |
| | | |
| | | // Store the unique (page, chapter) pair |
| var observer = new IntersectionObserver(function(entries) { | | allPageChapters.push({ page: page, chapter: chapter }); |
| 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) {
| | // Bind individual button click |
| observer.observe(placeholder); | | button.addEventListener('click', function () { |
| | console.log('[SaveDiv] Clicked button for page:', page, 'and chapter:', chapter); |
| | copyPageContents(page, chapter); |
| | }); |
| }); | | }); |
| | | |
| function loadChunk(placeholder) { | | console.log("Save-page-buttons ready for saving."); |
| var id = placeholder.id; // e.g., v-1
| | console.log("All page/chapter combinations:", allPageChapters); |
| | |
| 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();
| | // Optional: Set up a "copy all" button |
| xhr.open('GET', '/w/' + nextPage + '?action=render', true);
| | var copyAllButton = document.querySelector('#copyAllButton'); |
| xhr.onreadystatechange = function () {
| | if (copyAllButton) { |
| if (xhr.readyState === 4) {
| | copyAllButton.addEventListener('click', function () { |
| if (xhr.status === 200) {
| | console.log('[SaveDiv] Copying all pages...'); |
| placeholder.innerHTML = xhr.responseText;
| | allPageChapters.forEach(function (pair) { |
| placeholder.dataset.loaded = 'true';
| | copyPageContents(pair.page, pair.chapter); |
| | | }); |
| 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" | | // Auto-create links for notes such as "v. 2 preferred diagram" |
Line 528: |
Line 465: |
| // If no toggle links are found, print a warning | | // If no toggle links are found, print a warning |
| if (hoverElements.length === 0) { | | if (hoverElements.length === 0) { |
| console.warn("No hover elements found on the page."); | | //console.warn("No hover elements found on the page."); |
| } | | } |
| | | |
Line 599: |
Line 536: |
| }); | | }); |
|
| |
|
| | // Rotating caret for collapsible elements |
| | // This ensures the code runs after the entire page (DOM) is loaded. |
| | jQuery( function( $ ) { |
|
| |
|
| | console.log('Caret toggle JS loaded (Fresh Start Version).'); |
|
| |
|
| | // Select all toggle spans that have our dedicated controlling class. |
| | // This makes the script work for multiple toggles on the same page. |
| | $( '.js-toggle-caret-controller' ).each( function() { |
| | var $toggleSpan = $( this ); // The current toggle span in the loop |
|
| |
|
| /* ====================================== | | // --- Extract the unique name for matching toggle span and div --- |
| | // This is crucial for matching the 'mw-customtoggle-NAME' with 'mw-customcollapsible-NAME'. |
| | var uniqueName = null; |
| | var classList = $toggleSpan.attr('class').split(' '); // Get all classes as an array |
|
| |
|
| | | // Loop through the classes to find the one starting with 'mw-customtoggle-' |
| BuildTextTable js | | for (var i = 0; i < classList.length; i++) { |
| | | // Checks if a class starts with "mw-customtoggle-" (ES5 compatible) |
| | | if (classList[i].indexOf('mw-customtoggle-') === 0) { |
| ======================================*/
| | uniqueName = classList[i].replace('mw-customtoggle-', ''); // Extract the unique name (e.g., "HomeIntro") |
|
| | break; // Found it, no need to check further |
| 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;
| |
| } | | } |
| }
| |
| }
| |
| });
| |
| }
| |
|
| |
|
| | | if (uniqueName === null) { |
| | | console.warn('Skipping toggle: Could not find "mw-customtoggle-" class on span for:', $toggleSpan); |
| /*
| | return; // Skip this span if its unique name cannot be determined |
| | |
| 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;
| |
| } | | } |
| | // --- End unique name extraction --- |
|
| |
|
| for (var k = words.length - 1; k >= 0; k--) { | | // Construct the ID of the corresponding collapsible div (e.g., #mw-customcollapsible-HomeIntro) |
| var part = words[k];
| | var $collapsibleDiv = $( '#mw-customcollapsible-' + uniqueName ); |
| var newSpan = document.createElement("span");
| |
|
| |
|
| if (/^\s+$/.test(part)) {
| | // Log findings for this specific toggle pair (for debugging) |
| newSpan.className = "gap";
| | //console.log('Processing toggle: ' + uniqueName); |
| newSpan.textContent = part;
| | //console.log(' Collapsible div found:', $collapsibleDiv.length > 0 ? 'Yes' : 'No'); |
| newSpan.addEventListener("click", function (event) {
| | //console.log(' Toggle span found:', $toggleSpan.length > 0 ? 'Yes' : 'No'); |
| attemptGapMerge(this, event);
| |
| });
| |
| } else {
| |
| newSpan.className = "english id-none";
| |
| newSpan.textContent = part;
| |
| attachSpanClickHandler(newSpan);
| |
| }
| |
|
| |
|
| line.insertBefore(newSpan, span);
| | // Stop processing this toggle if its corresponding div is not found |
| | if ($collapsibleDiv.length === 0) { |
| | console.warn('Skipping toggle ' + uniqueName + ': Corresponding collapsible div (#mw-customcollapsible-' + uniqueName + ') not found.'); |
| | return; |
| } | | } |
|
| |
|
| line.removeChild(span); | | // Function to update the 'is-expanded-toggle' class on the span |
| }
| | // This class will trigger the CSS rotation. |
| }
| | function updateCaretDirection() { |
| | // Check the div's current state: does it have the 'mw-collapsed' class? |
| | var isCollapsed = $collapsibleDiv.hasClass( 'mw-collapsed' ); |
| | //console.log(' updateCaretDirection called for ' + uniqueName + '. Div has mw-collapsed:', isCollapsed); |
|
| |
|
| // PART 2: Insert gap spans between adjacent spans (if no element exists between)
| | if ( isCollapsed ) { |
| var updatedSpans = Array.from(line.querySelectorAll("span"));
| | // If the DIV is collapsed, remove our custom class from the toggle SPAN |
| | | $toggleSpan.removeClass( 'is-expanded-toggle' ); |
| for (var j = updatedSpans.length - 1; j > 0; j--) {
| | //console.log(' Caret state for ' + uniqueName + ': collapsed (removed is-expanded-toggle)'); |
| var prev = updatedSpans[j - 1];
| | } else { |
| var next = updatedSpans[j];
| | // If the DIV is expanded, add our custom class to the toggle SPAN |
| | | $toggleSpan.addClass( 'is-expanded-toggle' ); |
| if (prev.nextElementSibling !== next) {
| | //console.log(' Caret state for ' + uniqueName + ': expanded (added is-expanded-toggle)'); |
| 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) {
| | // --- Event Handling for this specific toggle --- |
| div.style.backgroundColor = ''; | |
| });
| |
|
| |
|
| console.log("Removed annotation for line:", lineId);
| | // 1. Set the initial state of the caret on page load. |
| return;
| | // This is important if the toggle starts collapsed (which it usually does). |
| }
| | updateCaretDirection(); |
|
| |
|
| // Normal click: add annotation if not present
| | // 2. Attach a click listener directly to this specific toggle span. |
| if (!coloredWords.some(function (w) { return w.id === lineId; })) {
| | // When the button is clicked, we'll update the caret. |
| coloredWords.push({
| | $toggleSpan.on( 'click', function() { |
| id: lineId,
| | //console.log(' Toggle button CLICKED for ' + uniqueName + '!'); |
| hebrew: lineText,
| | // Use a small delay (50ms) to ensure MediaWiki's native toggle logic |
| gloss: '',
| | // has finished updating the 'mw-collapsed' class on the div *before* our function runs. |
| annotation: selectedAnnotation,
| | setTimeout(updateCaretDirection, 50); |
| 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);
| | }); // End of .each() loop: This ensures the above logic runs for every toggle found. |
| return headerRow.children.length; // default: append
| |
| }
| |
|
| |
|
| | }); // End of jQuery(function($)): This ensures the script runs when the DOM is ready. |
|
| |
|
| function getLineIdFromRow(row) {
| | // Re-collapse expanded elements when clicking outside the element |
| if (!row){
| | document.addEventListener('click', function (event) { |
| console.warn("Attempting to get line from a null row");
| | var collapsibles = document.querySelectorAll('[id^="mw-customcollapsible-"]'); |
| 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) {
| | for (var i = 0; i < collapsibles.length; i++) { |
| if (!lineId || !overlayTable) return null;
| | var collapsible = collapsibles[i]; |
|
| | var id = collapsible.id; |
| var rows = overlayTable.getElementsByTagName('tr');
| | var shortId = id.replace('mw-customcollapsible-', ''); |
|
| | var toggle = document.querySelector('.mw-customtoggle-' + shortId); |
| 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 (!toggle || !collapsible) { |
|
| | continue; |
| 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;
| | var isClickInside = collapsible.contains(event.target) || toggle.contains(event.target); |
|
| |
|
| // Get section-* class from cellA
| | if (!isClickInside && !collapsible.classList.contains('mw-collapsed')) { |
| var sectionClassA = null;
| | toggle.click(); // Trigger collapse |
| 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;
| |
| }
| |
|
| |
|
| |
| }); | | }); |