/** @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][modal]) { 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); pointer-events: all; transform: translate(var(--tp-dialog-offset-x, 0px), var(--tp-dialog-offset-y, 0px)); } .close-icon { position: absolute; right: 4px; top: 5px; z-index: 3; --tp-icon-width: 18px; --tp-icon-height: 18px; } .drag-handle { position: absolute; top: 0; left: 0; right: 0; height: 30px; cursor: move; z-index: 2; } ` ]; } render() { const { showClose, unmovable } = this; return html` ${!unmovable ? html`
` : null} ${showClose ? html`
` : null}
`; } static get properties() { return { open: { type: Boolean, reflect: true }, showClose: { type: Boolean }, icon: { type: Object }, closeOnEsc: { type: Boolean }, closeOnOutsideClick: { type: Boolean }, modal: { type: Boolean, reflect: true }, unmovable: { type: Boolean, reflect: true }, }; } static get closeIcon() { return svg``; } get dialog() { return this.shadowRoot.querySelector('dialog'); } constructor() { super(); this._currentPromise = null; this._resolvePromise = null; this.modal = false; this.unmovable = false; this._offsetX = 0; this._offsetY = 0; } 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'); // Clean up pointer drag listeners if (this._boundDragMove) { window.removeEventListener('pointermove', this._boundDragMove); } if (this._boundDragEnd) { window.removeEventListener('pointerup', this._boundDragEnd); window.removeEventListener('pointercancel', this._boundDragEnd); } // 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.modal = false; this._resetPosition(); 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.modal = true; this._resetPosition(); 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; } } _resetPosition() { this._offsetX = 0; this._offsetY = 0; this.style.removeProperty('--tp-dialog-offset-x'); this.style.removeProperty('--tp-dialog-offset-y'); } _onDragStart(e) { if (this.unmovable) return; if (e.button !== 0) return; // Only drag with left/main pointer button this._startX = e.clientX; this._startY = e.clientY; this._startOffsetX = this._offsetX || 0; this._startOffsetY = this._offsetY || 0; const dragHandle = this.shadowRoot.querySelector('.drag-handle'); if (dragHandle && typeof dragHandle.setPointerCapture === 'function') { dragHandle.setPointerCapture(e.pointerId); } this._boundDragMove = this._onDragMove.bind(this); this._boundDragEnd = this._onDragEnd.bind(this); window.addEventListener('pointermove', this._boundDragMove); window.addEventListener('pointerup', this._boundDragEnd); window.addEventListener('pointercancel', this._boundDragEnd); } _onDragMove(e) { const dx = e.clientX - this._startX; const dy = e.clientY - this._startY; this._offsetX = this._startOffsetX + dx; this._offsetY = this._startOffsetY + dy; this.style.setProperty('--tp-dialog-offset-x', `${this._offsetX}px`); this.style.setProperty('--tp-dialog-offset-y', `${this._offsetY}px`); } _onDragEnd(e) { const dragHandle = this.shadowRoot.querySelector('.drag-handle'); if (dragHandle && typeof dragHandle.releasePointerCapture === 'function') { try { dragHandle.releasePointerCapture(e.pointerId); } catch (err) {} } window.removeEventListener('pointermove', this._boundDragMove); window.removeEventListener('pointerup', this._boundDragEnd); window.removeEventListener('pointercancel', this._boundDragEnd); } _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);