@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.
- package/helium.js +101 -146
- 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 = (
|
|
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 $ =
|
|
6
|
-
const html = (s)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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",
|
|
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 (
|
|
74
|
-
return () => expr;
|
|
75
|
-
}
|
|
51
|
+
} catch {return () => expr}
|
|
76
52
|
};
|
|
77
53
|
|
|
78
|
-
const trackDependencies
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
-
|
|
119
|
-
|
|
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,
|
|
133
|
-
|
|
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 = {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
176
|
-
} else if (name.startsWith("@") || name.startsWith("data-he
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (["keydown","keyup","keypress"].includes(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
214
|
-
receiver.addEventListener(
|
|
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,
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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);
|