The Browser Already Computed the Layout. Build Your CAD Editor on Top of It.
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 Choice | Coordinate Tracking System | Layout Reflow Mechanism | Document Serialization |
|---|---|---|---|
| Canvas / Absolute Positioning | Custom JS Coordinate Math | Manual coordinate recalculations | Lossy (Fails to map clean HTML) |
| DOM Box Model + CSS Grid | Browser getBoundingClientRect() | Native Browser Reflow Engine | 100% 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.