mez-easyjs 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/LICENSE +21 -0
- package/README.md +40 -0
- package/bin/create-easy-app.js +60 -0
- package/bin/easy.js +7 -0
- package/compiler.d.ts +37 -0
- package/package.json +74 -0
- package/runtime.d.ts +103 -0
- package/src/commands/build.js +284 -0
- package/src/commands/create.js +476 -0
- package/src/commands/dev.js +434 -0
- package/src/commands/generate.js +104 -0
- package/src/commands/ws.js +157 -0
- package/src/compiler/codegen.js +1334 -0
- package/src/compiler/css.js +117 -0
- package/src/compiler/errors.js +65 -0
- package/src/compiler/expr.js +142 -0
- package/src/compiler/index.js +105 -0
- package/src/compiler/parse.js +115 -0
- package/src/compiler/strip-ts.js +39 -0
- package/src/compiler/template.js +419 -0
- package/src/index.js +128 -0
- package/src/runtime/app.js +115 -0
- package/src/runtime/component.js +222 -0
- package/src/runtime/css.js +35 -0
- package/src/runtime/errors.js +121 -0
- package/src/runtime/escape.js +62 -0
- package/src/runtime/events.js +151 -0
- package/src/runtime/hmr.js +157 -0
- package/src/runtime/index.js +40 -0
- package/src/runtime/overlay.js +95 -0
- package/src/runtime/reactive.js +172 -0
- package/src/runtime/router.js +423 -0
- package/src/runtime/scheduler.js +39 -0
- package/src/runtime/store.js +42 -0
- package/src/transform.js +106 -0
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/easy.config.js +7 -0
- package/templates/minimal/index.html +13 -0
- package/templates/minimal/package.json +14 -0
- package/templates/minimal/src/App.easy +24 -0
- package/templates/minimal/src/main.js +4 -0
- package/templates/minimal/src/styles.css +14 -0
- package/templates/starter/README.md +18 -0
- package/templates/starter/easy.config.js +7 -0
- package/templates/starter/index.html +13 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/App.easy +69 -0
- package/templates/starter/src/components/Counter.easy +76 -0
- package/templates/starter/src/components/Greeting.easy +24 -0
- package/templates/starter/src/main.js +11 -0
- package/templates/starter/src/pages/About.easy +20 -0
- package/templates/starter/src/pages/Home.easy +69 -0
- package/templates/starter/src/styles.css +14 -0
- package/templates/tailwind/README.md +23 -0
- package/templates/tailwind/easy.config.js +7 -0
- package/templates/tailwind/index.html +19 -0
- package/templates/tailwind/package.json +14 -0
- package/templates/tailwind/src/App.easy +53 -0
- package/templates/tailwind/src/main.js +4 -0
- package/templates/tailwind/src/styles.css +4 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { reactive } from './reactive.js';
|
|
2
|
+
import { injectStyles } from './css.js';
|
|
3
|
+
import { queueJob } from './scheduler.js';
|
|
4
|
+
import { EasyError, reportError, peekBoundary } from './errors.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* defineComponent — used by compiled SFCs and hand-written components.
|
|
8
|
+
*/
|
|
9
|
+
export function defineComponent(options) {
|
|
10
|
+
if (typeof options === 'function') {
|
|
11
|
+
return defineComponent({ setup: options });
|
|
12
|
+
}
|
|
13
|
+
const comp = { ...options, __isEasyComponent: true };
|
|
14
|
+
return comp;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mount a component definition into a host element.
|
|
19
|
+
*/
|
|
20
|
+
export function mountComponent(defn, host, props = {}, appContext = {}) {
|
|
21
|
+
if (!defn) {
|
|
22
|
+
const err = new EasyError('Cannot mount undefined component', {
|
|
23
|
+
hint: 'Check the import path and that the file exports default.',
|
|
24
|
+
filename: appContext.parent?.filename
|
|
25
|
+
});
|
|
26
|
+
reportError(err);
|
|
27
|
+
const boundary = peekBoundary();
|
|
28
|
+
if (boundary) boundary.capture(err);
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof defn === 'function' && !defn.__isEasyComponent && !defn.setup) {
|
|
33
|
+
return Promise.resolve(defn()).then((mod) => {
|
|
34
|
+
const resolved = mod?.default || mod;
|
|
35
|
+
return mountComponent(resolved, host, props, appContext);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (defn && typeof defn.then === 'function') {
|
|
39
|
+
return defn.then((mod) => {
|
|
40
|
+
const resolved = mod?.default || mod;
|
|
41
|
+
return mountComponent(resolved, host, props, appContext);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const options = defn.__isEasyComponent || defn.setup ? defn : defineComponent(defn);
|
|
46
|
+
const compName = options.name || options.filename || 'Anonymous';
|
|
47
|
+
|
|
48
|
+
if (options.styles && options.scopeId) {
|
|
49
|
+
injectStyles(options.scopeId, options.styles);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const instance = {
|
|
53
|
+
props: reactive({ ...props }),
|
|
54
|
+
appContext,
|
|
55
|
+
parent: appContext.parent || null,
|
|
56
|
+
filename: options.filename || null,
|
|
57
|
+
name: compName,
|
|
58
|
+
isMounted: false,
|
|
59
|
+
isUnmounted: false,
|
|
60
|
+
cleanups: [],
|
|
61
|
+
children: [],
|
|
62
|
+
emit(event, ...args) {
|
|
63
|
+
const handler = props[`on${capitalize(event)}`] || props[`on${event}`];
|
|
64
|
+
if (typeof handler === 'function') handler(...args);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const ctx = {
|
|
69
|
+
props: instance.props,
|
|
70
|
+
emit: instance.emit.bind(instance),
|
|
71
|
+
appContext,
|
|
72
|
+
expose(api) {
|
|
73
|
+
Object.assign(instance, api);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let setupResult = {};
|
|
78
|
+
try {
|
|
79
|
+
if (typeof options.setup === 'function') {
|
|
80
|
+
setupResult = options.setup(instance.props, ctx) || {};
|
|
81
|
+
} else {
|
|
82
|
+
const data = typeof options.data === 'function' ? options.data.call({}) : (options.data || {});
|
|
83
|
+
const state = reactive(data);
|
|
84
|
+
const methods = {};
|
|
85
|
+
for (const [name, fn] of Object.entries(options.methods || {})) {
|
|
86
|
+
if (typeof fn === 'function') {
|
|
87
|
+
methods[name] = fn.bind(createBoundThis(state, methods, instance));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
setupResult = {
|
|
91
|
+
state,
|
|
92
|
+
methods,
|
|
93
|
+
mount(el) {
|
|
94
|
+
if (options.render) options.render.call(createBoundThis(state, methods, instance), el);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const easy = new EasyError(err.message || String(err), {
|
|
100
|
+
component: compName,
|
|
101
|
+
filename: options.filename,
|
|
102
|
+
cause: err,
|
|
103
|
+
hint: 'Fix the error in setup()/data() and save to retry via HMR.'
|
|
104
|
+
});
|
|
105
|
+
reportError(easy);
|
|
106
|
+
const boundary = peekBoundary();
|
|
107
|
+
if (boundary) {
|
|
108
|
+
boundary.capture(easy);
|
|
109
|
+
return { $el: host, state: null, unmount() {}, isUnmounted: true };
|
|
110
|
+
}
|
|
111
|
+
throw easy;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
instance.state = setupResult.state;
|
|
115
|
+
instance.methods = setupResult.methods || {};
|
|
116
|
+
instance.$el = host;
|
|
117
|
+
|
|
118
|
+
const boundThis = createBoundThis(instance.state || {}, instance.methods, instance);
|
|
119
|
+
for (const key of Object.keys(instance.methods)) {
|
|
120
|
+
const fn = instance.methods[key];
|
|
121
|
+
if (typeof fn === 'function') {
|
|
122
|
+
instance.methods[key] = fn.bind(boundThis);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
callHook(options.beforeMount || setupResult.beforeMount, boundThis, compName);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (typeof setupResult.mount === 'function') {
|
|
130
|
+
setupResult.mount(host, instance);
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const easy = new EasyError(err.message || String(err), {
|
|
134
|
+
component: compName,
|
|
135
|
+
filename: options.filename,
|
|
136
|
+
cause: err,
|
|
137
|
+
hint: 'Error while mounting the template. Check directives and child components.'
|
|
138
|
+
});
|
|
139
|
+
reportError(easy);
|
|
140
|
+
const boundary = peekBoundary();
|
|
141
|
+
if (boundary) {
|
|
142
|
+
boundary.capture(easy);
|
|
143
|
+
return {
|
|
144
|
+
$el: host,
|
|
145
|
+
state: instance.state,
|
|
146
|
+
appContext,
|
|
147
|
+
props: instance.props,
|
|
148
|
+
unmount() {
|
|
149
|
+
instance.isUnmounted = true;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
throw easy;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
instance.isMounted = true;
|
|
157
|
+
callHook(options.mounted || setupResult.mounted, boundThis, compName);
|
|
158
|
+
|
|
159
|
+
instance.unmount = () => {
|
|
160
|
+
if (instance.isUnmounted) return;
|
|
161
|
+
callHook(options.beforeUnmount || setupResult.beforeUnmount, boundThis, compName);
|
|
162
|
+
for (const child of instance.children) {
|
|
163
|
+
if (child.unmount) child.unmount();
|
|
164
|
+
}
|
|
165
|
+
if (typeof setupResult.unmount === 'function') setupResult.unmount(host, instance);
|
|
166
|
+
for (const fn of instance.cleanups) fn();
|
|
167
|
+
host.innerHTML = '';
|
|
168
|
+
instance.isUnmounted = true;
|
|
169
|
+
callHook(options.unmounted || setupResult.unmounted, boundThis, compName);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return instance;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function unmountComponent(instance) {
|
|
176
|
+
if (instance && instance.unmount) instance.unmount();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function callHook(hook, ctx, name) {
|
|
180
|
+
if (typeof hook === 'function') {
|
|
181
|
+
try {
|
|
182
|
+
hook.call(ctx);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
reportError(
|
|
185
|
+
new EasyError(e.message || String(e), {
|
|
186
|
+
component: name,
|
|
187
|
+
cause: e,
|
|
188
|
+
hint: 'Lifecycle hook threw. See stack below.'
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function capitalize(s) {
|
|
196
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function createBoundThis(state, methods, instance) {
|
|
200
|
+
return new Proxy(
|
|
201
|
+
{},
|
|
202
|
+
{
|
|
203
|
+
get(_, key) {
|
|
204
|
+
if (key in methods) return methods[key];
|
|
205
|
+
if (state && key in state) return state[key];
|
|
206
|
+
if (key === '$props') return instance.props;
|
|
207
|
+
if (key === '$emit') return instance.emit.bind(instance);
|
|
208
|
+
if (key === '$el') return instance.$el;
|
|
209
|
+
if (key === '$nextTick') return (fn) => queueJob(fn);
|
|
210
|
+
if (key === '$router') return instance.appContext?.router || null;
|
|
211
|
+
return undefined;
|
|
212
|
+
},
|
|
213
|
+
set(_, key, value) {
|
|
214
|
+
if (state) {
|
|
215
|
+
state[key] = value;
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const styleMap = new Map();
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inject scoped CSS once per scope id.
|
|
5
|
+
*/
|
|
6
|
+
export function injectStyles(scopeId, cssText) {
|
|
7
|
+
if (!cssText || typeof document === 'undefined') return;
|
|
8
|
+
if (styleMap.has(scopeId)) return;
|
|
9
|
+
const el = document.createElement('style');
|
|
10
|
+
el.setAttribute('data-easy-scope', scopeId);
|
|
11
|
+
el.textContent = cssText;
|
|
12
|
+
document.head.appendChild(el);
|
|
13
|
+
styleMap.set(scopeId, el);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Replace scoped CSS in-place (HMR) — preserves other app state.
|
|
18
|
+
*/
|
|
19
|
+
export function replaceStyles(scopeId, cssText) {
|
|
20
|
+
if (!scopeId || typeof document === 'undefined') return;
|
|
21
|
+
const existing = styleMap.get(scopeId);
|
|
22
|
+
if (existing) {
|
|
23
|
+
existing.textContent = cssText || '';
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (cssText) injectStyles(scopeId, cssText);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function removeStyles(scopeId) {
|
|
30
|
+
const el = styleMap.get(scopeId);
|
|
31
|
+
if (el) {
|
|
32
|
+
el.remove();
|
|
33
|
+
styleMap.delete(scopeId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime errors + error boundaries + optional hook for the overlay.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
let overlayHandler = null;
|
|
6
|
+
let boundaryStack = [];
|
|
7
|
+
|
|
8
|
+
export class EasyError extends Error {
|
|
9
|
+
constructor(message, options = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'EasyError';
|
|
12
|
+
this.component = options.component || null;
|
|
13
|
+
this.filename = options.filename || null;
|
|
14
|
+
this.hint = options.hint || null;
|
|
15
|
+
this.cause = options.cause || null;
|
|
16
|
+
this.code = options.code || 'E_RUNTIME';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toString() {
|
|
20
|
+
const loc = [this.component, this.filename].filter(Boolean).join(' · ');
|
|
21
|
+
const hint = this.hint ? `\nHint: ${this.hint}` : '';
|
|
22
|
+
return `[easy] ${loc ? loc + '\n' : ''}${this.message}${hint}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setOverlayHandler(fn) {
|
|
27
|
+
overlayHandler = fn;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function reportError(err, context = {}) {
|
|
31
|
+
const easy =
|
|
32
|
+
err instanceof EasyError
|
|
33
|
+
? err
|
|
34
|
+
: new EasyError(err?.message || String(err), {
|
|
35
|
+
...context,
|
|
36
|
+
cause: err
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.error(easy.toString());
|
|
40
|
+
if (easy.cause && easy.cause !== err) console.error(easy.cause);
|
|
41
|
+
else if (err && err !== easy) console.error(err);
|
|
42
|
+
|
|
43
|
+
if (typeof overlayHandler === 'function') {
|
|
44
|
+
try {
|
|
45
|
+
overlayHandler(easy, context);
|
|
46
|
+
} catch (_) {
|
|
47
|
+
/* ignore overlay failures */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return easy;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function pushBoundary(boundary) {
|
|
55
|
+
boundaryStack.push(boundary);
|
|
56
|
+
return () => {
|
|
57
|
+
const i = boundaryStack.lastIndexOf(boundary);
|
|
58
|
+
if (i >= 0) boundaryStack.splice(i, 1);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function peekBoundary() {
|
|
63
|
+
return boundaryStack[boundaryStack.length - 1] || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run fn; on error, notify nearest boundary or overlay.
|
|
68
|
+
*/
|
|
69
|
+
export function withErrorBoundary(fn, context = {}) {
|
|
70
|
+
try {
|
|
71
|
+
return fn();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const boundary = peekBoundary();
|
|
74
|
+
if (boundary && typeof boundary.capture === 'function') {
|
|
75
|
+
boundary.capture(err, context);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
reportError(err, context);
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a boundary controller bound to a host element.
|
|
85
|
+
*/
|
|
86
|
+
export function createBoundary(host, options = {}) {
|
|
87
|
+
const fallback =
|
|
88
|
+
options.fallback ||
|
|
89
|
+
'Something went wrong in this section.';
|
|
90
|
+
let active = true;
|
|
91
|
+
|
|
92
|
+
const api = {
|
|
93
|
+
capture(err, context = {}) {
|
|
94
|
+
if (!active) return;
|
|
95
|
+
reportError(err, {
|
|
96
|
+
component: options.name || 'ErrorBoundary',
|
|
97
|
+
filename: context.filename,
|
|
98
|
+
hint: options.hint || 'Check the failing child component. The rest of the app should keep working.',
|
|
99
|
+
...context
|
|
100
|
+
});
|
|
101
|
+
if (host) {
|
|
102
|
+
host.innerHTML = '';
|
|
103
|
+
const box = document.createElement('div');
|
|
104
|
+
box.setAttribute('data-easy-boundary', '');
|
|
105
|
+
box.style.cssText =
|
|
106
|
+
'padding:1rem;border:1px solid rgba(240,160,96,.5);border-radius:8px;background:rgba(240,160,96,.08);color:#f0a060;font:14px/1.45 system-ui,sans-serif';
|
|
107
|
+
box.textContent = typeof fallback === 'function' ? fallback(err) : String(fallback);
|
|
108
|
+
host.appendChild(box);
|
|
109
|
+
}
|
|
110
|
+
if (typeof options.onError === 'function') options.onError(err);
|
|
111
|
+
},
|
|
112
|
+
reset() {
|
|
113
|
+
if (host) host.innerHTML = '';
|
|
114
|
+
},
|
|
115
|
+
dispose() {
|
|
116
|
+
active = false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return api;
|
|
121
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML escaping utilities — XSS protection by default.
|
|
3
|
+
* Runtime is CSP-safe: no eval, no inline handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ESC = {
|
|
7
|
+
'&': '&',
|
|
8
|
+
'<': '<',
|
|
9
|
+
'>': '>',
|
|
10
|
+
'"': '"',
|
|
11
|
+
"'": '''
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Attributes that can navigate / load content — must run through sanitizeUrl. */
|
|
15
|
+
export const URL_ATTRS = new Set([
|
|
16
|
+
'href',
|
|
17
|
+
'src',
|
|
18
|
+
'xlink:href',
|
|
19
|
+
'formaction',
|
|
20
|
+
'action',
|
|
21
|
+
'poster',
|
|
22
|
+
'cite',
|
|
23
|
+
'background',
|
|
24
|
+
'data',
|
|
25
|
+
'srcdoc'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function escapeHtml(value) {
|
|
29
|
+
if (value == null) return '';
|
|
30
|
+
return String(value).replace(/[&<>"']/g, (c) => ESC[c]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function escapeAttr(value) {
|
|
34
|
+
if (value == null) return '';
|
|
35
|
+
return String(value).replace(/[&<"']/g, (c) => ESC[c]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Block javascript: / vbscript: / data: URLs and common obfuscations
|
|
40
|
+
* (whitespace, control chars, HTML entities for colon).
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeUrl(value) {
|
|
43
|
+
if (value == null) return '';
|
|
44
|
+
let s = String(value).trim();
|
|
45
|
+
// Normalize obfuscation often used to bypass naive filters
|
|
46
|
+
s = s.replace(/[\u0000-\u001F\u007F\s]+/g, '');
|
|
47
|
+
s = s.replace(/�*58;|�*3a;|:/gi, ':');
|
|
48
|
+
if (/^(javascript|vbscript|data):/i.test(s)) return '';
|
|
49
|
+
// Reject protocol-relative data-like tricks after decode
|
|
50
|
+
if (/^data:/i.test(s)) return '';
|
|
51
|
+
return String(value).trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True for event handler attribute names (onclick, onload, …). */
|
|
55
|
+
export function isEventAttr(name) {
|
|
56
|
+
return /^on[a-z]/i.test(String(name || ''));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True for attributes that must be URL-sanitized when bound. */
|
|
60
|
+
export function isUrlAttr(name) {
|
|
61
|
+
return URL_ATTRS.has(String(name || '').toLowerCase());
|
|
62
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event delegation at the app root.
|
|
3
|
+
* Compiled templates set data-e-on="click:methodName" (or multiple).
|
|
4
|
+
* Never uses inline onclick — CSP-safe.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { reportError, EasyError, peekBoundary } from './errors.js';
|
|
8
|
+
|
|
9
|
+
const ROOTS = new WeakMap();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Live event-handler context. Never snapshot reactive state into a plain
|
|
13
|
+
* object — `this.count++` / reads must hit the reactive proxy so subscribers
|
|
14
|
+
* (DOM updaters) run.
|
|
15
|
+
*/
|
|
16
|
+
export function createEventContext(methods, state, extras = {}) {
|
|
17
|
+
const methodBag = methods || {};
|
|
18
|
+
const hasKey = (key) =>
|
|
19
|
+
key === 'methods' ||
|
|
20
|
+
Object.prototype.hasOwnProperty.call(extras, key) ||
|
|
21
|
+
Object.prototype.hasOwnProperty.call(methodBag, key) ||
|
|
22
|
+
(state != null && key in state);
|
|
23
|
+
|
|
24
|
+
return new Proxy(
|
|
25
|
+
{},
|
|
26
|
+
{
|
|
27
|
+
get(_, key) {
|
|
28
|
+
if (key === 'methods') return methodBag;
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(extras, key)) return extras[key];
|
|
30
|
+
if (Object.prototype.hasOwnProperty.call(methodBag, key)) return methodBag[key];
|
|
31
|
+
if (state != null && key in state) return state[key];
|
|
32
|
+
return undefined;
|
|
33
|
+
},
|
|
34
|
+
set(_, key, value) {
|
|
35
|
+
if (state != null) {
|
|
36
|
+
state[key] = value;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
extras[key] = value;
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
has(_, key) {
|
|
43
|
+
return hasKey(key);
|
|
44
|
+
},
|
|
45
|
+
ownKeys() {
|
|
46
|
+
const keys = new Set([
|
|
47
|
+
...Object.keys(methodBag),
|
|
48
|
+
...Object.keys(extras),
|
|
49
|
+
...(state != null ? Object.keys(state) : [])
|
|
50
|
+
]);
|
|
51
|
+
return [...keys];
|
|
52
|
+
},
|
|
53
|
+
getOwnPropertyDescriptor(_, key) {
|
|
54
|
+
if (hasKey(key)) {
|
|
55
|
+
return { configurable: true, enumerable: true, writable: true };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function setupDelegation(root, getContext) {
|
|
64
|
+
if (!root || ROOTS.has(root)) return;
|
|
65
|
+
|
|
66
|
+
const handler = (event) => {
|
|
67
|
+
let node = event.target;
|
|
68
|
+
while (node && node !== root) {
|
|
69
|
+
const attr = node.getAttribute && node.getAttribute('data-e-on');
|
|
70
|
+
if (attr) {
|
|
71
|
+
const parts = attr.split(';');
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
const [type, methodRaw, ...argParts] = part.split(':');
|
|
74
|
+
const method = (methodRaw || '').replace(/\(\s*\)$/, '').trim();
|
|
75
|
+
if (type === event.type && method) {
|
|
76
|
+
const ctx = getContext(node);
|
|
77
|
+
if (ctx) {
|
|
78
|
+
const fn =
|
|
79
|
+
ctx[method] ||
|
|
80
|
+
(ctx.methods && ctx.methods[method]) ||
|
|
81
|
+
(ctx.__modelHandlers && ctx.__modelHandlers[method]);
|
|
82
|
+
if (typeof fn === 'function') {
|
|
83
|
+
try {
|
|
84
|
+
const argStr = argParts.join(':');
|
|
85
|
+
if (argStr) {
|
|
86
|
+
try {
|
|
87
|
+
const arg = JSON.parse(argStr);
|
|
88
|
+
fn.call(ctx, arg, event);
|
|
89
|
+
} catch {
|
|
90
|
+
fn.call(ctx, event);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
fn.call(ctx, event);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const easy =
|
|
97
|
+
err instanceof EasyError
|
|
98
|
+
? err
|
|
99
|
+
: new EasyError(err.message || String(err), {
|
|
100
|
+
cause: err,
|
|
101
|
+
hint: `Error in event handler "${method}".`
|
|
102
|
+
});
|
|
103
|
+
reportError(easy);
|
|
104
|
+
let p = node;
|
|
105
|
+
let captured = false;
|
|
106
|
+
while (p && p !== root) {
|
|
107
|
+
if (p.__easyBoundary) {
|
|
108
|
+
p.__easyBoundary.capture(easy);
|
|
109
|
+
captured = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
p = p.parentElement;
|
|
113
|
+
}
|
|
114
|
+
if (!captured) {
|
|
115
|
+
const boundary = peekBoundary();
|
|
116
|
+
if (boundary) boundary.capture(easy);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
node = node.parentNode;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const types = [
|
|
130
|
+
'click', 'input', 'change', 'submit', 'keydown', 'keyup',
|
|
131
|
+
'mouseenter', 'mouseleave', 'focus', 'blur'
|
|
132
|
+
];
|
|
133
|
+
for (const type of types) {
|
|
134
|
+
root.addEventListener(
|
|
135
|
+
type,
|
|
136
|
+
handler,
|
|
137
|
+
type === 'focus' || type === 'blur' || type === 'mouseenter' || type === 'mouseleave'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ROOTS.set(root, handler);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function on(el, type, fn, options) {
|
|
145
|
+
el.addEventListener(type, fn, options);
|
|
146
|
+
return () => el.removeEventListener(type, fn, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function off(el, type, fn, options) {
|
|
150
|
+
el.removeEventListener(type, fn, options);
|
|
151
|
+
}
|