import { html, render } from "/js/lib/lit-html.js"; import { classnames, isMobileViewport } from "/js/utils.js"; import { Component, getChildrenFragment } from "/js/components/component.js"; import { scrollLocks } from "/js/scrollLocks.js"; import { hapticsImpactLight } from "/js/haptics.js"; import { closeWithAnimation } from "/js/dialogHelpers.js"; import { enableDragToDismiss } from "/js/dragHelpers.js"; class ContextMenu extends Component { connectedCallback() { if (this._initialized) { return; } this.setAttribute("data-dialog-wrapper", ""); this.scrollLock = null; this._childNodes = [...this.childNodes]; this.innerHTML = ""; this.isOpen = false; this.render(); this._initialized = true; this._observer = new MutationObserver(() => { this._refreshChildren(); }); this._observer.observe(this, { childList: true }); } disconnectedCallback() { // If scroll is still prevented, restore it this.scrollLock?.release(); this.scrollLock = null; if (this._observer) { this._observer.disconnect(); } } _refreshChildren() { const container = this.querySelector(".context-menu-container"); const newChildren = [...this.childNodes].filter( (node) => node !== container, ); if (newChildren.length === 0) return; this._childNodes = newChildren; this.render(); } render() { this._observer?.disconnect(); render( html`
`, this, ); this._observer?.observe(this, { childList: true }); } open(x, y) { hapticsImpactLight(); this.scrollLock ??= scrollLocks.acquire({ target: this }); const dialog = this.querySelector(".context-menu"); if (dialog?.open) return; dialog.showModal(); // On desktop, position the dialog at the mouse cursor - claude wrote this if (!isMobileViewport()) { const rect = dialog.getBoundingClientRect(); const margin = 8; let left = x; let top = y; // Adjust if dialog would go off-screen (with margin) const maxX = window.innerWidth - rect.width - margin; const maxY = window.innerHeight - rect.height - margin; if (left > maxX) left = maxX; if (top > maxY) top = maxY; if (left < margin) left = margin; if (top < margin) top = margin; dialog.style.left = `${left}px`; dialog.style.top = `${top}px`; } this.isOpen = true; this.render(); // Setup mobile swipe-to-dismiss enableDragToDismiss(this.querySelector(".context-menu"), { eventSource: this.querySelector(".context-menu-container"), onDismiss: () => this.close(), allowOppositeStretch: true, ignoreTouchTarget: (el) => el.closest("button, a") !== null, scrollContainer: this.querySelector(".context-menu-body"), }); } close() { return closeWithAnimation(this.querySelector(".context-menu")); } } ContextMenu.register();