rad-coder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # rad-coder
2
+
3
+ A development environment for testing ResponsiveAds creative custom JavaScript with hot-reload.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx rad-coder <creativeId>
9
+ ```
10
+
11
+ Or with a full preview URL:
12
+
13
+ ```bash
14
+ npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview
15
+ ```
16
+
17
+ ## What It Does
18
+
19
+ 1. **Creates files** in your current directory:
20
+ - `custom.js` - Your custom JavaScript code
21
+ - `AGENTS.md` - Instructions for AI coding assistants (Copilot, Claude, etc.)
22
+
23
+ 2. **Fetches creative config** from ResponsiveAds Studio automatically
24
+
25
+ 3. **Starts a dev server** at `http://localhost:3000`
26
+
27
+ 4. **Opens your browser** with the test page showing your creative
28
+
29
+ 5. **Hot-reload** - Edit `custom.js`, save, and the browser automatically reloads
30
+
31
+ ## Usage
32
+
33
+ ### Basic Usage
34
+
35
+ ```bash
36
+ # Create a new directory for your project
37
+ mkdir my-creative
38
+ cd my-creative
39
+
40
+ # Start rad-coder with your creative ID
41
+ npx rad-coder 697b80fcc6e904025f5147a0
42
+ ```
43
+
44
+ ### With AI Assistants
45
+
46
+ The generated `AGENTS.md` file contains instructions for AI coding assistants. When using VS Code with Copilot or other AI tools, they can read this file to understand:
47
+
48
+ - How to use the Radical API
49
+ - Available lifecycle hooks (`onBeforeRender`, `onLoad`, `onRender`)
50
+ - Component methods (Carousel, TextBox, etc.)
51
+ - Best practices for ResponsiveAds creatives
52
+
53
+ ### Workflow
54
+
55
+ 1. Run `npx rad-coder <creativeId>`
56
+ 2. Edit `custom.js` in your favorite editor
57
+ 3. Save the file - browser auto-reloads
58
+ 4. See your changes applied to the creative instantly
59
+
60
+ ## Features
61
+
62
+ - **Zero configuration** - Just provide a creative ID
63
+ - **Auto-detection** - Extracts flowline, sizes, and settings from Studio
64
+ - **Hot-reload** - Instant feedback when you save changes
65
+ - **AI-ready** - Includes documentation for AI coding assistants
66
+ - **Cross-platform** - Works on macOS, Linux, and Windows
67
+
68
+ ## Requirements
69
+
70
+ - Node.js 18.0.0 or higher
71
+
72
+ ## How It Works
73
+
74
+ rad-coder fetches your creative's configuration from the ResponsiveAds Studio preview page, extracts the flowline settings, and creates a local development environment. Your custom JavaScript is injected into the creative via the `customjs` config property.
75
+
76
+ The server watches your `custom.js` file for changes and uses WebSocket to signal the browser to reload when you save.
77
+
78
+ ## API Reference
79
+
80
+ See the generated `AGENTS.md` file for complete Radical API documentation, including:
81
+
82
+ - Lifecycle hooks
83
+ - Element manipulation
84
+ - Carousel and TextBox components
85
+ - Dynamic Content Optimization (DCO)
86
+ - Analytics tracking
87
+
88
+ ## Development
89
+
90
+ Instructions for developers who want to modify rad-coder itself.
91
+
92
+ ### Setup
93
+
94
+ ```bash
95
+ # Clone the repository
96
+ git clone https://github.com/nicatronTg/rad-coder.git
97
+ cd rad-coder
98
+
99
+ # Install dependencies
100
+ npm install
101
+
102
+ # Link the package globally for local testing
103
+ npm link
104
+ ```
105
+
106
+ ### Testing Your Changes
107
+
108
+ After making changes, test locally:
109
+
110
+ ```bash
111
+ # Create a test directory
112
+ mkdir /tmp/test-rad-coder
113
+ cd /tmp/test-rad-coder
114
+
115
+ # Run rad-coder (uses your linked local version)
116
+ rad-coder 697b80fcc6e904025f5147a0
117
+
118
+ # Or run directly without linking
119
+ node /path/to/rad-coder/bin/cli.js 697b80fcc6e904025f5147a0
120
+ ```
121
+
122
+ ### Debug Mode
123
+
124
+ Run the tool with Node.js debugger for step-through debugging:
125
+
126
+ ```bash
127
+ # From the rad-coder repository directory:
128
+
129
+ # Start with debugger (attach Chrome DevTools or VS Code)
130
+ npm run debug -- 697b80fcc6e904025f5147a0
131
+
132
+ # Start with debugger and break on first line
133
+ npm run debug-brk -- 697b80fcc6e904025f5147a0
134
+
135
+ # Or run directly with node inspect flags
136
+ node --inspect bin/cli.js 697b80fcc6e904025f5147a0
137
+ node --inspect-brk bin/cli.js 697b80fcc6e904025f5147a0
138
+ ```
139
+
140
+ **Connecting to the debugger:**
141
+
142
+ 1. **Chrome DevTools**: Open `chrome://inspect` in Chrome, click "inspect" under Remote Target
143
+ 2. **VS Code**: Use the "Attach to Node Process" debug configuration, or add this to `.vscode/launch.json`:
144
+
145
+ ```json
146
+ {
147
+ "version": "0.2.0",
148
+ "configurations": [
149
+ {
150
+ "type": "node",
151
+ "request": "launch",
152
+ "name": "Debug rad-coder",
153
+ "program": "${workspaceFolder}/bin/cli.js",
154
+ "args": ["697b80fcc6e904025f5147a0"],
155
+ "cwd": "/tmp/test-rad-coder"
156
+ }
157
+ ]
158
+ }
159
+ ```
160
+
161
+ **Available npm scripts:**
162
+
163
+ | Script | Description |
164
+ |--------|-------------|
165
+ | `npm run dev -- <creativeId>` | Run via CLI (copies templates to cwd) |
166
+ | `npm run server -- <creativeId>` | Run server directly (uses repo's templates dir for custom.js) |
167
+ | `npm run debug -- <creativeId>` | Run CLI with debugger attached |
168
+ | `npm run debug-brk -- <creativeId>` | Run CLI with debugger, break on first line |
169
+ | `npm run debug:server -- <creativeId>` | Run server directly with debugger attached |
170
+
171
+ **Difference between `dev` and `server`:**
172
+
173
+ - `npm run dev` - Runs `bin/cli.js` which copies template files to user's directory, then starts the server. Use this to test the full npx experience.
174
+ - `npm run server` - Runs `server/index.js` directly, using the `templates/` directory for `custom.js`. Use this when developing the server itself without needing to copy files.
175
+
176
+ ### Project Structure
177
+
178
+ ```
179
+ rad-coder/
180
+ ├── bin/
181
+ │ └── cli.js # CLI entry point - handles file copying and starts server
182
+ ├── server/
183
+ │ └── index.js # Express server - fetches config, serves files, hot-reload
184
+ ├── public/
185
+ │ └── test.html # Test page - loads creative with custom JS
186
+ ├── templates/
187
+ │ ├── custom.js # Template copied to user's directory
188
+ │ └── AGENTS.md # AI agent instructions copied to user's directory
189
+ └── package.json
190
+ ```
191
+
192
+ ### Key Files
193
+
194
+ | File | Purpose |
195
+ |------|---------|
196
+ | `bin/cli.js` | Entry point when user runs `npx rad-coder`. Copies template files to user's directory and starts the server. |
197
+ | `server/index.js` | Express server that fetches creative config from Studio, serves the test page, and handles hot-reload via WebSocket. |
198
+ | `public/test.html` | The test page that loads the creative and injects custom JS via the Radical config. |
199
+ | `templates/custom.js` | Template for the user's custom JS file. |
200
+ | `templates/AGENTS.md` | Documentation for AI coding assistants. |
201
+
202
+ ### Modifying Creative Rendering
203
+
204
+ The creative is rendered in `public/test.html`. Key areas:
205
+
206
+ 1. **Fetching config**: The page fetches `/api/config` which returns creative settings from Studio.
207
+
208
+ 2. **Loading custom JS**: The page fetches `/api/custom-js` which returns the user's `custom.js` content.
209
+
210
+ 3. **Radical config**: The creative is initialized with:
211
+ ```javascript
212
+ Radical.push([creativeId, {
213
+ flowline: config.flowlineId,
214
+ sizes: config.sizes,
215
+ isFluid: config.isFluid,
216
+ // ... other settings
217
+ config: {
218
+ _default: {
219
+ customjs: customJsCode // User's custom JS injected here
220
+ }
221
+ }
222
+ }]);
223
+ ```
224
+
225
+ ### Modifying Config Extraction
226
+
227
+ Creative config is fetched from Studio in `server/index.js` in the `fetchCreativeConfig()` function. This parses the preview page HTML to extract:
228
+
229
+ - `window.creativeId`
230
+ - `window.flowlines` (array of flowline objects)
231
+
232
+ The first flowline is selected by default. To change this behavior, modify the logic around line 150 in `server/index.js`.
233
+
234
+ ### Environment Variables
235
+
236
+ When run via `npx`, the CLI sets these environment variables:
237
+
238
+ | Variable | Description |
239
+ |----------|-------------|
240
+ | `RAD_CODER_USER_DIR` | User's current working directory (where `custom.js` lives) |
241
+ | `RAD_CODER_PACKAGE_DIR` | Package installation directory (where `public/` lives) |
242
+
243
+ ### Unlinking
244
+
245
+ When done testing, unlink the package:
246
+
247
+ ```bash
248
+ npm unlink -g rad-coder
249
+ ```
250
+
251
+ ## License
252
+
253
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ // Get the package root directory
7
+ const packageRoot = path.join(__dirname, '..');
8
+
9
+ // Get user's current working directory
10
+ const userDir = process.cwd();
11
+
12
+ // Files to copy to user's directory on first run
13
+ const filesToCopy = [
14
+ { template: 'custom.js', target: 'custom.js' },
15
+ { template: 'AGENTS.md', target: 'AGENTS.md' }
16
+ ];
17
+
18
+ // Copy template files if they don't exist
19
+ let filesCreated = false;
20
+ filesToCopy.forEach(({ template, target }) => {
21
+ const targetPath = path.join(userDir, target);
22
+ if (!fs.existsSync(targetPath)) {
23
+ const templatePath = path.join(packageRoot, 'templates', template);
24
+ if (fs.existsSync(templatePath)) {
25
+ fs.copyFileSync(templatePath, targetPath);
26
+ console.log(` Created ${target}`);
27
+ filesCreated = true;
28
+ }
29
+ }
30
+ });
31
+
32
+ if (filesCreated) {
33
+ console.log('');
34
+ }
35
+
36
+ // Set environment variables for the server
37
+ process.env.RAD_CODER_USER_DIR = userDir;
38
+ process.env.RAD_CODER_PACKAGE_DIR = packageRoot;
39
+
40
+ // Run the server
41
+ require('../server/index.js');
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "rad-coder",
3
+ "version": "1.0.0",
4
+ "description": "Development environment for testing ResponsiveAds creative custom JS with hot-reload",
5
+ "bin": {
6
+ "rad-coder": "./bin/cli.js"
7
+ },
8
+ "scripts": {
9
+ "dev": "node bin/cli.js",
10
+ "server": "node server/index.js"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "server/",
15
+ "public/",
16
+ "templates/"
17
+ ],
18
+ "keywords": [
19
+ "responsiveads",
20
+ "rad",
21
+ "ad",
22
+ "creative",
23
+ "testing",
24
+ "custom-js",
25
+ "hot-reload"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/ResponsiveAds/rad-coder"
30
+ },
31
+ "author": "ResponsiveAds",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "chokidar": "^3.5.3",
38
+ "cors": "^2.8.5",
39
+ "express": "^4.18.2",
40
+ "open": "^9.1.0",
41
+ "ws": "^8.14.2"
42
+ }
43
+ }
@@ -0,0 +1,297 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Ad Creative Tester</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: #1a1a2e;
17
+ min-height: 100vh;
18
+ }
19
+
20
+ .toolbar {
21
+ background: #16213e;
22
+ padding: 12px 20px;
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 20px;
26
+ border-bottom: 1px solid #0f3460;
27
+ }
28
+
29
+ .toolbar h1 {
30
+ color: #e94560;
31
+ font-size: 16px;
32
+ font-weight: 600;
33
+ }
34
+
35
+ .toolbar .status {
36
+ color: #4ecca3;
37
+ font-size: 12px;
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 6px;
41
+ }
42
+
43
+ .toolbar .status::before {
44
+ content: '';
45
+ width: 8px;
46
+ height: 8px;
47
+ background: #4ecca3;
48
+ border-radius: 50%;
49
+ animation: pulse 2s infinite;
50
+ }
51
+
52
+ @keyframes pulse {
53
+ 0%, 100% { opacity: 1; }
54
+ 50% { opacity: 0.5; }
55
+ }
56
+
57
+ .toolbar .creative-info {
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 2px;
61
+ }
62
+
63
+ .toolbar .creative-id {
64
+ color: #a0a0a0;
65
+ font-size: 12px;
66
+ font-family: monospace;
67
+ }
68
+
69
+ .toolbar .flowline-name {
70
+ color: #7ec8e3;
71
+ font-size: 11px;
72
+ }
73
+
74
+ .toolbar .reload-btn {
75
+ margin-left: auto;
76
+ background: #0f3460;
77
+ color: #fff;
78
+ border: none;
79
+ padding: 8px 16px;
80
+ border-radius: 4px;
81
+ cursor: pointer;
82
+ font-size: 12px;
83
+ }
84
+
85
+ .toolbar .reload-btn:hover {
86
+ background: #e94560;
87
+ }
88
+
89
+ .ad-container {
90
+ padding: 20px;
91
+ display: flex;
92
+ justify-content: center;
93
+ align-items: flex-start;
94
+ }
95
+
96
+ .ad-wrapper {
97
+ background: #fff;
98
+ border-radius: 8px;
99
+ overflow: hidden;
100
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
101
+ }
102
+
103
+ .inner-content {
104
+ width: 100%;
105
+ min-height: 300px;
106
+ }
107
+
108
+ /* Toast notification for reload */
109
+ .toast {
110
+ position: fixed;
111
+ bottom: 20px;
112
+ right: 20px;
113
+ background: #4ecca3;
114
+ color: #1a1a2e;
115
+ padding: 12px 20px;
116
+ border-radius: 6px;
117
+ font-size: 14px;
118
+ font-weight: 500;
119
+ transform: translateY(100px);
120
+ opacity: 0;
121
+ transition: all 0.3s ease;
122
+ }
123
+
124
+ .toast.show {
125
+ transform: translateY(0);
126
+ opacity: 1;
127
+ }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="toolbar">
132
+ <h1>Ad Creative Tester</h1>
133
+ <span class="status">Hot-reload active</span>
134
+ <div class="creative-info">
135
+ <span class="creative-id" id="creative-id">Loading...</span>
136
+ <span class="flowline-name" id="flowline-name"></span>
137
+ </div>
138
+ <button class="reload-btn" onclick="location.reload()">Reload</button>
139
+ </div>
140
+
141
+ <div class="ad-container">
142
+ <div class="ad-wrapper">
143
+ <div class="inner-content" id="ad-content">
144
+ <!-- Ad will be loaded here -->
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="toast" id="toast">Reloading...</div>
150
+
151
+ <script>
152
+ // Fetch config with retry (server may still be fetching from studio)
153
+ async function fetchConfigWithRetry(maxRetries = 10, delay = 500) {
154
+ for (let i = 0; i < maxRetries; i++) {
155
+ try {
156
+ const response = await fetch('/api/config');
157
+ if (response.status === 503) {
158
+ // Server not ready yet, retry
159
+ console.log(`Config not ready, retrying (${i + 1}/${maxRetries})...`);
160
+ await new Promise(r => setTimeout(r, delay));
161
+ continue;
162
+ }
163
+ if (!response.ok) {
164
+ throw new Error(`HTTP ${response.status}`);
165
+ }
166
+ return await response.json();
167
+ } catch (error) {
168
+ if (i === maxRetries - 1) throw error;
169
+ console.log(`Fetch failed, retrying (${i + 1}/${maxRetries})...`);
170
+ await new Promise(r => setTimeout(r, delay));
171
+ }
172
+ }
173
+ throw new Error('Max retries exceeded');
174
+ }
175
+
176
+ // Fetch config and initialize ad
177
+ async function init() {
178
+ try {
179
+ // Show loading state
180
+ document.getElementById('creative-id').textContent = 'Loading...';
181
+
182
+ const config = await fetchConfigWithRetry();
183
+
184
+ // Update UI
185
+ document.getElementById('creative-id').textContent = `ID: ${config.creativeId}`;
186
+ document.getElementById('flowline-name').textContent = `Flowline: ${config.flowlineName}`;
187
+
188
+ // Initialize Radical ad (async - fetches custom JS code)
189
+ await loadAd(config);
190
+
191
+ // Setup hot-reload WebSocket
192
+ setupHotReload();
193
+
194
+ } catch (error) {
195
+ console.error('Failed to load config:', error);
196
+ document.getElementById('ad-content').innerHTML =
197
+ '<p style="padding: 20px; color: red;">Failed to load configuration</p>';
198
+ }
199
+ }
200
+
201
+ async function loadAd(config) {
202
+ const container = document.getElementById('ad-content');
203
+ const id = config.creativeId;
204
+
205
+ // Create the ad insertion point
206
+ const ins = document.createElement('ins');
207
+ ins.id = `responsivead-${id}`;
208
+ container.innerHTML = '';
209
+ container.appendChild(ins);
210
+
211
+ // Initialize Radical
212
+ window.Radical = window.Radical || [];
213
+
214
+ // Fetch the custom JS code from server
215
+ let customJsCode = '';
216
+ try {
217
+ const response = await fetch('/api/custom-js');
218
+ const data = await response.json();
219
+ customJsCode = data.code || '';
220
+ console.log('Custom JS code loaded:', customJsCode.substring(0, 100) + '...');
221
+ } catch (error) {
222
+ console.error('Failed to load custom JS:', error);
223
+ }
224
+
225
+ // Push the ad configuration with customjs as inline code string
226
+ Radical.push([id, {
227
+ https: true,
228
+ flowline: config.flowlineId,
229
+ sizes: config.sizes,
230
+ adSource: config.adSource,
231
+ tracking: false,
232
+ isFluid: config.isFluid,
233
+ screenshot: false,
234
+ crossOrigin: true,
235
+ flSource: config.flSource,
236
+ config: {
237
+ _default: {
238
+ selectedVariant: '',
239
+ customjs: customJsCode // Inline JS code string (not URL)
240
+ }
241
+ }
242
+ }]);
243
+
244
+ // Load the Radical script if not already loaded
245
+ if (!document.querySelector('script[src*="radical.min.js"]')) {
246
+ const script = document.createElement('script');
247
+ script.src = config.radicalScript;
248
+ document.body.appendChild(script);
249
+ }
250
+
251
+ console.log('Ad loaded with customjs code injected');
252
+ }
253
+
254
+ function setupHotReload() {
255
+ const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
256
+ const ws = new WebSocket(`${wsProtocol}//${location.host}`);
257
+
258
+ ws.onopen = () => {
259
+ console.log('Hot-reload connected');
260
+ };
261
+
262
+ ws.onmessage = (event) => {
263
+ const data = JSON.parse(event.data);
264
+ if (data.type === 'reload') {
265
+ showToast('Reloading...');
266
+ // Small delay to show the toast
267
+ setTimeout(() => {
268
+ location.reload();
269
+ }, 300);
270
+ }
271
+ };
272
+
273
+ ws.onclose = () => {
274
+ console.log('Hot-reload disconnected, reconnecting...');
275
+ // Try to reconnect after a delay
276
+ setTimeout(setupHotReload, 2000);
277
+ };
278
+
279
+ ws.onerror = (error) => {
280
+ console.error('WebSocket error:', error);
281
+ };
282
+ }
283
+
284
+ function showToast(message) {
285
+ const toast = document.getElementById('toast');
286
+ toast.textContent = message;
287
+ toast.classList.add('show');
288
+ setTimeout(() => {
289
+ toast.classList.remove('show');
290
+ }, 2000);
291
+ }
292
+
293
+ // Initialize on page load
294
+ init();
295
+ </script>
296
+ </body>
297
+ </html>
@@ -0,0 +1,346 @@
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const { WebSocketServer } = require('ws');
4
+ const chokidar = require('chokidar');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const http = require('http');
8
+
9
+ // ============================================================
10
+ // Directory Configuration
11
+ // ============================================================
12
+
13
+ // When run via npx, these are set by bin/cli.js
14
+ // When run directly for development, use defaults
15
+ const userDir = process.env.RAD_CODER_USER_DIR || process.cwd();
16
+ const packageDir = process.env.RAD_CODER_PACKAGE_DIR || path.join(__dirname, '..');
17
+
18
+ // ============================================================
19
+ // CLI Argument Parsing
20
+ // ============================================================
21
+
22
+ const input = process.argv[2];
23
+
24
+ if (!input) {
25
+ console.error('\n Usage: npx rad-coder <creativeId or previewUrl>\n');
26
+ console.error(' Examples:');
27
+ console.error(' npx rad-coder 697b80fcc6e904025f5147a0');
28
+ console.error(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview\n');
29
+ process.exit(1);
30
+ }
31
+
32
+ /**
33
+ * Extract creative ID from URL or use directly
34
+ */
35
+ function extractCreativeId(input) {
36
+ // If it's a URL, extract the ID
37
+ const urlMatch = input.match(/creatives\/([a-f0-9]+)/i);
38
+ return urlMatch ? urlMatch[1] : input;
39
+ }
40
+
41
+ const creativeId = extractCreativeId(input);
42
+
43
+ // ============================================================
44
+ // Fetch Creative Config from Studio Preview Page
45
+ // ============================================================
46
+
47
+ let creativeConfig = null;
48
+
49
+ /**
50
+ * Fetch and parse creative configuration from studio preview page
51
+ */
52
+ async function fetchCreativeConfig(creativeId) {
53
+ const previewUrl = `https://studio.responsiveads.com/creatives/${creativeId}/preview`;
54
+
55
+ console.log(` Fetching creative config from studio...`);
56
+ console.log(` URL: ${previewUrl}\n`);
57
+
58
+ try {
59
+ const response = await fetch(previewUrl);
60
+ if (!response.ok) {
61
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
62
+ }
63
+
64
+ const html = await response.text();
65
+
66
+ // Extract window.creativeId
67
+ const creativeIdMatch = html.match(/window\.creativeId\s*=\s*['"]([^'"]+)['"]/);
68
+ const extractedCreativeId = creativeIdMatch ? creativeIdMatch[1] : creativeId;
69
+
70
+ // Extract flowlines - try multiple patterns
71
+ let flowlines;
72
+
73
+ // First try: Look for flowlinesString variable (cleaner JSON)
74
+ const flowlinesStringMatch = html.match(/var\s+flowlinesString\s*=\s*\('(\[[\s\S]*?\])'\)/);
75
+ if (flowlinesStringMatch) {
76
+ try {
77
+ // The string has escaped single quotes, replace them
78
+ const jsonStr = flowlinesStringMatch[1].replace(/\\'/g, "'");
79
+ flowlines = JSON.parse(jsonStr);
80
+ } catch (parseErr) {
81
+ // Continue to next method
82
+ }
83
+ }
84
+
85
+ // Second try: Look for window.flowlines = JSON.parse(flowlinesString)
86
+ // In this case, extract from the initial assignment
87
+ if (!flowlines) {
88
+ const flowlinesMatch = html.match(/window\.flowlines\s*=\s*(\[[\s\S]*?\]);[\s\n]/);
89
+ if (flowlinesMatch) {
90
+ try {
91
+ // Try to clean up escaped quotes
92
+ let jsonStr = flowlinesMatch[1];
93
+ jsonStr = jsonStr.replace(/\\'/g, "'");
94
+ flowlines = JSON.parse(jsonStr);
95
+ } catch (parseErr) {
96
+ // Continue to next method
97
+ }
98
+ }
99
+ }
100
+
101
+ // Third try: Use a more robust extraction by finding balanced brackets
102
+ if (!flowlines) {
103
+ const startMarker = 'window.flowlines = [';
104
+ const startIdx = html.indexOf(startMarker);
105
+ if (startIdx !== -1) {
106
+ let bracketCount = 0;
107
+ let inString = false;
108
+ let escapeNext = false;
109
+ let endIdx = startIdx + startMarker.length - 1;
110
+
111
+ for (let i = startIdx + startMarker.length - 1; i < html.length; i++) {
112
+ const char = html[i];
113
+
114
+ if (escapeNext) {
115
+ escapeNext = false;
116
+ continue;
117
+ }
118
+
119
+ if (char === '\\') {
120
+ escapeNext = true;
121
+ continue;
122
+ }
123
+
124
+ if (char === '"' && !inString) {
125
+ inString = true;
126
+ } else if (char === '"' && inString) {
127
+ inString = false;
128
+ }
129
+
130
+ if (!inString) {
131
+ if (char === '[') bracketCount++;
132
+ if (char === ']') bracketCount--;
133
+
134
+ if (bracketCount === 0) {
135
+ endIdx = i + 1;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+
141
+ try {
142
+ let jsonStr = html.substring(startIdx + startMarker.length - 1, endIdx);
143
+ jsonStr = jsonStr.replace(/\\'/g, "'");
144
+ flowlines = JSON.parse(jsonStr);
145
+ } catch (parseErr) {
146
+ throw new Error(`Failed to parse flowlines JSON: ${parseErr.message}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ if (!flowlines) {
152
+ throw new Error('Could not find or parse window.flowlines in preview page');
153
+ }
154
+
155
+ if (!flowlines || flowlines.length === 0) {
156
+ throw new Error('No flowlines found for this creative');
157
+ }
158
+
159
+ // Select first flowline by default
160
+ const fl = flowlines[0];
161
+
162
+ // Extract sizes from flowline.flowline.sizes or fluidLayouts
163
+ let sizes = [];
164
+ if (fl.flowline && fl.flowline.sizes) {
165
+ sizes = fl.flowline.sizes;
166
+ } else if (fl.fluidLayouts) {
167
+ sizes = fl.fluidLayouts.map(l => `${l.width}x${l.height}`);
168
+ }
169
+
170
+
171
+ return {
172
+ creativeId: extractedCreativeId,
173
+ flowlineId: fl._id || fl.id,
174
+ flowlineName: fl.name || 'Unknown',
175
+ sizes: sizes,
176
+ isFluid: fl.fullyFluid || false,
177
+ adSource: '//publish.responsiveads.com/ads/',
178
+ flSource: '//publish.responsiveads.com/flowlines/',
179
+ radicalScript: 'https://publish.responsiveads.com/libs/radical.r8.min.js',
180
+ server: {
181
+ port: 3000,
182
+ host: 'localhost'
183
+ },
184
+ // Store all flowlines for reference
185
+ allFlowlines: flowlines.map(f => ({
186
+ id: f._id || f.id,
187
+ name: f.name,
188
+ sizes: f.flowline?.sizes || [],
189
+ isFluid: f.fullyFluid
190
+ }))
191
+ };
192
+
193
+ } catch (error) {
194
+ console.error(`\n Failed to fetch creative config: ${error.message}\n`);
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ // ============================================================
200
+ // Express Server Setup
201
+ // ============================================================
202
+
203
+ const app = express();
204
+ const server = http.createServer(app);
205
+
206
+ // WebSocket server for hot-reload
207
+ const wss = new WebSocketServer({ server });
208
+
209
+ // Track connected clients
210
+ const clients = new Set();
211
+
212
+ wss.on('connection', (ws) => {
213
+ clients.add(ws);
214
+ console.log('Browser connected for hot-reload');
215
+
216
+ ws.on('close', () => {
217
+ clients.delete(ws);
218
+ console.log('Browser disconnected');
219
+ });
220
+ });
221
+
222
+ // Broadcast reload message to all connected browsers
223
+ function broadcastReload() {
224
+ const message = JSON.stringify({ type: 'reload' });
225
+ clients.forEach((client) => {
226
+ if (client.readyState === 1) { // WebSocket.OPEN
227
+ client.send(message);
228
+ }
229
+ });
230
+ }
231
+
232
+ // Enable CORS for all origins (needed for cross-origin script loading)
233
+ app.use(cors());
234
+
235
+ // Serve static files from public directory (in package)
236
+ app.use(express.static(path.join(packageDir, 'public')));
237
+
238
+ // Serve the custom JS code as a JSON response (for injection into Radical config)
239
+ // Custom JS is in user's directory
240
+ app.get('/api/custom-js', (req, res) => {
241
+ const customJsPath = path.join(userDir, 'custom.js');
242
+
243
+ // Set headers to prevent caching
244
+ res.set({
245
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
246
+ 'Pragma': 'no-cache',
247
+ 'Expires': '0'
248
+ });
249
+
250
+ if (fs.existsSync(customJsPath)) {
251
+ const code = fs.readFileSync(customJsPath, 'utf-8');
252
+ res.json({ code });
253
+ } else {
254
+ res.json({ code: '// custom.js not found - create custom.js in your current directory' });
255
+ }
256
+ });
257
+
258
+ // Serve dynamically fetched config as JSON for the test page
259
+ app.get('/api/config', (req, res) => {
260
+ if (!creativeConfig) {
261
+ res.status(503).json({ error: 'Config not yet loaded' });
262
+ return;
263
+ }
264
+ res.json(creativeConfig);
265
+ });
266
+
267
+ // Watch for changes in custom.js (in user's directory)
268
+ const customJsWatchPath = path.join(userDir, 'custom.js');
269
+ const watcher = chokidar.watch(customJsWatchPath, {
270
+ persistent: true,
271
+ ignoreInitial: true
272
+ });
273
+
274
+ watcher.on('change', (filePath) => {
275
+ console.log(`\n File changed: ${path.basename(filePath)}`);
276
+ console.log(' Reloading browsers...\n');
277
+ broadcastReload();
278
+ });
279
+
280
+ watcher.on('error', (error) => {
281
+ console.error('Watcher error:', error);
282
+ });
283
+
284
+ // ============================================================
285
+ // Start Server
286
+ // ============================================================
287
+
288
+ async function start() {
289
+ console.log('\n========================================');
290
+ console.log(' RAD Coder - ResponsiveAds Creative Tester');
291
+ console.log('========================================\n');
292
+
293
+ // Fetch creative config from studio
294
+ creativeConfig = await fetchCreativeConfig(creativeId);
295
+
296
+ console.log(' Creative Config:');
297
+ console.log(` - Creative ID: ${creativeConfig.creativeId}`);
298
+ console.log(` - Flowline: ${creativeConfig.flowlineName}`);
299
+ console.log(` - Flowline ID: ${creativeConfig.flowlineId}`);
300
+ console.log(` - Sizes: ${creativeConfig.sizes.join(', ')}`);
301
+ console.log(` - Is Fluid: ${creativeConfig.isFluid}`);
302
+
303
+ if (creativeConfig.allFlowlines.length > 1) {
304
+ console.log(`\n Available Flowlines (${creativeConfig.allFlowlines.length}):`);
305
+ creativeConfig.allFlowlines.forEach((fl, i) => {
306
+ const marker = i === 0 ? ' (selected)' : '';
307
+ console.log(` ${i + 1}. ${fl.name}${marker}`);
308
+ });
309
+ }
310
+
311
+ const { port, host } = creativeConfig.server;
312
+
313
+ server.listen(port, host, async () => {
314
+ console.log(`\n Server running at: http://${host}:${port}`);
315
+ console.log(` Test page: http://${host}:${port}/test.html`);
316
+ console.log(`\n Working directory: ${userDir}`);
317
+ console.log(' Edit custom.js and save to hot-reload\n');
318
+ console.log(' Press Ctrl+C to stop\n');
319
+
320
+ // Small delay to ensure server is fully ready before opening browser
321
+ await new Promise(resolve => setTimeout(resolve, 500));
322
+
323
+ // Auto-open browser
324
+ try {
325
+ const open = (await import('open')).default;
326
+ await open(`http://${host}:${port}/test.html`);
327
+ console.log(' Browser opened automatically\n');
328
+ } catch (err) {
329
+ console.log(' Could not auto-open browser:', err.message);
330
+ console.log(` Please open http://${host}:${port}/test.html manually\n`);
331
+ }
332
+ });
333
+ }
334
+
335
+ start();
336
+
337
+ // Graceful shutdown
338
+ process.on('SIGINT', () => {
339
+ console.log('\n Shutting down...');
340
+ watcher.close();
341
+ wss.close();
342
+ server.close(() => {
343
+ console.log(' Server stopped\n');
344
+ process.exit(0);
345
+ });
346
+ });
@@ -0,0 +1,372 @@
1
+ # RAD Agents Guide
2
+
3
+ You are an agent writing only JS for a responsive creative. The creative was build using responsiveAds editor. ResponsiveSds is online editor which helps designers build creatives, but they have an option to add some customJS. You are responsible for writing this customJS.
4
+
5
+ To do this you can only edit the `custom.js` file in this directory. When you edit and save this file the creative will be automatically loaded on the test page: http://localhost:3000/test.html. The code you wrote in `custom.js` will be applied to the creative.
6
+
7
+ Use modern JS standards and code practices.
8
+
9
+ We can use custom in situations when we want to add extra interactivity to our responsive creative. Use the Radical API to access elements added from the editor, update their behavior, and add custom functionalities to your ad.
10
+
11
+ You can use all available JavaScript functions to manipulate element position and size. This is usually done by changing the DOM element CSS `style` property. All elements in creative are positioned absolutely with inline styles set by our rendering script.
12
+
13
+ ---
14
+
15
+ # Radical API Reference for ResponsiveAds
16
+
17
+ This document outlines the specific implementation patterns and lifecycle hooks for the Radical API. Use this guide to programmatically control elements, manage dynamic data (DCO), and handle cross-window interactions in ResponsiveAds creatives.
18
+
19
+ ## 1. Initializing the Controller
20
+
21
+ Every script must first reference the ad instance and its container.
22
+
23
+ ```javascript
24
+ var rad = Radical.getAdByWindow(window);
25
+ var container = rad.getContainer();
26
+ ```
27
+
28
+ ## 2. Core Lifecycle Hooks
29
+
30
+ These hooks are critical for ensuring code executes in the correct order.
31
+
32
+ ### rad.onBeforeRender(arg)
33
+
34
+ The Data Injection Layer. Runs after the config is loaded but before elements are created in the DOM.
35
+
36
+ Use for: Swapping text, images, and click-through URLs (DCO).
37
+ Key Property: `arg.elementDefs` contains the blueprint for every element.
38
+
39
+ ```javascript
40
+ rad.onBeforeRender(function(arg) {
41
+ // Dynamically update a textbox
42
+ arg.elementDefs.headlineID.textboxWidget.text = "Custom Value";
43
+
44
+ // Update an image source
45
+ arg.elementDefs.heroImageID.image.src = "https://example.com/image.jpg";
46
+
47
+ // Update a click-through URL
48
+ arg.elementDefs.ctaID.onClick[0].data.url = "https://landingpage.com";
49
+ });
50
+ ```
51
+
52
+ ### rad.onLoad(callback)
53
+
54
+ The Event Registration Layer. Runs once the ad flow is loaded.
55
+
56
+ Use for: Adding DOM event listeners (click, touchstart, window events).
57
+ Benefit: Prevents duplicate listeners during ad resize/re-render.
58
+
59
+ ```javascript
60
+ rad.onLoad(function() {
61
+ window.addEventListener('message', function(e) {
62
+ console.log("External Data:", e.data);
63
+ });
64
+ });
65
+ ```
66
+
67
+ ### rad.onRender(callback)
68
+
69
+ The Post-Layout Layer. Runs after elements are physically positioned.
70
+
71
+ Use for: Initializing carousels, GSAP animations, or grabbing domNode references.
72
+ Note: Fires multiple times on browser resize.
73
+
74
+ ```javascript
75
+ rad.onRender(function() {
76
+ var element = rad.getElementById('e1');
77
+ if (element) {
78
+ element.domNode.style.borderRadius = "10px";
79
+ }
80
+ });
81
+ ```
82
+
83
+ ### container.onVisibilityChange(callback)
84
+
85
+ The Performance & Policy Layer.
86
+
87
+ Use for: Pausing video or audio when the ad scrolls out of view.
88
+
89
+ ```javascript
90
+ container.onVisibilityChange(function(isVisible) {
91
+ if (!isVisible) {
92
+ // Stop all active media
93
+ pauseAllVideo();
94
+ }
95
+ });
96
+ ```
97
+
98
+ ## 3. Component Interaction
99
+
100
+ ### Carousels
101
+
102
+ Access carousel-specific methods via `rad.getElementById('ID')`.
103
+
104
+ Method | Description
105
+ --- | ---
106
+ animateToSlide(index) | Smoothly transitions to a specific slide.
107
+ getVisibleSlideIndex() | Returns current index (0-based).
108
+ slideChangeCallback(fn) | Triggers a function when the slide changes.
109
+ getSlides() | Returns an array of all slide objects.
110
+
111
+ ### Textbox
112
+
113
+ To update text post-render, use the helper method to ensure layout recalculation:
114
+
115
+ ```javascript
116
+ var txt = rad.getElementById('text1');
117
+ txt.updateContent("New Text");
118
+ // rad.updateElementStyles handles the necessary updateShrink() internally
119
+ rad.updateElementStyles(txt.domNode, { opacity: 1 });
120
+ ```
121
+
122
+ ## 4. Advanced Patterns
123
+
124
+ ### Dynamic Content Optimization (DCO)
125
+
126
+ Retrieve parameters passed via the Ad Tag URL or Query String:
127
+
128
+ ```javascript
129
+ var options = rad.getAdTagOptions();
130
+ var dealerId = options.dealer_id || "default_001";
131
+ ```
132
+
133
+ ### Analytics Tracking
134
+
135
+ Trigger custom tracking events for reporting:
136
+
137
+ ```javascript
138
+ rad.sendAnalyticsEvent({
139
+ e: 'interact.scroll',
140
+ v: 50, // Value (e.g., 50% scrolled)
141
+ elId: 'scrolling_container'
142
+ });
143
+ ```
144
+
145
+ ### Cross-Origin Detection
146
+
147
+ Check if the ad can communicate with the top-level window (parent page):
148
+
149
+ ```javascript
150
+ if (!container.isCrossOrigin()) {
151
+ var parentDoc = container.getAdWindow().top.document;
152
+ // Safe to interact with parent window
153
+ }
154
+ ```
155
+
156
+ ## 5. Summary Checklist for AI Agents
157
+
158
+ - DCO logic? Use `onBeforeRender` to modify `arg.elementDefs`.
159
+ - Visual tweaks? Use `onRender` with `rad.getElementById('ID').domNode`.
160
+ - Click listeners? Use `onLoad` to prevent duplicates.
161
+ - Hiding elements? Use `rad.updateElementStyles(el, { visible: false })`.
162
+
163
+ ---
164
+
165
+ # Best Practices
166
+
167
+ When it comes to best practices for using the Radical API, you should keep a few things in mind. Here are some best practices to follow.
168
+
169
+ ## Use updateElementStyles
170
+
171
+ When it comes to updating the visibility of an element, you should use the `rad.updateElementStyles` method. This method is more efficient and easier to use than updating the style attribute directly. The problem with updating the style attribute directly is that for certain elements (like Textbox), you also need to call the `updateShrink()` method to make the Textbox visible. `rad.updateElementStyles` handles this for you.
172
+
173
+ ---
174
+
175
+ # Components
176
+
177
+ In this guide, we will take a look at different components a creative can have and how Radical API can help you manage them.
178
+
179
+ You can add Custom JavaScript to your creatives if you want to access and control the elements added from the editor.
180
+
181
+ ## Carousel
182
+
183
+ Carousels are a great way to display multiple images or videos in a single ad unit. You can use the Radical API to create and manage carousels in your creatives.
184
+
185
+ Name | Description
186
+ --- | ---
187
+ animateToSlide(index: number): void | Use this method to animate the carousel to a specific slide. The method takes a single argument, which is the index of the slide you want to animate to.
188
+ animateToSlideIndex(index: number): void | Use this method to animate the carousel to a specific slide. The method takes a single argument, which is the index of the slide you want to animate to.
189
+ getCarouselPlayUUID(): string | Use this method to get the UUID of the carousel play event.
190
+ getElementDataSource(): string | A Carousel component can have a data source. Users can add this data source from the editor. This method returns the data source of the carousel.
191
+ getSlides(): Carousel Slide[] | Use this method to get all the slides in the carousel.
192
+ getElementDataSource(): ElementDataSource | Use this method to get the data source of the carousel.
193
+ getVisibleSlideIndex(): number | Use this method to get the index of the currently visible slide.
194
+ moveToSlide(index: number): void; | Use this method to move the carousel to a specific slide. The method takes a single argument, which is the index of the slide you want to move to. This method does not animate the transition.
195
+ nextSlide(): void; | Use this method to move the carousel to the next slide. It will use the first slide if the current slide is the last one.
196
+ onRender(callback: (arg: any) => void): void; | Use this method to register a callback that will be called when the carousel is rendered.
197
+ pause(): void; | Use this method to pause the carousel.
198
+ previousSlide(): void | Use this method to move the carousel to the previous slide. It will use the last slide if the current slide is the first one.
199
+ removeSlide(index: number): void | Use this method to remove a slide from the carousel. The method takes a single argument, which is the index of the slide you want to remove.
200
+ rendered(): boolean | Use this method to check if the carousel is rendered.
201
+ resume(): void | Use this method to resume the carousel.
202
+ setPausedCarouselAfterSlideChange(paused: boolean): void | Use this method to set whether the carousel should be paused after a slide change.
203
+ setVisibleSlide(index: number): void; | Use this method to set the visible slide of the carousel. The method takes a single argument, which is the index of the slide you want to set as visible.
204
+ setVisibleSlideIndex(index: number): void; | Use this method to set the visible slide of the carousel. The method takes a single argument, which is the index of the slide you want to set as visible.
205
+ slideChangeCallback(callback: (arg: any) => void): void; | Use this method to register a callback that will be called when the slide changes.
206
+ updated(): boolean; | Use this method to check if the carousel is updated.
207
+
208
+ ## TextBox
209
+
210
+ Textboxes are used to display text in your creatives. They are added from the editor but you can edit them using the API.
211
+
212
+ Name | Description
213
+ --- | ---
214
+ updateShrink(): string | Use this method to update the text element. Any changes to the text element will be reflected in the DOM.
215
+ updateContent(text: string): void | Use this method to update the content of the textbox. The method takes a single argument, which is the content you want to update.
216
+
217
+ ---
218
+
219
+ # Radical API
220
+
221
+ Radical API is a set of methods and properties that you can use to manage your creatives. You can use the API to access and control the elements in your creatives.
222
+
223
+ ## RAD object
224
+
225
+ Our rendering library called Radical exposes some helper functions you can you while working on your creative you can access them by initializing the `rad` object by calling
226
+
227
+ ```javascript
228
+ var rad = Radical.getAdByWindow(window);
229
+ ```
230
+
231
+ The majority of functions available on the object are used internally by Radical, but a couple of them will come in handy when working with the creative.
232
+
233
+ ```javascript
234
+ rad.onRender(onAdRender);
235
+
236
+ function onAdRender() {
237
+ console.log('rendered');
238
+ }
239
+ ```
240
+
241
+ This is called after all the elements in the creation are rendered and positioned by the Radical. Use the onLoad event to add DOM event listeners (click, mouse enter, mouse leave, ...) because onRender is called multiple times when you resize the ad, and the listeners would be added multiple times.
242
+
243
+ ```javascript
244
+ rad.onLoad(onAdLoaded);
245
+ ```
246
+
247
+ This is called after the flowline for the creative was loaded but before the render. This is a good place to add any DOM event listeners, as this is called only once.
248
+
249
+ ```javascript
250
+ rad.onBeforeRender(onBeforeRender);
251
+ ```
252
+
253
+ This event returns elements object with all the properties it they will render. You have an option to change these properties before the rendere here. Just update the object. The object is composed of keys that are element ID-s and values that are corresponding properties.
254
+
255
+ ```javascript
256
+ var sizes = rad.getMergedContent().sizes;
257
+ ```
258
+
259
+ An array of all layout sizes we can render from. In the case of Fully-fluid format, this array is empty.
260
+
261
+ ```javascript
262
+ var layoutSize = rad.getRenderedSize();
263
+ ```
264
+
265
+ Size object of the layout currently rendering.
266
+
267
+ ```javascript
268
+ var element = rad.getElementById('e2');
269
+ ```
270
+
271
+ You can use `getElementById` to get an element reference, make sure you are calling this inside `onRender` callback. Check boilerplate code, for example. `getElementById` returns an object with `domNode` property pointing to actual DOM element and some other information about the element. Some elements, like the video, also contain useful functions to control the element.
272
+
273
+ ---
274
+
275
+ # All Functions
276
+
277
+ You can follow the Custom JavaScript Guide to learn how to use the Radical API in your creatives.
278
+
279
+ Name | Description
280
+ --- | ---
281
+ addGlobalEventListener(event: string, callback: (arg: any) => void) | Adds a global event listener to the creative. The event listener will be triggered when the specified event occurs in the creative.
282
+ animationTime(time: number) | Sets the animation time of the creative to the specified time in milliseconds.
283
+ callTrackerURL(url: string) | Call the specified tracker URL.
284
+ clearElements(elements: HTMLElement[]) | Clears the specified elements from the creative.
285
+ condeHideAd() | Hides the creative.
286
+ createLightboxContainer() | Creates a lightbox container.
287
+ creativeTimeSpent() | Returns the time spent on the creative in milliseconds.
288
+ domEventHandler(event: string, callback: (arg: any) => void) | Adds a DOM event listener to the creative. The event listener will be triggered when the specified event occurs in the creative.
289
+ forceRender() | Forces the creative to render.
290
+ generateUUID() | Generates a UUID.
291
+ getActiveConfig() | Returns the active config of the creative.
292
+ getAdContent() | Returns the ad content of the creative.
293
+ getAdRenderer() | Returns the ad renderer of the creative.
294
+ getAdTagOptions() | Returns the ad tag options of the creative.
295
+ getAdVersion() | Returns the ad version of the creative.
296
+ getAnimationTimeline() | Returns the animation timeline of the creative.
297
+ getAssets() | Returns the assets of the creative.
298
+ getConfig() | Returns the config of the creative.
299
+ getContainer() | Returns the container of the creative.
300
+ getCookie(name: string) | Returns the value of the specified cookie.
301
+ getCurrentMediaquery() | Returns the current media query of the creative.
302
+ getDataLayer() | Returns the data layer of the creative.
303
+ getDeviceInfo() | Returns the device info of the creative.
304
+ getElementById(id: string) | Returns the element with the specified ID.
305
+ getElementChildren(element: HTMLElement) | Returns the children of the specified element.
306
+ getElementStyles(element: HTMLElement) | Returns the styles of the specified element.
307
+ getExtension(extensionType: string) | Returns the extension of the specified type.
308
+ getFormatLayouts() | Returns the format layouts of the creative.
309
+ getLbNro() | Returns the lightbox number of the creative.
310
+ getMergedContent() | Returns the merged content of the creative.
311
+ getRenderedSize() | Returns the rendered size of the creative.
312
+ getSizeFilter() | Returns the size filter of the creative.
313
+ getState() | Returns the state of the creative.
314
+ getUUID() | Returns the UUID of the creative.
315
+ getUserInfo() | Returns the user info of the creative.
316
+ getVisibleElementChildren(element: HTMLElement) | Returns the visible children of the specified element.
317
+ hideCreative() | Hides the creative.
318
+ isAdContentAvailable() | Returns true if the ad content is available, otherwise false.
319
+ isOpenStateAdObj() | Returns true if the ad object is in open state, otherwise false.
320
+ onAdContentAvailable(callback: (arg: any) => void) | Adds a callback to be triggered when the ad content is available.
321
+ onAdHover(callback: (arg: any) => void) | Adds a callback to be triggered when the ad is hovered.
322
+ onAnimationProgress(callback: (arg: any) => void) | Adds a callback to be triggered when the animation progresses.
323
+ onBeforeRender(callback: (arg: any) => void) | Adds a callback to be triggered before the creative is rendered.
324
+ onCarouselFirstSlide(callback: (arg: any) => void) | Adds a callback to be triggered when the carousel is on the first slide.
325
+ onCarouselLastSlide(callback: (arg: any) => void) | Adds a callback to be triggered when the carousel is on the last slide.
326
+ onCarouselMiddleSlide(callback: (arg: any) => void) | Adds a callback to be triggered when the carousel is on the middle slide.
327
+ onClick(callback: (arg: any) => void) | Adds a callback to be triggered when the creative is clicked.
328
+ onCountdownFinished(callback: (arg: any) => void) | Adds a callback to be triggered when the countdown finishes.
329
+ onElementHover(callback: (arg: any) => void) | Adds a callback to be triggered when an element is hovered.
330
+ onElementMouseOut(callback: (arg: any) => void) | Adds a callback to be triggered when the mouse leaves an element.
331
+ onLoad(callback: (arg: any) => void) | Adds a callback to be triggered when the creative is loaded.
332
+ onMediaEnded(callback: (arg: any) => void) | Adds a callback to be triggered when the media ends.
333
+ onMediaPause(callback: (arg: any) => void) | Adds a callback to be triggered when the media is paused.
334
+ onMediaPlaying(callback: (arg: any) => void) | Adds a callback to be triggered when the media is playing.
335
+ onPreviewVideoEnd(callback: (arg: any) => void) | Adds a callback to be triggered when the preview video ends.
336
+ onPreviewVideoStart(callback: (arg: any) => void) | Adds a callback to be triggered when the preview video starts.
337
+ onRender(callback: (arg: any) => void) | Adds a callback to be triggered when the creative is rendered.
338
+ onVideoEnd(callback: (arg: any) => void) | Adds a callback to be triggered when the video ends.
339
+ onVideoMuted(callback: (arg: any) => void) | Adds a callback to be triggered when the video is muted.
340
+ onVideoPause(callback: (arg: any) => void) | Adds a callback to be triggered when the video is paused.
341
+ onVideoPlay(callback: (arg: any) => void) | Adds a callback to be triggered when the video is played.
342
+ onVideoTimeUpdate(callback: (arg: any) => void) | Adds a callback to be triggered when the video time is updated.
343
+ onVideoUnMuted(callback: (arg: any) => void) | Adds a callback to be triggered when the video is unmuted.
344
+ onVideoUnableToAutoplay(callback: (arg: any) => void) | Adds a callback to be triggered when the video is unable to autoplay.
345
+ pauseAnimation() | Pauses the animation of the creative.
346
+ playAnimation() | Plays the animation of the creative.
347
+ previewPageForceRender() | Forces the creative to render the preview page.
348
+ refreshInteractionDisable() | Disables the refresh interaction of the creative.
349
+ removeGlobalEventListener(event: string, callback: (arg: any) => void) | Removes the specified global event listener from the creative.
350
+ renderElement(element: HTMLElement) | Renders the specified element in the creative.
351
+ renderElementWithStyles(element: HTMLElement, styles: any) | Renders the specified element in the creative with the specified styles.
352
+ restartAnimation() | Restarts the animation of the creative.
353
+ seekAnimation(time: number) | Seeks the animation of the creative to the specified time in milliseconds.
354
+ sendAdformAnalyticsEvent(event: string, data: any) | Sends an Adform analytics event with the specified event and data.
355
+ sendAnalyticsEvent(event: string) | Sends an analytics event with the specified event.
356
+ setConfig(config: any) | Sets the config of the creative to the specified config.
357
+ setCookie(name: string, value: string, options: any) | Sets the specified cookie with the specified value and options.
358
+ setCustomTracker(tracker: any) | Sets the custom tracker of the creative to the specified tracker.
359
+ setLbNro(nro: number) | Sets the lightbox number of the creative to the specified number.
360
+ setProgressAnimation(progress: number) | Sets the progress of the animation of the creative to the specified progress.
361
+ setResponsiveMode(mode: string) | Sets the responsive mode of the creative to the specified mode.
362
+ setSizeFilter(filter: any) | Sets the size filter of the creative to the specified filter.
363
+ setState(state: any) | Sets the state of the creative to the specified state.
364
+ setVideoInViewPercetage(percentage: number) | Sets the percentage of the video in view to the specified percentage.
365
+ start() | Starts the creative.
366
+ startAnimation() | Starts the animation of the creative.
367
+ stopAnimation() | Stops the animation of the creative.
368
+ updateAgTagOptions(options: any) | Updates the ad tag options of the creative to the specified options.
369
+ updateAnimationTimeline(timeline: any, time: number) | Updates the animation timeline of the creative to the specified timeline and time.
370
+ updateCustomCSS(css: string) | Updates the custom CSS of the creative to the specified CSS.
371
+ updateCustomJs(js: string) | Updates the custom JavaScript of the creative to the specified JavaScript.
372
+ updateElementStyles(element: HTMLElement, styles: any) | Updates the styles of the specified element to the specified styles.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * [CustomJS] Ad Creative
3
+ * Animates the QR code image in from the right side after the creative loads.
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ var rad = Radical.getAdByWindow(window);
10
+ var animated = false;
11
+
12
+ rad.onRender(function () {
13
+ var qrElement = rad.getElementById('f5');
14
+ if (!qrElement || !qrElement.domNode) return;
15
+
16
+ var node = qrElement.domNode;
17
+
18
+ if (!animated) {
19
+ animated = true;
20
+
21
+ // Read the original transform set by Radical (e.g. "translateX(-50%)")
22
+ var originalTransform = getComputedStyle(node).transform || node.style.transform;
23
+
24
+ // Start off-screen to the right
25
+ node.style.transition = 'none';
26
+ node.style.transform = 'translateX(100%)';
27
+ node.style.opacity = '0';
28
+
29
+ // Force reflow so the start position takes effect
30
+ void node.offsetWidth;
31
+
32
+ // Animate to original position
33
+ node.style.transition = 'transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.8s ease';
34
+ setTimeout(function () {
35
+ node.style.transform = originalTransform;
36
+ node.style.opacity = '1';
37
+ }, 300);
38
+ }
39
+ });
40
+ })();