Engineering Journal
Pdf Processor
Pdf Processor

The Browser Already Computed the Layout. Build Your CAD Editor on Top of It.

2026-05-21

TLDR

Building interactive document block drag-and-drop editors does not require complex canvas layers or custom spatial coordinate tracking systems. The DOM box model, combined with CSS Grid (display: grid), already functions as a complete spatial layout engine. Drag handles positioned absolutely over relative containers allow users to reorder DOM nodes using native insertBefore operations, relying on the browser to execute reflow automatically.
Architecture ChoiceCoordinate Tracking SystemLayout Reflow MechanismDocument Serialization
Canvas / Absolute PositioningCustom JS Coordinate MathManual coordinate recalculationsLossy (Fails to map clean HTML)
DOM Box Model + CSS GridBrowser getBoundingClientRect()Native Browser Reflow Engine100% Clean HTML/Markdown output

Technical architecture: DOM-driven reordering

Structured document regions are rendered inside CSS Grid containers:

<section class="pdf-page" data-page="1">
  <div class="zone cols-2">
    <div class="column column--left">
      <div class="region" data-x="40" data-y="100">
        <h3>Section Title</h3>
      </div>
    </div>
    <div class="column column--right">
      <div class="region" data-x="320" data-y="100">
        <table>...</table>
      </div>
    </div>
  </div>
</section>

Technical implementation patterns

1. Non-Disruptive drag handle overlay

Drag handles prepended as flow children disrupt CSS Grid layouts. Position drag handles absolutely over relatively positioned containers:
/ Selection Mode Chrome Styling /
.selection-mode .zone,
.selection-mode .region {
  position: relative;
  outline: 1px dashed var(--border-accent);
}

.selection-mode .drag-handle { position: absolute; top: 4px; left: 4px; z-index: 100; cursor: grab; }


2. Multi-Column quadrant drop splitting

Dropping a block on the left or right $25\%$ of an existing region splits the block into a two-column CSS Grid zone:
export function splitRegionIntoColumns(draggedNode, targetNode, dropSide) {
  // 1. Create a DOM bookmark to preserve precise insertion index
  const bookmark = document.createElement('div');
  targetNode.before(bookmark);

// 2. Determine left and right column element ordering const [leftEl, rightEl] = dropSide === 'left' ? [draggedNode, targetNode] : [targetNode, draggedNode];

// 3. Construct 2-column CSS Grid container const zone = document.createElement('div'); zone.className = 'zone cols-2';

const colLeft = document.createElement('div'); colLeft.className = 'column column--left'; colLeft.appendChild(leftEl);

const colRight = document.createElement('div'); colRight.className = 'column column--right'; colRight.appendChild(rightEl);

zone.appendChild(colLeft); zone.appendChild(colRight);

// 4. Replace bookmark with new structured CSS Grid zone bookmark.replaceWith(zone); }


3. State synchronization without UI Chrome leakage

Strip drag handles and selection overlays before serializing HTML to canonical state stores:
export function syncDocumentState(previewContainer) {
  // Step 1: Remove selection mode UI chrome elements
  removeDragHandlesAndDividers(previewContainer);

// Step 2: Push clean document HTML to active surfaces (Editor, Diff View) stateStore.updateContent(previewContainer.innerHTML);

// Step 3: Re-inject handles if selection mode remains active if (isSelectionModeActive()) { attachDragHandles(previewContainer); injectResizeDividers(previewContainer); } }

Rule of thumb: Use the DOM box model and CSS Grid for layout reordering, reserving position: absolute exclusively for non-rendering UI handles.
Read this post in the full Engineering Journal →