MediaWiki:Common.js
From Psalms: Layer by Layer
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
importScript('MediaWiki:Overlays.js'); importScript('MediaWiki:Insertions.js'); importScript('MediaWiki:Lineation.js'); importScript('MediaWiki:AutoLoad.js'); importScript('MediaWiki:Compare.js'); /* Any JavaScript here will be loaded for all users on every page load. */ // Function to check if all <pre class="mermaid"> elements are processed function checkMermaidProcessed() { var mermaidPreElements = document.querySelectorAll('pre.mermaid'); var allProcessed = true; mermaidPreElements.forEach(function (element) { if (!element.hasAttribute('data-processed') || element.getAttribute('data-processed') !== 'true') { allProcessed = false; } }); return allProcessed; } // Function to wait until all Mermaid diagrams are processed function waitForMermaidProcessing(callback) { var interval = setInterval(function () { if (checkMermaidProcessed()) { clearInterval(interval); callback(); // Once all elements are processed, run the callback } }, 100); // Check every 100ms } function toggleVisibility(containerId, className) { //console.log("Toggling visibility for " + className + " in " + containerId); var container = document.getElementById(containerId); if (container) { if (className==="alternative"){ // Match all elements with pinkish FILL or STROKE var elements = container.querySelectorAll('[fill^="#f4"], [stroke^="#f4"], [fill^="#f6"], [stroke^="#f6"]'); elements.forEach(function(el) { if (!el.hasAttribute('data-original-display')) { el.setAttribute('data-original-display', el.style.display || ""); } if (el.style.display === "none") { el.style.display = el.getAttribute('data-original-display') || ""; } else { el.style.display = "none"; } }); }else{ var elements = container.querySelectorAll("g." + className); for (var i = 0; i < elements.length; i++) { var element = elements[i]; if (element.style.display === "none") { element.style.display = ""; // Show element } else { element.style.display = "none"; // Hide element } } } } else { console.warn("Container with ID \"" + containerId + "\" not found."); } } function attachToggleListeners() { //console.log("Attaching event listeners to toggle links."); var toggleLinks = document.querySelectorAll("[data-container-id][data-class]"); //console.log("Found " + toggleLinks.length + " links."); // If no toggle links are found, print a warning if (toggleLinks.length === 0) { //console.warn("No toggle links found on the page."); } for (var i = 0; i < toggleLinks.length; i++) { (function (toggleLink) { toggleLink.addEventListener("click", function (event) { event.preventDefault(); // Prevent default link behavior var containerId = toggleLink.getAttribute("data-container-id"); var className = toggleLink.getAttribute("data-class"); toggleVisibility(containerId, className); }); })(toggleLinks[i]); } } // =========================== $(document).ready(function () { // =========================== // Auto-create links for notes such as "v. 2 preferred diagram" // =========================== var headingLinks = {}; if (document.getElementById("createDiagramLinks")) { // 1. Build a mapping from "v. 2 preferred" ➔ "Preferred_2" var currentVerseRange = ""; var headings = document.querySelectorAll('h1, h2'); for (var i = 0; i < headings.length; i++) { var heading = headings[i]; if (heading.tagName === 'H1') { currentVerseRange = heading.innerText.trim().toLowerCase(); // e.g., "v. 2" } if (heading.tagName === 'H2') { var description = heading.innerText.trim(); var combinedKey = (currentVerseRange + " " + description).toLowerCase(); // e.g., "v. 2 preferred" // Ensure heading has a usable ID if (!heading.id) { heading.id = description.replace(/\s+/g, "_") + "_" + currentVerseRange.replace(/[^0-9]/g, ""); } headingLinks[combinedKey] = heading.id; } } // 3. Apply to whole document linkifyTextNodes(document.body); } // 2. Search and replace text nodes function linkifyTextNodes(node) { if (node.nodeType === Node.TEXT_NODE) { var text = node.nodeValue; var parent = node.parentNode; for (var phrase in headingLinks) { var regex = new RegExp("\\b(" + phrase + ") diagram\\b", "i"); var match = regex.exec(text); if (match) { var before = text.slice(0, match.index); var after = text.slice(match.index + match[0].length); var anchor = document.createElement('a'); anchor.href = "#" + headingLinks[phrase]; anchor.textContent = match[0]; parent.insertBefore(document.createTextNode(before), node); parent.insertBefore(anchor, node); if (after) parent.insertBefore(document.createTextNode(after), node); parent.removeChild(node); break; // Stop after first match in this node } } } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'A') { for (var j = 0; j < node.childNodes.length; j++) { linkifyTextNodes(node.childNodes[j]); } } } // =========================== // End links for notes such as "v. 2 preferred diagram" // =========================== //console.log("Document ready. Attaching event listeners to toggle links."); // Now attach event listeners for toggling visibility attachToggleListeners(); // Wait until all Mermaid diagrams are processed waitForMermaidProcessing(function () { //console.log("Mermaid diagrams are fully processed."); $('div[id^="verse-"]').each(function () { var parentDiv = $(this); var svg = parentDiv.find('svg'); if (svg.length > 0) { var preElement = parentDiv.find('pre.mermaid'); // The <pre> element containing the SVG var preWidth = preElement.width(); var preHeight = preElement.height(); var viewBox = svg[0].getAttribute('viewBox'); if (viewBox) { var viewBoxValues = viewBox.split(' '); var viewBoxWidth = parseFloat(viewBoxValues[2]); var viewBoxHeight = parseFloat(viewBoxValues[3]); var scaleX = preWidth / viewBoxWidth; var scaleY = preHeight / viewBoxHeight; var scale = Math.min(scaleX, scaleY); svg.css({ 'width': (preWidth) + 'px', 'max-width': (preWidth) + 'px', 'height': (viewBoxHeight * scale) + 'px', 'position': 'relative', // Ensure the SVG has a positioning context 'left': '-10px' // Offset the SVG to the left, because firefox and others misalign it to the right. This removes the horizontal scrollbar }); // Initialize panzoom var panZoomInstance = Panzoom(svg[0], { contain: 'outside', minScale: 1, // default is 0.125 maxScale: 10, // default is 4 panOnlyWhenZoomed: true, //default is false zoomSpeed: 0.040, // default is 6.5% per mouse wheel event pinchSpeed: 1.5 // default is zoom two times faster than the distance between fingers }); parentDiv[0].addEventListener('wheel', function (e) { e.preventDefault(); panZoomInstance.zoomWithWheel(e, { step: 0.04 }); // custom override per event }); parentDiv[0].addEventListener('dblclick', function (event) { var rect = parentDiv[0].getBoundingClientRect(); var offsetX = event.clientX - rect.left; var offsetY = event.clientY - rect.top; if (event.shiftKey) { // Shift + Double-click → Zoom Out panZoomInstance.zoomOut({ focal: { x: offsetX, y: offsetY } }); } else { // Regular Double-click → Zoom In panZoomInstance.zoomIn({ focal: { x: offsetX, y: offsetY } }); } }); // Resize handler to keep SVG scaled on window resize var resizeHandler = function () { var newWidth = preElement.width(); svg.css({ 'width': (newWidth) + 'px', 'max-width': (newWidth) + 'px' // Do not change the height to avoid reflowing the html page }); }; // Listen for resize events $(window).on('resize', resizeHandler); } } }); // Initially hide elements with the "highlight-phrase" class document.querySelectorAll(".highlight-phrase").forEach(function (element) { element.style.display = "none"; // Hide elements initially }); // Bind lightbox functionality $('.lightbox-button').on('click', function () { // Get the target <div> ID from the button's data-target attribute var targetDivId = $(this).data('target'); // e.g., '#verse-1' var parentDiv = $(targetDivId); // Find the corresponding <div> by ID var associatedSvg = parentDiv.find('svg'); // Find the SVG inside the <pre> if (associatedSvg.length > 0) { openLightbox(associatedSvg[0]); } }); // Open the lightbox and display the SVG in full-screen function openLightbox(svgElement) { // Create lightbox container if it doesn't exist var lightbox = $('<div id="lightbox-overlay" class="lightbox-overlay">') .appendTo('body') .css({ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: 'rgba(128, 128, 128, 0.8)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999, }); // Create the SVG container in the lightbox var lightboxSvgContainer = $('<div class="lightbox-svg-container">') .appendTo(lightbox) .css({ width: '95%', height: '95%', overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 1.0)', }); var lightboxSvg = $(svgElement).clone().appendTo(lightboxSvgContainer); // Clone the SVG // resize the svg to the available space lightboxSvg.css({ 'width': '100%', 'max-width': '100%', 'height': '100%' }); // Apply panzoom to the cloned SVG in the lightbox var panZoomInstanceLightbox = Panzoom(lightboxSvg[0], { contain: 'outside', minScale: 1, // default is 0.125 maxScale: 10, // default is 4 panOnlyWhenZoomed: true, //default is false zoomSpeed: 0.040, // default is 6.5% per mouse wheel event pinchSpeed: 1.5 // default is zoom two times faster than the distance between fingers }); lightboxSvg[0].addEventListener('wheel', function (e) { e.preventDefault(); panZoomInstanceLightbox.zoomWithWheel(e, { step: 0.04 }); // custom override per event }); lightboxSvg[0].addEventListener('dblclick', function (event) { var rect = lightboxSvg[0].getBoundingClientRect(); var offsetX = event.clientX - rect.left; var offsetY = event.clientY - rect.top; if (event.shiftKey) { // Shift + Double-click → Zoom Out panZoomInstanceLightbox.zoomOut({ focal: { x: offsetX, y: offsetY } }); } else { // Regular Double-click → Zoom In panZoomInstanceLightbox.zoomIn({ focal: { x: offsetX, y: offsetY } }); } }); var closeButton = $('<button class="lightbox-close-button">Close</button>') .appendTo(lightbox) .css({ position: 'absolute', top: '10px', right: '10px', backgroundColor: '#fff', color: '#000', border: '1px solid #bbb', borderRadius: '1rem', padding: '10px 20px', cursor: 'pointer', zIndex: 10000 }) .on('click', function () { // Close the lightbox when the close button is clicked lightbox.remove(); }); lightbox.on('click', function (event) { // Close the lightbox when clicking outside the SVG if ($(event.target).is(lightbox)) { lightbox.remove(); } }); // Close the lightbox with the Escape key $(document).on('keydown', function (event) { if (event.key === "Escape" || event.keyCode === 27) { lightbox.remove(); $(document).off('keydown'); // Remove the keydown listener to prevent multiple bindings } }); } }); // Bidirectional hover for Hebrew and Gloss (ES5-compatible) var hoverElements = document.querySelectorAll(".hebrew, .gloss"); // If no toggle links are found, print a warning if (hoverElements.length === 0) { //console.warn("No hover elements found on the page."); } for (var i = 0; i < hoverElements.length; i++) { (function (el) { el.addEventListener("mouseenter", function () { //var classList = el.className.split(" "); var className = (typeof el.className === 'object' && el.className.baseVal) ? el.className.baseVal : el.className; var classList = className.split(" "); for (var j = 0; j < classList.length; j++) { var cls = classList[j]; if (cls.indexOf("id-") === 0) { var matches = document.getElementsByClassName(cls); for (var k = 0; k < matches.length; k++) { matches[k].classList.add("highlighted"); } } } }); el.addEventListener("mouseleave", function () { //var classList = el.className.split(" "); var className = (typeof el.className === 'object' && el.className.baseVal) ? el.className.baseVal : el.className; var classList = className.split(" "); for (var j = 0; j < classList.length; j++) { var cls = classList[j]; if (cls.indexOf("id-") === 0) { var matches = document.getElementsByClassName(cls); for (var k = 0; k < matches.length; k++) { matches[k].classList.remove("highlighted"); } } } }); })(hoverElements[i]); } }); // Stopping fixed elements $(document).ready(function () { var $box = $('.fixed-box'); var $wrapper = $box.parent(); var stopY = 515; $(window).on('scroll', function () { var scrollY = window.scrollY || window.pageYOffset; if (scrollY >= stopY) { $box.css({ position: 'absolute', top: stopY + 'px', // place it exactly where it was fixed left: 0, width: '100%' }); } else { $box.css({ position: 'fixed', top: '0px', left: 0, width: '100%' }); } }); }); // 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]; 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 } } });