MediaWiki: Common.js: Difference between revisions
From Psalms: Layer by Layer
No edit summary |
mNo edit summary |
||
(313 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
/ | 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 | |||
} | |||
} | |||
}); |
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 } } });