@daz4126/helium 1.0.0-beta.5 → 1.0.0-rc.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/package.json
CHANGED
|
@@ -1,629 +0,0 @@
|
|
|
1
|
-
const parseEx=v=>{try{return Function(`return(${v})`)()}catch{return v}}
|
|
2
|
-
const tryNum=v=>{const t=String(v).trim(),n=+t;return t&&!isNaN(n)?n:v}
|
|
3
|
-
const isValidIdentifier = v => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
|
|
4
|
-
const RESERVED = new Set(['undefined', 'null', 'true', 'false', 'NaN', 'Infinity', 'this', 'arguments']);
|
|
5
|
-
const INPUT_EVENTS = { form: "submit", input: "input", textarea:"input", select:"change" };
|
|
6
|
-
const KEY_MODS = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
|
|
7
|
-
const getEvent = el => INPUT_EVENTS[el.tagName.toLowerCase()] || "click";
|
|
8
|
-
const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))}
|
|
9
|
-
|
|
10
|
-
// Default expression engine using new Function()
|
|
11
|
-
const defaultEngine = {
|
|
12
|
-
compile(expr, withReturn = true) {
|
|
13
|
-
try {
|
|
14
|
-
const fn = new Function(
|
|
15
|
-
"$scope",
|
|
16
|
-
withReturn
|
|
17
|
-
? `with($scope){with($scope.$data){return(${expr.trim()})}}`
|
|
18
|
-
: `with($scope){with($scope.$data){${expr.trim()}}}`
|
|
19
|
-
);
|
|
20
|
-
return {
|
|
21
|
-
execute: (scope) => fn(scope),
|
|
22
|
-
getIds: null // Will use trackDependencies instead
|
|
23
|
-
};
|
|
24
|
-
} catch {
|
|
25
|
-
return {
|
|
26
|
-
execute: () => expr,
|
|
27
|
-
getIds: null
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
createScope(ctx) {
|
|
33
|
-
return {
|
|
34
|
-
$: ctx.$,
|
|
35
|
-
$data: ctx.state,
|
|
36
|
-
$event: ctx.event,
|
|
37
|
-
$el: ctx.el,
|
|
38
|
-
$html: ctx.html,
|
|
39
|
-
$get: ctx.get,
|
|
40
|
-
$post: ctx.post,
|
|
41
|
-
$put: ctx.put,
|
|
42
|
-
$patch: ctx.patch,
|
|
43
|
-
$delete: ctx.del,
|
|
44
|
-
...ctx.refs
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// Factory function to create helium with custom engine
|
|
50
|
-
export function createHelium(options = {}) {
|
|
51
|
-
const engine = options.engine ?? defaultEngine;
|
|
52
|
-
|
|
53
|
-
// Engine-aware expression parser (CSP-safe when using jexpr engine)
|
|
54
|
-
const evalExpr = v => {
|
|
55
|
-
try {
|
|
56
|
-
const compiled = engine.compile(v, true);
|
|
57
|
-
const scope = engine.createScope ? engine.createScope({ state: {}, el: null, event: {}, refs: {}, $: null, html: null, get: null, post: null, put: null, patch: null, del: null }) : {};
|
|
58
|
-
return compiled.execute(scope);
|
|
59
|
-
} catch { return v; }
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const match = (name, ...attrs) => {
|
|
63
|
-
const p = name.split(/[.:]/)[0];
|
|
64
|
-
if (p === ":" || p === "") return false;
|
|
65
|
-
return attrs.some(a => p === `@${a}` || p === 'data-he-' + a);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Single global object to hold all settings (scoped to this instance)
|
|
69
|
-
let HELIUM = null;
|
|
70
|
-
|
|
71
|
-
async function helium(initialState = {}) {
|
|
72
|
-
const inits = [];
|
|
73
|
-
const ALL = Symbol("all");
|
|
74
|
-
const root = document.querySelector('[\\@helium]') || document.querySelector('[data-helium]') || document.body;
|
|
75
|
-
const storageKey = root.getAttribute('@local-storage') || root.getAttribute('data-he-local-storage');
|
|
76
|
-
|
|
77
|
-
// Initialize or reuse HELIUM object
|
|
78
|
-
if (!HELIUM) {
|
|
79
|
-
HELIUM = {
|
|
80
|
-
observer: null,
|
|
81
|
-
bindings: new Map(),
|
|
82
|
-
refs: new Map(),
|
|
83
|
-
listeners: new WeakMap(),
|
|
84
|
-
processed: new WeakSet(),
|
|
85
|
-
parentKeys: new WeakMap(),
|
|
86
|
-
fnCache: new Map(),
|
|
87
|
-
proxyCache: new WeakMap()
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const $ = s => document.querySelector(s);
|
|
92
|
-
const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
|
|
93
|
-
|
|
94
|
-
const update = (data,targets,actions,template) => {
|
|
95
|
-
const newTargets = [];
|
|
96
|
-
(targets || []).forEach((target,i) => {
|
|
97
|
-
const element = target instanceof Node ? target : (HELIUM.refs.get(target.trim()) || $(target.trim()));
|
|
98
|
-
if(element){
|
|
99
|
-
const content = template ? template(data) : data;
|
|
100
|
-
const htmlContent = html(content)
|
|
101
|
-
actions?.[i] ? element[actions[i]=="replace"?"replaceWith":actions[i]](htmlContent) : element.innerHTML = content;
|
|
102
|
-
newTargets.push(actions?.[i] ? htmlContent : element)
|
|
103
|
-
} else state[target] = data
|
|
104
|
-
})
|
|
105
|
-
return newTargets
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// SSE handling with retry support
|
|
109
|
-
const handleSSE = async (res, options, reconnect) => {
|
|
110
|
-
const reader = res.body.getReader();
|
|
111
|
-
const decoder = new TextDecoder();
|
|
112
|
-
let buffer = '', lastId = null, retryDelay = options.retry || 3000;
|
|
113
|
-
|
|
114
|
-
const processEvents = () => {
|
|
115
|
-
const parts = buffer.split('\n\n');
|
|
116
|
-
buffer = parts.pop();
|
|
117
|
-
for (const block of parts) {
|
|
118
|
-
if (!block.trim()) continue;
|
|
119
|
-
let data = '', id = null;
|
|
120
|
-
for (const line of block.split('\n')) {
|
|
121
|
-
if (line.startsWith('data:')) data += (data ? '\n' : '') + line.slice(5).trimStart();
|
|
122
|
-
else if (line.startsWith('id:')) id = line.slice(3).trim();
|
|
123
|
-
else if (line.startsWith('retry:')) retryDelay = parseInt(line.slice(6).trim()) || retryDelay;
|
|
124
|
-
}
|
|
125
|
-
if (id) lastId = id;
|
|
126
|
-
if (data) {
|
|
127
|
-
update(data, options.target, options.action, options.template);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
while (true) {
|
|
134
|
-
const {done, value} = await reader.read();
|
|
135
|
-
if (done) break;
|
|
136
|
-
buffer += decoder.decode(value, {stream: true});
|
|
137
|
-
processEvents();
|
|
138
|
-
}
|
|
139
|
-
// Process any remaining data
|
|
140
|
-
if (buffer.trim()) { buffer += '\n\n'; processEvents(); }
|
|
141
|
-
// Reconnect if configured
|
|
142
|
-
if (options.retryMode === 'always') setTimeout(() => reconnect(lastId), retryDelay);
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.error("SSE:", e.message);
|
|
145
|
-
if (options.retryMode === 'always' || options.retryMode === 'error') {
|
|
146
|
-
setTimeout(() => reconnect(lastId), retryDelay);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const ajax = (url,method,options={},params={}) => {
|
|
152
|
-
if(options.loading) {
|
|
153
|
-
const loadingTargets = update(options.loading,options.target,options.action);
|
|
154
|
-
if(loadingTargets.length) options.target = loadingTargets;
|
|
155
|
-
}
|
|
156
|
-
const fd = params instanceof FormData, token = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
157
|
-
const path = new URL(url, window.location.href);
|
|
158
|
-
const sameOrigin = path.origin === window.location.origin;
|
|
159
|
-
|
|
160
|
-
const doFetch = (lastEventId) => fetch(url, {
|
|
161
|
-
method,
|
|
162
|
-
headers: {
|
|
163
|
-
Accept:"text/event-stream,text/vnd.turbo-stream.html,application/json,text/html",
|
|
164
|
-
...(!fd && method !== "GET" && {"Content-Type":"application/json"}),
|
|
165
|
-
...(sameOrigin && token ? {"X-CSRF-Token": token} : {}),
|
|
166
|
-
...(lastEventId ? {"Last-Event-ID": lastEventId} : {})
|
|
167
|
-
},
|
|
168
|
-
body: method === "GET" ? null : (fd ? params : JSON.stringify(params)),
|
|
169
|
-
credentials: sameOrigin ? "same-origin" : "omit"
|
|
170
|
-
})
|
|
171
|
-
.then(res => {
|
|
172
|
-
const type = res.headers.get("content-type") || "";
|
|
173
|
-
if (type.includes("event-stream")) return handleSSE(res, options, doFetch);
|
|
174
|
-
return (type.includes("turbo-stream") ? res.text().then(data => ({ turbo: true, data })) :
|
|
175
|
-
type.includes("json") ? res.json() :
|
|
176
|
-
res.text());
|
|
177
|
-
}).then(data => {
|
|
178
|
-
if (data === undefined) return; // SSE handled separately
|
|
179
|
-
return (data.turbo && window.Turbo)
|
|
180
|
-
? Turbo.renderStreamMessage(data.data)
|
|
181
|
-
: update(data, options.target, options.loading ? (options.action || []).map(a => a && "replace") : options.action, options.template)
|
|
182
|
-
}).catch(e => console.error("AJAX:", e.message));
|
|
183
|
-
|
|
184
|
-
doFetch(null);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const opts = o => typeof o === "string" ? { target: [o], action: [null] } : o;
|
|
188
|
-
const get = (url, options={}) => ajax(url, "GET", opts(options));
|
|
189
|
-
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(method => (url, params, options={}) => ajax(url, method, opts(options), params));
|
|
190
|
-
|
|
191
|
-
// Track bindings currently being applied to prevent infinite recursion
|
|
192
|
-
const applyingBindings = new Set();
|
|
193
|
-
|
|
194
|
-
const safeApplyBinding = (b) => {
|
|
195
|
-
if (applyingBindings.has(b)) return;
|
|
196
|
-
applyingBindings.add(b);
|
|
197
|
-
try {
|
|
198
|
-
applyBinding(b);
|
|
199
|
-
} finally {
|
|
200
|
-
applyingBindings.delete(b);
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const handler = {
|
|
205
|
-
// 'has' trap needed for 'with' statement - it uses 'in' operator to check property existence
|
|
206
|
-
// Return false for $-prefixed names so they fall through to outer scope
|
|
207
|
-
// (where $el, $event, $html, and refs like $form are defined without proxy wrapping)
|
|
208
|
-
has(t, p) {
|
|
209
|
-
if (typeof p === 'string' && p.startsWith('$')) return false;
|
|
210
|
-
if (Reflect.has(t, p)) return true;
|
|
211
|
-
if (typeof globalThis[p] !== 'undefined') return false; // Allow globals like Date, Math, Array, console
|
|
212
|
-
return true;
|
|
213
|
-
},
|
|
214
|
-
get(t,p,r) {
|
|
215
|
-
const v = Reflect.get(t,p,r);
|
|
216
|
-
if (v && typeof v === "object") {
|
|
217
|
-
if (HELIUM.proxyCache.has(v)) return HELIUM.proxyCache.get(v);
|
|
218
|
-
const proxy = new Proxy(v, handler);
|
|
219
|
-
HELIUM.proxyCache.set(v, proxy);
|
|
220
|
-
HELIUM.parentKeys.set(v, p);
|
|
221
|
-
return proxy;
|
|
222
|
-
}
|
|
223
|
-
return v;
|
|
224
|
-
},
|
|
225
|
-
set: (t,p,v) => {
|
|
226
|
-
const res = Reflect.set(t,p,v);
|
|
227
|
-
// Trigger bindings for this property
|
|
228
|
-
HELIUM.bindings.get(p)?.forEach(safeApplyBinding);
|
|
229
|
-
// Also trigger parent object's bindings (for nested updates like user.name)
|
|
230
|
-
const parentKey = HELIUM.parentKeys.get(t);
|
|
231
|
-
if (parentKey) HELIUM.bindings.get(parentKey)?.forEach(safeApplyBinding);
|
|
232
|
-
HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
|
|
233
|
-
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state));
|
|
234
|
-
return res
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// Initialize state if it doesn't exist
|
|
239
|
-
const state = new Proxy({}, handler);
|
|
240
|
-
|
|
241
|
-
// Load from localStorage if key exists
|
|
242
|
-
if (storageKey) try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey))); } catch {}
|
|
243
|
-
|
|
244
|
-
// Merge initial state
|
|
245
|
-
Object.assign(state, initialState);
|
|
246
|
-
|
|
247
|
-
// Build refs object for scope
|
|
248
|
-
const getRefsObject = () => {
|
|
249
|
-
const refs = {};
|
|
250
|
-
for (const [name, element] of HELIUM.refs) {
|
|
251
|
-
refs[name] = element;
|
|
252
|
-
}
|
|
253
|
-
return refs;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Create scope for expression execution
|
|
257
|
-
const createScope = (el, event = {}) => {
|
|
258
|
-
return engine.createScope({
|
|
259
|
-
$,
|
|
260
|
-
state,
|
|
261
|
-
event,
|
|
262
|
-
el,
|
|
263
|
-
html,
|
|
264
|
-
get,
|
|
265
|
-
post,
|
|
266
|
-
put,
|
|
267
|
-
patch,
|
|
268
|
-
del,
|
|
269
|
-
refs: getRefsObject()
|
|
270
|
-
});
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
// Compile expression with caching
|
|
274
|
-
const compile = (expr, withReturn = true) => {
|
|
275
|
-
const key = `${withReturn}:${expr}`;
|
|
276
|
-
if (HELIUM.fnCache.has(key)) return HELIUM.fnCache.get(key);
|
|
277
|
-
const compiled = engine.compile(expr, withReturn);
|
|
278
|
-
HELIUM.fnCache.set(key, compiled);
|
|
279
|
-
return compiled;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
function applyBinding(b,e={},elCtx=b.el){
|
|
283
|
-
const {el,prop,compiled,calc,isHiddenAttr,isRadio}=b;
|
|
284
|
-
const scope = createScope(elCtx, e);
|
|
285
|
-
const r = compiled.execute(scope);
|
|
286
|
-
if (calc) state[calc] = r
|
|
287
|
-
|
|
288
|
-
if (prop==="innerHTML") {
|
|
289
|
-
const content = Array.isArray(r)?r.join``:r;
|
|
290
|
-
return typeof Idiomorph === "object"
|
|
291
|
-
? Idiomorph.morph(el, content,{morphStyle:'innerHTML'})
|
|
292
|
-
: el.innerHTML = content;
|
|
293
|
-
}
|
|
294
|
-
if (prop==="class" && r && typeof r==="object")
|
|
295
|
-
return Object.entries(r).forEach(([k,v]) =>
|
|
296
|
-
k.split(/\s+/).forEach(c => el.classList.toggle(c,v)));
|
|
297
|
-
|
|
298
|
-
if (prop==="style" && r && typeof r==="object")
|
|
299
|
-
return el.style.cssText = Object.entries(r)
|
|
300
|
-
.filter(([_, v]) => v != null && v !== false)
|
|
301
|
-
.map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
|
|
302
|
-
.join("; ");
|
|
303
|
-
|
|
304
|
-
if (prop === "hidden") {
|
|
305
|
-
// @hidden="x" -> hide when x is truthy
|
|
306
|
-
// @visible="x" -> hide when x is falsy (invert via isHiddenAttr)
|
|
307
|
-
el.hidden = isHiddenAttr ? !!r : !r;
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (prop in el) {
|
|
312
|
-
if (isRadio && prop !== "checked") el.checked = el.value===r;
|
|
313
|
-
else if (el.type === "radio" && prop !== "checked") el.checked = el.value===r;
|
|
314
|
-
else el[prop] = prop==="textContent" ? (r ?? '') : parseEx(r);
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
el.setAttribute(prop, parseEx(r));
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Track dependencies using proxy (fallback when getIds is not available)
|
|
322
|
-
const trackDependenciesProxy = (compiled, el, excludeChanged = false) => {
|
|
323
|
-
const accessed = excludeChanged ? new Map() : new Set();
|
|
324
|
-
const trackProxy = new Proxy(state, {
|
|
325
|
-
// 'has' trap is needed for 'with' statement to work correctly
|
|
326
|
-
// Without it, 'with' checks if property exists using 'in' operator,
|
|
327
|
-
// and if not found, continues up scope chain causing ReferenceError
|
|
328
|
-
has(target, prop) {
|
|
329
|
-
return true;
|
|
330
|
-
},
|
|
331
|
-
get(target, prop) {
|
|
332
|
-
if (typeof prop == 'string') {
|
|
333
|
-
excludeChanged ? !accessed.has(prop) && accessed.set(prop, target[prop]) : accessed.add(prop);
|
|
334
|
-
}
|
|
335
|
-
const val = target[prop];
|
|
336
|
-
return typeof val == "object" && val != null ? new Proxy(val, this) : val;
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const scope = engine.createScope({
|
|
342
|
-
$,
|
|
343
|
-
state: trackProxy,
|
|
344
|
-
event: {},
|
|
345
|
-
el,
|
|
346
|
-
html,
|
|
347
|
-
get,
|
|
348
|
-
post,
|
|
349
|
-
put,
|
|
350
|
-
patch,
|
|
351
|
-
del,
|
|
352
|
-
refs: getRefsObject()
|
|
353
|
-
});
|
|
354
|
-
compiled.execute(scope);
|
|
355
|
-
} catch {}
|
|
356
|
-
|
|
357
|
-
return excludeChanged ? [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop)) : [...accessed];
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Get dependencies - use getIds() if available, otherwise fall back to proxy tracking
|
|
361
|
-
// Note: When excludeChanged is true, we always use proxy tracking because getIds()
|
|
362
|
-
// doesn't distinguish between read and write operations, which can cause infinite loops
|
|
363
|
-
// when expressions both read and write state (e.g., @effect="lastSeen = count")
|
|
364
|
-
const getDependencies = (compiled, el, excludeChanged = false) => {
|
|
365
|
-
if (compiled.getIds && !excludeChanged) {
|
|
366
|
-
return compiled.getIds([]);
|
|
367
|
-
}
|
|
368
|
-
return trackDependenciesProxy(compiled, el, excludeChanged);
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const cleanup = el => {
|
|
372
|
-
[el,...el.querySelectorAll('*')].forEach(e => {
|
|
373
|
-
HELIUM.listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
|
|
374
|
-
HELIUM.listeners.delete(e);
|
|
375
|
-
});
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
async function processElements(element) {
|
|
379
|
-
const newBindings = [];
|
|
380
|
-
const deferredBindings = [];
|
|
381
|
-
|
|
382
|
-
const heElements = [element, ...element.querySelectorAll("*")].filter(e => {
|
|
383
|
-
if (HELIUM.processed.has(e)) return false;
|
|
384
|
-
for (let i = 0; i < e.attributes.length; i++) {
|
|
385
|
-
const n = e.attributes[i].name;
|
|
386
|
-
if (n[0] === '@' || n[0] === ':' || n.startsWith('data-he')) return true;
|
|
387
|
-
}
|
|
388
|
-
return false;
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const addBinding = (val, b) => {
|
|
392
|
-
HELIUM.bindings.set(val, b.calc ? [b,...(HELIUM.bindings.get(val) || [])] : [...(HELIUM.bindings.get(val) || []), b]);
|
|
393
|
-
b.calc ? newBindings.unshift(b) : newBindings.push(b);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const importPromises = [];
|
|
397
|
-
|
|
398
|
-
heElements.forEach((el) => {
|
|
399
|
-
const importAttr = el.getAttribute("@import") || el.getAttribute('data-he-import');
|
|
400
|
-
if (importAttr) {
|
|
401
|
-
importAttr.split(",").map((m) => m.trim()).forEach((moduleName) => {
|
|
402
|
-
// Build path: use as-is for URLs, otherwise resolve relative to document location
|
|
403
|
-
const isUrl = moduleName.startsWith("http://") || moduleName.startsWith("https://");
|
|
404
|
-
const hasExtension = moduleName.endsWith(".js");
|
|
405
|
-
const hasPathPrefix = moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../");
|
|
406
|
-
const relativePath = (isUrl || hasPathPrefix ? "" : "./") + moduleName + (isUrl || hasExtension ? "" : ".js");
|
|
407
|
-
// Ensure base URL ends with / so relative paths resolve correctly
|
|
408
|
-
const baseUrl = location.href.endsWith('/') || location.href.includes('.html') ? location.href : location.href + '/';
|
|
409
|
-
const path = isUrl ? relativePath : new URL(relativePath, baseUrl).href;
|
|
410
|
-
importPromises.push(
|
|
411
|
-
import(path)
|
|
412
|
-
.then((module) => {
|
|
413
|
-
Object.keys(module).forEach(
|
|
414
|
-
(key) => (state[key] = module[key]),
|
|
415
|
-
);
|
|
416
|
-
})
|
|
417
|
-
.catch((error) => {
|
|
418
|
-
console.error(
|
|
419
|
-
`Failed to import module: ${path}`,
|
|
420
|
-
error.message,
|
|
421
|
-
);
|
|
422
|
-
}),
|
|
423
|
-
);
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
if (importPromises.length > 0) await Promise.all(importPromises);
|
|
429
|
-
|
|
430
|
-
heElements.forEach(el => {
|
|
431
|
-
HELIUM.processed.add(el);
|
|
432
|
-
|
|
433
|
-
const attrs = el.attributes;
|
|
434
|
-
const execExpr = v => {
|
|
435
|
-
const compiled = compile(v, true);
|
|
436
|
-
const scope = createScope(el);
|
|
437
|
-
return compiled.execute(scope);
|
|
438
|
-
};
|
|
439
|
-
const inputType = el.type?.toLowerCase();
|
|
440
|
-
const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
|
|
441
|
-
|
|
442
|
-
// Single loop through attributes with early skipping
|
|
443
|
-
for (let i = 0; i < attrs.length; i++) {
|
|
444
|
-
const {name, value} = attrs[i];
|
|
445
|
-
|
|
446
|
-
// Skip non-helium attributes early
|
|
447
|
-
if (name[0] !== '@' && name[0] !== ':' && !name.startsWith('data-he')) continue;
|
|
448
|
-
|
|
449
|
-
// Initialize state first if needed (skip reserved words, only if not already set)
|
|
450
|
-
if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
|
|
451
|
-
state[value] = match(name, "bind") ? (el.type == "checkbox" ? el.checked : tryNum(el.value)) : tryNum(el.textContent);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Process the attribute
|
|
455
|
-
if (name === "@data" || name === 'data-he') {
|
|
456
|
-
Object.assign(state, evalExpr(value));
|
|
457
|
-
}
|
|
458
|
-
else if (name.startsWith(":") || name.startsWith('data-he-attr:')) {
|
|
459
|
-
const propName = name.startsWith(":") ? name.slice(1) : name.slice(13);
|
|
460
|
-
deferredBindings.push({el, prop: propName, compiled: compile(value, true)});
|
|
461
|
-
}
|
|
462
|
-
else if (match(name, "ref")) {
|
|
463
|
-
HELIUM.refs.set("$" + value, el);
|
|
464
|
-
}
|
|
465
|
-
else if (match(name, "text", "html")) {
|
|
466
|
-
deferredBindings.push({el, prop: match(name, "text") ? "textContent" : "innerHTML", compiled: compile(value, true)});
|
|
467
|
-
}
|
|
468
|
-
else if (match(name, "bind")) {
|
|
469
|
-
const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
|
|
470
|
-
const prop = isCheckbox ? "checked" : "value";
|
|
471
|
-
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
472
|
-
el.addEventListener(event, inputHandler);
|
|
473
|
-
if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
|
|
474
|
-
HELIUM.listeners.get(el).push({receiver: el, event, handler: inputHandler});
|
|
475
|
-
deferredBindings.push({el, prop, compiled: compile(value, true), isRadio});
|
|
476
|
-
if (isCheckbox) el.checked = !!state[value];
|
|
477
|
-
else if (isRadio) el.checked = el.value == state[value];
|
|
478
|
-
else el.value = state[value] ?? "";
|
|
479
|
-
}
|
|
480
|
-
else if (match(name, "hidden", "visible")) {
|
|
481
|
-
const isHidden = match(name, "hidden");
|
|
482
|
-
deferredBindings.push({el, prop: "hidden", compiled: compile(value, true), isHiddenAttr: isHidden});
|
|
483
|
-
}
|
|
484
|
-
else if (match(name, "calculate")) {
|
|
485
|
-
const calcName = name.split(":")[1];
|
|
486
|
-
if (!(calcName in state)) state[calcName] = undefined; // Initialize so other bindings can reference it
|
|
487
|
-
deferredBindings.push({el, calc: calcName, prop: null, compiled: compile(value, true)});
|
|
488
|
-
}
|
|
489
|
-
else if (match(name, "effect")) {
|
|
490
|
-
deferredBindings.push({el, prop: null, compiled: compile(value, true), keys: name.split(":").slice(1)});
|
|
491
|
-
}
|
|
492
|
-
else if (match(name, "init")) {
|
|
493
|
-
inits.push({ compiled: compile(value, true), el });
|
|
494
|
-
}
|
|
495
|
-
else if (name.startsWith("@") || name.startsWith(`data-${prefix}`)) {
|
|
496
|
-
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(prefix.length + 6);
|
|
497
|
-
const [eventName, ...mods] = fullName.split(".");
|
|
498
|
-
const isHttpMethod = ["get", "post", "put", "patch", "delete"].includes(eventName);
|
|
499
|
-
const event = isHttpMethod ? getEvent(el) : eventName;
|
|
500
|
-
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
501
|
-
const debounceMod = mods.find(m => m.startsWith("debounce"));
|
|
502
|
-
const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
|
|
503
|
-
const _handler = e => {
|
|
504
|
-
if (mods.includes("prevent")) e.preventDefault();
|
|
505
|
-
if (mods.includes("stop")) e.stopPropagation();
|
|
506
|
-
for (const [mod, prop] of Object.entries(KEY_MODS)) if (mods.includes(mod) && !e[prop]) return;
|
|
507
|
-
if (["keydown", "keyup", "keypress"].includes(event)) {
|
|
508
|
-
const last = mods[mods.length - 1];
|
|
509
|
-
if (last && !["prevent", "once", "outside", "document", "stop"].includes(last) && !last.startsWith("debounce") && !Object.keys(KEY_MODS).includes(last)) {
|
|
510
|
-
const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
|
|
511
|
-
if (keyName.toLowerCase() !== last.toLowerCase()) return;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
515
|
-
if (isHttpMethod) {
|
|
516
|
-
const getAttr = name => el.getAttribute(`data-${prefix}-${name}`) || el.getAttribute(`@${name}`);
|
|
517
|
-
const pairs = (getAttr('target') || "").split(",").map(p => p.split(":").map(s => s.trim()));
|
|
518
|
-
const target = pairs.map(([target]) => target);
|
|
519
|
-
const action = pairs.map(([, action]) => action);
|
|
520
|
-
const options = {
|
|
521
|
-
...(getAttr("options") && evalExpr(getAttr("options") || "{}")),
|
|
522
|
-
...(target && { target }),
|
|
523
|
-
...(action && { action }),
|
|
524
|
-
...(getAttr("template") && { template: execExpr(getAttr("template")),}),
|
|
525
|
-
...(getAttr("loading") && { loading: execExpr(getAttr("loading"))}),
|
|
526
|
-
};
|
|
527
|
-
let paramsAttr = getAttr("params");
|
|
528
|
-
if (!paramsAttr && el.hasAttribute("name")) {
|
|
529
|
-
const keys = el.getAttribute("name").match(/\w+/g).map(key => `${key}:`).join``;
|
|
530
|
-
paramsAttr = keys + (isCheckbox ? "checked" : "value");
|
|
531
|
-
} else paramsAttr ||= "{}";
|
|
532
|
-
if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
|
|
533
|
-
const props = paramsAttr.split(":").map(s => s.trim());
|
|
534
|
-
paramsAttr = props.reduceRight((acc, key, i) => `{ ${key}: ${i == 1 ? `'${el[acc]}'` : acc} }`);
|
|
535
|
-
}
|
|
536
|
-
const scope = createScope(el, e);
|
|
537
|
-
const paramsCompiled = compile(paramsAttr, true);
|
|
538
|
-
const params = paramsCompiled.execute(scope);
|
|
539
|
-
ajax(value, eventName.toUpperCase(), options, params);
|
|
540
|
-
} else {
|
|
541
|
-
const scope = createScope(el, e);
|
|
542
|
-
const compiled = compile(value, true);
|
|
543
|
-
compiled.execute(scope);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if (mods.includes("once")) receiver.removeEventListener(event, handler);
|
|
547
|
-
};
|
|
548
|
-
const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
549
|
-
const eventOptions = mods.includes("once") ? { once: true } : undefined;
|
|
550
|
-
receiver.addEventListener(event, handler, eventOptions);
|
|
551
|
-
if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
|
|
552
|
-
HELIUM.listeners.get(el).push({receiver, event, handler});
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
deferredBindings.forEach(b => {
|
|
557
|
-
const tracked = b.keys?.includes("*") ? [ALL] : getDependencies(b.compiled, b.el, true).concat(b.keys || []);
|
|
558
|
-
tracked.forEach(key => addBinding(key, b));
|
|
559
|
-
// If no dependencies were found, still apply the binding once (for literals like @text="true")
|
|
560
|
-
if (tracked.length === 0) {
|
|
561
|
-
newBindings.push(b);
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
return newBindings;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Disconnect old observer if exists, then create new one
|
|
568
|
-
HELIUM.observer?.disconnect();
|
|
569
|
-
HELIUM.observer = new MutationObserver(async (ms) => {
|
|
570
|
-
for (const m of ms) {
|
|
571
|
-
m.removedNodes.forEach((n) => n.nodeType === 1 && cleanup(n));
|
|
572
|
-
|
|
573
|
-
for (const n of m.addedNodes) {
|
|
574
|
-
if (n.nodeType === 1 && !HELIUM.processed.has(n)) {
|
|
575
|
-
const bindings = await processElements(n);
|
|
576
|
-
bindings?.forEach(applyBinding);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
HELIUM.observer.observe(root, { childList: true, subtree: true });
|
|
582
|
-
|
|
583
|
-
const initialBindings = await processElements(root);
|
|
584
|
-
initialBindings.forEach(applyBinding);
|
|
585
|
-
inits.forEach(({ compiled, el }) => {
|
|
586
|
-
const scope = createScope(el);
|
|
587
|
-
compiled.execute(scope);
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
return state;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Teardown function
|
|
594
|
-
function heliumTeardown() {
|
|
595
|
-
if (HELIUM?.observer) HELIUM.observer.disconnect();
|
|
596
|
-
HELIUM = null;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return { helium, heliumTeardown };
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Create default instance for backwards compatibility
|
|
603
|
-
const { helium: defaultHelium, heliumTeardown: _defaultTeardown } = createHelium();
|
|
604
|
-
|
|
605
|
-
// Flag to disable default auto-init (used by helium-csp.js and other variants)
|
|
606
|
-
let defaultDisabled = false;
|
|
607
|
-
|
|
608
|
-
// Wrapped teardown that also prevents future auto-init
|
|
609
|
-
function defaultTeardown() {
|
|
610
|
-
defaultDisabled = true;
|
|
611
|
-
_defaultTeardown();
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Setup browser globals and auto-initialization
|
|
615
|
-
if (typeof window !== 'undefined') {
|
|
616
|
-
window.helium = defaultHelium;
|
|
617
|
-
window.heliumTeardown = defaultTeardown;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Initialize on load
|
|
621
|
-
if (typeof document !== 'undefined') {
|
|
622
|
-
document.addEventListener("DOMContentLoaded",_ => !defaultDisabled && defaultHelium());
|
|
623
|
-
// Turbo integration
|
|
624
|
-
document.addEventListener("turbo:before-render",_ => defaultTeardown());
|
|
625
|
-
document.addEventListener("turbo:render",_ => !defaultDisabled && defaultHelium());
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
export { defaultTeardown };
|
|
629
|
-
export default defaultHelium;
|