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: Every bug came from a wrong assumption about how the browser handles one of: drag events, contenteditable, CSS Grid, Vite module bundling, or mutable state in callbacks.


Bug 1: Shift+marquee cleared the selection immediately

Symptom: Shift+drag draws the marquee, regions highlight, mouseup fires — then all highlights disappear. Group button never appears.

Root cause: _onMarqueeEnd had this line:

if (!e.ctrlKey && !e.metaKey) _clearSelection();

Shift is held during the entire marquee drag. On mouseup, e.shiftKey is true but neither Ctrl nor Meta is. The condition evaluated to true and wiped everything just selected.

Fix:

if (!e.ctrlKey && !e.metaKey && !e.shiftKey) _clearSelection();

Guard: Any "clear selection unless modifier key" check must include all relevant modifiers. Shift, Ctrl, and Meta all have different conventional meanings — don't forget Shift.


Bug 2: Ghost column showed on cols-1 zones even at the limit

Symptom: A zone that already had 4 columns still showed a ghost column on hover. A zone with 1 column showed no ghost at all.

Root cause: Ghost count was read from child elements:

const cols = zone.querySelectorAll(':scope > .pdf-col').length;
if (cols >= 4) return;

A pdf-zone--cols-1 has no pdf-col children — regions are direct children. length returned 0. Ghost appeared for every 1-column zone regardless of position.

Fix:

const colsMatch = zone.className.match(/pdf-zone--cols-(\d)/);
const cols = colsMatch ? parseInt(colsMatch[1]) : 1;

Read the column count from the class name, not the DOM structure. The class is the source of truth for column count.

Guard: Never infer structural state from child counts when a data attribute or class carries that state explicitly.


Bug 3: Column resize left _resizeDrag set during _syncState

Symptom: After releasing a column resize drag, the drag state persisted into the sync call. Dividers were removed and re-injected with the wrong positions.

Root cause:

function _onDividerMouseUp() {
    const { dividerEl, zoneEl } = _resizeDrag;
    const finalWidths = getComputedStyle(zoneEl).gridTemplateColumns.split(' ').map(parseFloat);
    _saveColWidths(zoneEl, finalWidths); // ← _syncState fires here
    _resizeDrag = null;                 // ← too late
}

_saveColWidths calls _syncState which calls _removeAllDividers. _removeAllDividers accesses nothing from _resizeDrag, but the state being non-null during the sync created ordering issues on the next re-inject.

Fix: Null _resizeDrag before calling _saveColWidths:

_resizeDrag = null;
_saveColWidths(zoneEl, finalWidths);

Guard: Any mutable state that guards a flow must be cleared before triggering downstream effects that read or depend on that state.


Bug 4: Quadrant drop indicator flickered on every dragover event

Symptom: The sel-drop-left / sel-drop-right CSS class (box-shadow highlight) flickered visibly during drag. The highlight appeared and disappeared multiple times per second.

Root cause: dragover fires continuously while the cursor moves. At the top of _onDragOver:

_removeIndicator(); // ← this stripped sel-drop-left/right immediately

Then two lines later it was re-added. Every frame: remove class → re-add class → browser repaints.

Fix: Track the quadrant target separately. Only update when the target changes:

if (_quadrantTarget && _quadrantTarget !== target) _clearQuadrantHighlight();
_quadrantTarget = target;
target.classList.remove('sel-drop-left', 'sel-drop-right');
target.classList.add(onLeft ? 'sel-drop-left' : 'sel-drop-right');

_clearQuadrantHighlight() only fires on target change or drag end — not on every event.

Guard: dragover fires at 60fps. Any state change inside it must be idempotent or gated on a diff check.


Bug 5: window.monaco was undefined in Vite builds

Symptom: Edit Code dialog opened. Monaco container was blank. No console error.

Root cause:

return window.monaco.editor.create(_container, { ... });

Vite bundles Monaco as an ES module and tree-shakes it into the app bundle. It never assigns window.monaco. The expression threw TypeError: Cannot read properties of undefined silently because the dialog's toggle event swallowed it.

Fix:

import * as monaco from 'monaco-editor';
// ...
return monaco.editor.create(_container, { ... });

Guard: Never access Vite-bundled libraries through window. If you didn't put it on window explicitly, it isn't there. Import it directly in every module that uses it.

Read this post in the full Engineering Journal →