UNPKG

web-mojo

Version:

WEB-MOJO - A lightweight JavaScript framework for building data-driven web applications

1,382 lines (1,381 loc) 44 kB
import { V as View } from "./WebApp-B2r2EDj7.js"; class Dialog extends View { static _openDialogs = []; static _baseZIndex = { backdrop: 1050, modal: 1055 }; static _busyIndicator = null; static _busyCounter = 0; static _busyTimeout = null; /** * Shows a full-screen busy indicator. * Manages a counter for nested calls, only showing one indicator. * @param {object} options - Options { timeout, message } */ static showBusy(options = {}) { const { timeout = 3e4, message = "Loading..." } = options; this._busyCounter++; if (this._busyCounter === 1) { if (this._busyTimeout) { clearTimeout(this._busyTimeout); } if (!this._busyIndicator) { this._busyIndicator = document.createElement("div"); this._busyIndicator.className = "mojo-busy-indicator"; this._busyIndicator.innerHTML = ` <div class="mojo-busy-spinner"> <div class="spinner-border text-light" role="status"> <span class="visually-hidden">Loading...</span> </div> <p class="mojo-busy-message mt-3 text-light">${message}</p> </div> <style> .mojo-busy-indicator { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.15s linear; } .mojo-busy-indicator.show { opacity: 1; } .mojo-busy-spinner .spinner-border { width: 3rem; height: 3rem; } </style> `; document.body.appendChild(this._busyIndicator); } const msgElement = this._busyIndicator.querySelector(".mojo-busy-message"); if (msgElement) msgElement.textContent = message; setTimeout(() => this._busyIndicator.classList.add("show"), 10); this._busyTimeout = setTimeout(() => { console.error("Busy indicator timed out."); this.hideBusy(true); this.alert({ title: "Operation Timed Out", message: "The operation took too long. Please check your connection and try again.", type: "danger" }); }, timeout); } } /** * Hides the full-screen busy indicator. * Decrements the counter and only hides when the counter reaches zero. * @param {boolean} force - If true, forces the indicator to hide immediately. */ static hideBusy(force = false) { if (force) { this._busyCounter = 0; } else { this._busyCounter--; } if (this._busyCounter <= 0) { this._busyCounter = 0; if (this._busyTimeout) { clearTimeout(this._busyTimeout); this._busyTimeout = null; } if (this._busyIndicator) { this._busyIndicator.classList.remove("show"); setTimeout(() => { if (this._busyIndicator && this._busyCounter === 0) { this._busyIndicator.remove(); this._busyIndicator = null; } }, 150); } } } constructor(options = {}) { const modalId = options.id || `modal-${Date.now()}`; super({ ...options, id: modalId, // Pass the ID to parent constructor tagName: "div", className: `modal ${options.fade !== false ? "fade" : ""} ${options.className || ""}`, attributes: { tabindex: "-1", "aria-hidden": "true", "aria-labelledby": options.labelledBy || `${modalId}-label`, "aria-describedby": options.describedBy || null, ...options.attributes } }); this.modalId = modalId; this.title = options.title || ""; this.titleId = `${this.modalId}-label`; this.size = options.size || ""; this.centered = options.centered !== void 0 ? options.centered : false; this.scrollable = options.scrollable !== void 0 ? options.scrollable : false; this.autoSize = options.autoSize || options.size === "auto"; this.backdrop = options.backdrop !== void 0 ? options.backdrop : true; this.keyboard = options.keyboard !== void 0 ? options.keyboard : true; this.focus = options.focus !== void 0 ? options.focus : true; this.header = options.header !== void 0 ? options.header : true; this.headerContent = options.headerContent || null; this.closeButton = options.closeButton !== void 0 ? options.closeButton : true; this.contextMenu = options.contextMenu || null; this.body = options.body || options.content || ""; this.bodyView = null; this.bodyClass = options.bodyClass || ""; this.noBodyPadding = options.noBodyPadding || false; this.minWidth = options.minWidth || 300; this.minHeight = options.minHeight || 200; this.maxWidthPercent = options.maxWidthPercent || 0.9; this.maxHeightPercent = options.maxHeightPercent || 0.8; this._processBodyContent(this.body); this.footer = options.footer || null; this.footerView = null; this.footerClass = options.footerClass || ""; this._processFooterContent(this.footer); this.buttons = options.buttons || null; this.onShow = options.onShow || null; this.onShown = options.onShown || null; this.onHide = options.onHide || null; this.onHidden = options.onHidden || null; this.onHidePrevented = options.onHidePrevented || null; this.autoShow = options.autoShow !== void 0 ? options.autoShow : false; this.modal = null; this.relatedTarget = options.relatedTarget || null; } /** * Process body content to detect and handle View instances */ _processBodyContent(body) { if (body && body.render) { this.bodyView = body; this.body = ""; this.addChild(this.bodyView); } else if (typeof body === "function") { try { const result = body(); if (result instanceof View) { this.bodyView = result; this.body = ""; this.addChild(this.bodyView); } else if (result instanceof Promise) { this.bodyPromise = result; this.body = '<div class="text-center"><div class="spinner-border spinner-border-sm"></div></div>'; } else { this.body = result; } } catch (error) { console.error("Error processing body function:", error); this.body = body; } } else { this.body = body; } } /** * Process footer content to detect and handle View instances */ _processFooterContent(footer) { if (footer instanceof View) { this.footerView = footer; this.footer = null; this.addChild(this.footerView); } else if (typeof footer === "function") { try { const result = footer(); if (result instanceof View) { this.footerView = result; this.footer = null; this.addChild(this.footerView); } else if (result instanceof Promise) { this.footerPromise = result; this.footer = '<div class="text-center"><div class="spinner-border spinner-border-sm"></div></div>'; } else { this.footer = result; } } catch (error) { console.error("Error processing footer function:", error); this.footer = footer; } } else { this.footer = footer; } } /** * Get dialog template with all Bootstrap 5 features */ async getTemplate() { const dialogClasses = ["modal-dialog"]; if (this.size && this.size !== "auto") { if (this.size.startsWith("fullscreen")) { dialogClasses.push(`modal-${this.size}`); } else if (["sm", "lg", "xl"].includes(this.size)) { dialogClasses.push(`modal-${this.size}`); } } if (this.centered) { dialogClasses.push("modal-dialog-centered"); } if (this.scrollable) { dialogClasses.push("modal-dialog-scrollable"); } return ` <div class="${dialogClasses.join(" ")}"> <div class="modal-content"> ${await this.buildHeader()} ${await this.buildBody()} ${await this.buildFooter()} </div> </div> `; } /** * Build modal header */ async buildHeader() { if (!this.header) { return ""; } if (this.headerContent) { return `<div class="modal-header">${this.headerContent}</div>`; } let headerActions = ""; if (this.contextMenu && this.contextMenu.items && this.contextMenu.items.length > 0) { headerActions = await this.buildContextMenu(); } else if (this.closeButton) { headerActions = '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>'; } return ` <div class="modal-header"> ${this.title ? `<h5 class="modal-title" id="${this.titleId}">${this.title}</h5>` : ""} ${headerActions} </div> `; } async buildContextMenu() { const menuItems = await this.filterContextMenuItems(); if (menuItems.length === 0) { return this.closeButton ? '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>' : ""; } const triggerIcon = this.contextMenu.icon || "bi-three-dots-vertical"; const buttonClass = this.contextMenu.buttonClass || "btn btn-link p-1 mojo-modal-context-menu-btn"; const menuItemsHtml = menuItems.map((item) => { if (item.type === "divider") { return '<li><hr class="dropdown-divider"></li>'; } const icon = item.icon ? `<i class="${item.icon} me-2"></i>` : ""; const label = item.label || ""; if (item.href) { return `<li><a class="dropdown-item" href="${item.href}"${item.target ? ` target="${item.target}"` : ""}>${icon}${label}</a></li>`; } else if (item.action) { const dataAttrs = Object.keys(item).filter((key) => key.startsWith("data-")).map((key) => `${key}="${item[key]}"`).join(" "); return `<li><a class="dropdown-item" data-action="${item.action}" ${dataAttrs}>${icon}${label}</a></li>`; } return ""; }).join(""); return ` <div class="dropdown"> <button class="${buttonClass}" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <i class="${triggerIcon}"></i> </button> <ul class="dropdown-menu dropdown-menu-end"> ${menuItemsHtml} </ul> </div> `; } async filterContextMenuItems() { if (!this.contextMenu || !this.contextMenu.items) { return []; } const filteredItems = []; for (const item of this.contextMenu.items) { if (item.type === "divider") { filteredItems.push(item); continue; } if (item.permissions) { try { const app = this.getApp?.(); let user = null; if (app) { user = app.activeUser || app.getState?.("activeUser"); } if (!user && typeof window !== "undefined" && window.getApp) { try { const globalApp = window.getApp(); user = globalApp?.activeUser; } catch (e) { } } if (user && user.hasPermission) { if (!user.hasPermission(item.permissions)) { continue; } } else { continue; } } catch (error) { console.warn("Error checking permissions for context menu item:", error); continue; } } filteredItems.push(item); } return filteredItems; } /** * Build modal body */ async buildBody() { if (this.bodyView) { this.bodyView.replaceById = true; const bodyClass2 = this.noBodyPadding ? `modal-body p-0 ${this.bodyClass}` : `modal-body ${this.bodyClass}`; return `<div class="${bodyClass2}" data-view-container="body"> <!-- View will be mounted here --> <div id="${this.bodyView.id}"></div> </div>`; } if (!this.body && this.body !== "") { return ""; } const bodyClass = this.noBodyPadding ? `modal-body p-0 ${this.bodyClass}` : `modal-body ${this.bodyClass}`; return ` <div class="${bodyClass}"> ${this.body} </div> `; } /** * Build modal footer */ async buildFooter() { if (this.footerView) { return `<div class="modal-footer ${this.footerClass}" data-view-container="footer"></div>`; } if (this.footer !== null && typeof this.footer === "string") { return `<div class="modal-footer ${this.footerClass}">${this.footer}</div>`; } if (this.buttons && this.buttons.length > 0) { const buttonsHtml = this.buttons.map((btn) => { const dismissAttr = btn.dismiss ? 'data-bs-dismiss="modal"' : ""; const actionAttr = btn.action ? `data-action="${btn.action}"` : ""; const idAttr = btn.id ? `id="${btn.id}"` : ""; const disabledAttr = btn.disabled ? "disabled" : ""; return ` <button type="${btn.type || "button"}" class="btn ${btn.class || "btn-secondary"}" ${idAttr} ${dismissAttr} ${actionAttr} ${disabledAttr}> ${btn.icon ? `<i class="bi ${btn.icon} me-1"></i>` : ""} ${btn.text || "Button"} </button> `; }).join(""); return `<div class="modal-footer ${this.footerClass}">${buttonsHtml}</div>`; } return ""; } /** * Override mount to not require a container for dialogs * Dialogs are appended to body directly */ async mount(_container = null) { if (this.mounted || this.destroyed) { return; } if (!this.element) { throw new Error("Cannot mount dialog without element"); } await this.onBeforeMount(); document.body.appendChild(this.element); this.bindEvents(); this.mounted = true; await this.onAfterMount(); this.emit("mounted", { view: this }); return this; } /** * After render - prepare for View instances and apply syntax highlighting */ async onAfterRender() { await super.onAfterRender(); if (window.Prism && this.element) { const codeBlocks = this.element.querySelectorAll("pre code"); if (codeBlocks.length > 0) { window.Prism.highlightAllUnder(this.element); } } if (this.autoSize) { this.setupAutoSizing(); } } /** * After mount - initialize Bootstrap modal and mount child views */ async onAfterMount() { await super.onAfterMount(); if (typeof window !== "undefined" && window.bootstrap && window.bootstrap.Modal) { if (this.backdrop === "static") { this.element.setAttribute("data-bs-backdrop", "static"); } if (!this.keyboard) { this.element.setAttribute("data-bs-keyboard", "false"); } this.modal = new window.bootstrap.Modal(this.element, { backdrop: this.backdrop, keyboard: this.keyboard, focus: this.focus }); this.bindBootstrapEvents(); if (this.autoShow) { this.show(this.relatedTarget); } } } /** * Setup auto-sizing - wait for modal animation to complete */ setupAutoSizing() { if (!this.element) return; this.element.addEventListener("shown.bs.modal", () => { this.applyAutoSizing(); }, { once: true }); setTimeout(() => { if (this.isShown()) { this.applyAutoSizing(); } }, 100); } /** * Apply auto-sizing based on content dimensions */ applyAutoSizing() { if (!this.element) return; try { const modalDialog = this.element.querySelector(".modal-dialog"); const modalContent = this.element.querySelector(".modal-content"); const modalBody = this.element.querySelector(".modal-body"); if (!modalDialog || !modalContent || !modalBody) { console.warn("Dialog auto-sizing: Required elements not found"); return; } if (this.bodyView && !this.bodyView.element) { setTimeout(() => this.applyAutoSizing(), 50); return; } const originalStyles = { dialogMaxWidth: modalDialog.style.maxWidth, dialogWidth: modalDialog.style.width, contentWidth: modalContent.style.width, contentMaxHeight: modalContent.style.maxHeight, bodyOverflow: modalBody.style.overflowY }; modalDialog.style.maxWidth = "none"; modalDialog.style.width = "auto"; modalContent.style.width = "auto"; modalContent.style.maxHeight = "none"; modalContent.offsetHeight; const contentRect = modalContent.getBoundingClientRect(); const viewportMargin = 40; const maxWidth = Math.min( window.innerWidth * this.maxWidthPercent, window.innerWidth - viewportMargin ); const maxHeight = Math.min( window.innerHeight * this.maxHeightPercent, window.innerHeight - viewportMargin ); let optimalWidth = Math.max(this.minWidth, Math.ceil(contentRect.width + 20)); let optimalHeight = Math.max(this.minHeight, Math.ceil(contentRect.height)); optimalWidth = Math.min(optimalWidth, maxWidth); const heightExceedsMax = contentRect.height > maxHeight; modalDialog.style.maxWidth = `${optimalWidth}px`; modalDialog.style.width = `${optimalWidth}px`; if (heightExceedsMax) { modalContent.style.maxHeight = `${maxHeight}px`; modalBody.style.overflowY = "auto"; optimalHeight = maxHeight; } this.autoSizedWidth = optimalWidth; this.autoSizedHeight = optimalHeight; this._originalStyles = originalStyles; } catch (error) { console.error("Error in dialog auto-sizing:", error); this.element.querySelector(".modal-dialog").style.maxWidth = ""; } } /** * Reset auto-sizing and restore original modal dimensions */ resetAutoSizing() { if (!this.autoSize || !this._originalStyles || !this.element) return; try { const modalDialog = this.element.querySelector(".modal-dialog"); const modalContent = this.element.querySelector(".modal-content"); const modalBody = this.element.querySelector(".modal-body"); if (modalDialog && modalContent && modalBody) { modalDialog.style.maxWidth = this._originalStyles.dialogMaxWidth || ""; modalDialog.style.width = this._originalStyles.dialogWidth || ""; modalContent.style.width = this._originalStyles.contentWidth || ""; modalContent.style.maxHeight = this._originalStyles.contentMaxHeight || ""; modalBody.style.overflowY = this._originalStyles.bodyOverflow || ""; delete this.autoSizedWidth; delete this.autoSizedHeight; delete this._originalStyles; } } catch (error) { console.error("Error resetting dialog auto-sizing:", error); } } /** * Bind Bootstrap modal events */ bindBootstrapEvents() { this.element.addEventListener("show.bs.modal", (e) => { const stackIndex = Dialog._openDialogs.length; const newZIndex = Dialog._baseZIndex.modal + stackIndex * 20; this.element.style.zIndex = newZIndex; Dialog._openDialogs.push(this); setTimeout(() => { const backdrops = document.querySelectorAll(".modal-backdrop"); const backdrop = backdrops[backdrops.length - 1]; if (backdrop) { backdrop.style.zIndex = newZIndex - 5; } }, 0); if (this.onShow) this.onShow(e); this.emit("show", { dialog: this, relatedTarget: e.relatedTarget }); }); this.element.addEventListener("shown.bs.modal", (e) => { if (this.onShown) this.onShown(e); this.emit("shown", { dialog: this, relatedTarget: e.relatedTarget }); if (this.focus) { const firstInput = this.element.querySelector('input:not([type="hidden"]), textarea, select'); if (firstInput) { firstInput.focus(); } } }); this.element.addEventListener("hide.bs.modal", (e) => { const focusedElement = this.element.querySelector(":focus"); if (focusedElement) { focusedElement.blur(); } if (this.onHide) { const result = this.onHide(e); if (result === false) { e.preventDefault(); return; } } this.emit("hide", { dialog: this }); }); this.element.addEventListener("hidden.bs.modal", (e) => { const index = Dialog._openDialogs.indexOf(this); if (index > -1) { Dialog._openDialogs.splice(index, 1); } if (Dialog._openDialogs.length > 0) { document.body.classList.add("modal-open"); const topDialog = Dialog._openDialogs[Dialog._openDialogs.length - 1]; const topZIndex = parseInt(topDialog.element.style.zIndex, 10); setTimeout(() => { const backdrops = document.querySelectorAll(".modal-backdrop"); const backdrop = backdrops[backdrops.length - 1]; if (backdrop) { backdrop.style.zIndex = topZIndex - 5; } }, 0); } if (this.previousFocus && document.body.contains(this.previousFocus)) { this.previousFocus.focus(); } if (this.onHidden) this.onHidden(e); this.emit("hidden", { dialog: this }); }); this.element.addEventListener("hidePrevented.bs.modal", (e) => { if (this.onHidePrevented) this.onHidePrevented(e); this.emit("hidePrevented", { dialog: this }); }); } /** * Show the dialog * @param {HTMLElement} relatedTarget - Optional element that triggered the modal */ show(relatedTarget = null) { this.previousFocus = document.activeElement; window.lastDialog = this; if (this.modal) { this.modal.show(relatedTarget); } } /** * Hide the dialog */ hide() { const focusedElement = this.element?.querySelector(":focus"); if (focusedElement) { focusedElement.blur(); } if (this.modal) { this.modal.hide(); } } /** * Toggle the dialog * @param {HTMLElement} relatedTarget - Optional element that triggered the modal */ toggle(relatedTarget = null) { if (this.modal) { this.modal.toggle(relatedTarget); } } /** * Destroy the dialog and clean up resources */ async destroy() { if (this.modal) { const focusedElement = this.element?.querySelector(":focus"); if (focusedElement) { focusedElement.blur(); } this.modal.dispose(); this.modal = null; } if (this.previousFocus && document.body.contains(this.previousFocus)) { this.previousFocus.focus(); this.previousFocus = null; } if (this.autoSize) { this.resetAutoSizing(); } await super.destroy(); } /** * Handle dynamic height changes */ handleUpdate() { if (this.modal) { this.modal.handleUpdate(); } } /** * Update dialog content * @param {string|View} content - New content (string or View instance) */ async setContent(content) { if (content instanceof View) { if (this.bodyView) { await this.bodyView.destroy(); this.removeChild(this.bodyView); } this.bodyView = content; this.body = ""; this.addChild(this.bodyView); const bodyEl = this.element?.querySelector(".modal-body"); if (bodyEl) { bodyEl.innerHTML = ""; await this.bodyView.render(bodyEl); } } else { this.body = content; const bodyEl = this.element?.querySelector(".modal-body"); if (bodyEl) { bodyEl.innerHTML = content; } } this.handleUpdate(); } /** * Update dialog title */ setTitle(title) { this.title = title; const titleEl = this.element?.querySelector(".modal-title"); if (titleEl) { titleEl.textContent = title; } } /** * Set loading state */ setLoading(loading = true, message = "Loading...") { const bodyEl = this.element?.querySelector(".modal-body"); if (bodyEl) { if (loading) { bodyEl.innerHTML = ` <div class="text-center py-4"> <div class="spinner-border text-primary mb-3" role="status"> <span class="visually-hidden">Loading...</span> </div> <p>${message}</p> </div> `; } else if (this.bodyView) { bodyEl.replaceChildren(this.bodyView.element); } } } /** * Clean up */ async onBeforeDestroy() { if (this.bodyView) { await this.bodyView.destroy(); } if (this.footerView) { await this.footerView.destroy(); } await super.onBeforeDestroy(); if (this.modal) { this.modal.dispose(); this.modal = null; } } /** * Static method to show code in a dialog */ static async showCode(options = {}) { const dialog = new Dialog({ title: options.title || "Source Code", size: options.size || "lg", scrollable: true, body: Dialog.formatCode(options.code, options.language), buttons: [ { text: "Copy to Clipboard", class: "btn-primary", icon: "bi-clipboard", action: "copy" }, { text: "Close", class: "btn-secondary", dismiss: true } ] }); dialog.on("action:copy", async () => { if (navigator.clipboard) { try { await navigator.clipboard.writeText(options.code); dialog.showCopySuccess(); } catch (err) { console.error("Failed to copy:", err); } } }); await dialog.render(true, document.body); if (window.Prism && dialog.element) { window.Prism.highlightAllUnder(dialog.element); } dialog.show(); dialog.on("hidden", () => { dialog.destroy(); dialog.element.remove(); }); return dialog; } /** * Format code for display with syntax highlighting support */ static formatCode(code, language = "javascript") { let highlightedCode; if (window.Prism && window.Prism.languages[language]) { highlightedCode = window.Prism.highlight(code, window.Prism.languages[language], language); } else { highlightedCode = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); } const prismClass = window.Prism ? `language-${language}` : ""; const codeStyles = ` max-height: 60vh; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 1.25rem; border-radius: 0.5rem; margin: 0; font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Consolas', 'Monaco', monospace; font-size: 0.9rem; line-height: 1.6; border: 1px solid #2d2d30; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); `.replace(/\s+/g, " ").trim(); return ` <style> /* Custom Prism theme overrides for Dialog */ .dialog-code-block .token.comment { color: #6a9955; } .dialog-code-block .token.string { color: #ce9178; } .dialog-code-block .token.keyword { color: #569cd6; } .dialog-code-block .token.function { color: #dcdcaa; } .dialog-code-block .token.number { color: #b5cea8; } .dialog-code-block .token.operator { color: #d4d4d4; } .dialog-code-block .token.class-name { color: #4ec9b0; } .dialog-code-block .token.punctuation { color: #d4d4d4; } .dialog-code-block .token.boolean { color: #569cd6; } .dialog-code-block .token.property { color: #9cdcfe; } .dialog-code-block .token.tag { color: #569cd6; } .dialog-code-block .token.attr-name { color: #9cdcfe; } .dialog-code-block .token.attr-value { color: #ce9178; } .dialog-code-block ::selection { background: #264f78; } </style> <pre class="${prismClass} dialog-code-block" style="${codeStyles}"> <code class="${prismClass}" style="color: inherit; background: transparent; text-shadow: none;">${highlightedCode}</code> </pre> `; } /** * Trigger Prism highlighting on already rendered code blocks * Call this after inserting code into the DOM if not using formatCode */ static highlightCodeBlocks(container = document) { if (window.Prism && window.Prism.highlightAllUnder) { window.Prism.highlightAllUnder(container); } } /** * Show copy success feedback */ showCopySuccess() { const btn = this.element.querySelector('[data-action="copy"]'); if (btn) { const originalHtml = btn.innerHTML; btn.innerHTML = '<i class="bi bi-check me-1"></i>Copied!'; btn.classList.remove("btn-primary"); btn.classList.add("btn-success"); btn.disabled = true; setTimeout(() => { btn.innerHTML = originalHtml; btn.classList.remove("btn-success"); btn.classList.add("btn-primary"); btn.disabled = false; }, 2e3); } } /** * Show a dialog with promise-based button handling * - If a button has a handler, it will be called. Return semantics: * - true or undefined: resolve and close (with button.value || button.action || index) * - null or false: keep dialog open (do not resolve) * - any other value: resolve with that value and close * - If no handler, resolve with action/value/index and close * @param {Object} options - Dialog options * @returns {Promise} Resolves with value/action/index or null on dismiss */ static async showDialog(options = {}) { if (typeof options === "string") { const message = arguments[0]; const title2 = arguments[1] || "Alert"; const opts = arguments[2] || {}; options = { ...opts, body: message, title: title2 }; } const { title = "Dialog", content, body = content || "", size = "md", centered = true, buttons = [ { text: "OK", class: "btn-primary", value: true } ], rejectOnDismiss = false, // Default to return null on dismissal ...dialogOptions } = options; const dialog = new Dialog({ title, body, size, centered, buttons, ...dialogOptions }); await dialog.render(true, document.body); return new Promise((resolve, reject) => { let resolved = false; const buttonElements = dialog.element.querySelectorAll(".modal-footer button"); buttonElements.forEach((btnElement, index) => { const buttonConfig = buttons[index]; if (!buttonConfig) return; btnElement.addEventListener("click", async (e) => { if (resolved) return; const defaultResolveValue = buttonConfig.value !== void 0 ? buttonConfig.value : buttonConfig.action ?? index; if (typeof buttonConfig.handler === "function") { try { const result = await buttonConfig.handler({ dialog, button: buttonConfig, index, event: e }); if (result === null || result === false) { return; } const valueToResolve = result === true || result === void 0 ? defaultResolveValue : result; resolved = true; if (!buttonConfig.dismiss) { dialog.hide(); } resolve(valueToResolve); } catch (err) { console.error("Dialog button handler error:", err); return; } } else { resolved = true; if (!buttonConfig.dismiss) { dialog.hide(); } resolve(defaultResolveValue); } }); }); dialog.on("hidden", () => { if (!resolved) { resolved = true; if (rejectOnDismiss) { reject(new Error("Dialog dismissed")); } else { resolve(null); } } setTimeout(() => { dialog.destroy(); dialog.element.remove(); }, 100); }); dialog.show(); }); } /** * Static alert dialog helper * @param {Object|string} options - Alert options or message string * @returns {Promise} Resolves when OK is clicked */ static async alert(options = {}) { if (typeof options === "string") { options = { message: options, title: "Alert" }; } const { message = "", title = "Alert", type = "info", // info, success, warning, danger ...dialogOptions } = options; let icon = ""; let titleClass = ""; switch (type) { case "success": icon = '<i class="bi bi-check-circle-fill text-success me-2"></i>'; titleClass = "text-success"; break; case "warning": icon = '<i class="bi bi-exclamation-triangle-fill text-warning me-2"></i>'; titleClass = "text-warning"; break; case "danger": case "error": icon = '<i class="bi bi-x-circle-fill text-danger me-2"></i>'; titleClass = "text-danger"; break; default: icon = '<i class="bi bi-info-circle-fill text-info me-2"></i>'; titleClass = "text-info"; } return Dialog.showDialog({ title: `<span class="${titleClass}">${icon}${title}</span>`, body: `<p>${message}</p>`, size: "sm", centered: true, buttons: [ { text: "OK", class: "btn-primary", value: true } ], ...dialogOptions }); } /** * Static confirm dialog */ static async confirm(message, title = "Confirm", options = {}) { if (typeof message === "object") { options = message; message = options.message; title = options.title || title; } const dialog = new Dialog({ title, body: `<p>${message}</p>`, size: options.size || "sm", centered: true, backdrop: "static", buttons: [ { text: options.cancelText || "Cancel", class: "btn-secondary", dismiss: true, action: "cancel" }, { text: options.confirmText || "Confirm", class: options.confirmClass || "btn-primary", action: "confirm" } ], ...options }); await dialog.render(true, document.body); dialog.show(); return new Promise((resolve) => { let result = false; dialog.on("action:confirm", () => { result = true; dialog.hide(); }); dialog.on("hidden", () => { dialog.destroy(); dialog.element.remove(); resolve(result); }); }); } /** * Static prompt dialog */ static async prompt(message, title = "Input", options = {}) { const inputId = `prompt-input-${Date.now()}`; const defaultValue = options.defaultValue || ""; const inputType = options.inputType || "text"; const placeholder = options.placeholder || ""; const dialog = new Dialog({ title, body: ` <p>${message}</p> <input type="${inputType}" class="form-control" id="${inputId}" value="${defaultValue}" placeholder="${placeholder}"> `, size: options.size || "sm", centered: true, backdrop: "static", buttons: [ { text: "Cancel", class: "btn-secondary", dismiss: true }, { text: "OK", class: "btn-primary", action: "ok" } ], ...options }); await dialog.render(true, document.body); dialog.show(); dialog.on("shown", () => { const input = dialog.element.querySelector(`#${inputId}`); if (input) { input.focus(); input.select(); } }); return new Promise((resolve) => { let result = null; dialog.on("action:ok", () => { const input = dialog.element.querySelector(`#${inputId}`); result = input ? input.value : null; dialog.hide(); }); dialog.on("hidden", () => { dialog.destroy(); dialog.element.remove(); resolve(result); }); }); } /** * Get Bootstrap modal instance */ getModal() { return this.modal; } /** * Check if modal is shown */ isShown() { return this.element?.classList.contains("show") || false; } /** * Show form in a dialog for simple data collection (no model saving) * @param {object} options - Configuration options * @returns {Promise} Promise that resolves with form data or null if cancelled */ static async showForm(options = {}) { const { title = "Form", formConfig = {}, size = "md", centered = true, submitText = "Submit", cancelText = "Cancel", ...dialogOptions } = options; const FormView = (await import("./FormView-fUbbKQQU.js").then((n) => n.b)).default; const formView = new FormView({ fileHandling: options.fileHandling || "base64", data: options.data, defaults: options.defaults, model: options.model, formConfig: { fields: formConfig.fields || options.fields, ...formConfig, submitButton: false, resetButton: false } }); const dialog = new Dialog({ title, body: formView, size, centered, buttons: [ { text: cancelText, class: "btn-secondary", action: "cancel" }, { text: submitText, class: "btn-primary", action: "submit" } ], ...dialogOptions }); await dialog.render(true, document.body); dialog.show(); return new Promise((resolve) => { let resolved = false; dialog.on("action:submit", async () => { if (resolved) return; if (!formView.validate()) { formView.focusFirstError(); return; } if (options.autoSave && options.model) { dialog.setLoading(true); const result = await formView.saveModel(); if (!result.success) { dialog.setLoading(false); dialog.render(); dialog.getApp().toast.error(result.message); return; } resolved = true; dialog.hide(); resolve(result); } try { const formData = await formView.getFormData(); resolved = true; dialog.hide(); resolve(formData); } catch (error) { console.error("Error collecting form data:", error); formView.showError("Error collecting form data"); } }); dialog.on("action:cancel", () => { if (resolved) return; resolved = true; dialog.hide(); resolve(null); }); dialog.on("hidden", () => { if (!resolved) { resolved = true; resolve(null); } setTimeout(() => { formView.destroy(); dialog.destroy(); }, 100); }); }); } /** * Show form in a dialog with automatic model saving * @param {object} options - Configuration options (requires model) * @returns {Promise} Promise that resolves with save result or null if cancelled */ static async showModelForm(options = {}) { const { title = "Edit", formConfig = {}, size = "md", centered = true, submitText = "Save", cancelText = "Cancel", model, ...dialogOptions } = options; if (!model) { throw new Error("showModelForm requires a model"); } const FormView = (await import("./FormView-fUbbKQQU.js").then((n) => n.b)).default; const formView = new FormView({ fileHandling: options.fileHandling || "base64", model, data: options.data, defaults: options.defaults, formConfig: { fields: formConfig.fields || options.fields, ...formConfig, submitButton: false, resetButton: false } }); const dialog = new Dialog({ title, body: formView, size, centered, buttons: [ { text: cancelText, class: "btn-secondary", action: "cancel" }, { text: submitText, class: "btn-primary", action: "submit" } ], ...dialogOptions }); await dialog.render(true, document.body); dialog.show(); return new Promise((resolve) => { let resolved = false; dialog.on("action:submit", async () => { if (resolved) return; dialog.setLoading(true, "Saving..."); try { const result = await formView.handleSubmit(); if (result.success) { resolved = true; dialog.hide(); resolve(result); } else { dialog.setLoading(false); let errmsg = result.error; if (result.data && result.data.error) { errmsg = result.data.error; } dialog.getApp().toast.error(errmsg); } } catch (error) { console.error("Error saving form:", error); await dialog.setContent(formView); formView.showError(error.message || "An error occurred while saving"); } }); dialog.on("action:cancel", () => { if (resolved) return; resolved = true; dialog.hide(); resolve(null); }); dialog.on("hidden", () => { if (!resolved) { resolved = true; resolve(null); } setTimeout(() => { formView.destroy(); dialog.destroy(); }, 100); }); }); } /** * Show data in a dialog using DataView component * @param {object} options - Configuration options * @returns {Promise} Promise that resolves when dialog is closed */ static async showData(options = {}) { const { title = "Data View", data = {}, model = null, fields = [], columns = 2, responsive = true, showEmptyValues = false, emptyValueText = "—", size = "lg", centered = true, closeText = "Close", ...dialogOptions } = options; const DataView = (await import("./DataView-UjG66gmW.js")).default; const dataView = new DataView({ data, model, fields, columns, responsive, showEmptyValues, emptyValueText }); const dialog = new Dialog({ title, body: dataView, size, centered, buttons: [ { text: closeText, class: "btn-secondary", value: "close" } ], ...dialogOptions }); await dialog.render(true, document.body); dialog.show(); return new Promise((resolve) => { let resolved = false; const closeBtn = dialog.element.querySelector(".modal-footer button"); const handleClose = () => { if (resolved) return; resolved = true; dialog.hide(); resolve(true); }; closeBtn?.addEventListener("click", handleClose); dialog.on("hidden", () => { if (!resolved) { resolved = true; resolve(true); } setTimeout(() => { dataView.destroy(); dialog.destroy(); dialog.element.remove(); }, 100); }); dataView.on("field:click", (data2) => { dialog.emit("dataview:field:click", data2); }); dataView.on("error", (data2) => { dialog.emit("dataview:error", data2); }); }); } } Dialog.showConfirm = Dialog.confirm; Dialog.showError = Dialog.alert; export { Dialog as default }; //# sourceMappingURL=Dialog-eoLNdg-d.js.map