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