@daz4126/helium 1.0.0-beta.3 → 1.0.0-beta.4

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.
@@ -220,17 +220,11 @@
220
220
  </head>
221
221
  <body>
222
222
 
223
- <div class="page" data-helium @data="{ count: 0 }">
223
+ <div class="page" data-helium>
224
224
  <h1>Helium Examples</h1>
225
225
 
226
226
  <div class="examples">
227
227
 
228
- <!-- @data Test -->
229
- <div class="card" data-he="{ word: 'Hello',number: 42 }">
230
- <h2 data-he-text="word"></h2>
231
- <p>Number: <b @text="number"></b></p>
232
- </div>
233
-
234
228
  <!-- Counter -->
235
229
  <div class="card">
236
230
  <h2>Counter</h2>
@@ -328,7 +322,7 @@
328
322
  </div>
329
323
 
330
324
  <!-- Debounced Input -->
331
- <div class="card" @data="{ search: '', debouncedSearch: '' }">
325
+ <div class="card">
332
326
  <h2>Debounced Input</h2>
333
327
  <p>Type and wait 500ms to see the debounced value update.</p>
334
328
  <input placeholder="Search..." @bind="search" @input.debounce:500="debouncedSearch = search">
@@ -340,13 +334,13 @@
340
334
  <div class="card" @data="{ tab: 'one' }">
341
335
  <h2>Tabs</h2>
342
336
  <div class="tabs">
343
- <button class="tab" :class="{ active: tab === 'one' }" @click="tab = 'one'">Tab 1</button>
344
- <button class="tab" :class="{ active: tab === 'two' }" @click="tab = 'two'">Tab 2</button>
345
- <button class="tab" :class="{ active: tab === 'three' }" @click="tab = 'three'">Tab 3</button>
337
+ <button class="tab" :class="{ active: tab == 'one' }" @click="tab = 'one'">Tab 1</button>
338
+ <button class="tab" :class="{ active: tab == 'two' }" @click="tab = 'two'">Tab 2</button>
339
+ <button class="tab" :class="{ active: tab == 'three' }" @click="tab = 'three'">Tab 3</button>
346
340
  </div>
347
- <div class="tab-content" @visible="tab === 'one'">Content for Tab 1</div>
348
- <div class="tab-content" @visible="tab === 'two'">Content for Tab 2</div>
349
- <div class="tab-content" @visible="tab === 'three'">Content for Tab 3</div>
341
+ <div class="tab-content" @visible="tab == 'one'">Content for Tab 1</div>
342
+ <div class="tab-content" @visible="tab == 'two'">Content for Tab 2</div>
343
+ <div class="tab-content" @visible="tab == 'three'">Content for Tab 3</div>
350
344
  </div>
351
345
 
352
346
  <!-- Element References -->
@@ -395,7 +389,7 @@
395
389
  </div>
396
390
 
397
391
  <!-- Character Counter -->
398
- <div class="card" @data="{ text: '', maxLength: 100 }">
392
+ <div class="card" @data="{ maxLength: 100 }">
399
393
  <h2>Character Counter</h2>
400
394
  <textarea @bind="text" placeholder="Type something..." rows="3"></textarea>
401
395
  <span @calculate:remaining="maxLength - text.length"></span>
@@ -405,7 +399,7 @@
405
399
  </div>
406
400
 
407
401
  <!-- Password Strength (uses @import) -->
408
- <div class="card" @data="{ password: '' }" @import="password-utils">
402
+ <div class="card" @import="https://codepen.io/daz4126/pen/xbOzGNO.js">
409
403
  <h2>Password Strength</h2>
410
404
  <input type="password" @bind="password" placeholder="Enter password">
411
405
  <span @calculate:result="checkStrength(password)"></span>
@@ -418,7 +412,7 @@
418
412
  </div>
419
413
 
420
414
  <!-- Init Example -->
421
- <div class="card" @data="{ loadTime: '' }">
415
+ <div class="card">
422
416
  <h2>Init Example</h2>
423
417
  <p @init="loadTime = new Date().toLocaleTimeString()">
424
418
  Page loaded at: <b @text="loadTime"></b>
@@ -427,7 +421,7 @@
427
421
  </div>
428
422
 
429
423
  <!-- Effect Example -->
430
- <div class="card" @data="{ clicks: 0, history: [] }">
424
+ <div class="card" @data="{ history: [] }">
431
425
  <h2>Effect (Side Effects)</h2>
432
426
  <p>Click the button and watch the history build up.</p>
433
427
  <button @click="clicks++">Click Me</button>
@@ -436,21 +430,12 @@
436
430
  <p>History: <span @text="history.join(', ') || 'None yet'">None yet</span></p>
437
431
  </div>
438
432
 
439
- <!-- Clipboard Copy -->
440
- <div class="card" @data="{ copied: false, textToCopy: 'Hello from Helium!' }">
441
- <h2>Copy to Clipboard</h2>
442
- <input @bind="textToCopy" placeholder="Text to copy">
443
- <button @click="navigator.clipboard.writeText(textToCopy).then(() => { copied = true; setTimeout(() => copied = false, 2000) })">
444
- <span @text="copied ? 'Copied!' : 'Copy'"></span>
445
- </button>
446
- </div>
447
-
448
433
  </div>
449
434
 
450
435
  <!-- AJAX Examples Section -->
451
436
  <div class="page" style="margin-top: 2rem;" @data="{
452
437
  templates: {
453
- post: data => `<section class='post'><h1>${data && data.title || 'Loading...'}</h1><p>${data && data.body || ''}</p></section>`
438
+ post: data => `<section class='post'><h1>${data?.title || 'Loading...'}</h1><p>${data?.body || ''}</p></section>`
454
439
  }
455
440
  }">
456
441
  <h1>AJAX Examples</h1>
@@ -0,0 +1,109 @@
1
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2
+ body {
3
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
4
+ background: #f5f5f5;
5
+ min-height: 100vh;
6
+ }
7
+ .todoapp {
8
+ max-width: 550px;
9
+ margin: 40px auto;
10
+ background: #fff;
11
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2), 0 25px 50px rgba(0,0,0,0.1);
12
+ }
13
+ .todoapp h1 {
14
+ font-size: 80px;
15
+ font-weight: 200;
16
+ text-align: center;
17
+ color: #b83f45;
18
+ padding: 10px 0;
19
+ }
20
+ .new-todo {
21
+ width: 100%;
22
+ padding: 16px 16px 16px 20px;
23
+ font-size: 24px;
24
+ border: none;
25
+ border-bottom: 1px solid #ededed;
26
+ font-family: inherit;
27
+ outline: none;
28
+ }
29
+ .new-todo::placeholder { color: #d9d9d9; font-style: italic; }
30
+ .toggle-all-container {
31
+ display: flex;
32
+ align-items: center;
33
+ padding: 12px 15px;
34
+ border-bottom: 1px solid #ededed;
35
+ color: #777;
36
+ cursor: pointer;
37
+ }
38
+ .toggle-all-container input { margin-right: 10px; }
39
+ .todo-list { list-style: none; }
40
+ .todo-item {
41
+ display: flex;
42
+ align-items: center;
43
+ padding: 15px;
44
+ border-bottom: 1px solid #ededed;
45
+ font-size: 24px;
46
+ }
47
+ .todo-item.completed .todo-text {
48
+ color: #949494;
49
+ text-decoration: line-through;
50
+ }
51
+ .toggle {
52
+ width: 24px;
53
+ height: 24px;
54
+ margin-right: 15px;
55
+ cursor: pointer;
56
+ flex-shrink: 0;
57
+ accent-color: #85cda8;
58
+ }
59
+ .todo-text { flex: 1; word-break: break-all; }
60
+ .destroy {
61
+ display: none;
62
+ background: none;
63
+ border: none;
64
+ color: #cc9a9a;
65
+ font-size: 28px;
66
+ cursor: pointer;
67
+ padding: 0 10px;
68
+ line-height: 1;
69
+ }
70
+ .destroy:hover { color: #af5b5e; }
71
+ .todo-item:hover .destroy { display: block; }
72
+ .footer {
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+ padding: 10px 15px;
77
+ color: #777;
78
+ font-size: 14px;
79
+ border-top: 1px solid #e6e6e6;
80
+ }
81
+ .filters { display: flex; gap: 3px; }
82
+ .filters button {
83
+ background: none;
84
+ border: 1px solid transparent;
85
+ padding: 3px 7px;
86
+ border-radius: 3px;
87
+ cursor: pointer;
88
+ font-size: 14px;
89
+ color: inherit;
90
+ font-family: inherit;
91
+ }
92
+ .filters button:hover { border-color: #db7676; }
93
+ .filters button.selected { border-color: #ce4646; }
94
+ .clear-completed {
95
+ background: none;
96
+ border: none;
97
+ cursor: pointer;
98
+ color: inherit;
99
+ font-size: 14px;
100
+ font-family: inherit;
101
+ }
102
+ .clear-completed:hover { text-decoration: underline; }
103
+ .info {
104
+ text-align: center;
105
+ margin: 30px 0;
106
+ color: #bbb;
107
+ font-size: 12px;
108
+ }
109
+ .info a { color: inherit; }
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Helium TodoMVC</title>
7
+ <link rel="stylesheet" href="todo.css">
8
+ </head>
9
+ <body>
10
+
11
+ <section class="todoapp" data-helium @local-storage="helium-todos" @import="todo"
12
+ @data="{ todos: [], newTodo: '', filter: 'all', nextId: 1 }">
13
+
14
+ <span hidden @calculate:remaining="todos.filter(t => !t.completed).length"></span>
15
+ <span hidden @calculate:filtered="filter === 'all' ? todos : filter === 'active' ? todos.filter(t => !t.completed) : todos.filter(t => t.completed)"></span>
16
+
17
+ <header>
18
+ <h1>todos</h1>
19
+ <input class="new-todo" @bind="newTodo" @keydown.enter="addTodo($data)" placeholder="What needs to be done?" autofocus>
20
+ </header>
21
+
22
+ <section @visible="todos.length > 0">
23
+ <label class="toggle-all-container">
24
+ <input type="checkbox" @click="toggleAll($data)" :checked="todos.length > 0 && todos.every(t => t.completed)">
25
+ Mark all as complete
26
+ </label>
27
+ <ul class="todo-list" @html="renderTodos(filtered)"></ul>
28
+ </section>
29
+
30
+ <footer class="footer" @visible="todos.length > 0">
31
+ <span @text="remaining + (remaining === 1 ? ' item' : ' items') + ' left'"></span>
32
+ <nav class="filters">
33
+ <button :class="{ selected: filter === 'all' }" @click="filter = 'all'">All</button>
34
+ <button :class="{ selected: filter === 'active' }" @click="filter = 'active'">Active</button>
35
+ <button :class="{ selected: filter === 'completed' }" @click="filter = 'completed'">Completed</button>
36
+ </nav>
37
+ <button class="clear-completed" @click="clearCompleted($data)" @visible="todos.some(t => t.completed)">Clear completed</button>
38
+ </footer>
39
+
40
+ </section>
41
+
42
+ <p class="info">Built with <a href="https://github.com/daz-codes/helium">Helium</a></p>
43
+
44
+ <script type="module" src="../helium.js"></script>
45
+ </body>
46
+ </html>
@@ -0,0 +1,36 @@
1
+ function esc(s) {
2
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3
+ }
4
+
5
+ export function renderTodos(list) {
6
+ return list.map(t => `<li class='todo-item${t.completed ? ' completed' : ''}'>
7
+ <input type='checkbox' class='toggle'${t.completed ? ' checked' : ''} @click='toggleTodo($data, ${t.id})'>
8
+ <span class='todo-text'>${esc(t.text)}</span>
9
+ <button class='destroy' @click='removeTodo($data, ${t.id})'>&#215;</button>
10
+ </li>`).join('');
11
+ }
12
+
13
+ export function addTodo($data) {
14
+ const text = $data.newTodo.trim();
15
+ if (text) {
16
+ $data.todos = [...$data.todos, { id: $data.nextId++, text, completed: false }];
17
+ $data.newTodo = '';
18
+ }
19
+ }
20
+
21
+ export function toggleTodo($data, id) {
22
+ $data.todos = $data.todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
23
+ }
24
+
25
+ export function removeTodo($data, id) {
26
+ $data.todos = $data.todos.filter(t => t.id !== id);
27
+ }
28
+
29
+ export function toggleAll($data) {
30
+ const allDone = $data.todos.every(t => t.completed);
31
+ $data.todos = $data.todos.map(t => ({ ...t, completed: !allDone }));
32
+ }
33
+
34
+ export function clearCompleted($data) {
35
+ $data.todos = $data.todos.filter(t => !t.completed);
36
+ }
package/helium-lite.js CHANGED
@@ -4,19 +4,47 @@
4
4
  * No AJAX, no @import, no SSE, no Turbo
5
5
  */
6
6
 
7
- const parseEx = v => { try { return Function(`return(${v})`)() } catch { return v } };
8
- const tryNum = v => { const t = String(v).trim(), n = +t; return t && !isNaN(n) ? n : v };
9
- const isValidIdentifier = v => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
10
- const RESERVED = new Set(['undefined', 'null', 'true', 'false', 'NaN', 'Infinity', 'this', 'arguments']);
11
- const KEY_MODS = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
12
- const debounce = (f, d) => { let t; return (...a) => (clearTimeout(t), t = setTimeout(f, d, ...a)) };
7
+ const parseEx = (v) => {
8
+ try {
9
+ return Function(`return(${v})`)();
10
+ } catch {
11
+ return v;
12
+ }
13
+ };
14
+ const tryNum = (v) => {
15
+ const t = String(v).trim(),
16
+ n = +t;
17
+ return t && !isNaN(n) ? n : v;
18
+ };
19
+ const isValidIdentifier = (v) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
20
+ const RESERVED = new Set([
21
+ "undefined",
22
+ "null",
23
+ "true",
24
+ "false",
25
+ "NaN",
26
+ "Infinity",
27
+ "this",
28
+ "arguments",
29
+ ]);
30
+ const KEY_MODS = {
31
+ shift: "shiftKey",
32
+ ctrl: "ctrlKey",
33
+ alt: "altKey",
34
+ meta: "metaKey",
35
+ };
36
+ const debounce = (f, d) => {
37
+ let t;
38
+ return (...a) => (clearTimeout(t), (t = setTimeout(f, d, ...a)));
39
+ };
13
40
 
14
41
  const ATTR_REGEX = /^(@|:|data-he)/;
15
42
 
16
43
  const match = (name, ...attrs) => {
17
44
  const p = name.split(/[.:]/)[0];
18
- if (p === ":" || p === "") return false;
19
- return attrs.some(a => p === `@${a}` || p === `data-he-${a}`);
45
+ return p == ":" || p == ""
46
+ ? false
47
+ : attrs.some((a) => p === `@${a}` || p === `data-he-${a}`);
20
48
  };
21
49
 
22
50
  let HELIUM = null;
@@ -24,8 +52,13 @@ let HELIUM = null;
24
52
  async function helium(initialState = {}) {
25
53
  const inits = [];
26
54
  const ALL = Symbol("all");
27
- const root = document.querySelector('[\\@helium]') || document.querySelector('[data-helium]') || document.body;
28
- const storageKey = root.getAttribute('@local-storage') || root.getAttribute('data-he-local-storage');
55
+ const root =
56
+ document.querySelector("[\\@helium]") ||
57
+ document.querySelector("[data-helium]") ||
58
+ document.body;
59
+ const storageKey =
60
+ root.getAttribute("@local-storage") ||
61
+ root.getAttribute("data-he-local-storage");
29
62
 
30
63
  if (!HELIUM) {
31
64
  HELIUM = {
@@ -36,12 +69,14 @@ async function helium(initialState = {}) {
36
69
  processed: new WeakSet(),
37
70
  parentKeys: new WeakMap(),
38
71
  fnCache: new Map(),
39
- proxyCache: new WeakMap()
72
+ proxyCache: new WeakMap(),
40
73
  };
41
74
  }
42
75
 
43
- const $ = s => document.querySelector(s);
44
- const html = s => Object.assign(document.createElement("template"), { innerHTML: s.trim() }).content.firstChild;
76
+ const $ = (s) => document.querySelector(s);
77
+ const html = (s) =>
78
+ Object.assign(document.createElement("template"), { innerHTML: s.trim() })
79
+ .content.firstChild;
45
80
 
46
81
  const applyingBindings = new Set();
47
82
 
@@ -57,9 +92,9 @@ async function helium(initialState = {}) {
57
92
 
58
93
  const handler = {
59
94
  has(t, p) {
60
- if (typeof p === 'string' && p.startsWith('$')) return false;
95
+ if (typeof p === "string" && p.startsWith("$")) return false;
61
96
  if (Reflect.has(t, p)) return true;
62
- if (typeof globalThis[p] !== 'undefined') return false;
97
+ if (typeof globalThis[p] !== "undefined") return false;
63
98
  return true;
64
99
  },
65
100
  get(t, p, r) {
@@ -81,21 +116,16 @@ async function helium(initialState = {}) {
81
116
  HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
82
117
  if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state));
83
118
  return res;
84
- }
119
+ },
85
120
  };
86
121
 
87
122
  const state = new Proxy({}, handler);
88
- if (storageKey) try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey))); } catch {}
123
+ if (storageKey)
124
+ try {
125
+ Object.assign(state, JSON.parse(localStorage.getItem(storageKey)));
126
+ } catch {}
89
127
  Object.assign(state, initialState);
90
128
 
91
- const getRefsObject = () => {
92
- const refs = {};
93
- for (const [name, element] of HELIUM.refs) {
94
- refs[name] = element;
95
- }
96
- return refs;
97
- };
98
-
99
129
  const createScope = (el, event = {}) => {
100
130
  return {
101
131
  $,
@@ -103,7 +133,7 @@ async function helium(initialState = {}) {
103
133
  $event: event,
104
134
  $el: el,
105
135
  $html: html,
106
- ...getRefsObject()
136
+ ...Object.fromEntries(HELIUM.refs),
107
137
  };
108
138
  };
109
139
 
@@ -115,7 +145,7 @@ async function helium(initialState = {}) {
115
145
  "$scope",
116
146
  withReturn
117
147
  ? `with($scope){with($scope.$data){return(${expr.trim()})}}`
118
- : `with($scope){with($scope.$data){${expr.trim()}}}`
148
+ : `with($scope){with($scope.$data){${expr.trim()}}}`,
119
149
  );
120
150
  const compiled = { execute: (scope) => fn(scope), getIds: null };
121
151
  HELIUM.fnCache.set(key, compiled);
@@ -136,18 +166,22 @@ async function helium(initialState = {}) {
136
166
  if (prop === "innerHTML") {
137
167
  const content = Array.isArray(r) ? r.join`` : r;
138
168
  return typeof Idiomorph === "object"
139
- ? Idiomorph.morph(el, content, { morphStyle: 'innerHTML' })
140
- : el.innerHTML = content;
169
+ ? Idiomorph.morph(el, content, { morphStyle: "innerHTML" })
170
+ : (el.innerHTML = content);
141
171
  }
142
172
  if (prop === "class" && r && typeof r === "object")
143
173
  return Object.entries(r).forEach(([k, v]) =>
144
- k.split(/\s+/).forEach(c => el.classList.toggle(c, v)));
174
+ k.split(/\s+/).forEach((c) => el.classList.toggle(c, v)),
175
+ );
145
176
 
146
177
  if (prop === "style" && r && typeof r === "object")
147
- return el.style.cssText = Object.entries(r)
178
+ return (el.style.cssText = Object.entries(r)
148
179
  .filter(([_, v]) => v != null && v !== false)
149
- .map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
150
- .join("; ");
180
+ .map(
181
+ ([k, v]) =>
182
+ `${k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase())}:${v}`,
183
+ )
184
+ .join("; "));
151
185
 
152
186
  if (prop === "hidden") {
153
187
  el.hidden = isHiddenAttr ? !!r : !r;
@@ -156,8 +190,9 @@ async function helium(initialState = {}) {
156
190
 
157
191
  if (prop in el) {
158
192
  if (isRadio && prop !== "checked") el.checked = el.value === r;
159
- else if (el.type === "radio" && prop !== "checked") el.checked = el.value === r;
160
- else el[prop] = prop === "textContent" ? (r ?? '') : parseEx(r);
193
+ else if (el.type === "radio" && prop !== "checked")
194
+ el.checked = el.value === r;
195
+ else el[prop] = prop === "textContent" ? (r ?? "") : parseEx(r);
161
196
  return;
162
197
  }
163
198
 
@@ -171,12 +206,16 @@ async function helium(initialState = {}) {
171
206
  return true;
172
207
  },
173
208
  get(target, prop) {
174
- if (typeof prop == 'string') {
175
- excludeChanged ? !accessed.has(prop) && accessed.set(prop, target[prop]) : accessed.add(prop);
209
+ if (typeof prop == "string") {
210
+ excludeChanged
211
+ ? !accessed.has(prop) && accessed.set(prop, target[prop])
212
+ : accessed.add(prop);
176
213
  }
177
214
  const val = target[prop];
178
- return typeof val == "object" && val != null ? new Proxy(val, this) : val;
179
- }
215
+ return typeof val == "object" && val != null
216
+ ? new Proxy(val, this)
217
+ : val;
218
+ },
180
219
  });
181
220
 
182
221
  try {
@@ -186,21 +225,29 @@ async function helium(initialState = {}) {
186
225
  $event: {},
187
226
  $el: el,
188
227
  $html: html,
189
- ...getRefsObject()
228
+ ...Object.fromEntries(HELIUM.refs),
190
229
  };
191
230
  compiled.execute(scope);
192
231
  } catch {}
193
232
 
194
- return excludeChanged ? [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop)) : [...accessed];
233
+ return excludeChanged
234
+ ? [...accessed.keys()].filter(
235
+ (prop) => state[prop] === accessed.get(prop),
236
+ )
237
+ : [...accessed];
195
238
  };
196
239
 
197
240
  const getDependencies = (compiled, el, excludeChanged = false) => {
198
241
  return trackDependenciesProxy(compiled, el, excludeChanged);
199
242
  };
200
243
 
201
- const cleanup = el => {
202
- [el, ...el.querySelectorAll('*')].forEach(e => {
203
- HELIUM.listeners.get(e)?.forEach(({ receiver, event, handler }) => receiver.removeEventListener(event, handler));
244
+ const cleanup = (el) => {
245
+ [el, ...el.querySelectorAll("*")].forEach((e) => {
246
+ HELIUM.listeners
247
+ .get(e)
248
+ ?.forEach(({ receiver, event, handler }) =>
249
+ receiver.removeEventListener(event, handler),
250
+ );
204
251
  HELIUM.listeners.delete(e);
205
252
  });
206
253
  };
@@ -209,91 +256,154 @@ async function helium(initialState = {}) {
209
256
  const newBindings = [];
210
257
  const deferredBindings = [];
211
258
 
212
- const heElements = [element, ...element.querySelectorAll("*")].filter(e => {
213
- if (HELIUM.processed.has(e)) return false;
214
- for (let i = 0; i < e.attributes.length; i++) {
215
- if (ATTR_REGEX.test(e.attributes[i].name)) return true;
216
- }
217
- return false;
218
- });
259
+ const heElements = [element, ...element.querySelectorAll("*")].filter(
260
+ (e) => {
261
+ if (HELIUM.processed.has(e)) return false;
262
+ for (let i = 0; i < e.attributes.length; i++) {
263
+ if (ATTR_REGEX.test(e.attributes[i].name)) return true;
264
+ }
265
+ return false;
266
+ },
267
+ );
219
268
 
220
269
  const addBinding = (val, b) => {
221
- HELIUM.bindings.set(val, b.calc ? [b, ...(HELIUM.bindings.get(val) || [])] : [...(HELIUM.bindings.get(val) || []), b]);
270
+ HELIUM.bindings.set(
271
+ val,
272
+ b.calc
273
+ ? [b, ...(HELIUM.bindings.get(val) || [])]
274
+ : [...(HELIUM.bindings.get(val) || []), b],
275
+ );
222
276
  b.calc ? newBindings.unshift(b) : newBindings.push(b);
223
277
  };
224
278
 
225
- heElements.forEach(el => {
279
+ heElements.forEach((el) => {
226
280
  HELIUM.processed.add(el);
227
281
 
228
282
  const attrs = el.attributes;
229
283
  const inputType = el.type?.toLowerCase();
230
- const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
284
+ const isCheckbox = inputType == "checkbox",
285
+ isRadio = inputType == "radio",
286
+ isSelect = el.tagName == "SELECT";
231
287
 
232
288
  for (let i = 0; i < attrs.length; i++) {
233
289
  const { name, value } = attrs[i];
234
290
 
235
- if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith('data-he')) continue;
291
+ if (
292
+ !name.startsWith("@") &&
293
+ !name.startsWith(":") &&
294
+ !name.startsWith("data-he")
295
+ )
296
+ continue;
236
297
 
237
298
  // Initialize state first if needed (skip reserved words, only if not already set)
238
- if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
239
- state[value] = match(name, "bind") ? (el.type == "checkbox" ? el.checked : tryNum(el.value)) : tryNum(el.textContent);
299
+ if (
300
+ match(name, "text", "html", "bind") &&
301
+ isValidIdentifier(value) &&
302
+ !RESERVED.has(value) &&
303
+ state[value] == null
304
+ ) {
305
+ state[value] = match(name, "bind")
306
+ ? el.type == "checkbox"
307
+ ? el.checked
308
+ : tryNum(el.value)
309
+ : tryNum(el.textContent);
240
310
  }
241
311
 
242
312
  // Process the attribute
243
313
  if (name === "@data" || name === "data-he") {
244
314
  Object.assign(state, parseEx(value));
245
- }
246
- else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
247
- const propName = name.startsWith(":") ? name.slice(1) : name.slice(13);
248
- deferredBindings.push({ el, prop: propName, compiled: compile(value, true) });
249
- }
250
- else if (match(name, "ref")) {
315
+ } else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
316
+ const propName = name.startsWith(":")
317
+ ? name.slice(1)
318
+ : name.slice(13);
319
+ deferredBindings.push({
320
+ el,
321
+ prop: propName,
322
+ compiled: compile(value, true),
323
+ });
324
+ } else if (match(name, "ref")) {
251
325
  HELIUM.refs.set("$" + value, el);
252
- }
253
- else if (match(name, "text", "html")) {
254
- deferredBindings.push({ el, prop: match(name, "text") ? "textContent" : "innerHTML", compiled: compile(value, true) });
255
- }
256
- else if (match(name, "bind")) {
257
- const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
326
+ } else if (match(name, "text", "html")) {
327
+ deferredBindings.push({
328
+ el,
329
+ prop: match(name, "text") ? "textContent" : "innerHTML",
330
+ compiled: compile(value, true),
331
+ });
332
+ } else if (match(name, "bind")) {
333
+ const event = isCheckbox || isRadio || isSelect ? "change" : "input";
258
334
  const prop = isCheckbox ? "checked" : "value";
259
- const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
335
+ const inputHandler = (e) =>
336
+ (state[value] = isCheckbox ? e.target.checked : e.target.value);
260
337
  el.addEventListener(event, inputHandler);
261
338
  if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
262
- HELIUM.listeners.get(el).push({ receiver: el, event, handler: inputHandler });
263
- deferredBindings.push({ el, prop, compiled: compile(value, true), isRadio });
339
+ HELIUM.listeners
340
+ .get(el)
341
+ .push({ receiver: el, event, handler: inputHandler });
342
+ deferredBindings.push({
343
+ el,
344
+ prop,
345
+ compiled: compile(value, true),
346
+ isRadio,
347
+ });
264
348
  if (isCheckbox) el.checked = !!state[value];
265
349
  else if (isRadio) el.checked = el.value == state[value];
266
350
  else el.value = state[value] ?? "";
267
- }
268
- else if (match(name, "hidden", "visible")) {
351
+ } else if (match(name, "hidden", "visible")) {
269
352
  const isHidden = match(name, "hidden");
270
- deferredBindings.push({ el, prop: "hidden", compiled: compile(value, true), isHiddenAttr: isHidden });
271
- }
272
- else if (match(name, "calculate")) {
353
+ deferredBindings.push({
354
+ el,
355
+ prop: "hidden",
356
+ compiled: compile(value, true),
357
+ isHiddenAttr: isHidden,
358
+ });
359
+ } else if (match(name, "calculate")) {
273
360
  const calcName = name.split(":")[1];
274
361
  if (!(calcName in state)) state[calcName] = undefined;
275
- deferredBindings.push({ el, calc: calcName, prop: null, compiled: compile(value, true) });
276
- }
277
- else if (match(name, "effect")) {
278
- deferredBindings.push({ el, prop: null, compiled: compile(value, true), keys: name.split(":").slice(1) });
279
- }
280
- else if (match(name, "init")) {
362
+ deferredBindings.push({
363
+ el,
364
+ calc: calcName,
365
+ prop: null,
366
+ compiled: compile(value, true),
367
+ });
368
+ } else if (match(name, "effect")) {
369
+ deferredBindings.push({
370
+ el,
371
+ prop: null,
372
+ compiled: compile(value, true),
373
+ keys: name.split(":").slice(1),
374
+ });
375
+ } else if (match(name, "init")) {
281
376
  inits.push({ compiled: compile(value, true), el });
282
- }
283
- else if (name.startsWith("@") || name.startsWith("data-he")) {
377
+ } else if (name.startsWith("@") || name.startsWith("data-he")) {
284
378
  const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
285
379
  const [event, ...mods] = fullName.split(".");
286
- const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
287
- const debounceMod = mods.find(m => m.startsWith("debounce"));
288
- const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
289
- const _handler = e => {
380
+ const receiver =
381
+ mods.includes("outside") || mods.includes("document")
382
+ ? document
383
+ : el;
384
+ const debounceMod = mods.find((m) => m.startsWith("debounce"));
385
+ const debounceDelay = debounceMod
386
+ ? ((t) => (t && !isNaN(t) ? Number(t) : 300))(
387
+ debounceMod.split(":")[1],
388
+ )
389
+ : 0;
390
+ const _handler = (e) => {
290
391
  if (mods.includes("prevent")) e.preventDefault();
291
392
  if (mods.includes("stop")) e.stopPropagation();
292
- for (const [mod, prop] of Object.entries(KEY_MODS)) if (mods.includes(mod) && !e[prop]) return;
393
+ for (const [mod, prop] of Object.entries(KEY_MODS))
394
+ if (mods.includes(mod) && !e[prop]) return;
293
395
  if (["keydown", "keyup", "keypress"].includes(event)) {
294
396
  const last = mods[mods.length - 1];
295
- if (last && !["prevent", "once", "outside", "document", "stop"].includes(last) && !last.startsWith("debounce") && !Object.keys(KEY_MODS).includes(last)) {
296
- const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
397
+ if (
398
+ last &&
399
+ !["prevent", "once", "outside", "document", "stop"].includes(
400
+ last,
401
+ ) &&
402
+ !last.startsWith("debounce") &&
403
+ !Object.keys(KEY_MODS).includes(last)
404
+ ) {
405
+ const keyName =
406
+ e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
297
407
  if (keyName.toLowerCase() !== last.toLowerCase()) return;
298
408
  }
299
409
  }
@@ -302,10 +412,14 @@ async function helium(initialState = {}) {
302
412
  const compiled = compile(value, true);
303
413
  compiled.execute(scope);
304
414
  }
305
- if (mods.includes("once")) receiver.removeEventListener(event, handler);
415
+ if (mods.includes("once"))
416
+ receiver.removeEventListener(event, handler);
306
417
  };
307
- const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
308
- const eventOptions = mods.includes("once") ? { once: true } : undefined;
418
+ const handler =
419
+ debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
420
+ const eventOptions = mods.includes("once")
421
+ ? { once: true }
422
+ : undefined;
309
423
  receiver.addEventListener(event, handler, eventOptions);
310
424
  if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
311
425
  HELIUM.listeners.get(el).push({ receiver, event, handler });
@@ -313,9 +427,11 @@ async function helium(initialState = {}) {
313
427
  }
314
428
  });
315
429
 
316
- deferredBindings.forEach(b => {
317
- const tracked = b.keys?.includes("*") ? [ALL] : getDependencies(b.compiled, b.el, true).concat(b.keys || []);
318
- tracked.forEach(key => addBinding(key, b));
430
+ deferredBindings.forEach((b) => {
431
+ const tracked = b.keys?.includes("*")
432
+ ? [ALL]
433
+ : getDependencies(b.compiled, b.el, true).concat(b.keys || []);
434
+ tracked.forEach((key) => addBinding(key, b));
319
435
  if (tracked.length === 0) {
320
436
  newBindings.push(b);
321
437
  }
@@ -355,13 +471,13 @@ function heliumTeardown() {
355
471
  }
356
472
 
357
473
  // Browser globals
358
- if (typeof window !== 'undefined') {
474
+ if (typeof window !== "undefined") {
359
475
  window.helium = helium;
360
476
  window.heliumTeardown = heliumTeardown;
361
477
  }
362
478
 
363
479
  // Auto-initialize
364
- if (typeof document !== 'undefined') {
480
+ if (typeof document !== "undefined") {
365
481
  document.addEventListener("DOMContentLoaded", () => helium());
366
482
  }
367
483
 
package/helium.js CHANGED
@@ -244,15 +244,6 @@ export function createHelium(options = {}) {
244
244
  // Merge initial state
245
245
  Object.assign(state, initialState);
246
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
247
  // Create scope for expression execution
257
248
  const createScope = (el, event = {}) => {
258
249
  return engine.createScope({
@@ -266,7 +257,7 @@ export function createHelium(options = {}) {
266
257
  put,
267
258
  patch,
268
259
  del,
269
- refs: getRefsObject()
260
+ refs: Object.fromEntries(HELIUM.refs)
270
261
  });
271
262
  };
272
263
 
@@ -349,7 +340,7 @@ export function createHelium(options = {}) {
349
340
  put,
350
341
  patch,
351
342
  del,
352
- refs: getRefsObject()
343
+ refs: Object.fromEntries(HELIUM.refs)
353
344
  });
354
345
  compiled.execute(scope);
355
346
  } catch {}
@@ -404,9 +395,7 @@ export function createHelium(options = {}) {
404
395
  const hasExtension = moduleName.endsWith(".js");
405
396
  const hasPathPrefix = moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../");
406
397
  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;
398
+ const path = isUrl ? moduleName : new URL(relativePath, location.href).href;
410
399
  importPromises.push(
411
400
  import(path)
412
401
  .then((module) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.4",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "exports": {