@daz4126/helium 0.9.0 → 0.10.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 +121 -101
- package/package.json +1 -1
package/helium.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export default function helium(data = {}) {
|
|
2
|
+
const he = (...args) => args.flatMap(b => [`@${b}`,`data-he-${b}`])
|
|
2
3
|
const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
|
|
3
|
-
const [bindings, refs] = [new Map(), new Map()];
|
|
4
|
+
const [bindings, refs, listeners] = [new Map(), new Map(), new WeakMap()];
|
|
4
5
|
const $ = (s) => document.querySelector(s);
|
|
5
6
|
const html = (s) => {
|
|
6
7
|
const t = document.createElement("template");
|
|
@@ -11,11 +12,10 @@ export default function helium(data = {}) {
|
|
|
11
12
|
const ajax = (url, method, target, opts = {}) => {
|
|
12
13
|
const headers = {
|
|
13
14
|
"Content-Type": "application/json",
|
|
14
|
-
"Accept": "text/vnd.turbo-stream.html,
|
|
15
|
+
"Accept": "text/vnd.turbo-stream.html,application/json,text/html"
|
|
15
16
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (document.querySelector('meta[name="csrf-token"]')?.content) headers["X-CSRF-Token"] = csrfToken;
|
|
17
|
+
const token = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
18
|
+
if (token) headers["X-CSRF-Token"] = token;
|
|
19
19
|
|
|
20
20
|
fetch(url, {
|
|
21
21
|
method,
|
|
@@ -23,35 +23,25 @@ export default function helium(data = {}) {
|
|
|
23
23
|
body: method === "GET" ? null : JSON.stringify(opts),
|
|
24
24
|
credentials: "same-origin"
|
|
25
25
|
})
|
|
26
|
-
.then(r =>
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
.then(d => state[target] = d)
|
|
26
|
+
.then(r => r.headers.get("content-type")?.includes("application/json") ? r.json() : r.text())
|
|
27
|
+
.then(d => {if(target) state[target] = d})
|
|
30
28
|
.catch(e => console.error("AJAX error:", e.message));
|
|
31
29
|
};
|
|
32
30
|
|
|
33
31
|
const get = (u,t) => ajax(u,"GET",t);
|
|
34
|
-
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(
|
|
32
|
+
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
|
|
35
33
|
|
|
36
34
|
const handler = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} else {
|
|
46
|
-
for (const [key, bound] of bindings.entries()) {
|
|
47
|
-
if (key === Object.keys(data).find(k => data[k] === target)) {
|
|
48
|
-
bound.forEach(applyBinding);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
35
|
+
get(target, prop, receiver) {
|
|
36
|
+
const val = Reflect.get(target, prop, receiver);
|
|
37
|
+
return typeof val === "object" && val !== null ? new Proxy(val, handler) : val;
|
|
38
|
+
},
|
|
39
|
+
set(target, prop, val) {
|
|
40
|
+
const result = Reflect.set(target, prop, val);
|
|
41
|
+
bindings.get(prop)?.forEach(applyBinding);
|
|
42
|
+
return result;
|
|
51
43
|
}
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
};
|
|
44
|
+
};
|
|
55
45
|
|
|
56
46
|
const state = new Proxy(data, handler);
|
|
57
47
|
let isUpdatingDOM = false;
|
|
@@ -63,120 +53,138 @@ export default function helium(data = {}) {
|
|
|
63
53
|
isUpdatingDOM = true;
|
|
64
54
|
el.innerHTML = result;
|
|
65
55
|
isUpdatingDOM = false;
|
|
66
|
-
} else if(prop == "class" && typeof
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
el.classList.add(...add);
|
|
75
|
-
el.classList.remove(...remove);
|
|
76
|
-
} else if(prop == "style" && typeof(result) == "object"){
|
|
77
|
-
el.style = Object.entries(result).map(([key, value]) => value ? `${key}: ${value};` : "").join``
|
|
78
|
-
} else if (prop in el) {
|
|
79
|
-
el[prop] = result;
|
|
56
|
+
} else if(prop == "class" && typeof result == "object"){
|
|
57
|
+
Object.entries(result).forEach(([k, v]) => el.classList.toggle(k, v));
|
|
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;
|
|
80
63
|
} else {
|
|
81
64
|
el.setAttribute(prop, result);
|
|
82
65
|
}
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
|
|
68
|
+
const compile = (expr, withReturn = false) => {
|
|
86
69
|
try {
|
|
87
70
|
return new Function("$", "$data", "$event", "$el", "$html", "$get", "$post", "$put", "$patch", "$delete",
|
|
88
71
|
...Object.keys(data), ...[...refs.keys()],
|
|
89
|
-
`with($data)
|
|
72
|
+
`with($data){${withReturn ? "return" : ""}(${expr.trim()})}`);
|
|
90
73
|
} catch (err) {
|
|
91
74
|
return () => expr;
|
|
92
75
|
}
|
|
93
|
-
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const trackDependencies = (fn, el) => {
|
|
79
|
+
const accessed = new Set();
|
|
80
|
+
const trackProxy = new Proxy(data, {
|
|
81
|
+
get(target, prop) {
|
|
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
|
+
};
|
|
91
|
+
|
|
92
|
+
const cleanup = el => {
|
|
93
|
+
[el, ...el.querySelectorAll('*')].forEach(e => {
|
|
94
|
+
listeners.get(e)?.forEach(({receiver, eventName, handler}) =>
|
|
95
|
+
receiver.removeEventListener(eventName, handler));
|
|
96
|
+
listeners.delete(e);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
94
99
|
|
|
95
100
|
function processElements(element) {
|
|
96
101
|
const newBindings = [];
|
|
97
|
-
if (element.nodeType === 1
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Array.from(el.attributes).some((attr) => attr.name.startsWith("data-he-")))];
|
|
102
|
+
if (element.nodeType === 1) {
|
|
103
|
+
if (element.hasAttribute?.("data-he-p")) return newBindings;
|
|
104
|
+
element.setAttribute("data-he-p", "");
|
|
105
|
+
}
|
|
102
106
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const heElements = [element, ...element.querySelectorAll("*")].filter(el =>
|
|
108
|
+
Array.from(el.attributes).some(attr => /^(@|:|data-he-)/.test(attr.name)
|
|
109
|
+
)
|
|
110
|
+
);
|
|
106
111
|
|
|
107
112
|
// Seed default state values
|
|
108
|
-
heElements.forEach(
|
|
113
|
+
heElements.forEach(el => {
|
|
109
114
|
for (const { name, value } of el.attributes || []) {
|
|
110
|
-
if (
|
|
115
|
+
if (he("text","html","bind").includes(name)) {
|
|
111
116
|
try {
|
|
112
|
-
new Function(`let ${value}
|
|
113
|
-
state[value] ||=
|
|
117
|
+
new Function(`let ${value}=1`);
|
|
118
|
+
state[value] ||= he("bind").includes(name) ? el.type == "checkbox" ? el.checked : el.value : el.textContent;
|
|
114
119
|
} catch (e) {}
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
});
|
|
118
123
|
|
|
124
|
+
const addBinding = (val, b) => {
|
|
125
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
126
|
+
newBindings.push(b);
|
|
127
|
+
};
|
|
119
128
|
|
|
120
129
|
// Register bindings
|
|
121
|
-
heElements.forEach(
|
|
122
|
-
|
|
130
|
+
heElements.forEach(el => {
|
|
131
|
+
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
123
132
|
for (const { name, value } of el.attributes || []) {
|
|
124
133
|
if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
newBindings.push(b);
|
|
132
|
-
});
|
|
134
|
+
if (he("ref").includes(name)) refs.set("$" + value, el);
|
|
135
|
+
|
|
136
|
+
if (he("text","html").includes(name)) {
|
|
137
|
+
const fn = compile(value, true);
|
|
138
|
+
const b = { el, prop: he("text").includes(name) ? "textContent" : "innerHTML", fn };
|
|
139
|
+
trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
if (he("bind").includes(name)) {
|
|
143
|
+
const inputType = el.type?.toLowerCase();
|
|
144
|
+
const isCheckbox = inputType === "checkbox";
|
|
145
|
+
const isRadio = inputType === "radio";
|
|
146
|
+
const event = (isCheckbox || isRadio || el.tagName === "SELECT") ? "change" : "input";
|
|
147
|
+
const prop = isCheckbox ? "checked" : "value";
|
|
148
|
+
|
|
149
|
+
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
150
|
+
el.addEventListener(event, inputHandler);
|
|
151
|
+
if (!listeners.has(el)) listeners.set(el, []);
|
|
152
|
+
listeners.get(el).push({receiver: el, eventName: event, handler: inputHandler});
|
|
153
|
+
|
|
154
|
+
addBinding(value, { el, prop, fn: compile(value, true) });
|
|
155
|
+
|
|
156
|
+
if (isCheckbox) el.checked = !!state[value];
|
|
157
|
+
else if (isRadio) el.checked = el.value == state[value];
|
|
158
|
+
else el.value = state[value] ?? "";
|
|
141
159
|
}
|
|
142
160
|
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
161
|
+
if (he("hidden","visible").includes(name)) {
|
|
162
|
+
const fn = compile(`${he("hidden").includes(name) ? "!" : ""}!(${value})`, true);
|
|
163
|
+
const b = { el, prop: "hidden", fn };
|
|
164
|
+
|
|
165
|
+
trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
if (name.startsWith(":")) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
newBindings.push(b);
|
|
156
|
-
});
|
|
169
|
+
const fn = compile(value, true);
|
|
170
|
+
const b = { el, prop: name.slice(1), fn };
|
|
171
|
+
trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
|
|
157
172
|
}
|
|
158
173
|
|
|
159
|
-
if (
|
|
160
|
-
|
|
174
|
+
if (he("init").includes(name)) {
|
|
175
|
+
compile(value, false)($, state, undefined, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
161
176
|
} else if (name.startsWith("@") || name.startsWith("data-he-on")) {
|
|
162
177
|
const [eventName, ...mods] = name.slice(name.startsWith("@") ? 1 : 10).split(".");
|
|
163
178
|
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
164
|
-
|
|
165
|
-
let timeout;
|
|
166
|
-
return (...args) => {
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
timeout = setTimeout(() => fn(...args), delay);
|
|
169
|
-
};
|
|
170
|
-
};
|
|
171
|
-
|
|
179
|
+
|
|
172
180
|
let debounceDelay = 0;
|
|
173
|
-
const debounceMod = mods.find(
|
|
181
|
+
const debounceMod = mods.find(m => m.startsWith("debounce"));
|
|
174
182
|
if (debounceMod) {
|
|
175
183
|
const time = debounceMod.split(":")[1];
|
|
176
184
|
debounceDelay = time && !isNaN(time) ? Number(time) : 300;
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
|
|
187
|
+
const _handler = e => {
|
|
180
188
|
if (mods.includes("prevent")) e.preventDefault();
|
|
181
189
|
const keyMods = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
|
|
182
190
|
for (const [mod, prop] of Object.entries(keyMods)) {
|
|
@@ -190,13 +198,23 @@ el.style = Object.entries(result).map(([key, value]) => value ? `${key}: ${value
|
|
|
190
198
|
}
|
|
191
199
|
}
|
|
192
200
|
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
193
|
-
|
|
201
|
+
compile(value, false)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
194
202
|
}
|
|
195
203
|
if (mods.includes("once")) el.removeEventListener(eventName, debouncedHandler);
|
|
196
|
-
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const debounce = (fn, delay) => {
|
|
207
|
+
let timeout;
|
|
208
|
+
return (...args) => {
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
timeout = setTimeout(() => fn(...args), delay);
|
|
211
|
+
};
|
|
212
|
+
};
|
|
197
213
|
|
|
198
214
|
const debouncedHandler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
199
215
|
receiver.addEventListener(eventName, debouncedHandler);
|
|
216
|
+
if (!listeners.has(el)) listeners.set(el, []);
|
|
217
|
+
listeners.get(el).push({receiver, eventName, handler: debouncedHandler});
|
|
200
218
|
}
|
|
201
219
|
}
|
|
202
220
|
});
|
|
@@ -204,13 +222,15 @@ el.style = Object.entries(result).map(([key, value]) => value ? `${key}: ${value
|
|
|
204
222
|
return newBindings;
|
|
205
223
|
}
|
|
206
224
|
|
|
207
|
-
const observer = new MutationObserver(
|
|
225
|
+
const observer = new MutationObserver(mutations => {
|
|
208
226
|
if (isUpdatingDOM) return;
|
|
209
227
|
for (const m of mutations) {
|
|
228
|
+
for (const node of m.removedNodes) {
|
|
229
|
+
if (node.nodeType === 1) cleanup(node);
|
|
230
|
+
}
|
|
210
231
|
for (const node of m.addedNodes) {
|
|
211
|
-
if (node.nodeType === 1 && !node.hasAttribute("data-
|
|
212
|
-
|
|
213
|
-
newly.forEach(applyBinding);
|
|
232
|
+
if (node.nodeType === 1 && !node.hasAttribute("data-he-p")) {
|
|
233
|
+
processElements(node).forEach(applyBinding);
|
|
214
234
|
}
|
|
215
235
|
}
|
|
216
236
|
}
|