@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.
- package/helium.js +105 -159
- 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 = (
|
|
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 =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
};
|
|
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
|
-
|
|
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
|
-
}
|
|
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",
|
|
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 (
|
|
74
|
-
return () => expr;
|
|
75
|
-
}
|
|
57
|
+
} catch {return () => expr}
|
|
76
58
|
};
|
|
77
59
|
|
|
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
|
-
};
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
-
|
|
119
|
-
|
|
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,
|
|
133
|
-
|
|
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 = {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
} else if (name.startsWith("@") || name.startsWith("data-he
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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,
|
|
172
|
+
listeners.get(el).push({receiver, event, handler});
|
|
217
173
|
}
|
|
218
174
|
}
|
|
219
175
|
});
|
|
220
|
-
|
|
221
176
|
return newBindings;
|
|
222
177
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
}
|