Appearance
Custom Scripts (Tag/JS Operator)
The Tag operator (displayed as "JS" in the Builder) lets users write custom JavaScript that executes during the conversation flow. This document covers how custom scripts work and common pitfalls.
How It Works
- User writes JavaScript in the Tag operator's textarea (or the large code editor)
- The code is stored in
operator.properties.body.inputText.value, wrapped in<script>tags - At runtime,
Script.tsin the Creative Engine extracts the code, creates a<script>element, appends it todocument.head, and removes it after execution
User writes code in Builder (TagOp.vue / OperatorLargeCode.vue)
|
Stored as: <script>user code here</script>
|
Composer bakes it into the published creative
|
Engine flow reaches the operator
|
Script.ts extracts code, appends <script> to document.head
|
Code executes synchronously, <script> element removedExecution Context
- Scripts run inside the creative's iframe (preview iframe or published creative iframe)
documentrefers to the creative's document, not the Builderwindowis the creative's window object- Scripts have access to special globals when detected in the code:
DATASTORE,ANALYTICS,changeText(),changeImage(),changeUrl(),changeVideo(),showElement(),hideElement(),resetCreative(),restartFlow()
Gotchas
innerText returns empty on flow elements
Problem: document.querySelector('.response').innerText returns an empty string even though the element exists and contains text.
Cause: innerText respects CSS rendering. If responseProperties.hidden is true in the conversation block settings, the response bubble gets visibility: hidden (from MessageHolder.vue styles). The element still exists in the DOM -- querySelector finds it and innerHTML returns its content -- but innerText returns empty because the element is visually hidden.
How to hit this: Toggle the "Hidden" switch on Response properties in the Conversation block configuration. This is easy to do accidentally while styling messages and choices.
Fix: Use textContent instead of innerText:
js
// Bad -- returns empty when response has visibility: hidden
const answer = document.querySelector('.response').innerText
// Good -- always returns the text content regardless of CSS
const answer = document.querySelector('.response').textContent.trim()| Property | Respects CSS? | Returns empty when visibility: hidden? |
|---|---|---|
innerHTML | No | No (returns raw HTML) |
textContent | No | No (returns raw text) |
innerText | Yes | Yes |
This applies to any element styled with visibility: hidden or display: none. When querying flow elements (messages, choices, responses) from custom scripts, prefer textContent over innerText to avoid silent failures from styling changes.
Script execution timing
Scripts execute synchronously when the flow reaches them. The engine appends a <script> element to document.head -- it runs immediately, then gets removed. This means:
- Vue DOM updates from the previous flow step (e.g. rendering a response bubble after a choice click) may not have happened yet -- they are queued for the next microtask
- If querying elements created by the immediately preceding step, wrap in
setTimeout:
js
setTimeout(() => {
const el = document.querySelector('.response')
// ...
}, 50)- All globals set on
window(e.g.window.quizScore) persist across script operators within the same creative session - After a preview restart,
windowproperties survive but stored DOM references become stale becausepreview.innerHTMLis cleared and the creative remounts
CSS class names in the creative DOM
Flow elements use these CSS classes for querying:
| Element | Class | Generated by |
|---|---|---|
| Message bubble | .message | translateFlowComponentType('Text') |
| Choice buttons | .choice | Component type |
| Response (answered choice) | .response | translateFlowComponentType('Choice', true) |
| Inner text content | .msg-inner | Text.vue template |
| Message content wrapper | .message-content | MessageHolder.vue template |
| Text block | .t1, .t2, ... | Block abbreviation |
| Button block | .b1, .b2, ... | Block abbreviation |
| Graphic block | .g1, .g2, ... | Block abbreviation |
Note: Class name obfuscation exists (obfuscator.ts) but is currently disabled (DataStore.obfuscateStyles defaults to false). If enabled in the future, all these class names will be randomized and custom scripts relying on them will break.
Key Source Files
| File | Repo | Purpose |
|---|---|---|
src/components/blocks/functional/Script.ts | Creative-Engine | Script execution, special access injection |
src/logic-system/processors/conversationFlow.ts | Creative-Engine | Flow step processing, calls funcBlocks.Tag() |
src/utils/funcBlocks.ts | Creative-Engine | Maps operator types to handler functions |
src/components/conversationflow/MessageHolder.vue | Creative-Engine | Renders messages, choices, responses |
src/components/blocks/basic/Text.vue | Creative-Engine | Renders text content inside bubbles |
src/pages/.../operators/TagOp.vue | Application-Frontend | Builder textarea for script input |
src/pages/.../flowactors/OperatorLargeCode.vue | Application-Frontend | Large code editor overlay |