Selection-based DOM transforms must not assume a fixed depth
TLDR
Editor features operating on user selections (e.g. splitting paragraphs into multi-column containers) break silently when assuming a fixed DOM nesting depth (.pdf-page-content > p). Document extraction pipelines wrap content in nested positioning wrappers (.pdf-zone > .pdf-region > p), rendering child queries empty. Querying range.commonAncestorContainer dynamically identifies the true shared parent element regardless of nesting depth.
| Selection Boundary Engine | DOM Nesting Assumption | Behavior on Pipeline HTML | Transformation Result |
|---|---|---|---|
| Fixed Container Depth | Direct Page Children ( depth = 1 ) | Candidate array evaluates empty | Silent Failure (No-op) |
| Dynamic Common Ancestor | range.commonAncestorContainer | Locates true parent wrapper | 100% Correct Split |
Technical implementation: selection-driven operating scope
// Derive transformation target scope dynamically from Selection Range
export function applyColumnSplitToSelection(colsCount, fallbackSurface) {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0); let ancestorNode = range.commonAncestorContainer;
// Resolve text nodes to parent Element if (ancestorNode.nodeType === Node.TEXT_NODE) { ancestorNode = ancestorNode.parentElement; }
let containerElement = ancestorNode.nodeType === Node.ELEMENT_NODE ? ancestorNode : fallbackSurface; let candidates = Array.from(containerElement.children); let selectedChildren = candidates.filter(child => range.intersectsNode(child));
// Single leaf retry: Climb 1 level up if selection collapses inside a single nested node if (selectedChildren.length < 2 && containerElement.parentElement) { containerElement = containerElement.parentElement; candidates = Array.from(containerElement.children); selectedChildren = candidates.filter(child => range.intersectsNode(child)); }
if (selectedChildren.length < 2) return;
// Perform column split transformation across selected children redistributeNodesIntoColumns(containerElement, selectedChildren, colsCount); }
Rule of thumb: Derive DOM transformation target scopes dynamically using range.commonAncestorContainer rather than assuming fixed container nesting depths.