@zenithbuild/core 0.1.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/.eslintignore +15 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
- package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/discord-changelog.yml +141 -0
- package/.github/workflows/discord-notify.yml +242 -0
- package/.github/workflows/discord-version.yml +195 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/.zen.d.ts +15 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/app/components/Button.zen +46 -0
- package/app/components/Link.zen +11 -0
- package/app/favicon.ico +0 -0
- package/app/layouts/Main.zen +59 -0
- package/app/pages/about.zen +23 -0
- package/app/pages/blog/[id].zen +53 -0
- package/app/pages/blog/index.zen +32 -0
- package/app/pages/dynamic-dx.zen +712 -0
- package/app/pages/dynamic-primitives.zen +453 -0
- package/app/pages/index.zen +154 -0
- package/app/pages/navigation-demo.zen +229 -0
- package/app/pages/posts/[...slug].zen +61 -0
- package/app/pages/primitives-demo.zen +273 -0
- package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
- package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
- package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
- package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
- package/assets/logos/README.md +54 -0
- package/assets/logos/zen.icns +0 -0
- package/bun.lock +39 -0
- package/compiler/README.md +380 -0
- package/compiler/errors/compilerError.ts +24 -0
- package/compiler/finalize/finalizeOutput.ts +163 -0
- package/compiler/finalize/generateFinalBundle.ts +82 -0
- package/compiler/index.ts +44 -0
- package/compiler/ir/types.ts +83 -0
- package/compiler/legacy/binding.ts +254 -0
- package/compiler/legacy/bindings.ts +338 -0
- package/compiler/legacy/component-process.ts +1208 -0
- package/compiler/legacy/component.ts +301 -0
- package/compiler/legacy/event.ts +50 -0
- package/compiler/legacy/expression.ts +1149 -0
- package/compiler/legacy/mutation.ts +280 -0
- package/compiler/legacy/parse.ts +299 -0
- package/compiler/legacy/split.ts +608 -0
- package/compiler/legacy/types.ts +32 -0
- package/compiler/output/types.ts +34 -0
- package/compiler/parse/detectMapExpressions.ts +102 -0
- package/compiler/parse/parseScript.ts +22 -0
- package/compiler/parse/parseTemplate.ts +425 -0
- package/compiler/parse/parseZenFile.ts +66 -0
- package/compiler/parse/trackLoopContext.ts +82 -0
- package/compiler/runtime/dataExposure.ts +291 -0
- package/compiler/runtime/generateDOM.ts +144 -0
- package/compiler/runtime/generateHydrationBundle.ts +383 -0
- package/compiler/runtime/hydration.ts +309 -0
- package/compiler/runtime/navigation.ts +432 -0
- package/compiler/runtime/thinRuntime.ts +160 -0
- package/compiler/runtime/transformIR.ts +256 -0
- package/compiler/runtime/wrapExpression.ts +84 -0
- package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
- package/compiler/spa-build.ts +1000 -0
- package/compiler/test/validate-test.ts +104 -0
- package/compiler/transform/generateBindings.ts +47 -0
- package/compiler/transform/generateHTML.ts +28 -0
- package/compiler/transform/transformNode.ts +126 -0
- package/compiler/transform/transformTemplate.ts +38 -0
- package/compiler/validate/validateExpressions.ts +168 -0
- package/core/index.ts +135 -0
- package/core/lifecycle/index.ts +49 -0
- package/core/lifecycle/zen-mount.ts +182 -0
- package/core/lifecycle/zen-unmount.ts +88 -0
- package/core/reactivity/index.ts +54 -0
- package/core/reactivity/tracking.ts +167 -0
- package/core/reactivity/zen-batch.ts +57 -0
- package/core/reactivity/zen-effect.ts +139 -0
- package/core/reactivity/zen-memo.ts +146 -0
- package/core/reactivity/zen-ref.ts +52 -0
- package/core/reactivity/zen-signal.ts +121 -0
- package/core/reactivity/zen-state.ts +180 -0
- package/core/reactivity/zen-untrack.ts +44 -0
- package/docs/COMMENTS.md +111 -0
- package/docs/COMMITS.md +36 -0
- package/docs/CONTRIBUTING.md +116 -0
- package/docs/STYLEGUIDE.md +62 -0
- package/package.json +44 -0
- package/router/index.ts +76 -0
- package/router/manifest.ts +314 -0
- package/router/navigation/ZenLink.zen +231 -0
- package/router/navigation/index.ts +78 -0
- package/router/navigation/zen-link.ts +584 -0
- package/router/runtime.ts +458 -0
- package/router/types.ts +168 -0
- package/runtime/build.ts +17 -0
- package/runtime/serve.ts +93 -0
- package/scripts/webhook-proxy.ts +213 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// compiler/component.ts
|
|
2
|
+
// Phase 3: Component discovery, parsing, and metadata extraction
|
|
3
|
+
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { parseZen } from "./parse";
|
|
7
|
+
import { extractStateDeclarations } from "./parse";
|
|
8
|
+
import * as parse5 from "parse5";
|
|
9
|
+
|
|
10
|
+
export interface ComponentMetadata {
|
|
11
|
+
name: string; // PascalCase component name (e.g., "Button", "UIButton")
|
|
12
|
+
filePath: string;
|
|
13
|
+
isLayout: boolean; // true for layouts, false for components
|
|
14
|
+
props: Map<string, string>; // prop name -> default value expression (empty string = no default)
|
|
15
|
+
stateDeclarations: Map<string, string>; // state name -> initial value
|
|
16
|
+
hasSlots: boolean; // true if component uses <Slot /> or <Slot name="..."/>
|
|
17
|
+
slotNames: Set<string>; // set of slot names used (includes "default" for default slot)
|
|
18
|
+
html: string; // component HTML (with slots)
|
|
19
|
+
scripts: string[]; // component scripts
|
|
20
|
+
styles: string[]; // component styles
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert filename to PascalCase component name
|
|
25
|
+
* Examples:
|
|
26
|
+
* components/Button.zen -> Button
|
|
27
|
+
* components/ui/Button.zen -> UIButton
|
|
28
|
+
* layouts/Main.zen -> Main
|
|
29
|
+
* MyComponent.zen -> MyComponent
|
|
30
|
+
*/
|
|
31
|
+
function filenameToComponentName(filePath: string, baseDir: string): string {
|
|
32
|
+
// Get relative path from base directory
|
|
33
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
34
|
+
// Remove .zen extension
|
|
35
|
+
const withoutExt = relativePath.replace(/\.zen$/, "");
|
|
36
|
+
// Split by path separator
|
|
37
|
+
const parts = withoutExt.split(path.sep);
|
|
38
|
+
|
|
39
|
+
// Strip "components" or "layouts" prefix (first part if it's one of these)
|
|
40
|
+
const filteredParts = parts.filter((part, index) => {
|
|
41
|
+
// Keep all parts except the first if it's "components" or "layouts"
|
|
42
|
+
return index > 0 || (part !== "components" && part !== "layouts");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Convert each part to PascalCase
|
|
46
|
+
const pascalParts = filteredParts.map(part => {
|
|
47
|
+
// Handle kebab-case, snake_case, or camelCase
|
|
48
|
+
return part
|
|
49
|
+
.split(/[-_]/)
|
|
50
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
51
|
+
.join("");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return pascalParts.join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract props from script content
|
|
59
|
+
* Looks for:
|
|
60
|
+
* 1. TypeScript-style: type Props = { propName?: type; ... }
|
|
61
|
+
* 2. props.propName = value;
|
|
62
|
+
* 3. props = { propName: value, ... }
|
|
63
|
+
* Returns Map of prop name -> default value expression (empty string = no default, "?" = optional)
|
|
64
|
+
*/
|
|
65
|
+
function extractProps(scriptContent: string): Map<string, string> {
|
|
66
|
+
const props = new Map<string, string>();
|
|
67
|
+
|
|
68
|
+
// Pattern 1: TypeScript-style type Props = { propName?: type; ... }
|
|
69
|
+
// Match: type Props = { ... } (multiline, handles nested braces and function types)
|
|
70
|
+
// This regex handles: type Props = { prop?: type; } with nested braces in function types
|
|
71
|
+
const typePropsRegex = /type\s+Props\s*=\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s;
|
|
72
|
+
const typeMatch = typePropsRegex.exec(scriptContent);
|
|
73
|
+
if (typeMatch && typeMatch[1]) {
|
|
74
|
+
const propsBody = typeMatch[1];
|
|
75
|
+
// Parse prop definitions: propName?: type; or propName: type;
|
|
76
|
+
// Match prop name, optional ?, type (can include function types like (x: number) => void, generics, etc.)
|
|
77
|
+
// Use a more robust regex that handles function types with nested parentheses
|
|
78
|
+
const propDefRegex = /(\w+)(\?)?\s*:\s*((?:\([^)]*\)\s*=>\s*[^;]+|[^;]+?))(?:\s*;|\s*$)/g;
|
|
79
|
+
let propMatch: RegExpExecArray | null;
|
|
80
|
+
while ((propMatch = propDefRegex.exec(propsBody)) !== null) {
|
|
81
|
+
const propName = propMatch[1];
|
|
82
|
+
if (!propName) continue; // Skip if prop name is missing
|
|
83
|
+
const isOptional = propMatch[2] === '?';
|
|
84
|
+
// For type definitions, we don't store the type, just mark as optional if needed
|
|
85
|
+
// Optional props get "?" as default value indicator
|
|
86
|
+
// This allows the component to know which props are optional
|
|
87
|
+
props.set(propName, isOptional ? "?" : "");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pattern 2: props.propName = value;
|
|
92
|
+
const dotPropRegex = /props\.(\w+)\s*=\s*([^;]+?)(?:\s*;|\s*$)/gm;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = dotPropRegex.exec(scriptContent)) !== null) {
|
|
95
|
+
const propName = match[1];
|
|
96
|
+
const defaultValue = match[2]?.trim() || "";
|
|
97
|
+
if (propName) {
|
|
98
|
+
// Only override if not already set from type Props (preserve type info)
|
|
99
|
+
if (!props.has(propName)) {
|
|
100
|
+
props.set(propName, defaultValue);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Pattern 3: props = { propName: value, ... }
|
|
106
|
+
const objPropRegex = /props\s*=\s*\{([^}]+)\}/s;
|
|
107
|
+
const objMatch = objPropRegex.exec(scriptContent);
|
|
108
|
+
if (objMatch) {
|
|
109
|
+
const propsObj = objMatch[1];
|
|
110
|
+
// Parse key: value pairs
|
|
111
|
+
const propPairs = propsObj?.match(/(\w+)\s*:\s*([^,}]+)/g);
|
|
112
|
+
if (propPairs) {
|
|
113
|
+
for (const pair of propPairs) {
|
|
114
|
+
const propMatch = pair.match(/(\w+)\s*:\s*(.+)/);
|
|
115
|
+
if (propMatch) {
|
|
116
|
+
const propName = propMatch[1];
|
|
117
|
+
const defaultValue = propMatch[2]?.trim() || "";
|
|
118
|
+
// Only override if not already set from type Props
|
|
119
|
+
if (propName && typeof propName === 'string' && !props.has(propName)) {
|
|
120
|
+
props.set(propName.trim(), defaultValue.trim());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return props;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract slot usage from HTML
|
|
132
|
+
* Returns: { hasSlots: boolean, slotNames: Set<string> }
|
|
133
|
+
*/
|
|
134
|
+
function extractSlots(html: string): { hasSlots: boolean; slotNames: Set<string> } {
|
|
135
|
+
const slotNames = new Set<string>();
|
|
136
|
+
const document = parse5.parse(html);
|
|
137
|
+
|
|
138
|
+
function walk(node: any) {
|
|
139
|
+
if (node.tagName === "Slot" || node.tagName === "slot") {
|
|
140
|
+
slotNames.add("default"); // Default slot
|
|
141
|
+
|
|
142
|
+
// Check for name attribute
|
|
143
|
+
const nameAttr = node.attrs?.find((attr: any) => attr.name === "name");
|
|
144
|
+
if (nameAttr?.value) {
|
|
145
|
+
slotNames.add(nameAttr.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (node.childNodes) {
|
|
150
|
+
node.childNodes.forEach(walk);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
walk(document);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
hasSlots: slotNames.size > 0,
|
|
158
|
+
slotNames
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Discover and parse all component files in a directory
|
|
164
|
+
*/
|
|
165
|
+
export function discoverComponents(
|
|
166
|
+
componentsDir: string,
|
|
167
|
+
baseDir: string
|
|
168
|
+
): Map<string, ComponentMetadata> {
|
|
169
|
+
const components = new Map<string, ComponentMetadata>();
|
|
170
|
+
|
|
171
|
+
if (!fs.existsSync(componentsDir)) {
|
|
172
|
+
return components; // Return empty map if directory doesn't exist
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function scanDirectory(dir: string): void {
|
|
176
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const fullPath = path.join(dir, entry.name);
|
|
180
|
+
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
scanDirectory(fullPath); // Recurse into subdirectories
|
|
183
|
+
} else if (entry.isFile() && entry.name.endsWith(".zen")) {
|
|
184
|
+
try {
|
|
185
|
+
const zenFile = parseZen(fullPath);
|
|
186
|
+
const componentName = filenameToComponentName(fullPath, baseDir);
|
|
187
|
+
|
|
188
|
+
// Extract metadata from all scripts
|
|
189
|
+
const allProps = new Map<string, string>();
|
|
190
|
+
const allStateDeclarations = new Map<string, string>();
|
|
191
|
+
|
|
192
|
+
for (const script of zenFile.scripts) {
|
|
193
|
+
const props = extractProps(script.content);
|
|
194
|
+
const stateDecls = extractStateDeclarations(script.content);
|
|
195
|
+
|
|
196
|
+
// Merge props (later scripts override earlier ones)
|
|
197
|
+
for (const [name, value] of props.entries()) {
|
|
198
|
+
allProps.set(name, value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Merge state declarations (later scripts override earlier ones)
|
|
202
|
+
for (const [name, value] of stateDecls.entries()) {
|
|
203
|
+
allStateDeclarations.set(name, value);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract slots
|
|
208
|
+
const { hasSlots, slotNames } = extractSlots(zenFile.html);
|
|
209
|
+
|
|
210
|
+
const metadata: ComponentMetadata = {
|
|
211
|
+
name: componentName,
|
|
212
|
+
filePath: fullPath,
|
|
213
|
+
isLayout: false,
|
|
214
|
+
props: allProps,
|
|
215
|
+
stateDeclarations: allStateDeclarations,
|
|
216
|
+
hasSlots,
|
|
217
|
+
slotNames,
|
|
218
|
+
html: zenFile.html,
|
|
219
|
+
scripts: zenFile.scripts.map(s => s.content),
|
|
220
|
+
styles: zenFile.styles.map(s => s.content)
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
components.set(componentName, metadata);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.warn(`Warning: Failed to parse component ${fullPath}:`, error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
scanDirectory(componentsDir);
|
|
232
|
+
return components;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Discover and parse all layout files in a directory
|
|
237
|
+
*/
|
|
238
|
+
export function discoverLayouts(
|
|
239
|
+
layoutsDir: string,
|
|
240
|
+
baseDir: string
|
|
241
|
+
): Map<string, ComponentMetadata> {
|
|
242
|
+
const layouts = new Map<string, ComponentMetadata>();
|
|
243
|
+
|
|
244
|
+
if (!fs.existsSync(layoutsDir)) {
|
|
245
|
+
return layouts; // Return empty map if directory doesn't exist
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const entries = fs.readdirSync(layoutsDir, { withFileTypes: true });
|
|
249
|
+
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
if (entry.isFile() && entry.name.endsWith(".zen")) {
|
|
252
|
+
const fullPath = path.join(layoutsDir, entry.name);
|
|
253
|
+
try {
|
|
254
|
+
const zenFile = parseZen(fullPath);
|
|
255
|
+
const layoutName = filenameToComponentName(fullPath, baseDir);
|
|
256
|
+
|
|
257
|
+
// Extract metadata from all scripts
|
|
258
|
+
const allProps = new Map<string, string>();
|
|
259
|
+
const allStateDeclarations = new Map<string, string>();
|
|
260
|
+
|
|
261
|
+
for (const script of zenFile.scripts) {
|
|
262
|
+
const props = extractProps(script.content);
|
|
263
|
+
const stateDecls = extractStateDeclarations(script.content);
|
|
264
|
+
|
|
265
|
+
// Merge props (later scripts override earlier ones)
|
|
266
|
+
for (const [name, value] of props.entries()) {
|
|
267
|
+
allProps.set(name, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Merge state declarations (later scripts override earlier ones)
|
|
271
|
+
for (const [name, value] of stateDecls.entries()) {
|
|
272
|
+
allStateDeclarations.set(name, value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Extract slots
|
|
277
|
+
const { hasSlots, slotNames } = extractSlots(zenFile.html);
|
|
278
|
+
|
|
279
|
+
const metadata: ComponentMetadata = {
|
|
280
|
+
name: layoutName,
|
|
281
|
+
filePath: fullPath,
|
|
282
|
+
isLayout: true,
|
|
283
|
+
props: allProps,
|
|
284
|
+
stateDeclarations: allStateDeclarations,
|
|
285
|
+
hasSlots,
|
|
286
|
+
slotNames,
|
|
287
|
+
html: zenFile.html,
|
|
288
|
+
scripts: zenFile.scripts.map(s => s.content),
|
|
289
|
+
styles: zenFile.styles.map(s => s.content)
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
layouts.set(layoutName, metadata);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.warn(`Warning: Failed to parse layout ${fullPath}:`, error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return layouts;
|
|
300
|
+
}
|
|
301
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
// compiler/event.ts
|
|
3
|
+
// Phase 2: fully dynamic, compiler-driven DOM event system.
|
|
4
|
+
// Uses a single generic delegate function for all event types and adds DOM helper methods.
|
|
5
|
+
|
|
6
|
+
export function generateEventBindingRuntime(eventTypes: string[]): string {
|
|
7
|
+
// Phase 2: Generate DOM helper methods and a single generic delegate function
|
|
8
|
+
// The delegate function dynamically handles any event type by checking data-zen-* attributes
|
|
9
|
+
|
|
10
|
+
const eventTypesStr = JSON.stringify(eventTypes);
|
|
11
|
+
|
|
12
|
+
return `
|
|
13
|
+
// Phase 2 runtime helpers - DOM manipulation methods on HTMLElement prototype
|
|
14
|
+
HTMLElement.prototype.show = function() { this.style.display = "block"; }
|
|
15
|
+
HTMLElement.prototype.hide = function() { this.style.display = "none"; }
|
|
16
|
+
HTMLElement.prototype.addClass = function(name) { this.classList.add(name); }
|
|
17
|
+
HTMLElement.prototype.removeClass = function(name) { this.classList.remove(name); }
|
|
18
|
+
HTMLElement.prototype.setText = function(text) { this.textContent = text; }
|
|
19
|
+
HTMLElement.prototype.setHTML = function(html) { this.innerHTML = html; }
|
|
20
|
+
|
|
21
|
+
// Phase 2: Dynamic delegated event listener - single generic function for all event types
|
|
22
|
+
function delegate(event) {
|
|
23
|
+
let type = event.type;
|
|
24
|
+
let el = event.target;
|
|
25
|
+
// Ensure el is an Element (not text node, etc.) before checking attributes
|
|
26
|
+
while (el && el.nodeType === 1 && !el.hasAttribute(\`data-zen-\${type}\`)) {
|
|
27
|
+
el = el.parentElement;
|
|
28
|
+
}
|
|
29
|
+
// Also handle case where initial target wasn't an element
|
|
30
|
+
if (el && el.nodeType !== 1) {
|
|
31
|
+
el = el.parentElement;
|
|
32
|
+
while (el && el.nodeType === 1 && !el.hasAttribute(\`data-zen-\${type}\`)) {
|
|
33
|
+
el = el.parentElement;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (el && el.nodeType === 1) {
|
|
37
|
+
const handlerName = el.getAttribute(\`data-zen-\${type}\`);
|
|
38
|
+
if (handlerName && typeof window[handlerName] === "function") {
|
|
39
|
+
window[handlerName](event, el);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Attach delegate function to all detected event types dynamically
|
|
45
|
+
const zenEvents = ${eventTypesStr};
|
|
46
|
+
zenEvents.forEach(type => {
|
|
47
|
+
document.addEventListener(type, delegate);
|
|
48
|
+
});
|
|
49
|
+
`;
|
|
50
|
+
}
|