@daz4126/helium 0.10.2 → 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 +101 -146
  2. package/package.json +1 -1
package/helium.js CHANGED
@@ -1,98 +1,64 @@
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 = (name,...args) => args.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${name}|`)
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 = Array.isArray(result) ? result.join`` : 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
  };
@@ -104,20 +70,15 @@ const trackDependencies = (fn, el) => {
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 || []) {
76
+ for (const {name, value} of el.attributes || []) {
115
77
  if (he(name,"text","html","bind")) {
116
78
  try {
117
79
  new Function(`let ${value}=1`);
118
- const val = he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
119
- state[value] ||= Number.isNaN(Number(val)) ? val : Number(val);
120
- } catch (e) {}
80
+ state[value] ||= he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
81
+ } catch {}
121
82
  }
122
83
  }
123
84
  });
@@ -127,31 +88,30 @@ const trackDependencies = (fn, el) => {
127
88
  newBindings.push(b);
128
89
  };
129
90
 
130
- // Register bindings
131
91
  heElements.forEach(el => {
132
- const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
133
- for (const { name, value } of el.attributes || []) {
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";
95
+
96
+ for (const {name, value} of el.attributes || []) {
134
97
  if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
135
98
  if (he(name,"ref")) refs.set("$" + value, el);
99
+
136
100
  if (he(name,"text","html")) {
137
101
  const fn = compile(value, true);
138
- const b = { el, prop: he(name,"text") ? "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
106
  if (he(name,"bind")) {
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";
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];
@@ -160,60 +120,62 @@ const trackDependencies = (fn, el) => {
160
120
 
161
121
  if (he(name,"hidden","visible")) {
162
122
  const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
163
- const b = { el, prop: "hidden", fn };
164
-
165
- trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
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
131
  if (he(name,"init")) {
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
- let debounceDelay = 0;
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;
180
139
  const debounceMod = mods.find(m => m.startsWith("debounce"));
181
- if (debounceMod) {
182
- const time = debounceMod.split(":")[1];
183
- debounceDelay = time && !isNaN(time) ? Number(time) : 300;
184
- }
140
+ const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
185
141
 
186
142
  const _handler = e => {
187
143
  if (mods.includes("prevent")) e.preventDefault();
188
- const keyMods = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
189
- for (const [mod, prop] of Object.entries(keyMods)) {
190
- if (mods.includes(mod) && !e[prop]) return;
191
- }
192
- 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)) {
193
149
  const last = mods[mods.length - 1];
194
- if (last) {
195
- 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;
196
152
  if (keyName.toLowerCase() !== last.toLowerCase()) return;
197
153
  }
198
154
  }
155
+
199
156
  if (!mods.includes("outside") || !el.contains(e.target)) {
200
- 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
+ }
201
170
  }
202
- if (mods.includes("once")) el.removeEventListener(eventName, debouncedHandler);
203
- };
204
-
205
- const debounce = (fn, delay) => {
206
- let timeout;
207
- return (...args) => {
208
- clearTimeout(timeout);
209
- timeout = setTimeout(() => fn(...args), delay);
210
- };
171
+
172
+ if (mods.includes("once")) receiver.removeEventListener(event, handler);
211
173
  };
212
174
 
213
- const debouncedHandler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
214
- receiver.addEventListener(eventName, debouncedHandler);
175
+ const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
176
+ receiver.addEventListener(event, handler);
215
177
  if (!listeners.has(el)) listeners.set(el, []);
216
- listeners.get(el).push({receiver, eventName, handler: debouncedHandler});
178
+ listeners.get(el).push({receiver, event, handler});
217
179
  }
218
180
  }
219
181
  });
@@ -221,20 +183,13 @@ const trackDependencies = (fn, el) => {
221
183
  return newBindings;
222
184
  }
223
185
 
224
- const observer = new MutationObserver(mutations => {
225
- if (isUpdatingDOM) return;
226
- for (const m of mutations) {
227
- for (const node of m.removedNodes) {
228
- if (node.nodeType == 1) cleanup(node);
229
- }
230
- for (const node of m.addedNodes) {
231
- if (node.nodeType == 1 && !node.hasAttribute("data-he-p")) {
232
- processElements(node).forEach(applyBinding);
233
- }
234
- }
235
- }
236
- });
237
- 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});
238
193
 
239
194
  processElements(root);
240
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.2",
3
+ "version": "0.11.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {