/**
* Modal utilities for handling dynamic content loading
*/
class ModalHandler {
constructor(modalId, contentElementId) {
this.modalElement = document.getElementById(modalId);
this.contentElement = document.getElementById(contentElementId);
this.modal = null;
if (this.modalElement && typeof bootstrap !== "undefined") {
this.modal = new bootstrap.Modal(this.modalElement);
// Set up global event delegation for modal close buttons
this.setupGlobalCloseHandlers();
}
}
/**
* Load content into modal via AJAX and show it
* @param {string} url - URL to fetch content from
* @param {string} errorMessage - Message to show on error
*/
async loadAndShow(url, errorMessage = "Error loading content.") {
if (!this.modal || !this.contentElement) {
console.error("Modal or content element not found");
return;
}
try {
const response = await fetch(url);
const html = await response.text();
this.contentElement.innerHTML = html;
// Set up close button handlers after content is loaded
this.setupCloseHandlers();
// Format any JSON content in the modal
this.formatJsonContent();
this.modal.show();
} catch (error) {
console.error("Error loading modal content:", error);
this.contentElement.innerHTML = `
${errorMessage}
`;
this.modal.show();
}
}
/**
* Set up click handlers for elements that should open the modal
* @param {string} selector - CSS selector for clickable elements
* @param {string} urlAttribute - Attribute name containing the URL (default: 'data-url')
*/
setupClickHandlers(selector, urlAttribute = "data-url") {
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(selector).forEach((element) => {
element.addEventListener("click", (e) => {
e.preventDefault();
const url = element.getAttribute(urlAttribute);
if (url) {
this.loadAndShow(url);
}
});
});
});
}
/**
* Show the modal with custom content
* @param {string} content - HTML content to display
*/
showWithContent(content) {
if (!this.modal || !this.contentElement) return;
this.contentElement.innerHTML = content;
// Set up close button handlers after content is loaded
this.setupCloseHandlers();
this.modal.show();
}
/**
* Set up global event delegation for modal close buttons
*/
setupGlobalCloseHandlers() {
// Use event delegation to handle dynamically loaded close buttons
this.modalElement.addEventListener("click", (e) => {
if (
e.target.matches('[data-bs-dismiss="modal"]') ||
e.target.closest('[data-bs-dismiss="modal"]') ||
e.target.matches(".btn-close") ||
e.target.closest(".btn-close")
) {
e.preventDefault();
this.hide();
}
});
// Handle ESC key press
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
this.modal &&
this.modalElement.classList.contains("show")
) {
this.hide();
}
});
}
/**
* Set up close button event handlers for dynamically loaded content
*/
setupCloseHandlers() {
// This method is now mostly redundant due to global event delegation
// but we'll keep it for backward compatibility
// Handle close buttons with data-bs-dismiss="modal"
const closeButtons = this.contentElement.querySelectorAll(
'[data-bs-dismiss="modal"]'
);
closeButtons.forEach((button) => {
button.addEventListener("click", (e) => {
e.preventDefault();
this.hide();
});
});
// Handle close buttons with .btn-close class
const closeButtonsClass =
this.contentElement.querySelectorAll(".btn-close");
closeButtonsClass.forEach((button) => {
button.addEventListener("click", (e) => {
e.preventDefault();
this.hide();
});
});
// Also handle ESC key press
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
this.modal &&
this.modalElement.classList.contains("show")
) {
this.hide();
}
});
}
/**
* Format JSON content in the modal after it's loaded
*/
formatJsonContent() {
// Format JSON in extra data if present
const extraDataElement = this.contentElement.querySelector(
"#extra-data-content"
);
if (extraDataElement && extraDataElement.textContent.trim()) {
try {
const jsonData = JSON.parse(extraDataElement.textContent);
// Pretty-format the JSON with proper indentation
const formattedJson = JSON.stringify(jsonData, null, 2);
extraDataElement.textContent = formattedJson;
// Add syntax highlighting classes if the JSON is complex
if (typeof jsonData === "object" && jsonData !== null) {
extraDataElement.parentElement.classList.add("json-formatted");
}
} catch (e) {
// If it's not valid JSON, leave it as is but still format if it looks like JSON
const text = extraDataElement.textContent.trim();
if (text.startsWith("{") || text.startsWith("[")) {
// Try to fix common JSON issues and reformat
try {
const fixedJson = text
.replace(/'/g, '"')
.replace(/None/g, "null")
.replace(/True/g, "true")
.replace(/False/g, "false");
const parsed = JSON.parse(fixedJson);
extraDataElement.textContent = JSON.stringify(parsed, null, 2);
} catch (fixError) {
// If still can't parse, just leave as is
console.debug("Extra data is not valid JSON:", e);
}
}
}
}
// Also format old_value and new_value if they contain JSON
const preElements = this.contentElement.querySelectorAll("pre code");
preElements.forEach(function (codeElement) {
if (codeElement && codeElement.textContent.trim()) {
const text = codeElement.textContent.trim();
if (
(text.startsWith("{") && text.endsWith("}")) ||
(text.startsWith("[") && text.endsWith("]"))
) {
try {
const jsonData = JSON.parse(text);
codeElement.textContent = JSON.stringify(jsonData, null, 2);
} catch (e) {
// Not JSON, leave as is
}
}
}
});
}
/**
* Hide the modal
*/
hide() {
if (this.modal) {
this.modal.hide();
}
}
}