MediaWiki: Common.js: Difference between revisions

From Psalms: Layer by Layer
Jump to: navigation, search
No edit summary
mNo edit summary
 
(313 intermediate revisions by 3 users not shown)
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
importScript('MediaWiki:Overlays.js');
importScript('MediaWiki:Insertions.js');
importScript('MediaWiki:Lineation.js');
importScript('MediaWiki:AutoLoad.js');
importScript('MediaWiki:Compare.js');
//importScript('MediaWiki:Diagrams.js');


//console.log("Hello world!");
 
 
/* Any JavaScript here will be loaded for all users on every page load. */
 
function deletePageByTitle(sourcePage, suffix) {
  var api = new mw.Api();
 
  console.log("[DeletePage] Starting with source:", sourcePage);
 
  // Replace "200" with chapter in the sourcePage string to get the target page
  var targetPage = sourcePage + "/" + suffix;
 
  console.log("[DeletePage] Target page:", targetPage);
 
  // Step: Delete the page
  api.postWithToken("csrf", {
    action: "delete",
    title: targetPage,
    reason: "Clean-up: removing auto-generated page for " + sourcePage
  }).then(function (result) {
    if (result && result.delete && result.delete.title) {
      console.log("[DeletePage] ✅ Deleted:", result.delete.title);
    } else {
      console.error("[DeletePage] ❌ Deletion failed:", result);
    }
  }).catch(function (err) {
    console.error("[DeletePage] ❌ API error:", err);
  });
}
 
function copyPageContents(sourcePage, chapter, overrideTargetPage) {
  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);
  if (overrideTargetPage)
targetPage = overrideTargetPage;
  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 toggleVisibility(containerId, className) {
    //console.log("Toggling visibility for " + className + " in " + containerId);
    var container = document.getElementById(containerId);
    var elements = null;
    if (container) {
    if (className==="alternative"){
// Match all elements with pinkish FILL or STROKE
elements = container.querySelectorAll('[fill^="#f4"], [stroke^="#f4"], [fill^="#f6"], [stroke^="#f6"]');
elements.forEach(function(el) {
    if (!el.hasAttribute('data-original-display')) {
        el.setAttribute('data-original-display', el.style.display || "");
    }
    if (el.style.display === "none") {
        el.style.display = el.getAttribute('data-original-display') || "";
    } else {
        el.style.display = "none";
    }
});
    }else{
        elements = container.querySelectorAll("g." + className);
        for (var i = 0; i < elements.length; i++) {
            var element = elements[i];
           
            if (element.style.display === "none") {
                element.style.display = ""; // Show element
            } else {
                element.style.display = "none"; // Hide element
            }
        }
    }
    } else {
        console.warn("Container with ID \"" + containerId + "\" not found.");
    }
}
function attachToggleListeners() {
    //console.log("Attaching event listeners to toggle links.");
    var toggleLinks = document.querySelectorAll("[data-container-id][data-class]");
    //console.log("Found " + toggleLinks.length + " links.");
    // If no toggle links are found, print a warning
    if (toggleLinks.length === 0) {
        //console.warn("No toggle links found on the page.");
    }
   
    for (var i = 0; i < toggleLinks.length; i++) {
        (function (toggleLink) {
            toggleLink.addEventListener("click", function (event) {
                event.preventDefault(); // Prevent default link behavior
                var containerId = toggleLink.getAttribute("data-container-id");
                var className = toggleLink.getAttribute("data-class");
                toggleVisibility(containerId, className);
            });
        })(toggleLinks[i]);
    }
}
 
// ===========================
 
// Mermaid code that, if stored elsewhere, doesn't load in time
 
// ===========================
// Function to check if all <pre class="mermaid"> elements are processed
function checkMermaidProcessed() {
    var mermaidPreElements = document.querySelectorAll('pre.mermaid');
    var allProcessed = true;
    mermaidPreElements.forEach(function (element) {
        if (!element.hasAttribute('data-processed') || element.getAttribute('data-processed') !== 'true') {
            allProcessed = false;
        }
    });
if (!allProcessed)
console.log("Mermaid items still remaining to be processed.");
    return allProcessed;
}
 
 
function createAutoDiagramLinks(){
// ===========================
// Auto-create links for notes such as "v. 2 preferred diagram"
// ===========================
    var headingLinks = {};
if (document.getElementById("createDiagramLinks")) {
    // 1. Build a mapping from "v. 2 preferred" ➔ "Preferred_2"
    var currentVerseRange = "";
    var headings = document.querySelectorAll('h1, h2');
    for (var i = 0; i < headings.length; i++) {
        var heading = headings[i];
        if (heading.tagName === 'H1') {
            currentVerseRange = heading.innerText.trim().toLowerCase(); // e.g., "v. 2"
        }
        if (heading.tagName === 'H2') {
            var description = heading.innerText.trim();
            var combinedKey = (currentVerseRange + " " + description).toLowerCase(); // e.g., "v. 2 preferred"
            // Ensure heading has a usable ID
            if (!heading.id) {
                heading.id = description.replace(/\s+/g, "_") + "_" + currentVerseRange.replace(/[^0-9]/g, "");
            }
            headingLinks[combinedKey] = heading.id;
        }
    }
    // 3. Apply to whole document
    linkifyTextNodes(document.body);
}
    // 2. Search and replace text nodes
    function linkifyTextNodes(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            var text = node.nodeValue;
            var parent = node.parentNode;
            for (var phrase in headingLinks) {
                var regex = new RegExp("\\b(" + phrase + ") diagram\\b", "i");
                var match = regex.exec(text);
                if (match) {
                    var before = text.slice(0, match.index);
                    var after = text.slice(match.index + match[0].length);
                    var anchor = document.createElement('a');
                    anchor.href = "#" + headingLinks[phrase];
                    anchor.textContent = match[0];
                    parent.insertBefore(document.createTextNode(before), node);
                    parent.insertBefore(anchor, node);
                    if (after) parent.insertBefore(document.createTextNode(after), node);
                    parent.removeChild(node);
                    break; // Stop after first match in this node
                }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'A') {
            for (var j = 0; j < node.childNodes.length; j++) {
                linkifyTextNodes(node.childNodes[j]);
            }
        }
    }
}
// ===========================
// End links for notes such as "v. 2 preferred diagram"
// ===========================
function attachResizeHandler($pre, $svg) {
var resizeHandler = function () {
var newWidth = $pre.width();
$svg.css({
width: newWidth + 'px',
'max-width': newWidth + 'px'
// Note: do not touch height to prevent layout reflow
});
};
$(window).on('resize', resizeHandler);
}
function initializePanZoom(container, svgElement) {
var panZoomInstance = Panzoom(svgElement, {
contain: 'outside',
minScale: 1,
maxScale: 10,
panOnlyWhenZoomed: true,
zoomSpeed: 0.040,
pinchSpeed: 1.5
});
container.addEventListener('wheel', function (e) {
e.preventDefault();
panZoomInstance.zoomWithWheel(e, { step: 0.04 });
});
container.addEventListener('dblclick', function (e) {
var rect = container.getBoundingClientRect();
var offsetX = e.clientX - rect.left;
var offsetY = e.clientY - rect.top;
if (e.shiftKey) {
panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
} else {
panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
}
});
}
 
function processMermaidContainer(container) {
var $container = $(container);
var $svg = $container.find('svg');
if ($svg.length === 0) {
console.log("Found no svg", container);
return;
}
var $pre = $container.find('pre.mermaid');
var preWidth = $pre.width();
var preHeight = $pre.height();
var viewBox = $svg[0].getAttribute('viewBox');
if (!viewBox) {
console.log("Found no viewBox", container);
return;
}
var viewBoxValues = viewBox.split(' ');
var viewBoxWidth = parseFloat(viewBoxValues[2]);
var viewBoxHeight = parseFloat(viewBoxValues[3]);
var scaleX = preWidth / viewBoxWidth;
var scaleY = preHeight / viewBoxHeight;
var scale = Math.min(scaleX, scaleY);
$svg.css({
width: preWidth + 'px',
'max-width': preWidth + 'px',
height: (viewBoxHeight * scale) + 'px',
position: 'relative',
left: '-10px'
});
initializePanZoom($container[0], $svg[0]);
attachResizeHandler($pre, $svg);
}
 
function processAllMermaidContainers(container) {
var c = container || document;
var verseDivs = c.querySelectorAll('div[id^="verse-"]');
verseDivs.forEach(function (div) {
processMermaidContainer(div);
});
}
 
// Function to wait until all Mermaid diagrams are processed
function waitForMermaidProcessing(callback) {
    var interval = setInterval(function () {
        if (checkMermaidProcessed()) {
            clearInterval(interval);
            callback(); // Once all elements are processed, run the callback
        }
    }, 100); // Check every 100ms
}
 
function hideHighlightPhrases(container) {
var c = container || document;
var elements = c.querySelectorAll(".highlight-phrase");
for (var i = 0; i < elements.length; i++) {
elements[i].style.display = "none";
}
}
 
function openLightbox(svgElement) {
var lightbox = $('<div id="lightbox-overlay" class="lightbox-overlay">')
.appendTo('body')
.css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(128, 128, 128, 0.8)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999
});
var lightboxSvgContainer = $('<div class="lightbox-svg-container">')
.appendTo(lightbox)
.css({
width: '95%',
height: '95%',
overflow: 'hidden',
backgroundColor: 'rgba(255, 255, 255, 1.0)'
});
var lightboxSvg = $(svgElement).clone().appendTo(lightboxSvgContainer).css({
width: '100%',
'max-width': '100%',
height: '100%'
});
var panZoomInstance = Panzoom(lightboxSvg[0], {
contain: 'outside',
minScale: 1,
maxScale: 10,
panOnlyWhenZoomed: true,
zoomSpeed: 0.040,
pinchSpeed: 1.5
});
lightboxSvg[0].addEventListener('wheel', function (e) {
e.preventDefault();
panZoomInstance.zoomWithWheel(e, { step: 0.04 });
});
lightboxSvg[0].addEventListener('dblclick', function (e) {
var rect = lightboxSvg[0].getBoundingClientRect();
var offsetX = e.clientX - rect.left;
var offsetY = e.clientY - rect.top;
if (e.shiftKey) {
panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } });
} else {
panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } });
}
});
var closeButton = $('<button class="lightbox-close-button">Close</button>')
.appendTo(lightbox)
.css({
position: 'absolute',
top: '10px',
right: '10px',
backgroundColor: '#fff',
color: '#000',
border: '1px solid #bbb',
borderRadius: '1rem',
padding: '10px 20px',
cursor: 'pointer',
zIndex: 10000
})
.on('click', function () {
lightbox.remove();
});
lightbox.on('click', function (e) {
if ($(e.target).is(lightbox)) {
lightbox.remove();
}
});
$(document).on('keydown.lightbox', function (e) {
if (e.key === "Escape" || e.keyCode === 27) {
lightbox.remove();
$(document).off('keydown.lightbox');
}
});
}
 
function bindLightboxButtons() {
$('.lightbox-button').off('click').on('click', function () {
var targetDivId = $(this).data('target');
var parentDiv = $(targetDivId);
var associatedSvg = parentDiv.find('svg');
if (associatedSvg.length > 0) {
openLightbox(associatedSvg[0]);
}
});
}
 
function initializeMermaidSVGScaling(container) {
var c = container || document;
waitForMermaidProcessing(function () {
processAllMermaidContainers(c);
hideHighlightPhrases(c);
});
}
 
function initializeLazyLoadedEnhancements(container) {
var c = container || document;
initializeMermaidSVGScaling(c);
bindLightboxButtons(c);
mw.loader.using('jquery.makeCollapsible', function () {
$(c).find('.mw-collapsible').makeCollapsible();
});
}
 
 
 
$(document).ready(function () {
 
var allPageChapters = [];
var buttons = document.querySelectorAll('.save-page-button');
buttons.forEach(function (button) {
  var page = button.dataset.page;
  var chapter = button.dataset.chapter;
  // Store the unique (page, chapter) pair
  allPageChapters.push({ page: page, chapter: chapter });
  // Bind individual button click
  button.addEventListener('click', function () {
    console.log('[SaveDiv] Clicked button for page:', page, 'and chapter:', chapter);
    copyPageContents(page, chapter);
  });
});
console.log("Save-page-buttons ready for saving.");
console.log("All page/chapter combinations:", allPageChapters);
// Optional: Set up a "copy all" button
var copyAllButton = document.querySelector('#copyAllButton');
if (copyAllButton) {
  copyAllButton.addEventListener('click', function () {
    console.log('[SaveDiv] Copying all pages...');
    allPageChapters.forEach(function (pair) {
      copyPageContents(pair.page, pair.chapter);
    });
  });
}
 
    //console.log("Document ready. Attaching event listeners to toggle links.");
 
    // Now attach event listeners for toggling visibility
    attachToggleListeners();
   
    initializeLazyLoadedEnhancements(document);
 
 
// Bidirectional hover for Hebrew and Gloss (ES5-compatible)
var hoverElements = document.querySelectorAll(".hebrew, .english");
 
// If no toggle links are found, print a warning
    if (hoverElements.length === 0) {
        //console.warn("No hover elements found on the page.");
    }
   
for (var i = 0; i < hoverElements.length; i++) {
    (function (el) {
        el.addEventListener("mouseenter", function () {
            //var classList = el.className.split(" ");
           
            var className = (typeof el.className === 'object' && el.className.baseVal) ? el.className.baseVal : el.className;
var classList = className.split(" ");
 
            for (var j = 0; j < classList.length; j++) {
                var cls = classList[j];
                if (cls.indexOf("id-") === 0) {
                    var matches = document.getElementsByClassName(cls);
                    for (var k = 0; k < matches.length; k++) {
                        matches[k].classList.add("highlighted");
                    }
                }
            }
        });
        el.addEventListener("mouseleave", function () {
            //var classList = el.className.split(" ");
           
            var className = (typeof el.className === 'object' && el.className.baseVal) ? el.className.baseVal : el.className;
var classList = className.split(" ");
            for (var j = 0; j < classList.length; j++) {
                var cls = classList[j];
                if (cls.indexOf("id-") === 0) {
                    var matches = document.getElementsByClassName(cls);
                    for (var k = 0; k < matches.length; k++) {
                        matches[k].classList.remove("highlighted");
                    }
                }
            }
        });
    })(hoverElements[i]);
}
//createAutoDiagramLinks();
mw.loader.using([], function () {
setTimeout(function () {
if (typeof createAutoDiagramLinks === 'function') {
createAutoDiagramLinks();
} else {
console.warn('createAutoDiagramLinks is still not available.');
}
}, 200); // or more, depending on network
});
 
 
 
 
});
 
// Stopping fixed elements
$(document).ready(function () {
  var $box = $('.fixed-box');
  var $wrapper = $box.parent();
  var stopY = 515;
 
  $(window).on('scroll', function () {
    var scrollY = window.scrollY || window.pageYOffset;
 
    if (scrollY >= stopY) {
      $box.css({
        position: 'absolute',
        top: stopY + 'px', // place it exactly where it was fixed
        left: 0,
        width: '100%'
      });
    } else {
      $box.css({
        position: 'fixed',
        top: '0px',
        left: 0,
        width: '100%'
      });
    }
  });
});
 
// Rotating caret for collapsible elements
// This ensures the code runs after the entire page (DOM) is loaded.
jQuery( function( $ ) {
 
    console.log('Caret toggle JS loaded (Fresh Start Version).');
 
    // Select all toggle spans that have our dedicated controlling class.
    // This makes the script work for multiple toggles on the same page.
    $( '.js-toggle-caret-controller' ).each( function() {
        var $toggleSpan = $( this ); // The current toggle span in the loop
 
        // --- Extract the unique name for matching toggle span and div ---
        // This is crucial for matching the 'mw-customtoggle-NAME' with 'mw-customcollapsible-NAME'.
        var uniqueName = null;
        var classList = $toggleSpan.attr('class').split(' '); // Get all classes as an array
 
        // Loop through the classes to find the one starting with 'mw-customtoggle-'
        for (var i = 0; i < classList.length; i++) {
            // Checks if a class starts with "mw-customtoggle-" (ES5 compatible)
            if (classList[i].indexOf('mw-customtoggle-') === 0) {
                uniqueName = classList[i].replace('mw-customtoggle-', ''); // Extract the unique name (e.g., "HomeIntro")
                break; // Found it, no need to check further
            }
        }
 
        if (uniqueName === null) {
            console.warn('Skipping toggle: Could not find "mw-customtoggle-" class on span for:', $toggleSpan);
            return; // Skip this span if its unique name cannot be determined
        }
        // --- End unique name extraction ---
 
        // Construct the ID of the corresponding collapsible div (e.g., #mw-customcollapsible-HomeIntro)
        var $collapsibleDiv = $( '#mw-customcollapsible-' + uniqueName );
 
        // Log findings for this specific toggle pair (for debugging)
        //console.log('Processing toggle: ' + uniqueName);
        //console.log('  Collapsible div found:', $collapsibleDiv.length > 0 ? 'Yes' : 'No');
        //console.log('  Toggle span found:', $toggleSpan.length > 0 ? 'Yes' : 'No');
 
        // Stop processing this toggle if its corresponding div is not found
        if ($collapsibleDiv.length === 0) {
            console.warn('Skipping toggle ' + uniqueName + ': Corresponding collapsible div (#mw-customcollapsible-' + uniqueName + ') not found.');
            return;
        }
 
        // Function to update the 'is-expanded-toggle' class on the span
        // This class will trigger the CSS rotation.
        function updateCaretDirection() {
            // Check the div's current state: does it have the 'mw-collapsed' class?
            var isCollapsed = $collapsibleDiv.hasClass( 'mw-collapsed' );
            //console.log('  updateCaretDirection called for ' + uniqueName + '. Div has mw-collapsed:', isCollapsed);
 
            if ( isCollapsed ) {
                // If the DIV is collapsed, remove our custom class from the toggle SPAN
                $toggleSpan.removeClass( 'is-expanded-toggle' );
                //console.log('  Caret state for ' + uniqueName + ': collapsed (removed is-expanded-toggle)');
            } else {
                // If the DIV is expanded, add our custom class to the toggle SPAN
                $toggleSpan.addClass( 'is-expanded-toggle' );
                //console.log('  Caret state for ' + uniqueName + ': expanded (added is-expanded-toggle)');
            }
        }
 
        // --- Event Handling for this specific toggle ---
 
        // 1. Set the initial state of the caret on page load.
        // This is important if the toggle starts collapsed (which it usually does).
        updateCaretDirection();
 
        // 2. Attach a click listener directly to this specific toggle span.
        // When the button is clicked, we'll update the caret.
        $toggleSpan.on( 'click', function() {
            //console.log('  Toggle button CLICKED for ' + uniqueName + '!');
            // Use a small delay (50ms) to ensure MediaWiki's native toggle logic
            // has finished updating the 'mw-collapsed' class on the div *before* our function runs.
            setTimeout(updateCaretDirection, 50);
        });
 
    }); // End of .each() loop: This ensures the above logic runs for every toggle found.
 
}); // End of jQuery(function($)): This ensures the script runs when the DOM is ready.
 
// Re-collapse expanded elements when clicking outside the element
document.addEventListener('click', function (event) {
  var collapsibles = document.querySelectorAll('[id^="mw-customcollapsible-"]');
 
  for (var i = 0; i < collapsibles.length; i++) {
    var collapsible = collapsibles[i];
 
    // ✅ Skip if it has opt-out flag
    if (collapsible.hasAttribute('data-no-auto-collapse') || collapsible.classList.contains('no-auto-collapse')) {
      continue;
    }
 
    var id = collapsible.id;
    var shortId = id.replace('mw-customcollapsible-', '');
    var toggle = document.querySelector('.mw-customtoggle-' + shortId);
 
    if (!toggle || !collapsible) {
      continue;
    }
 
    var isClickInside = collapsible.contains(event.target) || toggle.contains(event.target);
 
    if (!isClickInside && !collapsible.classList.contains('mw-collapsed')) {
      toggle.click(); // Trigger collapse
    }
  }
});

Latest revision as of 17:46, 26 June 2025

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



	/* Any JavaScript here will be loaded for all users on every page load. */

function deletePageByTitle(sourcePage, suffix) {
  var api = new mw.Api();

  console.log("[DeletePage] Starting with source:", sourcePage);

  // Replace "200" with chapter in the sourcePage string to get the target page
  var targetPage = sourcePage + "/" + suffix;
  
  console.log("[DeletePage] Target page:", targetPage);

  // Step: Delete the page
  api.postWithToken("csrf", {
    action: "delete",
    title: targetPage,
    reason: "Clean-up: removing auto-generated page for " + sourcePage
  }).then(function (result) {
    if (result && result.delete && result.delete.title) {
      console.log("[DeletePage] ✅ Deleted:", result.delete.title);
    } else {
      console.error("[DeletePage] ❌ Deletion failed:", result);
    }
  }).catch(function (err) {
    console.error("[DeletePage] ❌ API error:", err);
  });
}

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

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

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

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


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

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

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

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

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

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

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

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

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



$(document).ready(function () {

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

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

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


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

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

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




});

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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