Skip to content

Iframe DOM Inspector - Design Spec

Date: 2026-05-05 Status: Approved Branch: TBD (likely dev-tools-dom-inspector or continuation of improve-dev-tools)

Problem

Browser DevTools lose focus when the creative preview iframe refreshes. Switching between main page and iframe console is awkward. Developers need a way to inspect the creative DOM without fighting the browser.

Solution

A built-in DOM inspector inside the existing DevToolPanel system. Three tabs: Elements (DOM tree + element info), Console (eval + captured output), and Styles (computed styles + box model). The inspector is Cavai-aware - it maps DOM elements to block names and shortcodes.

Pop-out + fullscreen on a second monitor makes this a full replacement for browser DevTools when working with creatives.

Key Architectural Decision: Direct DOM Access

The preview iframe loads from /preview-frame.html (same-origin). This means AF can access iframe.contentDocument and iframe.contentWindow directly. This eliminates the need for serializing/deserializing the DOM tree via postMessage.

What this means:

  • DOM tree: read directly from iframe.contentDocument (no serialization)
  • Computed styles: iframe.contentWindow.getComputedStyle(element) directly
  • Eval: iframe.contentWindow.eval(expression) directly
  • Highlight overlay: inject elements directly into iframe DOM from AF
  • MutationObserver: runs in AF, observing iframe.contentDocument

What still needs postMessage:

  • Console interception: must patch console inside the iframe to capture output (CE-side, minimal code)

Trade-off: If the preview iframe ever becomes cross-origin, the DOM access approach would need to be replaced with postMessage serialization. This is unlikely given the local build architecture, and the simpler implementation outweighs the risk.

Architecture

New files

Creative-Engine (minimal):

  • src/devtools/consoleInterceptor.ts - console monkey-patching only
  • Gated behind DataStore.devMode.value at runtime (the #include annotations are documentation markers, not preprocessor directives - all devtools code ships in bundle but is gated at runtime)
  • Must store original console references and restore on cleanup
  • Re-initializes automatically after creative rebuild via preview-iframe-ready message

Application-Frontend:

  • src/components/DevTools/DomInspector.vue - main component with tab bar and three tabs
  • src/components/DevTools/DomTreeNode.vue - DOM-specific recursive tree (HTML-like rendering)
  • src/components/DevTools/InspectorConsole.vue - console tab
  • src/components/DevTools/InspectorStyles.vue - styles tab with box model diagram

Iframe access

DomInspector.vue gets the iframe reference from the builder. The iframe element is available via the preview component. On mount and on each preview-iframe-ready message, the inspector:

  1. Gets iframe.contentDocument and iframe.contentWindow
  2. Attaches MutationObserver to observe DOM changes (debounced 500ms)
  3. Builds the initial DOM tree
  4. Sets up click listener for inspect mode

After creative rebuilds (detected via preview-iframe-ready postMessage), the observer and listeners are re-attached to the new document.

Console interception (CE-side)

The only CE-side code. In consoleInterceptor.ts:

ts
const originalConsole = { ...window.console }

export function initConsoleInterceptor() {
  if (!DataStore.devMode?.value) return
  for (const level of ['log', 'warn', 'error', 'info']) {
    window.console[level] = (...args) => {
      originalConsole[level](...args)
      window.parent.postMessage({
        type: 'cavai-dom-console',
        level,
        args: safeSerialize(args),
        timestamp: Date.now(),
      }, '*')
    }
  }
}

export function cleanupConsoleInterceptor() {
  Object.assign(window.console, originalConsole)
}

Original references are saved at module scope (before any patching). cleanupConsoleInterceptor restores them. Both are called from Creative.vue's mount/unmount lifecycle.

Elements Tab

DOM tree (DomTreeNode.vue)

Reads directly from iframe.contentDocument. Renders in Chrome DevTools-like format:

> <div .group-block .gp1>                    gp1
  > <div .gp-inner>
    > <div .t1-wrap>                          t1
      v <div .text-block .t1-1>
          "Hello world"
    > <div .b1-wrap>                          b1
  > <section .video-controls>
  • Tags in purple, classes in blue, text in gray, shortcode badge in blue (right-aligned)
  • Collapsible nodes
  • Hidden elements (display: none / visibility: hidden) shown dimmed with "hidden" marker
  • Max tree depth: 20 levels (safety cap, creatives rarely exceed 10)
  • Text content truncated to 80 characters

Shortcode and block mapping

Extract shortcode from element class names using the same patterns as extractShortcode() in devtoolsBridge.ts. Map shortcode back to blockName via the known prefix patterns (t=text, b=button, g=graphic, gp=group, etc.). This mapping runs in AF using the class names read directly from DOM elements.

Click-to-inspect

  • "Inspect mode" toggle button (cursor icon, like Chrome)
  • When active, clicks in the iframe are intercepted by a capture-phase listener added from AF
  • Clicked element is identified, tree scrolls to and highlights the matching node
  • Element info panel updates with selected element's data

Hover-highlight

  • Hover on tree node in AF: AF directly creates/positions an overlay <div> inside the iframe document
  • Overlay: semi-transparent blue (rgba(66, 133, 244, 0.3)) with pointer-events: none, position: fixed, z-index: 2147483647 (max int, above all creative content)
  • Shows dimensions label (e.g. "320 x 50") above or below the overlay
  • Uses element.getBoundingClientRect() for positioning
  • Mouse leave removes the overlay element

Element info panel (below tree)

For selected element:

  • Tag, id, classes
  • Block mapping: "Owned by textProperties-1 (t1)" - clicking dispatches setSelectedBlockPath in Vuex to select the block in the builder sidebar
  • Dimensions: width x height, position (top, left) via getBoundingClientRect()
  • Key styles: display, position, z-index, opacity, overflow

Console Tab

Output area

  • Captured console.log, .warn, .error, .info from iframe via postMessage
  • Each line shows: level icon (color-coded), timestamp, serialized arguments
  • Objects/arrays are expandable (reuse TreeNode component)
  • Auto-scroll to bottom, "Clear" button

Console interception lifecycle

The creative rebuilds on every block edit. After each rebuild:

  1. AF receives preview-iframe-ready message
  2. CE's new Creative.vue instance calls initConsoleInterceptor() on mount
  3. Previous instance already called cleanupConsoleInterceptor() on unmount
  4. Console output continues uninterrupted in the inspector

Argument serialization

safeSerialize() handles:

  • Primitives: pass through
  • Objects/arrays: JSON.stringify with circular reference detection (replace with "[Circular]")
  • DOM nodes: serialize as CSS selector string (e.g. <div.t1-wrap>)
  • Functions: serialize as "function functionName() { ... }"
  • Errors: serialize message + stack
  • Max depth: 5 levels for nested objects
  • Max string length: 1000 characters

Input field

  • Text input at bottom with > prompt
  • Enter to execute, Shift+Enter for multiline
  • Command history with up/down arrows (stored in component data, up to 50 entries)
  • Eval via iframe.contentWindow.eval(expression) (direct, same-origin)
  • Gated behind devMode check
  • Results shown in output (green for return values, red for errors)
  • Access to DataStore, document, full iframe context

Styles Tab

For selected element, computed styles via iframe.contentWindow.getComputedStyle(element), grouped in collapsible sections:

SectionProperties
Layoutdisplay, position, flex-direction, flex-wrap, align-items, justify-content
Box Modelwidth, height, margin-, padding-, border-*
Typographyfont-family, font-size, font-weight, color, line-height, text-align
Visualbackground-color, background-image, opacity, z-index, overflow, box-shadow
Transformtransform, transition, animation
  • Filter out common defaults to reduce noise: opacity: 1, z-index: auto, transform: none, overflow: visible, position: static, display: block (for div), display: inline (for span)
  • Color values show color swatches (reuse from TreeNode pattern)

Box model diagram

Simple nested-boxes visualization (like Chrome DevTools):

  • Shows margin / border / padding / content with pixel values
  • Color-coded layers (orange margin, green border, yellow padding, blue content)
  • Positioned at top of Styles tab
  • Values read directly via getComputedStyle()

Integration

DevTools.vue

Add 5th tool with all three required integration points:

  1. Entry in SWITCHABLE_TOOLS:
ts
{ key: 'domInspector', label: 'DOM Inspector' }
  1. Entry in tools array:
ts
{
  text: this.$t('devTools.domInspector'),
  disabled: () => false,
  onClick: () => this.switchTool('domInspector'),
}
  1. Conditional render in template:
html
<DomInspector
  v-if="activeTool === 'domInspector'"
  @close="activeTool = ''"
/>

CE Creative.vue

Add #include block for console interceptor:

ts
// #include when devTools exists
import { initConsoleInterceptor, cleanupConsoleInterceptor } from '@/devtools/consoleInterceptor'
// mounted: initConsoleInterceptor()
// beforeUnmount: cleanupConsoleInterceptor()
// #end

Tab bar in DomInspector.vue

Three tabs rendered inside the DevToolPanel content slot. Simple tab bar with active state, no external dependency. Tabs: Elements (default), Console, Styles.

Existing infrastructure reused

  • DevToolPanel: drag, resize, pop-out, fullscreen, tool switching
  • TreeNode: expandable objects in console output
  • JSONSearchBar pattern: search DOM nodes by tag/class/text
  • preview-iframe-ready message: detect creative rebuilds for re-initialization
  • extractShortcode() pattern from devtoolsBridge: map DOM classes to block shortcodes

Design principles

  1. Cavai-aware, not generic - map DOM to blocks, show shortcodes, link to config
  2. Direct DOM access - leverage same-origin iframe for simplicity and performance
  3. Minimal CE footprint - only console interception in CE, everything else in AF
  4. Runtime-gated - all devtools code gated behind DataStore.devMode.value, not preprocessor
  5. Rebuild-resilient - re-attach observers and listeners after each creative rebuild via preview-iframe-ready
  6. Familiar - Chrome DevTools visual language (colors, layout, interactions)
  7. Pop-out friendly - works in panel, pop-out, and fullscreen equally well

Internal documentation