/** @license Copyright (c) 2022 trading_peter This program is available under Apache License Version 2.0 */ import '@tp/tp-icon/tp-icon.js'; import { LitElement, html, css, svg } from 'lit'; import { EventHelpers } from '@tp/helpers/event-helpers.js'; import { closest } from '@tp/helpers/closest.js'; // Global stack to track opened dialogs with closeOnEsc const dialogStack = []; let escKeyListener = null; // Global escape key handler function handleGlobalEscKey(event) { if (event.key === 'Escape' && dialogStack.length > 0) { // Get the most recently opened dialog const lastDialog = dialogStack[dialogStack.length - 1]; if (lastDialog && lastDialog.closeOnEsc) { lastDialog.close(); } } } class TpDialog extends EventHelpers(LitElement) { static get styles() { return [ css` :host { display: flex; justify-content: center; align-items: center; position: fixed; inset: 0px; pointer-events: none; overflow: auto; z-index: 900; } :host([open]) { pointer-events: all; } dialog { position: relative; border-radius: var(--tp-dialog-border-radius); background-color: var(--tp-dialog-bg); color: var(--text); border: var(--tp-dialog-border); padding: var(--tp-dialog-padding); } .close-icon { position: absolute; right: 4px; top: 5px; z-index: 1; --tp-icon-width: 18px; --tp-icon-height: 18px; } ` ]; } render() { const { showClose } = this; return html` ${showClose ? html`
` : null}
`; } static get properties() { return { open: { type: Boolean, reflect: true }, showClose: { type: Boolean }, icon: { type: Object }, closeOnEsc: { type: Boolean }, closeOnOutsideClick: { type: Boolean }, }; } static get closeIcon() { return svg``; } get dialog() { return this.shadowRoot.querySelector('dialog'); } constructor() { super(); this._currentPromise = null; this._resolvePromise = null; } connectedCallback() { super.connectedCallback(); this.listen(this, 'click', '_onDialogClick'); this.listen(this, 'dialog-close', 'close'); this.listen(this, 'confirmed', '_handleConfirmed'); this.listen(this, 'dismissed', '_handleDismissed'); } disconnectedCallback() { super.disconnectedCallback(); this.unlisten(this, 'click', '_onDialogClick'); this.unlisten(this, 'dialog-close', 'close'); this.unlisten(this, 'confirmed', '_handleConfirmed'); this.unlisten(this, 'dismissed', '_handleDismissed'); // Remove this dialog from the stack this._removeFromDialogStack(); // Clean up promise if dialog is removed while open if (this._currentPromise && this._resolvePromise) { this._resolvePromise('dismissed'); this._currentPromise = null; this._resolvePromise = null; } } show() { this.dialog.show(); this.open = true; // Add to dialog stack if closeOnEsc is enabled this._addToDialogStack(); // Create and return a new promise this._currentPromise = new Promise((resolve) => { this._resolvePromise = resolve; }); return this._currentPromise; } showModal() { this.dialog.showModal(); this.open = true; // Add to dialog stack if closeOnEsc is enabled this._addToDialogStack(); if (this.closeOnOutsideClick) { this.addEventListener('click', this._handleOutsideClick, { once: true }); } // Create and return a new promise this._currentPromise = new Promise((resolve) => { this._resolvePromise = resolve; }); return this._currentPromise; } close(e) { if (e) { e.stopPropagation(); e.preventDefault(); } this.dialog.close(); this.dispatchEvent(new CustomEvent('closed', { detail: null, bubbles: true, composed: true })); this.open = false; // Remove from dialog stack this._removeFromDialogStack(); // If closed without explicit confirm/dismiss (like ESC key), treat as dismissed if (this._currentPromise && this._resolvePromise) { this._resolvePromise('dismissed'); this._currentPromise = null; this._resolvePromise = null; } } _handleConfirmed(event) { if (this._currentPromise && this._resolvePromise) { this._resolvePromise('confirmed'); this._currentPromise = null; this._resolvePromise = null; } } _handleDismissed(event) { if (this._currentPromise && this._resolvePromise) { this._resolvePromise('dismissed'); this._currentPromise = null; this._resolvePromise = null; } } _addToDialogStack() { if (this.closeOnEsc) { // Remove if already in stack (shouldn't happen, but just in case) this._removeFromDialogStack(); // Add to the end of the stack dialogStack.push(this); // Set up global listener if this is the first dialog if (dialogStack.length === 1 && !escKeyListener) { escKeyListener = handleGlobalEscKey; document.addEventListener('keydown', escKeyListener); } } } _removeFromDialogStack() { const index = dialogStack.indexOf(this); if (index > -1) { dialogStack.splice(index, 1); // Remove global listener if no more dialogs with closeOnEsc if (dialogStack.length === 0 && escKeyListener) { document.removeEventListener('keydown', escKeyListener); escKeyListener = null; } } } _onDialogClick(event) { if (this.closeOnOutsideClick) { const path = event.composedPath(); if (!path.includes(this.dialog)) { this.close(); event.stopPropagation(); return; } } var rootTarget = event.composedPath()[0]; var target = closest(rootTarget, '[dialog-dismiss]', true) || closest(rootTarget, '[dialog-confirm]', true); while (target && target !== this) { if (target.hasAttribute) { if (target.hasAttribute('dialog-dismiss')) { var reason = target.getAttribute('dialog-dismiss'); this.dispatchEvent(new CustomEvent('dismissed', { detail: reason.length > 0 ? reason : true, bubbles: true, composed: true })); this.close(); event.stopPropagation(); break; } else if (target.hasAttribute('dialog-confirm')) { var reason = target.getAttribute('dialog-confirm'); this.dispatchEvent(new CustomEvent('confirmed', { detail: reason.length > 0 ? reason : true, bubbles: true, composed: true })); this.close(); event.stopPropagation(); break; } } target = target.parentNode; } } _handleOutsideClick(event) { // This method should be implemented if closeOnOutsideClick functionality is needed // For now, just close the dialog when clicking outside this.close(); } } window.customElements.define('tp-dialog', TpDialog);