Engineering Journal
Pdf Processor
Pdf Processor

Selection-based DOM transforms must not assume a fixed depth

2026-07-11

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 EngineDOM Nesting AssumptionBehavior on Pipeline HTMLTransformation Result
Fixed Container DepthDirect Page Children ( depth = 1 )Candidate array evaluates emptySilent Failure (No-op)
Dynamic Common Ancestorrange.commonAncestorContainerLocates true parent wrapper100% 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.
Read this post in the full Engineering Journal →