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