My recent foray into building WYSIWYG editors and popovers yielded some fascinating insights into browser APIs. Maternity leave provided the perfect opportunity for deep dives into technical challenges without the pressure of deadlines.
My current focus is contributing to "codename goose," an open-source AI agent with a Rust backend and an Electron-based chat interface. I submitted a pull request to integrate a WYSIWYG editor, opting for a custom solution over existing packages to minimize bundle size. However, the maintainers suggested a popover toolbar to address space concerns.
Creating this floating toolbar proved unexpectedly challenging. My goals were straightforward:
Text areas presented unique complexities. Unlike standard HTML elements, where content manipulation and precise positioning are readily available, text areas expose only raw text and basic selection APIs. The browser handles the rendering internally.
To illustrate, consider this analogy:
Modern browsers offer a built-in Popover API for creating pop-up elements. Here's an example:
Despite its cross-browser compatibility and ease of use, the Popover API has limitations:
popovertarget
attribute restriction.Thanks to Mark Techson, who introduced me to the Popover API through Una Kravets' conference talk, "Less Cruft, More Power: Leverage the Power of the Web Platform."
To position the popover based on user text selection, I needed:
Colby Fayock's blog post, "How to Share Selected Text in React with the Selection API," introduced me to the Selection API
(accessible via window.getSelection()
). This API returns a Selection
object detailing the selected text.
getRangeAt(0)
The getRangeAt(0)
method within the Selection
object provides the selection's start and end offsets:
startOffset
: The selection's beginning index.endOffset
: The selection's ending index (after the last selected character).For example, in "Hello, World! Welcome.", selecting "World" yields startOffset
= 7 and endOffset
= 12.
Note: getRangeAt(0)
accesses the first selection. Browsers like Firefox allow multiple selections (Ctrl-click), but accessing indexes beyond 0 in single-selection browsers results in errors.
getBoundingClientRect()
getRangeAt(0)
provides access to getBoundingClientRect()
, which returns a bounding box with the selected text's top, right, bottom, left coordinates, width, and height. This allows for precise popover placement above the selection, as demonstrated:
However, this approach is limited within text areas.
The "mirrored div" technique, discovered through discussions with Claude, offers a workaround. An invisible div overlays the text area, mirroring its content and styling. User interactions occur within this div, providing full Selection API
access while maintaining a standard text area appearance.
Jhey Thompkins' blog post, "HOW TO: Where's the text cursor?", and the getComputedStyle()
method (which returns an element's computed CSS styles) were instrumental in precisely matching the text area's appearance in the overlay div.
However, this mirroring isn't perfect:
Many packages work well with standard DOM elements, but struggle with text areas due to the same fundamental limitations: restricted access to internal rendering and positioning.
Despite advancements in rich text interaction, working with text areas remains surprisingly complex. Exploring these browser APIs was a rewarding experience. Future APIs may simplify tasks like selection-based popovers. If you've encountered alternative solutions, I'd appreciate hearing about them.
The above is the detailed content of What Text Area Popovers Taught Me About Browser APIs. For more information, please follow other related articles on the PHP Chinese website!