Postmortem: Four Discarded Plans for Editing Structured HTML in Place
TLDR
A WYSIWYG editor on structured HTML is not a canvas problem, not a web component problem, and not a double-click problem. It is a CSS specificity problem, a drag event problem, and a DOM insertion-point problem. Four plans were thrown away before landing on the approach that works.
Repo: tools/pdf-processor
The Assumption That Seemed Reasonable
Injecting drag handles into a structured document and letting users reorder content by dragging is a well-understood pattern. The implementation plan seemed straightforward: add handles, wire drag events, update the DOM. One file, two days.
The assumption was that the existing document structure would be neutral to the editing layer. It was not. Every element of the approach, including the entry point for edit actions, the drag handle's position in the DOM, the Monaco editor, and the drag event firing pattern, conflicted with existing browser behaviors on the document.
Plan 1: Right-Click to Edit
A context menu intercept seemed clean. One event listener, one handler. The existing context menu was already wired to contextmenu on the editable area. Intercepting it killed the image insertion options that the existing menu provided.
Thrown away. The correct integration was adding edit actions as items inside the existing menu, calling the edit functions from within the existing handler.
Plan 2: Double-Click to Edit
Double-click is natural and discoverable. The problem: the editable area is contenteditable. Double-click in a contenteditable element selects the word under the cursor. The edit dialog opened with a word selected, no editor content, while the browser simultaneously fired text selection events underneath it.
Thrown away. The context menu entry was correct.
Plan 3: Drag Handles as Flow Children
Drag handles were prepended as <span> elements inside their target elements. Zones use CSS Grid. The <span> became a grid item.
In a two-column zone, the handle occupied column 0 and shifted all actual content right. In a single-column zone, the handle took left-side space and the layout in selection mode was completely different from the document layout in edit mode. Drag-and-drop edits were unpredictable because the zones the user saw in selection mode were not the zones the document would have after deactivating.
Thrown away. Changed handles to position: absolute. Parent zones and regions get position: relative in selection mode. Handles overlay elements without participating in layout. The document structure is now identical in both modes.
Plan 4: Monaco as a Global
The edit code dialog called window.monaco.editor.create(...). This worked in development. It crashed silently in production. The editor container stayed blank with no error message.
Root cause: the bundler (Vite) packages Monaco as an ES module. It does not assign window.monaco. The global does not exist after bundling. The fix is one line: import * as monaco from 'monaco-editor' instead of relying on the global.
This is a consistent trap in any Vite project that uses a library that also ships as a standalone global script. The development environment and the production bundle behave differently. Test with a production build before assuming a global is available.
What Survived
The core insight was correct from the beginning and survived every iteration: a structured HTML document is already a box model. getBoundingClientRect() gives position. insertBefore gives reorder. The browser reflows the grid. No coordinate system, no canvas, no layout engine needed.
What got thrown away were the approaches that tried to graft editing onto the document from the outside, specifically intercepting context menus, handling double-clicks, and prepending inline handles. Each one collided with an existing browser behavior.
What the Spec Missed
The features that turned out to be most valuable were not in the original plan: shift-click marquee selection for multi-region operations, quadrant drop to split a region into a column layout, and a floating properties panel for padding and transform. These came from actually using the tool during implementation.
Interaction features on editable surfaces have more hidden states than pure UI features. contenteditable, HTML5 drag events, and position: absolute all have browser-specific behaviors that only appear when real content is involved. The spec is a starting point. The actual behavior emerges from use.
The Lesson
When building an interaction layer on top of an editable surface, every drag handle, every click handler, and every DOM insert is in direct competition with the browser's native behaviors for that surface. Map those native behaviors first. The plan should account for them, not discover them.