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 +253 -0
- package/bin/cli.js +41 -0
- package/package.json +43 -0
- package/public/test.html +297 -0
- package/server/index.js +346 -0
- package/templates/AGENTS.md +372 -0
- package/templates/custom.js +40 -0
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
|
+
}
|
package/public/test.html
ADDED
|
@@ -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>
|
package/server/index.js
ADDED
|
@@ -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
|
+
})();
|