Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
4.0 kB · 140 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141import { 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` <div class=${classnames("context-menu-container", { open: this.isOpen, })} @click=${(e) => { // swallow all click events e.stopPropagation(); this.close(); }} > <dialog class="bottom-sheet context-menu" @click=${(e) => { // close the dialog if the user clicks outside of it if (e.target === e.currentTarget) { this.close(); } }} @cancel=${(event) => { event.preventDefault(); this.close(); }} @close=${() => { this.scrollLock?.release(); this.scrollLock = null; this.isOpen = false; this.render(); }} > <div class="context-menu-body sheet-scroll-region"> ${this._childNodes} </div> </dialog> </div> `, 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();