import { css, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { BaseElement } from "../../core/base-element.ts"; @customElement("ct-draggable") export class CtDraggable extends BaseElement { @property({ type: Number }) x = 0; @property({ type: Number }) y = 0; @property({ type: Boolean, reflect: true }) override hidden = false; @state() private isDragging = false; private dragStartX = 0; private dragStartY = 0; private initialMouseX = 0; private initialMouseY = 0; static override styles = css` :host { position: absolute; padding: 10px; background-color: #ffffcc; border: 1px solid #ddd; border-radius: 4px; max-width: 200px; cursor: move; user-select: none; } :host(.dragging) { opacity: 0.8; z-index: 1000; cursor: grabbing; } `; override connectedCallback() { super.connectedCallback(); // Set initial position from props this.style.left = `${this.x}px`; this.style.top = `${this.y}px`; // Add document-level listeners for mouse move and up document.addEventListener("mousemove", this.handleMouseMove); document.addEventListener("mouseup", this.handleMouseUp); } override disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener("mousemove", this.handleMouseMove); document.removeEventListener("mouseup", this.handleMouseUp); } private handleMouseDown = (event: MouseEvent) => { // Don't start drag if clicking on an input or button const target = event.target as HTMLElement; if ( target.tagName === "INPUT" || target.tagName === "BUTTON" || target.tagName === "CT-INPUT" || target.tagName === "CT-TEXTAREA" || target.tagName === "CT-SELECT" || target.tagName === "CT-BUTTON" || target.closest("common-send-message") ) { return; } event.preventDefault(); this.isDragging = true; this.dragStartX = this.x; this.dragStartY = this.y; this.initialMouseX = event.clientX; this.initialMouseY = event.clientY; this.classList.add("dragging"); }; private handleMouseMove = (event: MouseEvent) => { if (!this.isDragging) return; const deltaX = event.clientX - this.initialMouseX; const deltaY = event.clientY - this.initialMouseY; const newX = this.dragStartX + deltaX; const newY = this.dragStartY + deltaY; // Update position immediately for smooth dragging this.style.left = `${newX}px`; this.style.top = `${newY}px`; }; private handleMouseUp = (event: MouseEvent) => { if (!this.isDragging) return; // Stop the event from bubbling up to the canvas event.stopPropagation(); this.isDragging = false; this.classList.remove("dragging"); const deltaX = event.clientX - this.initialMouseX; const deltaY = event.clientY - this.initialMouseY; const newX = this.dragStartX + deltaX; const newY = this.dragStartY + deltaY; // Emit position change event with new coordinates this.emit("positionchange", { x: newX, y: newY }); }; override updated(changedProperties: Map) { // Update position from props only when not dragging if ( !this.isDragging && (changedProperties.has("x") || changedProperties.has("y")) ) { this.style.left = `${this.x}px`; this.style.top = `${this.y}px`; } } override render() { return html`
`; } } declare global { interface HTMLElementTagNameMap { "ct-draggable": CtDraggable; } }