@daz4126/helium 0.10.2 → 0.11.1

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