Selection Handles That Never Touch the DOM They Decorate
TLDR
Injecting selection handle nodes into<td> elements pollutes table HTML trees, breaking row counters and export logic. Mounting a single position: fixed overlay to document.body with pointer-events: none on its interior allows clicks to pass through to cells while handles capture gestures cleanly.
| Selection Chrome Approach | DOM Tree Impact | Export Cleanliness | Hit-Test Resolution |
|---|---|---|---|
| Injected Handle Spans | Pollutes <td> inner HTML | Fails (Requires manual stripping) | DOM node event target |
| Fixed Body Overlay | Zero DOM Tree Contamination | 100% Clean Data Exports | document.elementFromPoint() |
Appending selection elements directly inside table cells corrupts data exports
Adding drag handles, resize nodes, or fill boxes to editable table cells seems straightforward. However, appending helper <span> elements directly into <td> nodes pollutes table DOM structures.
As a result, every exporter, undo snapshot engine, and spatial grid mapper must continuously filter out helper chrome elements to prevent invalid HTML output.
Isolated body overlays with pointer-event layering shield table DOMs
We separated selection handles from table content by rendering a single body-level overlay positioned over target bounding boxes:
/ Overlay container passes clicks through to underlying cells /
.sel-overlay {
position: fixed;
pointer-events: none;
}
/ Handles intercept drag interactions /
.sel-overlay .fill-handle,
.sel-overlay .edge-node {
pointer-events: auto;
}
During drag operations, the overlay switches temporarily to pointer-events: none, allowing document.elementFromPoint() to resolve underlying cell elements directly:
// Resolve target cell beneath active cursor during drag operations
export function getCellUnderCursor(clientX, clientY, activeTableEl) {
// Temporary pointer-events bypass enables direct browser hit testing
const targetEl = document.elementFromPoint(clientX, clientY);
const cell = targetEl ? targetEl.closest('td, th') : null;
return cell && cell.closest('table') === activeTableEl ? cell : null;
}
Positioning calculations update on window scroll and resize events, keeping the DOM tree clean and export-ready.
Layered View:
[Cursor] --(Click/Drag)
|
v
+-----------------------------------------------------------+
| Overlay Layer (position: fixed, pointer-events: none) |
| - Fill Handle / Edge Node (pointer-events: auto) <=======|=== Stops cursor events here (gestures captured)
+-----------------------------------------------------------+
| (passes through rest of overlay)
v
+-----------------------------------------------------------+
| Table Layer (Clean DOM) |
| - Cell 1 | Cell 2 | Cell 3 |
+-----------------------------------------------------------+
Rule of thumb: Render selection chrome in an isolated body-level overlay using CSS pointer-events layering.