Bubble Menu
Select a few words and a small menu appears above them. It is anchored to the selection rectangle itself — not to the editor box — so it follows the text when you scroll, resize, or extend the selection across lines.
How it works
- The anchor is a virtual element, not a DOM node. There is no element to point at: a text selection is a
Range.bubbleMenu()hands@domphy/floatingan object withgetBoundingClientRect()/getClientRects()that re-readgetSelection().getRangeAt(0)on every call, soautoUpdaterecomputes against the live rect on scroll and resize instead of a stale snapshot from when the menu opened. inline()runs first in the middleware chain. A selection that wraps across two lines has a bounding box spanning both, and anchoring to its centre would park the menu in the middle of the paragraph.inline()picks the rect that actually matches where the selection starts. Thenoffset(8)lifts it off the text,flip()drops it below when there is no room above, andshift()keeps it inside the viewport.shouldShowgates visibility per selection change. The default is "editable, with a non-empty selection"; this demo also excludes code blocks, where inline marks are meaningless. It runs on everyselectionUpdate, so it can read anything off the editor.mousedownis swallowed on the panel. Without it, pressing a button blurs the editor, the selection collapses, and the menu hides before the click handler runs its command — the button would appear dead. Preventing the default onmousedownkeeps focus and selection in the editing surface.- The panel is inserted next to the app root, so it escapes the editor's overflow and stacking context, and it copies the nearest
[data-theme]from the editor — floating content is a DOM sibling of the page, not of the editor, so it would otherwise resolve theme variables against the wrong scope. - All of the imperative wiring lives in a
behavior(). The event subscriptions, theautoUpdatecleanup, and the inserted panel node attach once per real DOM node; re-rendering the host routes fresh props into that same instance instead of leaving listeners bound to an orphaned closure. This is the pattern described in reused-node lifecycle and used by every@domphy/uioverlay.