Appearance
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:
restartCount++causes Vue to destroy and re-create visual blocks (text, button, graphic, HTML) via:key="${block.blockName}-${restartCount}"- Vue re-renders blocks as direct children of
.base-block - Script operator runs again, creates a second
static-wrapper - 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")) returnKey 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:
- Change the creative's format to percentage values in the builder
- 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)
| File | Purpose |
|---|---|
src/components/blocks/functional/Restart.ts | Restart operator — partial reset, increments restartCount |
src/components/blocks/functional/Script.ts | Script operator — executes custom JS, provides special access |
src/services/dataStore.ts | Central reactive state (restartCount, liveFlow, overrides) |
src/components/creative/Creative.vue | Sets creative dimensions from format config |
src/components/creative/CreativeVideoBlock/CreativeVideoBlock.vue | Video block — new instance per changeVideo call |
src/components/creative/VisualElements/CreativeVisualElements.vue | Renders blocks with restartCount key |
src/components/creative/VisualElements/CreativeHtmlBlock.vue | HTML block — parseScripts() only in live mode |
Key Concepts
restartCount: Used as Vue key — incrementing forces block re-creationdevMode:truein builder,falsein live — affects script executionchangeVideo: Creates new component instances, doesn't update in-place- Engine CSS: Fixed pixel dimensions from format config, need
!importantto override
Debug Scripts Location
/Users/nicolay/CavaiProduct/Application-Frontend/debug/
doubleMidscrollSlider.js— Slider interaction + DOM element repositioningdoubleMidscrollAdvantage.js— Advantage scroll progress handlerdouble-midscroll-html.html— Layout containers (top/bottom)lensChanging.js— Glasses/lens radio button handler