@daz4126/helium 0.14.0 → 0.15.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/helium.js +119 -76
- package/package.json +1 -1
package/helium.js
CHANGED
|
@@ -6,7 +6,7 @@ export default function helium(data = {}) {
|
|
|
6
6
|
let initFn;
|
|
7
7
|
const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
|
|
8
8
|
const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
|
|
9
|
-
const [bindings, refs, listeners, processed, parentKeys] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap()];
|
|
9
|
+
const [bindings, refs, listeners, processed, parentKeys, fnCache, proxyCache] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap(), new Map(), new WeakMap()];
|
|
10
10
|
const $ = s => document.querySelector(s);
|
|
11
11
|
const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
|
|
12
12
|
|
|
@@ -43,15 +43,17 @@ const ajax = (u,m,o={},p={}) => {
|
|
|
43
43
|
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
|
|
44
44
|
|
|
45
45
|
const handler = {
|
|
46
|
-
get
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
}
|
|
46
|
+
get(t,p,r) {
|
|
47
|
+
const v = Reflect.get(t,p,r);
|
|
48
|
+
if (v && typeof v === "object") {
|
|
49
|
+
if (proxyCache.has(v)) return proxyCache.get(v);
|
|
50
|
+
const proxy = new Proxy(v, handler);
|
|
51
|
+
proxyCache.set(v, proxy);
|
|
52
|
+
parentKeys.set(v, p);
|
|
53
|
+
return proxy;
|
|
54
|
+
}
|
|
55
|
+
return v;
|
|
56
|
+
},
|
|
55
57
|
set: (t,p,v) => {
|
|
56
58
|
const res = Reflect.set(t,p,v);
|
|
57
59
|
if (Array.isArray(t) && !isNaN(p)) {
|
|
@@ -64,11 +66,40 @@ const handler = {
|
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
const state = new Proxy(data, handler);
|
|
67
|
-
|
|
69
|
+
|
|
68
70
|
function applyBinding(b,e={},elCtx=b.el){
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const {el,prop,fn}=b;
|
|
72
|
+
const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
|
|
73
|
+
|
|
74
|
+
if (prop==="innerHTML" && Array.isArray(r) && el.children.length > 0)
|
|
75
|
+
return updateList(el, r);
|
|
76
|
+
|
|
77
|
+
if (prop==="innerHTML") {
|
|
78
|
+
const content = Array.isArray(r)?r.join``:r;
|
|
79
|
+
return typeof Idiomorph === "object"
|
|
80
|
+
? Idiomorph.morph(el, content,{morphStyle:'innerHTML'})
|
|
81
|
+
: el.innerHTML = content;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (prop==="class" && r && typeof r==="object")
|
|
85
|
+
return Object.entries(r).forEach(([k,v]) =>
|
|
86
|
+
k.split(/\s+/).forEach(c => el.classList.toggle(c,v)));
|
|
87
|
+
|
|
88
|
+
if (prop==="style" && r && typeof r==="object")
|
|
89
|
+
return el.style = Object.entries(r).filter(([,v])=>v)
|
|
90
|
+
.map(([k,v])=>`${k}:${v}`).join(";");
|
|
91
|
+
|
|
92
|
+
if (prop in el) {
|
|
93
|
+
if(el.type==="radio") el.checked = el.value===r;
|
|
94
|
+
else el[prop] = prop==="textContent"?r:parseEx(r);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
el.setAttribute(prop, parseEx(r));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
function updateList(el,r){
|
|
72
103
|
const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
|
|
73
104
|
const newChildren = [...temp.children];
|
|
74
105
|
|
|
@@ -104,22 +135,23 @@ function applyBinding(b,e={},elCtx=b.el){
|
|
|
104
135
|
}
|
|
105
136
|
}
|
|
106
137
|
}
|
|
107
|
-
}
|
|
108
|
-
const content = Array.isArray(r)?r.join``:r
|
|
109
|
-
typeof Idiomorph === "object" ? Idiomorph.morph(el, content,{morphStyle:'innerHTML'}) : el.innerHTML = content;
|
|
110
|
-
}
|
|
111
|
-
else if(prop==="class"&&r&&typeof r==="object")for(const[k,v]of Object.entries(r))k.split(/\s+/).forEach(c=>el.classList.toggle(c,v));
|
|
112
|
-
else if(prop==="style"&&r&&typeof r==="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
|
|
113
|
-
else if(prop in el){if(el.type==="radio")el.checked=el.value===r;else el[prop]=prop==="textContent"?r:parseEx(r)}
|
|
114
|
-
else el.setAttribute(prop,parseEx(r))
|
|
115
|
-
}
|
|
138
|
+
}
|
|
116
139
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
const compile = (expr, withReturn = false) => {
|
|
141
|
+
const key = `${withReturn}:${expr}`;
|
|
142
|
+
if (fnCache.has(key)) return fnCache.get(key);
|
|
143
|
+
try {
|
|
144
|
+
const fn = new Function(
|
|
145
|
+
"$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
|
|
146
|
+
...Object.keys(data), ...[...refs.keys()],
|
|
147
|
+
`with($data){${withReturn?"return":""}(${expr.trim()})}`
|
|
148
|
+
);
|
|
149
|
+
fnCache.set(key, fn);
|
|
150
|
+
return fn;
|
|
151
|
+
} catch {
|
|
152
|
+
return () => expr;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
123
155
|
|
|
124
156
|
const trackDependencies = (fn, el) => {
|
|
125
157
|
const accessed = new Set();
|
|
@@ -142,24 +174,11 @@ const trackDependencies = (fn, el) => {
|
|
|
142
174
|
});
|
|
143
175
|
};
|
|
144
176
|
|
|
145
|
-
|
|
146
|
-
let count = 0
|
|
177
|
+
function processElements(element) {
|
|
147
178
|
const newBindings = [];
|
|
148
|
-
if (element.nodeType == 1 && processed.has(element)) return newBindings;
|
|
149
179
|
|
|
150
|
-
const heElements=[element
|
|
151
|
-
|
|
152
|
-
heElements.forEach(el => {
|
|
153
|
-
count++
|
|
154
|
-
for (const {name, value} of el.attributes || []) {
|
|
155
|
-
if (he(name,"text","html","bind")) {
|
|
156
|
-
try {
|
|
157
|
-
new Function(`let ${value}=1`);
|
|
158
|
-
state[value] ||= he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
|
|
159
|
-
} catch {}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
});
|
|
180
|
+
const heElements = [element, ...element.querySelectorAll("*")]
|
|
181
|
+
.filter(e => !processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
|
|
163
182
|
|
|
164
183
|
const addBinding = (val, b) => {
|
|
165
184
|
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
@@ -167,75 +186,99 @@ const trackDependencies = (fn, el) => {
|
|
|
167
186
|
};
|
|
168
187
|
|
|
169
188
|
heElements.forEach(el => {
|
|
170
|
-
if (processed.has(el)) return;
|
|
171
189
|
processed.add(el);
|
|
172
190
|
|
|
173
|
-
const
|
|
191
|
+
const attrs = el.attributes;
|
|
192
|
+
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
174
193
|
const inputType = el.type?.toLowerCase();
|
|
175
194
|
const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
|
|
176
195
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
// Single loop through attributes with early skipping
|
|
197
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
198
|
+
const {name, value} = attrs[i];
|
|
199
|
+
|
|
200
|
+
// Skip non-helium attributes early
|
|
201
|
+
if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith('data-he')) continue;
|
|
202
|
+
|
|
203
|
+
// Initialize state first if needed
|
|
204
|
+
if (he(name, "text", "html", "bind")) {
|
|
205
|
+
try {
|
|
206
|
+
new Function(`let ${value}=1`);
|
|
207
|
+
state[value] ??= he(name, "bind") ? (el.type == "checkbox" ? el.checked : el.value) : el.textContent;
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process the attribute
|
|
212
|
+
if (["@data", "data-he"].includes(name)) {
|
|
213
|
+
Object.assign(state, execFn(value));
|
|
214
|
+
}
|
|
215
|
+
else if (he(name, "ref")) {
|
|
216
|
+
refs.set("$" + value, el);
|
|
217
|
+
}
|
|
218
|
+
else if (he(name, "text", "html")) {
|
|
181
219
|
const fn = compile(value, true);
|
|
182
|
-
const b = {el, prop: he(name,"text") ? "textContent" : "innerHTML", fn};
|
|
220
|
+
const b = {el, prop: he(name, "text") ? "textContent" : "innerHTML", fn};
|
|
183
221
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
|
|
184
222
|
}
|
|
185
|
-
if (he(name,"bind")) {
|
|
223
|
+
else if (he(name, "bind")) {
|
|
186
224
|
const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
|
|
187
225
|
const prop = isCheckbox ? "checked" : "value";
|
|
188
|
-
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
226
|
+
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
189
227
|
el.addEventListener(event, inputHandler);
|
|
190
228
|
if (!listeners.has(el)) listeners.set(el, []);
|
|
191
229
|
listeners.get(el).push({receiver: el, event, handler: inputHandler});
|
|
192
|
-
addBinding(value, {el, prop, fn: compile(value, true)});
|
|
230
|
+
addBinding(value, {el, prop, fn: compile(value, true)});
|
|
193
231
|
if (isCheckbox) el.checked = !!state[value];
|
|
194
232
|
else if (isRadio) el.checked = el.value == state[value];
|
|
195
233
|
else el.value = state[value] ?? "";
|
|
196
234
|
}
|
|
197
|
-
if (he(name,"hidden","visible")) {
|
|
198
|
-
const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
|
|
235
|
+
else if (he(name, "hidden", "visible")) {
|
|
236
|
+
const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
|
|
199
237
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
|
|
200
238
|
}
|
|
201
|
-
if (name
|
|
239
|
+
else if (he(name, "class")) {
|
|
240
|
+
const fn = compile(value, true);
|
|
241
|
+
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "class", fn}));
|
|
242
|
+
}
|
|
243
|
+
else if (name.startsWith(":")) {
|
|
202
244
|
const fn = compile(value, true);
|
|
203
245
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
|
|
204
246
|
}
|
|
205
|
-
if (he(name,"init")) {
|
|
206
|
-
initFn = compile(value,true);
|
|
207
|
-
}
|
|
247
|
+
else if (he(name, "init")) {
|
|
248
|
+
initFn = compile(value, true);
|
|
249
|
+
}
|
|
250
|
+
else if (name.startsWith("@") || name.startsWith("data-he")) {
|
|
208
251
|
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
|
|
209
252
|
const [eventName, ...mods] = fullName.split(".");
|
|
210
|
-
const isHttpMethod = ["get","post","put","patch","delete"].includes(eventName);
|
|
253
|
+
const isHttpMethod = ["get", "post", "put", "patch", "delete"].includes(eventName);
|
|
211
254
|
const event = isHttpMethod ? getEvent(el) : eventName;
|
|
212
|
-
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
255
|
+
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
213
256
|
const debounceMod = mods.find(m => m.startsWith("debounce"));
|
|
214
257
|
const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
|
|
215
258
|
const _handler = e => {
|
|
216
|
-
const exFn = v => compile(v, true)($, state, e, el, html,get,post,put,patch,del, ...Object.values(data), ...[...refs.values()])
|
|
217
|
-
if (mods.includes("prevent")) e.preventDefault();
|
|
259
|
+
const exFn = v => compile(v, true)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()])
|
|
260
|
+
if (mods.includes("prevent")) e.preventDefault();
|
|
218
261
|
const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
|
|
219
|
-
for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
|
|
220
|
-
if (["keydown","keyup","keypress"].includes(event)) {
|
|
262
|
+
for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
|
|
263
|
+
if (["keydown", "keyup", "keypress"].includes(event)) {
|
|
221
264
|
const last = mods[mods.length - 1];
|
|
222
|
-
if (last && !["prevent","once","outside","document"].includes(last)) {
|
|
265
|
+
if (last && !["prevent", "once", "outside", "document"].includes(last)) {
|
|
223
266
|
const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
|
|
224
267
|
if (keyName.toLowerCase() !== last.toLowerCase()) return;
|
|
225
268
|
}
|
|
226
|
-
}
|
|
227
|
-
|
|
269
|
+
}
|
|
270
|
+
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
228
271
|
if (isHttpMethod) {
|
|
229
272
|
const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
|
|
230
273
|
const [target, action] = (getAttr('target') || "").split(":");
|
|
231
274
|
const options = {
|
|
232
275
|
...exFn(getAttr('options') || '{}'),
|
|
233
|
-
...(target && {
|
|
234
|
-
...(action && {
|
|
235
|
-
...execFn(getAttr('template')) && {
|
|
236
|
-
...getAttr('loading') && {
|
|
276
|
+
...(target && {target}),
|
|
277
|
+
...(action && {action}),
|
|
278
|
+
...execFn(getAttr('template')) && {template: execFn(getAttr('template'))},
|
|
279
|
+
...getAttr('loading') && {loading: getAttr('loading')}
|
|
237
280
|
};
|
|
238
|
-
let paramsAttr = getAttr('params') || '{}';
|
|
281
|
+
let paramsAttr = getAttr('params') || '{}';
|
|
239
282
|
if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
|
|
240
283
|
const props = paramsAttr.split(":").map(s => s.trim());
|
|
241
284
|
paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
|
|
@@ -245,7 +288,7 @@ const trackDependencies = (fn, el) => {
|
|
|
245
288
|
} else {
|
|
246
289
|
exFn(value);
|
|
247
290
|
}
|
|
248
|
-
}
|
|
291
|
+
}
|
|
249
292
|
if (mods.includes("once")) receiver.removeEventListener(event, handler);
|
|
250
293
|
};
|
|
251
294
|
const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|