@daz4126/helium 0.11.1 → 0.13.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 +86 -50
- package/package.json +1 -1
package/helium.js
CHANGED
|
@@ -3,68 +3,93 @@ const getEvent = el => ({form:"submit",input:"input",textarea:"input",select:"ch
|
|
|
3
3
|
const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))}
|
|
4
4
|
|
|
5
5
|
export default function helium(data = {}) {
|
|
6
|
+
let initFn;
|
|
6
7
|
const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
|
|
7
8
|
const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
|
|
8
|
-
const [bindings, refs, listeners] = [new Map(), new Map(), new WeakMap()];
|
|
9
|
+
const [bindings, refs, listeners, processed] = [new Map(), new Map(), new WeakMap(), new WeakSet()];
|
|
9
10
|
const $ = s => document.querySelector(s);
|
|
10
11
|
const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if(o.target)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
const update = (data,target,action,template) => {
|
|
14
|
+
const element = target instanceof Node ? target : (refs.get(target) || $(target));
|
|
15
|
+
if(element){
|
|
16
|
+
const content = html(template ? template(data) : data);
|
|
17
|
+
action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
|
|
18
|
+
return content
|
|
19
|
+
} else state[target] = data
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const ajax = (u,m,o={},p={}) => {
|
|
24
|
+
if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
|
|
25
|
+
const fd = p instanceof FormData, t = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
26
|
+
fetch(u,{method:m,headers:{
|
|
27
|
+
Accept:"text/vnd.turbo-stream.html,application/json,text/html",
|
|
28
|
+
...(!fd&&m!=="GET"&&{"Content-Type":"application/json"}),
|
|
29
|
+
...(t&&{"X-CSRF-Token":t})
|
|
30
|
+
},body:m==="GET"?null:(fd?p:JSON.stringify(p)),credentials:"same-origin"})
|
|
31
|
+
.then(r => {
|
|
32
|
+
const type = r.headers.get("content-type") || "";
|
|
33
|
+
return (type.includes("turbo-stream") ? r.text().then(d => ({ t: true, d })) :
|
|
34
|
+
type.includes("json") ? r.json() :
|
|
35
|
+
r.text());
|
|
36
|
+
}).then(d =>
|
|
37
|
+
d.t && "Turbo"
|
|
38
|
+
? Turbo.renderStreamMessage(d.d)
|
|
39
|
+
: update(d, o.target, o.loading ? "replace" : o.action, o.template)
|
|
40
|
+
).catch(e => console.error("AJAX:", e.message));
|
|
41
|
+
}
|
|
31
42
|
|
|
32
43
|
const get = (u,t) => ajax(u,"GET",t);
|
|
33
44
|
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
|
|
34
45
|
|
|
35
46
|
const handler = {
|
|
36
|
-
get: (t,
|
|
37
|
-
set: (t,
|
|
47
|
+
get: (t,p,r) => {const v = Reflect.get(t,p,r); return typeof v==="object"&&v!==null ? new Proxy(v,handler) : v},
|
|
48
|
+
set: (t,p,v) => {const res = Reflect.set(t,p,v); bindings.get(p)?.forEach(applyBinding); return res}
|
|
38
49
|
};
|
|
39
50
|
|
|
40
51
|
const state = new Proxy(data, handler);
|
|
41
52
|
let isUpdatingDOM = false;
|
|
42
53
|
|
|
43
54
|
function applyBinding(b,e={},elCtx=b.el){
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
55
|
+
const {el,prop,fn}=b;
|
|
56
|
+
const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
|
|
57
|
+
if(prop==="innerHTML"){
|
|
58
|
+
isUpdatingDOM=1;
|
|
59
|
+
const content = el.innerHTML=Array.isArray(r)?r.join``:r
|
|
60
|
+
typeof Idiomorph === "object" ? Idiomorph.morph(el, content,{morphStyle:'innerHTML'}) : el.innerHTML = content;
|
|
61
|
+
isUpdatingDOM=0
|
|
62
|
+
}
|
|
63
|
+
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));
|
|
64
|
+
else if(prop==="style"&&r&&typeof r==="object")el.style=Object.entries(r).filter(([,v])=>v).map(([k,v])=>`${k}:${v}`).join(";")
|
|
65
|
+
else if(prop in el){if(el.type==="radio")el.checked=el.value===r;else el[prop]=prop==="textContent"?r:parseEx(r)}
|
|
66
|
+
else el.setAttribute(prop,parseEx(r))
|
|
67
|
+
}
|
|
52
68
|
|
|
53
69
|
const compile = (expr, withReturn = false) => {
|
|
54
70
|
try {
|
|
55
|
-
return new Function("$",
|
|
56
|
-
`with($data){${withReturn
|
|
71
|
+
return new Function("$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",...Object.keys(data),...[...refs.keys()],
|
|
72
|
+
`with($data){${withReturn?"return":""}(${expr.trim()})}`);
|
|
57
73
|
} catch {return () => expr}
|
|
58
74
|
};
|
|
59
75
|
|
|
60
|
-
const trackDependencies=fn=>{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
const trackDependencies = (fn, el) => {
|
|
77
|
+
const accessed = new Set();
|
|
78
|
+
const trackProxy = new Proxy(data, {
|
|
79
|
+
get(target, prop) {
|
|
80
|
+
if (typeof prop == 'string') accessed.add(prop);
|
|
81
|
+
const val = target[prop];
|
|
82
|
+
return typeof val == "object" && val != null ? new Proxy(val, this) : val;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try { fn.call(null, $, trackProxy, refs); } catch {}
|
|
87
|
+
return [...accessed];
|
|
88
|
+
};
|
|
64
89
|
|
|
65
90
|
const cleanup = el => {
|
|
66
|
-
[el
|
|
67
|
-
listeners.get(e)?.forEach(({receiver,
|
|
91
|
+
[el,...el.querySelectorAll('*')].forEach(e => {
|
|
92
|
+
listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
|
|
68
93
|
listeners.delete(e);
|
|
69
94
|
});
|
|
70
95
|
};
|
|
@@ -72,8 +97,8 @@ const trackDependencies=fn=>{
|
|
|
72
97
|
function processElements(element) {
|
|
73
98
|
const newBindings = [];
|
|
74
99
|
if (element.nodeType == 1) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
100
|
+
if (processed.has(element)) return newBindings;
|
|
101
|
+
processed.add(element);
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
|
|
@@ -95,6 +120,9 @@ const trackDependencies=fn=>{
|
|
|
95
120
|
};
|
|
96
121
|
|
|
97
122
|
heElements.forEach(el => {
|
|
123
|
+
if (processed.has(el)) return;
|
|
124
|
+
processed.add(el);
|
|
125
|
+
|
|
98
126
|
const execFn = v => compile(v, true)($, state, {}, el, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()]);
|
|
99
127
|
const inputType = el.type?.toLowerCase();
|
|
100
128
|
const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
|
|
@@ -128,7 +156,7 @@ const trackDependencies=fn=>{
|
|
|
128
156
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
|
|
129
157
|
}
|
|
130
158
|
if (he(name,"init")) {
|
|
131
|
-
|
|
159
|
+
initFn = compile(value,true);
|
|
132
160
|
} else if (name.startsWith("@") || name.startsWith("data-he")) {
|
|
133
161
|
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
|
|
134
162
|
const [eventName, ...mods] = fullName.split(".");
|
|
@@ -151,14 +179,21 @@ const trackDependencies=fn=>{
|
|
|
151
179
|
}
|
|
152
180
|
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
153
181
|
if (isHttpMethod) {
|
|
154
|
-
const
|
|
155
|
-
|
|
182
|
+
const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
|
|
183
|
+
const [target, action] = (getAttr('target') || "").split(":");
|
|
184
|
+
const options = {
|
|
185
|
+
...exFn(getAttr('options') || '{}'),
|
|
186
|
+
...(target && { target }),
|
|
187
|
+
...(action && { action }),
|
|
188
|
+
...execFn(getAttr('template')) && { template: execFn(getAttr('template')) },
|
|
189
|
+
...getAttr('loading') && { loading: getAttr('loading') }
|
|
190
|
+
};
|
|
191
|
+
let paramsAttr = getAttr('params') || '{}';
|
|
156
192
|
if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
|
|
157
193
|
const props = paramsAttr.split(":").map(s => s.trim());
|
|
158
194
|
paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
|
|
159
195
|
}
|
|
160
196
|
const params = exFn(paramsAttr);
|
|
161
|
-
console.log("params: ",params)
|
|
162
197
|
ajax(value, eventName.toUpperCase(), options, params);
|
|
163
198
|
} else {
|
|
164
199
|
exFn(value);
|
|
@@ -176,12 +211,13 @@ const trackDependencies=fn=>{
|
|
|
176
211
|
return newBindings;
|
|
177
212
|
}
|
|
178
213
|
new MutationObserver(ms=>{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}).observe(root,{childList:1,subtree:1});
|
|
214
|
+
if(isUpdatingDOM)return;
|
|
215
|
+
for(const m of ms){
|
|
216
|
+
m.removedNodes.forEach(n=>n.nodeType===1&&cleanup(n));
|
|
217
|
+
m.addedNodes.forEach(n=>n.nodeType===1&&!processed.has(n)&&processElements(n).forEach(applyBinding));
|
|
218
|
+
}
|
|
219
|
+
}).observe(root,{childList:1,subtree:1});
|
|
185
220
|
processElements(root);
|
|
186
221
|
for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
|
|
222
|
+
if(initFn) initFn($, state, {}, {}, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()])
|
|
187
223
|
}
|