Appearance
Click Handling and Event Propagation
Overview
The Creative-Engine uses a layered click handling system where events bubble up through nested Vue components. Two things happen on click: navigation (opening clickthrough URLs) and analytics (tracking user interaction). The .stop modifier and stopPropagation() are used strategically to prevent clicks on interactive elements from triggering parent handlers — most importantly the background clickthrough.
Understanding this system is critical because removing or adding .stop in the wrong place can cause:
- Clickthrough URLs opening when users interact with video controls, forms, or buttons
- Double analytics events
- Lost first-interaction tracking for expandable creatives
Component Hierarchy
Creative.vue @click.self → creativeContainerClicked()
└── CreativeBody.vue @click.self → handleBgClick()
├── CreativeConversationBlock
│ └── MessageHolder @click.stop → onMessageWrapperClick()
│ └── Flow components (Text, Choice, Link, Slider...)
│
├── CreativeVideoBlock @click → $emit('bgClick')
│ ├── <video>
│ └── CreativeVideoControls @click.stop (section wrapper)
│ ├── togglePlay()
│ └── toggleMute()
│
├── CreativeSliderBlock
├── CreativeFormBlock @click.stop (form-block div)
│
├── VisualElements (via VisualElementMixin)
│ ├── CreativeButtonBlock @click → handleClick() (no propagation stop)
│ ├── CreativeTextBlock @click → handleClick() (no propagation stop)
│ ├── CreativeGraphicBlock @click → handleClick() (no propagation stop)
│ └── CreativeHtmlBlock @click → handleClick() (no propagation stop)
│
├── CloseButton @click.stop → handleButtonClick()
└── RemoveButton @click.stop → handleButtonClick()
CreativeHeader (expandable only, outside CreativeBody)
├── Tagline @click.stop → toggleExpand()
└── ExpandableIconClick Event Flow
User clicks something inside the creative
↓
Is it an interactive element? (button, video control, form input, tagline)
↓ YES ↓ NO
Component handles it Event bubbles up
(.stop prevents bubbling) ↓
↓ CreativeBody.vue handleBgClick()
Component-specific action ↓
(play video, submit form, Has backgroundClickthroughUrl?
open element clickthrough) ↓ YES ↓ NO
Opens the URL Nothing
Sends analyticsKey Files & Their Roles
| File | Role | Stops Propagation |
|---|---|---|
| Creative.vue | Root container. Tracks first interaction, clicks counter, goal timer | No — uses @click.self |
| CreativeBody.vue | Handles background clickthrough URL navigation | No — uses @click.self |
| MessageHolder.vue | Wraps conversation messages, emits bgClick only for wrapper clicks | Yes — @click.stop |
| CreativeVideoControls.vue | Play/pause and mute/unmute buttons | Yes — @click.stop on <section> |
| CreativeVideoBlock.vue | Video container, emits bgClick for background clicks | No |
| CreativeFormBlock.vue | Form wrapper, prevents form clicks from reaching background | Yes — @click.stop on inner div |
| CreativeButtonBlock.vue | Clickthrough button | No — @click without .stop |
| VisualElementMixin.ts | Shared click handler for visual elements with clickthrough URLs | No — handleClick() does not call stopPropagation() |
| Tagline.vue | Expandable creative header, triggers expand/collapse | Yes — @click.stop |
| CloseButton.vue | Close button for banners | Yes — @click.stop |
| RemoveButton.vue | Remove/close button for expandables | Yes — @click.stop |
| ToggleExpand.ts | Expand/collapse logic for expandable creatives | N/A (utility function) |
Background Clickthrough
The background clickthrough is the most important thing to protect against accidental triggers. It opens a URL when the user clicks the creative background.
javascript
// CreativeBody.vue — handleBgClick()
handleBgClick() {
const { backgroundClickthrough, backgroundClickthroughUrl }
= DataStore.creativeSettings.creativeDelivery
// Flow URL takes precedence over delivery config
const bgClickUrl = DataStore.currentBgClickUrl.value || clickthroughUrl
if (!bgClickUrl) return
handleLinkOpens(getLinkUrl(bgClickUrl)) // Navigate
Analytics.sendCount(COUNTABLE_METRICS.backgroundClick)
Analytics.sendEvent({ clickedLink: true, linkUrl: bgClickUrl })
}Why .stop matters: Without .stop on interactive elements, clicking a video play button or form input would bubble up to handleBgClick() and open the clickthrough URL. This is especially dangerous with CreativeVideoBlock which emits bgClick on every click inside it — so any child click that doesn't stop propagation will trigger navigation.
Analytics Events on Click
There are several analytics mechanisms that fire on click:
| Mechanism | What it does | When it fires |
|---|---|---|
sendFirstEvent() | Sets creativeClickedOnce, starts goal timer, sends secondsToActive | Once per creative lifetime, on first user interaction |
sendEvent({}) | Sends a general analytics event | On various interactions |
sendCount(metricId) | Sends a countable metric (-1 = header click, -2 = bg click) | On clickthrough navigation |
clicks counter | Increments Analytics.clicks, included in Bunny payload | On every click (tracked in Creative.vue) |
numUserActions | Increments user action counter, included as chatbotActions | On expand, form submit, other interactions |
First Event Flow
sendFirstEvent() is the gatekeeper for first-interaction tracking:
javascript
// analytics.ts
sendFirstEvent(expandable = false) {
if (DataStore.creativeClickedOnce.value) return // Only fires once
DataStore.creativeClickedOnce.value = true
if (expandable) this.numUserActions++ // Expandables count as action
Analytics.startGoalTimer() // Start tracking goal time
this.sendEvent({ secondsToActive: ... }) // Time from load to first click
this.postMessage({ creativeClickedOnce: true }) // Notify parent frame
}Important for expandable creatives: The expandable = true parameter is passed from ToggleExpand.ts and Tagline.vue because for expandables, the first interaction is clicking the tagline — not the creative background. Since Creative.vue uses @click.self, the container handler won't fire from tagline clicks, so sendFirstEvent must be called explicitly from the expand path.
Visual Element Clickthrough
Visual elements (buttons, text blocks, graphics, HTML blocks) use a shared mixin for click handling:
javascript
// VisualElementMixin.ts
handleClick() {
if (!this.clickthroughUrl) return // No URL = no action, click bubbles
this.trackClick()
}
trackClick() {
handleLinkOpens(getLinkUrl(this.clickthroughUrl))
Analytics.sendCount(COUNTABLE_METRICS.headerClick) // -1
Analytics.sendEvent({ clickedLink: true, linkUrl: this.clickthroughUrl })
}Note: handleClick() does NOT call stopPropagation(). Clicks always bubble up regardless of clickthrough URL. Visual elements without a URL produce no analytics at all -- see analytics/visual-element-tracking-gap.md for the full tracking gap analysis.
Expandable Creatives
Expandable creatives have a different click flow because interaction starts at the header (tagline), not the creative body.
User clicks Tagline
↓
Tagline.vue toggleExpand() — @click.stop
↓
Analytics.sendFirstEvent(true) ← expandable flag
↓
$emit('toggle-expand')
↓
ToggleExpand.ts executes
↓
DataStore.creativeExpanded = true
SafeFrame.triggerExpand()
eventEmitter.emitStartFlow() ← conversation startsOn collapse:
User clicks Tagline again (or CloseButton)
↓
ToggleExpand.ts
↓
DataStore.creativeExpanded = false
Analytics.clearGoalTimer()
Analytics.sendEvent({ secondsTotalActive: ... })URL Building
All clickthrough URLs go through getLinkUrl() before navigation:
javascript
// linkHelper.ts — getLinkUrl()
// 1. Append link params (UTM etc)
// 2. Append GDPR params
// 3. Prepend click macro (DSP tracking) — must be lastNavigation priority:
- MRAID (
window.mraid.open) — mobile in-app - VPAID (
AdClickThruevent) — Yahoo/proprietary players - Standard (
window.open) — regular browser
Rules of Thumb
- Any interactive element must stop propagation — otherwise clicks trigger
handleBgClick()and can open the clickthrough URL .stopgoes on the outermost clickable wrapper — e.g.CreativeVideoControlsstops at the<section>level, not individual buttons- Visual elements do NOT stop propagation --
VisualElementMixin.handleClick()never callsstopPropagation(). Clicks always bubble. Without a clickthrough URL, the handler returns immediately with no side effects. - First interaction is handled by
@click.captureon Creative.vue -- fires on any trusted click in the container. Expandables additionally call from Tagline/ToggleExpand. @click.selfis not a substitute for.stop— it only prevents the handler from firing on bubbled events, but intermediate components (likeCreativeVideoBlock) may re-emit the event explicitly
SPA Considerations
In SPA mode, multiple creatives load on the same page without refresh. The Analytics and Visibility objects are singletons that persist between creatives. State like numUserActions, flowSequenceNumber, clicks, and visibility tracking (bucketIndex, lastBucketSent) must be reset in teardown() to avoid stale data leaking into the next creative.