Engineering Journal
Pdf Processor
Pdf Processor

Error Fix: 5 Bugs in the HTML CAD Editor (And Why Each One Was Predictable)

2026-05-21

TLDR

Interactive DOM layout editors frequently encounter state synchronization, event listener, and CSS Grid bugs when combining drag-and-drop interactions with contenteditable surfaces. Five distinct bugs across marquee selection, ghost column rendering, state synchronization order, dragover frame rate thrashing, and Vite ES module bundling were isolated and resolved.
Defect AreaRoot CauseEngineering Solution
Marquee DeselectionMissing shiftKey check on mouseupCheck !e.ctrlKey && !e.metaKey && !e.shiftKey
Ghost Column LeakageInferring column count from DOM childrenRead explicit class string pdf-zone--cols-N
Divider Sync PersistenceAsync state clear after sync callClear dragState = null before state sync
Dragover ThrashingUn-throttled CSS toggles on 60fps dragGate class mutations on target element diff
Monaco Bundle CrashWindow global lookup (window.monaco)Import directly: import * as monaco from 'monaco-editor'

Technical defect diagnostics & fixes

1. Marquee deselection modifier check

// REFACTORED: Include Shift key in modifier preservation check
export function onMarqueeMouseUp(evt) {
  if (!evt.ctrlKey && !evt.metaKey && !evt.shiftKey) {
    clearSelection();
  }
}

2. Source-of-Truth column count inspection

Single-column zones contain regions as direct children (no .pdf-col wrapper). Counting .pdf-col children returned 0, causing ghost columns to render on 1-column containers:
// REFACTORED: Read column count explicitly from class name
export function getZoneColumnCount(zoneElement) {
  const match = zoneElement.className.match(/pdf-zone--cols-(\d)/);
  return match ? parseInt(match[1], 10) : 1;
}

3. Drag state nullification order

Setting _resizeDrag = null after calling state synchronization caused stale dividers to be re-injected:
// REFACTORED: Clear drag state BEFORE triggering state sync
export function finalizeColumnResize(zoneElement, computedWidths) {
  const currentDrag = _resizeDrag;
  _resizeDrag = null; // Clear state first!
  saveColumnWidths(zoneElement, computedWidths); // Triggers downstream syncState()
}

4. Idempotent quadrant dragover indicators

Calling removeIndicator() on every 60fps dragover frame caused severe UI flickering:
// REFACTORED: Gate indicator mutation on target change
export function updateQuadrantIndicator(targetElement, isLeftQuadrant) {
  if (_activeQuadrantTarget && _activeQuadrantTarget !== targetElement) {
    clearQuadrantHighlight(_activeQuadrantTarget);
  }

_activeQuadrantTarget = targetElement; targetElement.classList.remove('sel-drop-left', 'sel-drop-right'); targetElement.classList.add(isLeftQuadrant ? 'sel-drop-left' : 'sel-drop-right'); }


5. Vite module import strategy

Vite bundles libraries as ES modules, leaving window.monaco undefined in production builds:
// REFACTORED: Explicit ES module import
import * as monaco from 'monaco-editor';

export function initMonacoInstance(containerElement) { return monaco.editor.create(containerElement, { theme: 'vs-dark' }); }

Rule of thumb: Clear state flags before triggering sync callbacks, gate high-frequency dragover handlers on target diff checks, and import bundled dependencies explicitly.
Read this post in the full Engineering Journal →