diff --git a/package.json b/package.json index 4cdccfa..363d237 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tp/tp-dialog", - "version": "1.5.0", + "version": "1.5.1", "description": "", "main": "tp-dialog.js", "scripts": { diff --git a/tp-dialog.js b/tp-dialog.js index a14058b..b0dcabb 100644 --- a/tp-dialog.js +++ b/tp-dialog.js @@ -45,6 +45,7 @@ class TpDialog extends EventHelpers(LitElement) { dialog { position: relative; + box-sizing: border-box; border-radius: var(--tp-dialog-border-radius); background-color: var(--tp-dialog-bg); color: var(--text); @@ -52,6 +53,28 @@ class TpDialog extends EventHelpers(LitElement) { padding: var(--tp-dialog-padding); pointer-events: all; transform: translate(var(--tp-dialog-offset-x, 0px), var(--tp-dialog-offset-y, 0px)); + display: flex; + flex-direction: column; + } + + dialog:not([open]) { + display: none; + } + + /* Only clip/scroll once the dialog has been explicitly sized via resize. */ + :host([constrained]) dialog { + overflow: hidden; + } + + .scroll-wrapper { + flex: 1; + box-sizing: border-box; + min-height: 0; + overflow: visible; + } + + :host([constrained]) .scroll-wrapper { + overflow: auto; } .close-icon { @@ -72,12 +95,26 @@ class TpDialog extends EventHelpers(LitElement) { cursor: move; z-index: 2; } + + .resize-handle { + position: absolute; + bottom: 0; + right: 0; + width: 14px; + height: 14px; + cursor: se-resize; + z-index: 4; + background-image: linear-gradient(135deg, transparent 0%, transparent 50%, var(--tp-dialog-resize-handle-color, #888) 50%, var(--tp-dialog-resize-handle-color, #888) 60%, transparent 60%, transparent 70%, var(--tp-dialog-resize-handle-color, #888) 70%, var(--tp-dialog-resize-handle-color, #888) 80%, transparent 80%); + background-size: 8px 8px; + background-repeat: no-repeat; + background-position: bottom right; + } ` ]; } render() { - const { showClose, unmovable } = this; + const { showClose, unmovable, resizable } = this; return html` ${!unmovable ? html` @@ -88,7 +125,12 @@ class TpDialog extends EventHelpers(LitElement) { ` : null} - +
+ +
+ ${resizable ? html` +
+ ` : null}
`; } @@ -102,6 +144,7 @@ class TpDialog extends EventHelpers(LitElement) { closeOnOutsideClick: { type: Boolean }, modal: { type: Boolean, reflect: true }, unmovable: { type: Boolean, reflect: true }, + resizable: { type: Boolean, reflect: true }, }; } @@ -119,6 +162,7 @@ class TpDialog extends EventHelpers(LitElement) { this._resolvePromise = null; this.modal = false; this.unmovable = false; + this.resizable = false; this._offsetX = 0; this._offsetY = 0; } @@ -138,14 +182,8 @@ class TpDialog extends EventHelpers(LitElement) { 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); - } + // Clean up pointer drag/resize listeners + this._cleanupActiveListeners(); // Remove this dialog from the stack this._removeFromDialogStack(); @@ -208,6 +246,13 @@ class TpDialog extends EventHelpers(LitElement) { // Remove from dialog stack this._removeFromDialogStack(); + + // Clean up pointer drag/resize listeners if they are currently active + this._cleanupActiveListeners(); + + if (this.closeOnOutsideClick) { + this.removeEventListener('click', this._handleOutsideClick); + } // If closed without explicit confirm/dismiss (like ESC key), treat as dismissed if (this._currentPromise && this._resolvePromise) { @@ -217,11 +262,38 @@ class TpDialog extends EventHelpers(LitElement) { } } + _cleanupActiveListeners() { + if (this._boundDragMove) { + window.removeEventListener('pointermove', this._boundDragMove); + this._boundDragMove = null; + } + if (this._boundDragEnd) { + window.removeEventListener('pointerup', this._boundDragEnd); + window.removeEventListener('pointercancel', this._boundDragEnd); + this._boundDragEnd = null; + } + if (this._boundResizeMove) { + window.removeEventListener('pointermove', this._boundResizeMove); + this._boundResizeMove = null; + } + if (this._boundResizeEnd) { + window.removeEventListener('pointerup', this._boundResizeEnd); + window.removeEventListener('pointercancel', this._boundResizeEnd); + this._boundResizeEnd = null; + } + } + _resetPosition() { this._offsetX = 0; this._offsetY = 0; this.style.removeProperty('--tp-dialog-offset-x'); this.style.removeProperty('--tp-dialog-offset-y'); + this.removeAttribute('constrained'); + const dialogEl = this.dialog; + if (dialogEl) { + dialogEl.style.removeProperty('width'); + dialogEl.style.removeProperty('height'); + } } _onDragStart(e) { @@ -269,6 +341,74 @@ class TpDialog extends EventHelpers(LitElement) { window.removeEventListener('pointercancel', this._boundDragEnd); } + _onResizeStart(e) { + if (!this.resizable) return; + if (e.button !== 0) return; // Only resize with left/main pointer button + + e.preventDefault(); + e.stopPropagation(); + + // Once the user starts resizing, content must scroll inside the fixed box. + this.setAttribute('constrained', ''); + + const rect = this.dialog.getBoundingClientRect(); + this._startWidth = rect.width; + this._startHeight = rect.height; + this._startX = e.clientX; + this._startY = e.clientY; + this._startOffsetX = this._offsetX || 0; + this._startOffsetY = this._offsetY || 0; + + const resizeHandle = this.shadowRoot.querySelector('.resize-handle'); + if (resizeHandle && typeof resizeHandle.setPointerCapture === 'function') { + resizeHandle.setPointerCapture(e.pointerId); + } + + this._boundResizeMove = this._onResizeMove.bind(this); + this._boundResizeEnd = this._onResizeEnd.bind(this); + + window.addEventListener('pointermove', this._boundResizeMove); + window.addEventListener('pointerup', this._boundResizeEnd); + window.addEventListener('pointercancel', this._boundResizeEnd); + } + + _onResizeMove(e) { + const dw = e.clientX - this._startX; + const dh = e.clientY - this._startY; + + const newWidth = Math.max(150, this._startWidth + dw); + const newHeight = Math.max(100, this._startHeight + dh); + + const dw_actual = newWidth - this._startWidth; + const dh_actual = newHeight - this._startHeight; + + this._offsetX = this._startOffsetX + dw_actual / 2; + this._offsetY = this._startOffsetY + dh_actual / 2; + + const originalTop = (window.innerHeight - newHeight) / 2; + if (originalTop + this._offsetY < 0) { + this._offsetY = -originalTop; + } + + this.dialog.style.width = `${newWidth}px`; + this.dialog.style.height = `${newHeight}px`; + this.style.setProperty('--tp-dialog-offset-x', `${this._offsetX}px`); + this.style.setProperty('--tp-dialog-offset-y', `${this._offsetY}px`); + } + + _onResizeEnd(e) { + const resizeHandle = this.shadowRoot.querySelector('.resize-handle'); + if (resizeHandle && typeof resizeHandle.releasePointerCapture === 'function') { + try { + resizeHandle.releasePointerCapture(e.pointerId); + } catch (err) {} + } + + window.removeEventListener('pointermove', this._boundResizeMove); + window.removeEventListener('pointerup', this._boundResizeEnd); + window.removeEventListener('pointercancel', this._boundResizeEnd); + } + _handleConfirmed(event) { if (this._currentPromise && this._resolvePromise) { this._resolvePromise('confirmed');