/** * 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 = ``; 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(); } } }