MediaWiki: Common.js: Difference between revisions

From Psalms: Layer by Layer
Jump to: navigation, search
No edit summary
No edit summary
Line 452: Line 452:


// Rotating caret for collapsible elements
// Rotating caret for collapsible elements
// Ensure this code runs after the DOM is ready
jQuery( function( $ ) {
jQuery( function( $ ) {
    // Select the collapsible div and the toggle span
     var $collapsibleDiv = $( '#mw-customcollapsible-HomeIntro' );
     var $collapsibleDiv = $( '#mw-customcollapsible-HomeIntro' );
     var $toggleSpan = $( '.mw-customtoggle-HomeIntro' );
     var $toggleSpan = $( '.mw-customtoggle-HomeIntro' );


     // Function to update caret class
    // Log to console to confirm JS execution and element finding
    console.log('Caret toggle JS loaded.');
    console.log('Collapsible div found:', $collapsibleDiv.length > 0 ? 'Yes' : 'No');
    console.log('Toggle span found:', $toggleSpan.length > 0 ? 'Yes' : 'No');
 
     // Function to update the class on the toggle span
     function updateCaretDirection() {
     function updateCaretDirection() {
         if ( $collapsibleDiv.hasClass( 'mw-collapsed' ) ) {
         if ( $collapsibleDiv.hasClass( 'mw-collapsed' ) ) {
            // If the DIV is collapsed, remove our custom class from the toggle SPAN
             $toggleSpan.removeClass( 'is-expanded-toggle' );
             $toggleSpan.removeClass( 'is-expanded-toggle' );
            console.log('Caret: collapsed (removing is-expanded-toggle)');
         } else {
         } else {
            // If the DIV is expanded, add our custom class to the toggle SPAN
             $toggleSpan.addClass( 'is-expanded-toggle' );
             $toggleSpan.addClass( 'is-expanded-toggle' );
            console.log('Caret: expanded (adding is-expanded-toggle)');
         }
         }
     }
     }


     // Initial check on page load
     // Run once on page load to set the initial state (e.g., if it's already collapsed)
     updateCaretDirection();
     updateCaretDirection();


     // Listen for changes (MediaWiki's collapsible JS will toggle mw-collapsed)
     // Listen for MediaWiki's internal 'afterToggle' event on the collapsible div.
     // This is a custom event typically emitted by MediaWiki's collapsible script
     // This event fires after the 'mw-collapsed' class has been added/removed.
     $collapsibleDiv.on( 'afterToggle.mw-collapsible', updateCaretDirection );
     $collapsibleDiv.on( 'afterToggle.mw-collapsible', updateCaretDirection );
} );
} );

Revision as of 21:36, 5 June 2025

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
// Ensure this code runs after the DOM is ready
jQuery( function( $ ) {

    // Select the collapsible div and the toggle span
    var $collapsibleDiv = $( '#mw-customcollapsible-HomeIntro' );
    var $toggleSpan = $( '.mw-customtoggle-HomeIntro' );

    // Log to console to confirm JS execution and element finding
    console.log('Caret toggle JS loaded.');
    console.log('Collapsible div found:', $collapsibleDiv.length > 0 ? 'Yes' : 'No');
    console.log('Toggle span found:', $toggleSpan.length > 0 ? 'Yes' : 'No');

    // Function to update the class on the toggle span
    function updateCaretDirection() {
        if ( $collapsibleDiv.hasClass( 'mw-collapsed' ) ) {
            // If the DIV is collapsed, remove our custom class from the toggle SPAN
            $toggleSpan.removeClass( 'is-expanded-toggle' );
            console.log('Caret: collapsed (removing 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: expanded (adding is-expanded-toggle)');
        }
    }

    // Run once on page load to set the initial state (e.g., if it's already collapsed)
    updateCaretDirection();

    // Listen for MediaWiki's internal 'afterToggle' event on the collapsible div.
    // This event fires after the 'mw-collapsed' class has been added/removed.
    $collapsibleDiv.on( 'afterToggle.mw-collapsible', updateCaretDirection );
} );