@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.
- package/helium.js +109 -155
- 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 = (
|
|
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
|
};
|
|
99
65
|
|
|
100
66
|
function processElements(element) {
|
|
101
67
|
const newBindings = [];
|
|
102
|
-
if (element.nodeType
|
|
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
|
|
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 {
|
|
115
|
-
if (he("text","html","bind")
|
|
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")
|
|
119
|
-
} catch
|
|
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,
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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 = {
|
|
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")
|
|
143
|
-
const
|
|
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,
|
|
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")
|
|
162
|
-
const fn = compile(`${he("hidden")
|
|
163
|
-
|
|
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
|
-
|
|
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")
|
|
175
|
-
compile(value, false)($, state, undefined, el, html,
|
|
176
|
-
} else if (name.startsWith("@") || name.startsWith("data-he
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
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)) {
|
|
194
149
|
const last = mods[mods.length - 1];
|
|
195
|
-
if (last) {
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
+
|
|
172
|
+
if (mods.includes("once")) receiver.removeEventListener(event, handler);
|
|
204
173
|
};
|
|
205
174
|
|
|
206
|
-
const
|
|
207
|
-
|
|
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,
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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);
|