DEV Community

Bonzai2Carn
Bonzai2Carn

Posted on Originally published at ginexys.com

How to Build a CAD-Style Layout Editor on Top of Extracted HTML

TLDR

If you have extracted a PDF to HTML and want users to rearrange and edit the layout in the browser, you do not need position: absolute or canvas overlays. The extracted DOM is already a structured CSS Grid box model. HTML5 drag-and-drop combined with native DOM insertBefore/appendChild calls provides free layout reflow. Selection Mode toggles contentEditable, injects drag handles, supports marquee multi-select, and lets users group regions into new grid zones.

Component Step Underlying Mechanism Implementation Strategy Layout Reflow Behavior
Selection Mode Toggle contentEditable + CSS Class Flips contentEditable, attaches handles Browser Grid handles flow
Element Drag & Drop HTML5 Drag API + DOM Nodes target.before(_draggedEl) insertions Dynamic reflow (No math required)
Marquee Selection Overlay Box + getBoundingClientRect Overlap check against .pdf-region Multi-node bounding set
Code Editor Modal Monaco Editor in native <dialog> Deferred setValue() via requestAnimationFrame Full DOM node replacement

Absolute coordinates break responsive CSS document flow

When building visual layout tools for PDF-extracted HTML, developers often reach for absolute positioning (position: absolute) or canvas overlay frameworks (like Fabric.js or Konva).

However, forcing document elements into absolute coordinate positions breaks responsive document flow. Elements no longer reflow when text is edited, column layouts collapse, and exporting to clean HTML or Markdown becomes nearly impossible.


CSS Grid containers align extracted document flows natively

The geometry extraction pipeline outputs structured, semantic HTML:

<section class="pdf-page-content">
  <div class="pdf-zone pdf-zone--cols-2"> <!-- CSS Grid Container -->
    <div class="pdf-col pdf-col--left">
      <div class="pdf-region">
        <h3>Section Heading</h3>
      </div>
    </div>
    <div class="pdf-col pdf-col--right">
      <div class="pdf-region">
        <div class="pdf-table-wrap"><table>...</table></div>
      </div>
    </div>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Because zones use CSS Grid (grid-template-columns: repeat(N, 1fr)), regions are standard flow children. Attempting to drag regions using absolute pixel coordinates breaks the CSS Grid contract.


Visual document editing requires targeted DOM manipulation

                              [Toggle Selection Mode]
                                         |
                                         v
                      [Inject Handles & Marquee Listeners]
                                         |
                                         v
                                 User Interaction?
                       /        /                 \         \
                      v        v                   v         v
                [Drag & Drop] [Marquee Box]    [Group Button] [Edit Code]
                      |              |                 |           |
                      v              v                 v           v
                 (Insert Node) (Intersect Check) (Zone Wrap) (Monaco Dialog)
                      \              \                 /           /
                       \              \               /           /
                        v              v             v           v
                        +----------------------------------------+
                        |      applyHtmlEverywhere(HTML)         |
                        +----------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Dual mode toggles switch between layout design and text entry

A single mode toggle flips between Edit Mode (inline text editing) and Selection Mode (layout manipulation):

function toggleMode() {
  isActive = !isActive;
  preview.classList.toggle('selection-mode', isActive);
  preview.contentEditable = isActive ? 'false' : 'true';

  if (isActive) {
    attachDragHandles();
    preview.addEventListener('mousedown', onMarqueeStart);
  } else {
    clearSelection();
    removeDragHandles();
    preview.removeEventListener('mousedown', onMarqueeStart);
  }
}
Enter fullscreen mode Exit fullscreen mode

Drag and drop listeners handle relative DOM repositioning

Inject a drag handle into regions and handle drop events by mutating the DOM tree directly:

function onDrop(e) {
  e.preventDefault();
  const target = e.currentTarget;
  const rect = target.getBoundingClientRect();
  const insertAfter = e.clientY > rect.top + rect.height / 2;

  if (insertAfter) {
    target.after(draggedEl);
  } else {
    target.before(draggedEl);
  }

  applyHtmlEverywhere(preview.innerHTML, preview); // Synchronize state
}
Enter fullscreen mode Exit fullscreen mode

Overlap checks support multi-region marquee selection

To select multiple regions, create a visual marquee box on mousedown and test intersection on mouseup:

preview.querySelectorAll('.pdf-region').forEach(el => {
  const r = el.getBoundingClientRect();
  const overlaps = !(r.right < marqueeRect.left || r.left > marqueeRect.right ||
                     r.bottom < marqueeRect.top || r.top > marqueeRect.bottom);
  if (overlaps) {
    selectedSet.add(el);
    el.classList.add('sel-selected');
  }
});
Enter fullscreen mode Exit fullscreen mode

Grid wrapper insertions group selected document regions

Grouping wraps all selected regions into a new single-column grid zone (div.pdf-zone.pdf-zone--cols-1):

function groupSelected() {
  const regions = [...selectedSet].filter(el => el.classList.contains('pdf-region'));
  if (regions.length < 2) return;

  const firstParentZone = regions[0].closest('.pdf-zone') || regions[0].parentElement;
  const newZone = document.createElement('div');
  newZone.className = 'pdf-zone pdf-zone--cols-1';

  regions.forEach(r => newZone.appendChild(r));
  firstParentZone.before(newZone);
  clearSelection();
  applyHtmlEverywhere(preview.innerHTML, preview);
}
Enter fullscreen mode Exit fullscreen mode

Animation frames ensure Monaco layout initialization inside dialogs

For direct HTML editing, right-clicking an element opens a native <dialog> containing a Monaco editor. Crucially, Monaco requires container measurement after the dialog paints:

dialog.addEventListener('toggle', () => {
  if (!dialog.open) return;
  if (!editor) editor = monaco.editor.create(container, { language: 'html' });

  requestAnimationFrame(() => {
    editor.layout();
    editor.setValue(pendingHtml);
    editor.focus();
  });
});
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Use native browser layout engines (CSS Grid + DOM insertion) instead of canvas coordinate overlays when building visual document editors.

Try it

The layout editor described here runs in the browser: open PDF Processor and drop a PDF in. Extracted tables hand off to Table Formatter if you want to clean them up afterwards.


Ginexys — engineering document tools that run in your editor. Extract structured
data from PDFs, clean up tables, and edit schemas. Local-first: your documents never
leave your machine.

Top comments (0)