@daz4126/helium 0.10.1 → 0.11.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 +109 -155
  2. package/package.json +1 -1
package/helium.js CHANGED
@@ -1,122 +1,84 @@
1
+ const parseEx=v=>{try{return Function(`return(${v})`)()}catch{return v}}
2
+ const getEvent = el => ({form:"submit",input:"input",textarea:"input",select:"change"}[el.tagName.toLowerCase()]||"click")
3
+ const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))}
4
+
1
5
  export default function helium(data = {}) {
2
- const he = (...args) => args.flatMap(b => [`@${b}`,`data-he-${b}`])
6
+ const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
3
7
  const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
4
8
  const [bindings, refs, listeners] = [new Map(), new Map(), new WeakMap()];
5
- const $ = (s) => document.querySelector(s);
6
- const html = (s) => {
7
- const t = document.createElement("template");
8
- t.innerHTML = s;
9
- return t.content.firstChild;
10
- };
11
-
12
- const ajax = (url, method, target, opts = {}) => {
13
- const headers = {
14
- "Content-Type": "application/json",
15
- "Accept": "text/vnd.turbo-stream.html,application/json,text/html"
16
- };
17
- const token = document.querySelector('meta[name="csrf-token"]')?.content;
18
- if (token) headers["X-CSRF-Token"] = token;
19
-
20
- fetch(url, {
21
- method,
22
- headers,
23
- body: method === "GET" ? null : JSON.stringify(opts),
24
- credentials: "same-origin"
25
- })
26
- .then(r => r.headers.get("content-type")?.includes("application/json") ? r.json() : r.text())
27
- .then(d => {if(target) state[target] = d})
28
- .catch(e => console.error("AJAX error:", e.message));
29
- };
30
-
31
- const get = (u,t) => ajax(u,"GET",t);
32
- const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
9
+ const $ = s => document.querySelector(s);
10
+ const html = s => {const t = document.createElement("template"); t.innerHTML = s.trim(); return t.content.firstChild};
11
+
12
+ const ajax=(u,m,o={},p={})=>{
13
+ const fd=p instanceof FormData,t=document.querySelector('meta[name="csrf-token"]')?.content;
14
+ fetch(u,{method:m,headers:{
15
+ Accept:"text/vnd.turbo-stream.html,application/json,text/html",
16
+ ...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
17
+ ...(t&&{"X-CSRF-Token":t})
18
+ },body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
19
+ .then(r=>r.headers.get("content-type")?.includes("json")?r.json():r.text())
20
+ .then(d=>{
21
+ if(o.data)state[o.data]=d;
22
+ if(o.target){
23
+ const c=o.template?o.template(d):d;
24
+ o.action?o.target[o.action=="replace"?"replaceWith":o.action](html(c)):o.target.innerHTML=c;
25
+ }
26
+ }).catch(e=>console.error("AJAX:",e.message))
27
+ }
33
28
 
34
29
  const handler = {
35
- get(target, prop, receiver) {
36
- const val = Reflect.get(target, prop, receiver);
37
- return typeof val === "object" && val !== null ? new Proxy(val, handler) : val;
38
- },
39
- set(target, prop, val) {
40
- const result = Reflect.set(target, prop, val);
41
- bindings.get(prop)?.forEach(applyBinding);
42
- return result;
43
- }
44
- };
30
+ get: (t, p, r) => {const v = Reflect.get(t, p, r); return typeof v == "object" && v != null ? new Proxy(v, handler) : v},
31
+ set: (t, p, v) => {const res = Reflect.set(t, p, v); bindings.get(p)?.forEach(applyBinding); return res}
32
+ };
45
33
 
46
34
  const state = new Proxy(data, handler);
47
35
  let isUpdatingDOM = false;
48
36
 
49
- function applyBinding(binding, event = {}, elCtx = binding.el) {
50
- const { el, prop, fn } = binding;
51
- const result = fn($, state, event, elCtx, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
52
- if (prop == "innerHTML") {
53
- isUpdatingDOM = true;
54
- el.innerHTML = result;
55
- isUpdatingDOM = false;
56
- } else if(prop == "class" && typeof result == "object"){
57
- Object.entries(result).forEach(([k, v]) => el.classList.toggle(k, v));
58
- } else if(prop == "style" && typeof result == "object"){
59
- el.style = Object.entries(result).filter(([k, v]) => v).map(([k, v]) => `${k}:${v}`).join(";");
60
- } else if (prop in el) {
61
- if(el.type == "radio") el.checked = el.value == result;
62
- else el[prop] = result;
63
- } else {
64
- el.setAttribute(prop, result);
65
- }
66
- }
37
+ function applyBinding(b,e={},elCtx=b.el){
38
+ const {el,prop,fn}=b;
39
+ const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
40
+ if(prop=="innerHTML"){isUpdatingDOM=1;el.innerHTML=Array.isArray(r)?r.join``:r;isUpdatingDOM=0}
41
+ 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));
42
+ else if(prop=="style"&&r&&typeof r=="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
43
+ else if(prop in el){if(el.type=="radio")el.checked=el.value==r;else el[prop]=prop=="textContent"?r:parseEx(r)}
44
+ else el.setAttribute(prop,parseEx(r))
45
+ }
67
46
 
68
47
  const compile = (expr, withReturn = false) => {
69
48
  try {
70
- return new Function("$", "$data", "$event", "$el", "$html", "$get", "$post", "$put", "$patch", "$delete",
71
- ...Object.keys(data), ...[...refs.keys()],
49
+ return new Function("$", "$data", "$event", "$el", "$html", ...Object.keys(data), ...[...refs.keys()],
72
50
  `with($data){${withReturn ? "return" : ""}(${expr.trim()})}`);
73
- } catch (err) {
74
- return () => expr;
75
- }
51
+ } catch {return () => expr}
76
52
  };
77
53
 
78
- const trackDependencies = (fn, el) => {
79
- const accessed = new Set();
80
- const trackProxy = new Proxy(data, {
81
- get(target, prop) {
82
- if (typeof prop === 'string') accessed.add(prop);
83
- const val = target[prop];
84
- return typeof val === "object" && val !== null ? new Proxy(val, this) : val;
85
- }
86
- });
87
-
88
- try { fn.call(null, $, trackProxy, refs); } catch {}
89
- return [...accessed];
90
- };
54
+ const trackDependencies=fn=>{
55
+ const s=new Set(),p=new Proxy(data,{get:(t,k)=>{if(typeof k=="string")s.add(k);const v=t[k];return v&&typeof v=="object"?new Proxy(v,this):v}});
56
+ try{fn($,p,refs)}catch{};return[...s]
57
+ }
91
58
 
92
59
  const cleanup = el => {
93
60
  [el, ...el.querySelectorAll('*')].forEach(e => {
94
- listeners.get(e)?.forEach(({receiver, eventName, handler}) =>
95
- receiver.removeEventListener(eventName, handler));
61
+ listeners.get(e)?.forEach(({receiver, event, handler}) => receiver.removeEventListener(event, handler));
96
62
  listeners.delete(e);
97
63
  });
98
64
  };
99
65
 
100
66
  function processElements(element) {
101
67
  const newBindings = [];
102
- if (element.nodeType === 1) {
68
+ if (element.nodeType == 1) {
103
69
  if (element.hasAttribute?.("data-he-p")) return newBindings;
104
70
  element.setAttribute("data-he-p", "");
105
71
  }
106
72
 
107
- const heElements = [element, ...element.querySelectorAll("*")].filter(el =>
108
- Array.from(el.attributes).some(attr => /^(@|:|data-he-)/.test(attr.name)
109
- )
110
- );
73
+ const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
111
74
 
112
- // Seed default state values
113
75
  heElements.forEach(el => {
114
- for (const { name, value } of el.attributes || []) {
115
- if (he("text","html","bind").includes(name)) {
76
+ for (const {name, value} of el.attributes || []) {
77
+ if (he(name,"text","html","bind")) {
116
78
  try {
117
79
  new Function(`let ${value}=1`);
118
- state[value] ||= he("bind").includes(name) ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
119
- } catch (e) {}
80
+ state[value] ||= he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
81
+ } catch {}
120
82
  }
121
83
  }
122
84
  });
@@ -126,95 +88,94 @@ const trackDependencies = (fn, el) => {
126
88
  newBindings.push(b);
127
89
  };
128
90
 
129
- // Register bindings
130
91
  heElements.forEach(el => {
131
- const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
132
- for (const { name, value } of el.attributes || []) {
133
- if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
134
- if (he("ref").includes(name)) refs.set("$" + value, el);
92
+ const execFn = v => compile(v, true)($, state, {}, el, html, ...Object.values(data), ...[...refs.values()]);
93
+ const inputType = el.type?.toLowerCase();
94
+ const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
135
95
 
136
- if (he("text","html").includes(name)) {
96
+ for (const {name, value} of el.attributes || []) {
97
+ if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
98
+ if (he(name,"ref")) refs.set("$" + value, el);
99
+
100
+ if (he(name,"text","html")) {
137
101
  const fn = compile(value, true);
138
- const b = { el, prop: he("text").includes(name) ? "textContent" : "innerHTML", fn };
102
+ const b = {el, prop: he(name,"text") ? "textContent" : "innerHTML", fn};
139
103
  trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
140
104
  }
141
105
 
142
- if (he("bind").includes(name)) {
143
- const inputType = el.type?.toLowerCase();
144
- const isCheckbox = inputType === "checkbox";
145
- const isRadio = inputType === "radio";
146
- const event = (isCheckbox || isRadio || el.tagName === "SELECT") ? "change" : "input";
106
+ if (he(name,"bind")) {
107
+ const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
147
108
  const prop = isCheckbox ? "checked" : "value";
148
-
149
109
  const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
110
+
150
111
  el.addEventListener(event, inputHandler);
151
112
  if (!listeners.has(el)) listeners.set(el, []);
152
- listeners.get(el).push({receiver: el, eventName: event, handler: inputHandler});
153
-
154
- addBinding(value, { el, prop, fn: compile(value, true) });
113
+ listeners.get(el).push({receiver: el, event, handler: inputHandler});
114
+ addBinding(value, {el, prop, fn: compile(value, true)});
155
115
 
156
116
  if (isCheckbox) el.checked = !!state[value];
157
117
  else if (isRadio) el.checked = el.value == state[value];
158
118
  else el.value = state[value] ?? "";
159
119
  }
160
120
 
161
- if (he("hidden","visible").includes(name)) {
162
- const fn = compile(`${he("hidden").includes(name) ? "!" : ""}!(${value})`, true);
163
- const b = { el, prop: "hidden", fn };
164
-
165
- trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
121
+ if (he(name,"hidden","visible")) {
122
+ const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
123
+ trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
166
124
  }
167
125
 
168
126
  if (name.startsWith(":")) {
169
127
  const fn = compile(value, true);
170
- const b = { el, prop: name.slice(1), fn };
171
- trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
128
+ trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
172
129
  }
173
130
 
174
- if (he("init").includes(name)) {
175
- compile(value, false)($, state, undefined, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
176
- } else if (name.startsWith("@") || name.startsWith("data-he-on")) {
177
- const [eventName, ...mods] = name.slice(name.startsWith("@") ? 1 : 10).split(".");
178
- const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
179
-
180
- let debounceDelay = 0;
131
+ if (he(name,"init")) {
132
+ compile(value, false)($, state, undefined, el, html, ...Object.values(data), ...[...refs.values()]);
133
+ } else if (name.startsWith("@") || name.startsWith("data-he")) {
134
+ const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
135
+ const [eventName, ...mods] = fullName.split(".");
136
+ const isHttpMethod = ["get","post","put","patch","delete"].includes(eventName);
137
+ const event = isHttpMethod ? getEvent(el) : eventName;
138
+ const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
181
139
  const debounceMod = mods.find(m => m.startsWith("debounce"));
182
- if (debounceMod) {
183
- const time = debounceMod.split(":")[1];
184
- debounceDelay = time && !isNaN(time) ? Number(time) : 300;
185
- }
140
+ const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
186
141
 
187
142
  const _handler = e => {
188
143
  if (mods.includes("prevent")) e.preventDefault();
189
- const keyMods = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
190
- for (const [mod, prop] of Object.entries(keyMods)) {
191
- if (mods.includes(mod) && !e[prop]) return;
192
- }
193
- if (["keydown","keyup","keypress"].includes(eventName)) {
144
+
145
+ const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
146
+ for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
147
+
148
+ if (["keydown","keyup","keypress"].includes(event)) {
194
149
  const last = mods[mods.length - 1];
195
- if (last) {
196
- let keyName = e.key === " " ? "Space" : e.key === "Escape" ? "Esc" : e.key;
150
+ if (last && !["prevent","once","outside","document"].includes(last)) {
151
+ const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
197
152
  if (keyName.toLowerCase() !== last.toLowerCase()) return;
198
153
  }
199
154
  }
155
+
200
156
  if (!mods.includes("outside") || !el.contains(e.target)) {
201
- compile(value, false)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
157
+ if (isHttpMethod) {
158
+ const options = compile(el.getAttribute('data-he-options') || el.getAttribute('options') || '{}', true)($, state, e, el, html, ...Object.values(data), ...[...refs.values()]);
159
+ let paramsAttr = el.getAttribute('data-he-params') || el.getAttribute('params') || '{}';
160
+
161
+ if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
162
+ const props = paramsAttr.split(":").map(s => s.trim());
163
+ paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
164
+ }
165
+ const params = compile(paramsAttr, true)($, state, e, el, html, ...Object.values(data), ...[...refs.values()]);
166
+ ajax(value, eventName.toUpperCase(), options, params);
167
+ } else {
168
+ compile(value, false)($, state, e, el, html, ...Object.values(data), ...[...refs.values()]);
169
+ }
202
170
  }
203
- if (mods.includes("once")) el.removeEventListener(eventName, debouncedHandler);
171
+
172
+ if (mods.includes("once")) receiver.removeEventListener(event, handler);
204
173
  };
205
174
 
206
- const debounce = (fn, delay) => {
207
- let timeout;
208
- return (...args) => {
209
- clearTimeout(timeout);
210
- timeout = setTimeout(() => fn(...args), delay);
211
- };
212
- };
213
-
214
- const debouncedHandler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
215
- receiver.addEventListener(eventName, debouncedHandler);
175
+ const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
176
+ receiver.addEventListener(event, handler);
216
177
  if (!listeners.has(el)) listeners.set(el, []);
217
- listeners.get(el).push({receiver, eventName, handler: debouncedHandler});
178
+ listeners.get(el).push({receiver, event, handler});
218
179
  }
219
180
  }
220
181
  });
@@ -222,20 +183,13 @@ const trackDependencies = (fn, el) => {
222
183
  return newBindings;
223
184
  }
224
185
 
225
- const observer = new MutationObserver(mutations => {
226
- if (isUpdatingDOM) return;
227
- for (const m of mutations) {
228
- for (const node of m.removedNodes) {
229
- if (node.nodeType === 1) cleanup(node);
230
- }
231
- for (const node of m.addedNodes) {
232
- if (node.nodeType === 1 && !node.hasAttribute("data-he-p")) {
233
- processElements(node).forEach(applyBinding);
234
- }
235
- }
236
- }
237
- });
238
- observer.observe(root, { childList: true, subtree: true });
186
+ new MutationObserver(ms=>{
187
+ if(isUpdatingDOM)return;
188
+ for(const m of ms){
189
+ m.removedNodes.forEach(n=>n.nodeType==1&&cleanup(n));
190
+ m.addedNodes.forEach(n=>n.nodeType==1&&!n.hasAttribute("data-he-p")&&processElements(n).forEach(applyBinding));
191
+ }
192
+ }).observe(root,{childList:1,subtree:1});
239
193
 
240
194
  processElements(root);
241
195
  for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {