web-mojo
Version:
WEB-MOJO - A lightweight JavaScript framework for building data-driven web applications
1,382 lines (1,381 loc) • 44 kB
JavaScript
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
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