@daz4126/helium 0.13.0 → 0.15.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 +165 -76
- package/package.json +1 -1
package/helium.js
CHANGED
|
@@ -6,7 +6,7 @@ export default function helium(data = {}) {
|
|
|
6
6
|
let initFn;
|
|
7
7
|
const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
|
|
8
8
|
const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
|
|
9
|
-
const [bindings, refs, listeners, processed] = [new Map(), new Map(), new WeakMap(), new WeakSet()];
|
|
9
|
+
const [bindings, refs, listeners, processed, parentKeys, fnCache, proxyCache] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap(), new Map(), new WeakMap()];
|
|
10
10
|
const $ = s => document.querySelector(s);
|
|
11
11
|
const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
|
|
12
12
|
|
|
@@ -17,8 +17,7 @@ export default function helium(data = {}) {
|
|
|
17
17
|
action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
|
|
18
18
|
return content
|
|
19
19
|
} else state[target] = data
|
|
20
|
-
}
|
|
21
|
-
|
|
20
|
+
}
|
|
22
21
|
|
|
23
22
|
const ajax = (u,m,o={},p={}) => {
|
|
24
23
|
if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
|
|
@@ -43,35 +42,116 @@ const ajax = (u,m,o={},p={}) => {
|
|
|
43
42
|
const get = (u,t) => ajax(u,"GET",t);
|
|
44
43
|
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
get
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const handler = {
|
|
46
|
+
get(t,p,r) {
|
|
47
|
+
const v = Reflect.get(t,p,r);
|
|
48
|
+
if (v && typeof v === "object") {
|
|
49
|
+
if (proxyCache.has(v)) return proxyCache.get(v);
|
|
50
|
+
const proxy = new Proxy(v, handler);
|
|
51
|
+
proxyCache.set(v, proxy);
|
|
52
|
+
parentKeys.set(v, p);
|
|
53
|
+
return proxy;
|
|
54
|
+
}
|
|
55
|
+
return v;
|
|
56
|
+
},
|
|
57
|
+
set: (t,p,v) => {
|
|
58
|
+
const res = Reflect.set(t,p,v);
|
|
59
|
+
if (Array.isArray(t) && !isNaN(p)) {
|
|
60
|
+
const parentKey = parentKeys.get(t);
|
|
61
|
+
if (parentKey) bindings.get(parentKey)?.forEach(applyBinding);
|
|
62
|
+
}
|
|
63
|
+
bindings.get(p)?.forEach(applyBinding);
|
|
64
|
+
return res
|
|
65
|
+
}
|
|
66
|
+
};
|
|
50
67
|
|
|
51
68
|
const state = new Proxy(data, handler);
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
|
|
54
70
|
function applyBinding(b,e={},elCtx=b.el){
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else el.setAttribute(prop,parseEx(r))
|
|
71
|
+
const {el,prop,fn}=b;
|
|
72
|
+
const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
|
|
73
|
+
|
|
74
|
+
if (prop==="innerHTML" && Array.isArray(r) && el.children.length > 0)
|
|
75
|
+
return updateList(el, r);
|
|
76
|
+
|
|
77
|
+
if (prop==="innerHTML") {
|
|
78
|
+
const content = Array.isArray(r)?r.join``:r;
|
|
79
|
+
return typeof Idiomorph === "object"
|
|
80
|
+
? Idiomorph.morph(el, content,{morphStyle:'innerHTML'})
|
|
81
|
+
: el.innerHTML = content;
|
|
67
82
|
}
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
if (prop==="class" && r && typeof r==="object")
|
|
85
|
+
return Object.entries(r).forEach(([k,v]) =>
|
|
86
|
+
k.split(/\s+/).forEach(c => el.classList.toggle(c,v)));
|
|
87
|
+
|
|
88
|
+
if (prop==="style" && r && typeof r==="object")
|
|
89
|
+
return el.style = Object.entries(r).filter(([,v])=>v)
|
|
90
|
+
.map(([k,v])=>`${k}:${v}`).join(";");
|
|
91
|
+
|
|
92
|
+
if (prop in el) {
|
|
93
|
+
if(el.type==="radio") el.checked = el.value===r;
|
|
94
|
+
else el[prop] = prop==="textContent"?r:parseEx(r);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
el.setAttribute(prop, parseEx(r));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
function updateList(el,r){
|
|
103
|
+
const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
|
|
104
|
+
const newChildren = [...temp.children];
|
|
105
|
+
|
|
106
|
+
const newItems = newChildren.map((child, idx) => ({
|
|
107
|
+
key: child.getAttribute("key") || child.dataset.key,
|
|
108
|
+
element: child,
|
|
109
|
+
index: idx
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const existingItems = [...el.children].map((child, idx) => ({
|
|
113
|
+
key: child.getAttribute("key") || child.dataset.key,
|
|
114
|
+
element: child,
|
|
115
|
+
index: idx
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < Math.max(newItems.length, existingItems.length); i++) {
|
|
119
|
+
const existing = existingItems[i];
|
|
120
|
+
const newItem = newItems[i];
|
|
121
|
+
|
|
122
|
+
if (!newItem && existing) {
|
|
123
|
+
cleanup(existing.element);
|
|
124
|
+
existing.element.remove();
|
|
125
|
+
} else if (newItem && !existing) {
|
|
126
|
+
el.appendChild(newItem.element);
|
|
127
|
+
} else if (newItem && existing) {
|
|
128
|
+
if (existing.element.outerHTML !== newItem.element.outerHTML) {
|
|
129
|
+
if(typeof Idiomorph === "object") {
|
|
130
|
+
Idiomorph.morph(existing.element, newItem.element.outerHTML);
|
|
131
|
+
} else {
|
|
132
|
+
cleanup(existing.element);
|
|
133
|
+
existing.element.replaceWith(newItem.element);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const compile = (expr, withReturn = false) => {
|
|
141
|
+
const key = `${withReturn}:${expr}`;
|
|
142
|
+
if (fnCache.has(key)) return fnCache.get(key);
|
|
143
|
+
try {
|
|
144
|
+
const fn = new Function(
|
|
145
|
+
"$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
|
|
146
|
+
...Object.keys(data), ...[...refs.keys()],
|
|
147
|
+
`with($data){${withReturn?"return":""}(${expr.trim()})}`
|
|
148
|
+
);
|
|
149
|
+
fnCache.set(key, fn);
|
|
150
|
+
return fn;
|
|
151
|
+
} catch {
|
|
152
|
+
return () => expr;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
75
155
|
|
|
76
156
|
const trackDependencies = (fn, el) => {
|
|
77
157
|
const accessed = new Set();
|
|
@@ -94,25 +174,11 @@ const trackDependencies = (fn, el) => {
|
|
|
94
174
|
});
|
|
95
175
|
};
|
|
96
176
|
|
|
97
|
-
|
|
177
|
+
function processElements(element) {
|
|
98
178
|
const newBindings = [];
|
|
99
|
-
if (element.nodeType == 1) {
|
|
100
|
-
if (processed.has(element)) return newBindings;
|
|
101
|
-
processed.add(element);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const heElements=[element,...element.querySelectorAll("*")].filter(e=>[...e.attributes].some(a=>/^(@|:|data-he)/.test(a.name)))
|
|
105
179
|
|
|
106
|
-
heElements.
|
|
107
|
-
|
|
108
|
-
if (he(name,"text","html","bind")) {
|
|
109
|
-
try {
|
|
110
|
-
new Function(`let ${value}=1`);
|
|
111
|
-
state[value] ||= he(name,"bind") ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
|
|
112
|
-
} catch {}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
});
|
|
180
|
+
const heElements = [element, ...element.querySelectorAll("*")]
|
|
181
|
+
.filter(e => !processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
|
|
116
182
|
|
|
117
183
|
const addBinding = (val, b) => {
|
|
118
184
|
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
@@ -120,75 +186,99 @@ const trackDependencies = (fn, el) => {
|
|
|
120
186
|
};
|
|
121
187
|
|
|
122
188
|
heElements.forEach(el => {
|
|
123
|
-
if (processed.has(el)) return;
|
|
124
189
|
processed.add(el);
|
|
125
190
|
|
|
126
|
-
const
|
|
191
|
+
const attrs = el.attributes;
|
|
192
|
+
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
127
193
|
const inputType = el.type?.toLowerCase();
|
|
128
194
|
const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
|
|
129
195
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
// Single loop through attributes with early skipping
|
|
197
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
198
|
+
const {name, value} = attrs[i];
|
|
199
|
+
|
|
200
|
+
// Skip non-helium attributes early
|
|
201
|
+
if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith('data-he')) continue;
|
|
202
|
+
|
|
203
|
+
// Initialize state first if needed
|
|
204
|
+
if (he(name, "text", "html", "bind")) {
|
|
205
|
+
try {
|
|
206
|
+
new Function(`let ${value}=1`);
|
|
207
|
+
state[value] ??= he(name, "bind") ? (el.type == "checkbox" ? el.checked : el.value) : el.textContent;
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process the attribute
|
|
212
|
+
if (["@data", "data-he"].includes(name)) {
|
|
213
|
+
Object.assign(state, execFn(value));
|
|
214
|
+
}
|
|
215
|
+
else if (he(name, "ref")) {
|
|
216
|
+
refs.set("$" + value, el);
|
|
217
|
+
}
|
|
218
|
+
else if (he(name, "text", "html")) {
|
|
134
219
|
const fn = compile(value, true);
|
|
135
|
-
const b = {el, prop: he(name,"text") ? "textContent" : "innerHTML", fn};
|
|
220
|
+
const b = {el, prop: he(name, "text") ? "textContent" : "innerHTML", fn};
|
|
136
221
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
|
|
137
222
|
}
|
|
138
|
-
if (he(name,"bind")) {
|
|
223
|
+
else if (he(name, "bind")) {
|
|
139
224
|
const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
|
|
140
225
|
const prop = isCheckbox ? "checked" : "value";
|
|
141
|
-
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
226
|
+
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
142
227
|
el.addEventListener(event, inputHandler);
|
|
143
228
|
if (!listeners.has(el)) listeners.set(el, []);
|
|
144
229
|
listeners.get(el).push({receiver: el, event, handler: inputHandler});
|
|
145
|
-
addBinding(value, {el, prop, fn: compile(value, true)});
|
|
230
|
+
addBinding(value, {el, prop, fn: compile(value, true)});
|
|
146
231
|
if (isCheckbox) el.checked = !!state[value];
|
|
147
232
|
else if (isRadio) el.checked = el.value == state[value];
|
|
148
233
|
else el.value = state[value] ?? "";
|
|
149
234
|
}
|
|
150
|
-
if (he(name,"hidden","visible")) {
|
|
151
|
-
const fn = compile(`${he(name,"hidden") ? "!" : ""}!(${value})`, true);
|
|
235
|
+
else if (he(name, "hidden", "visible")) {
|
|
236
|
+
const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
|
|
152
237
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
|
|
153
238
|
}
|
|
154
|
-
if (name
|
|
239
|
+
else if (he(name, "class")) {
|
|
240
|
+
const fn = compile(value, true);
|
|
241
|
+
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "class", fn}));
|
|
242
|
+
}
|
|
243
|
+
else if (name.startsWith(":")) {
|
|
155
244
|
const fn = compile(value, true);
|
|
156
245
|
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
|
|
157
246
|
}
|
|
158
|
-
if (he(name,"init")) {
|
|
159
|
-
initFn = compile(value,true);
|
|
160
|
-
}
|
|
247
|
+
else if (he(name, "init")) {
|
|
248
|
+
initFn = compile(value, true);
|
|
249
|
+
}
|
|
250
|
+
else if (name.startsWith("@") || name.startsWith("data-he")) {
|
|
161
251
|
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
|
|
162
252
|
const [eventName, ...mods] = fullName.split(".");
|
|
163
|
-
const isHttpMethod = ["get","post","put","patch","delete"].includes(eventName);
|
|
253
|
+
const isHttpMethod = ["get", "post", "put", "patch", "delete"].includes(eventName);
|
|
164
254
|
const event = isHttpMethod ? getEvent(el) : eventName;
|
|
165
|
-
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
255
|
+
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
166
256
|
const debounceMod = mods.find(m => m.startsWith("debounce"));
|
|
167
257
|
const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
|
|
168
258
|
const _handler = e => {
|
|
169
|
-
const exFn = v => compile(v, true)($, state, e, el, html,get,post,put,patch,del, ...Object.values(data), ...[...refs.values()])
|
|
170
|
-
if (mods.includes("prevent")) e.preventDefault();
|
|
259
|
+
const exFn = v => compile(v, true)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()])
|
|
260
|
+
if (mods.includes("prevent")) e.preventDefault();
|
|
171
261
|
const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
|
|
172
|
-
for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
|
|
173
|
-
if (["keydown","keyup","keypress"].includes(event)) {
|
|
262
|
+
for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
|
|
263
|
+
if (["keydown", "keyup", "keypress"].includes(event)) {
|
|
174
264
|
const last = mods[mods.length - 1];
|
|
175
|
-
if (last && !["prevent","once","outside","document"].includes(last)) {
|
|
265
|
+
if (last && !["prevent", "once", "outside", "document"].includes(last)) {
|
|
176
266
|
const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
|
|
177
267
|
if (keyName.toLowerCase() !== last.toLowerCase()) return;
|
|
178
268
|
}
|
|
179
|
-
}
|
|
180
|
-
|
|
269
|
+
}
|
|
270
|
+
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
181
271
|
if (isHttpMethod) {
|
|
182
272
|
const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
|
|
183
273
|
const [target, action] = (getAttr('target') || "").split(":");
|
|
184
274
|
const options = {
|
|
185
275
|
...exFn(getAttr('options') || '{}'),
|
|
186
|
-
...(target && {
|
|
187
|
-
...(action && {
|
|
188
|
-
...execFn(getAttr('template')) && {
|
|
189
|
-
...getAttr('loading') && {
|
|
276
|
+
...(target && {target}),
|
|
277
|
+
...(action && {action}),
|
|
278
|
+
...execFn(getAttr('template')) && {template: execFn(getAttr('template'))},
|
|
279
|
+
...getAttr('loading') && {loading: getAttr('loading')}
|
|
190
280
|
};
|
|
191
|
-
let paramsAttr = getAttr('params') || '{}';
|
|
281
|
+
let paramsAttr = getAttr('params') || '{}';
|
|
192
282
|
if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
|
|
193
283
|
const props = paramsAttr.split(":").map(s => s.trim());
|
|
194
284
|
paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
|
|
@@ -198,7 +288,7 @@ const trackDependencies = (fn, el) => {
|
|
|
198
288
|
} else {
|
|
199
289
|
exFn(value);
|
|
200
290
|
}
|
|
201
|
-
}
|
|
291
|
+
}
|
|
202
292
|
if (mods.includes("once")) receiver.removeEventListener(event, handler);
|
|
203
293
|
};
|
|
204
294
|
const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
@@ -211,7 +301,6 @@ const trackDependencies = (fn, el) => {
|
|
|
211
301
|
return newBindings;
|
|
212
302
|
}
|
|
213
303
|
new MutationObserver(ms=>{
|
|
214
|
-
if(isUpdatingDOM)return;
|
|
215
304
|
for(const m of ms){
|
|
216
305
|
m.removedNodes.forEach(n=>n.nodeType===1&&cleanup(n));
|
|
217
306
|
m.addedNodes.forEach(n=>n.nodeType===1&&!processed.has(n)&&processElements(n).forEach(applyBinding));
|