@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.
Files changed (2) hide show
  1. package/helium.js +131 -81
  2. 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
- fetch(u,{method:m,headers:{
26
- Accept:"text/vnd.turbo-stream.html,application/json,text/html",
27
- ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
28
- ...(t&&{"X-CSRF-Token":t})
29
- },body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
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: (t,p,r) => {
47
- const v = Reflect.get(t,p,r);
48
- if (typeof v==="object" && v!==null) {
49
- const proxy = new Proxy(v, handler);
50
- parentKeys.set(v, p);
51
- return proxy;
52
- }
53
- return v;
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
- const {el,prop,fn}=b;
70
- const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
71
- if(prop==="innerHTML" && Array.isArray(r) && el.children.length > 0){
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
- } else if(prop==="innerHTML"){
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
- const compile = (expr, withReturn = false) => {
118
- try {
119
- return new Function("$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",...Object.keys(data),...[...refs.keys()],
120
- `with($data){${withReturn?"return":""}(${expr.trim()})}`);
121
- } catch {return () => expr}
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
- function processElements(element) {
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,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
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 execFn = v => compile(v, true)($, state, {}, el, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()]);
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
- for (const {name, value} of el.attributes || []) {
178
- if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
179
- if (he(name,"ref")) refs.set("$" + value, el);
180
- if (he(name,"text","html")) {
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.startsWith(":")) {
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
- } else if (name.startsWith("@") || name.startsWith("data-he")) {
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
- if (!mods.includes("outside") || !el.contains(e.target)) {
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 && { target }),
234
- ...(action && { action }),
235
- ...execFn(getAttr('template')) && { template: execFn(getAttr('template')) },
236
- ...getAttr('loading') && { loading: 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {