Skip to content

DoubleMidscrollSingleCreative — Debugging Notes

Collected during debugging of a Specsavers DoubleMidscrollSingleCreative creative (April 2026).


1. Restart Operator — Static-Wrapper Duplication Bug

Problem

Restart operator works in builder preview but fails in standalone/live preview. After restart, the creative breaks — duplicate elements appear and layout collapses.

Root Cause

The Script operator creates a div.static-wrapper via raw DOM manipulation (document.createElement). On restart:

  1. restartCount++ causes Vue to destroy and re-create visual blocks (text, button, graphic, HTML) via :key="${block.blockName}-${restartCount}"
  2. Vue re-renders blocks as direct children of .base-block
  3. Script operator runs again, creates a second static-wrapper
  4. The old wrapper gets nested inside the new one, breaking layout

This doesn't happen in builder preview because devMode=true skips parseScripts() in CreativeHtmlBlock.vue.

Fix

Add a guard check at the start of the static-wrapper script:

javascript
if (baseBlock.querySelector(":scope > .static-wrapper")) return

Key Insight

Any Script operator that creates DOM elements outside Vue's knowledge will break on restart. Vue re-renders to its own vDOM tree and has no awareness of manually created elements.


2. changeVideo Breaks Layout in DoubleMidscroll

Problem

Using changeVideo() (e.g., from responsive-video-swap) in a DoubleMidscroll layout causes the video to appear in the wrong position. The video was manually moved to .top_container via DOM manipulation, but changeVideo creates a NEW video component instance in the original Vue parent (CreativeBody).

Root Cause

changeVideo('v1', { video: { streamId } }) pushes a new video object to arrayOfVideoProperties, causing Vue to create a new CreativeVideoBlock component (.v1-wrap.v1-2, .v1-wrap.v1-3, etc.) as a sibling in CreativeBody.vue. The new instance appears in the default position, not in .top_container.

Fix

Use a MutationObserver on .base-block to catch new .v1-wrap elements and move them to .top_container:

javascript
const observer = new MutationObserver((mutations) => {
  for (const m of mutations) {
    for (const node of m.addedNodes) {
      if (node.nodeType === 1 && node.classList?.contains('v1-wrap')) {
        const topContainer = document.querySelector('.top_container')
        if (topContainer && !topContainer.contains(node)) {
          topContainer.prepend(node)
        }
      }
    }
  }
})
observer.observe(document.querySelector('.base-block'), { childList: true, subtree: true })

Key Insight

changeVideo does not update the existing video in-place — it creates a brand new Vue component instance. Any DOM-level repositioning is lost.


3. Video Not Filling Full Width

Problem

After fixing video positioning with MutationObserver, the video has a fixed pixel width (e.g., 1920px) instead of filling its container.

Root Cause

Engine's StyleAndClassNameGenerationMixin generates CSS with fixed pixel dimensions based on the creative's format settings:

css
.v1-wrap.v1-2 { width: 1920px; height: 1080px; }
.v1-wrap.v1-2 video { object-fit: fill; width: 1920px; height: 1080px; }

Fix

Override with CSS !important in the HTML block:

css
.top_container [class*='v1-wrap'] {
  width: 100% !important;
  height: 100% !important;
}
.top_container [class*='v1-wrap'] video {
  width: 100% !important;
  height: 100% !important;
  object-fit: cover !important;
}

4. Radio Button Clicks Unreliable in HTML Block

Problem

Radio buttons (for glasses/lens selection) inside an HTML block don't respond reliably to clicks.

Root Cause

Engine's CreativeHtmlBlock applies pointerEvents: 'none' on the wrapper and pointerEvents: 'auto' on the inner block. Combined with engine's CSS reset (& * { fontSize: 16px; padding: 0; margin: 0; }), this interferes with label-to-input click delegation.

Fix

Explicit JavaScript click handlers with stopPropagation:

javascript
document.querySelectorAll('#cavaiOptions label').forEach(label => {
  label.addEventListener('click', (e) => {
    e.stopPropagation()
    const input = document.getElementById(label.getAttribute('for'))
    if (input && !input.checked) {
      input.checked = true
      input.dispatchEvent(new Event('change', { bubbles: true }))
    }
  })
})

5. Fullscreen Width — DOUBLE_MIDSCROLL vs DOUBLE_MIDSCROLL_SINGLE_CREATIVE

Problem

Madington's Bergans campaign (DOUBLE_MIDSCROLL) goes truly fullscreen on tv2.no, while Specsavers (DOUBLE_MIDSCROLL_SINGLE_CREATIVE) stops at a fixed pixel width.

Root Cause

Not the Advantage wrapper — both use max-width: 1920px. The difference is inside the creative's format configuration.

In Creative.vue, the engine converts format dimensions to CSS:

typescript
const toCSS = (val) => typeof val === 'string' && val.includes('%') ? val : `${val}px`
  • Specsavers: format: { width: 428, height: 732 }width: 428px (fixed)
  • Bergans: Likely percentage-based or responsive format → fills container

Fix

Either:

  1. Change the creative's format to percentage values in the builder
  2. Override in the HTML block CSS (double-midscroll-html.html):
css
#creative-container { width: 100% !important; height: 100% !important; }

Architecture Notes

Key Files (Creative-Engine)

FilePurpose
src/components/blocks/functional/Restart.tsRestart operator — partial reset, increments restartCount
src/components/blocks/functional/Script.tsScript operator — executes custom JS, provides special access
src/services/dataStore.tsCentral reactive state (restartCount, liveFlow, overrides)
src/components/creative/Creative.vueSets creative dimensions from format config
src/components/creative/CreativeVideoBlock/CreativeVideoBlock.vueVideo block — new instance per changeVideo call
src/components/creative/VisualElements/CreativeVisualElements.vueRenders blocks with restartCount key
src/components/creative/VisualElements/CreativeHtmlBlock.vueHTML block — parseScripts() only in live mode

Key Concepts

  • restartCount: Used as Vue key — incrementing forces block re-creation
  • devMode: true in builder, false in live — affects script execution
  • changeVideo: Creates new component instances, doesn't update in-place
  • Engine CSS: Fixed pixel dimensions from format config, need !important to override

Debug Scripts Location

/Users/nicolay/CavaiProduct/Application-Frontend/debug/

  • doubleMidscrollSlider.js — Slider interaction + DOM element repositioning
  • doubleMidscrollAdvantage.js — Advantage scroll progress handler
  • double-midscroll-html.html — Layout containers (top/bottom)
  • lensChanging.js — Glasses/lens radio button handler

Internal documentation