@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.
- package/examples/index.html +13 -28
- package/examples/todo.css +109 -0
- package/examples/todo.html +46 -0
- package/examples/todo.js +36 -0
- package/helium-lite.js +216 -100
- package/helium.js +3 -14
- package/package.json +1 -1
package/examples/index.html
CHANGED
|
@@ -220,17 +220,11 @@
|
|
|
220
220
|
</head>
|
|
221
221
|
<body>
|
|
222
222
|
|
|
223
|
-
<div class="page" data-helium
|
|
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"
|
|
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
|
|
344
|
-
<button class="tab" :class="{ active: tab
|
|
345
|
-
<button class="tab" :class="{ active: tab
|
|
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
|
|
348
|
-
<div class="tab-content" @visible="tab
|
|
349
|
-
<div class="tab-content" @visible="tab
|
|
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="{
|
|
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" @
|
|
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"
|
|
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="{
|
|
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
|
|
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>
|
package/examples/todo.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function esc(s) {
|
|
2
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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})'>×</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 =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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 =
|
|
28
|
-
|
|
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 =
|
|
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 ===
|
|
95
|
+
if (typeof p === "string" && p.startsWith("$")) return false;
|
|
61
96
|
if (Reflect.has(t, p)) return true;
|
|
62
|
-
if (typeof globalThis[p] !==
|
|
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)
|
|
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
|
-
...
|
|
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:
|
|
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(
|
|
150
|
-
|
|
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")
|
|
160
|
-
|
|
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 ==
|
|
175
|
-
excludeChanged
|
|
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
|
|
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
|
-
...
|
|
228
|
+
...Object.fromEntries(HELIUM.refs),
|
|
190
229
|
};
|
|
191
230
|
compiled.execute(scope);
|
|
192
231
|
} catch {}
|
|
193
232
|
|
|
194
|
-
return excludeChanged
|
|
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(
|
|
203
|
-
HELIUM.listeners
|
|
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(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
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",
|
|
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 (
|
|
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 (
|
|
239
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 =>
|
|
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
|
|
263
|
-
|
|
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({
|
|
271
|
-
|
|
272
|
-
|
|
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({
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 =
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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))
|
|
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 (
|
|
296
|
-
|
|
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"))
|
|
415
|
+
if (mods.includes("once"))
|
|
416
|
+
receiver.removeEventListener(event, handler);
|
|
306
417
|
};
|
|
307
|
-
const handler =
|
|
308
|
-
|
|
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("*")
|
|
318
|
-
|
|
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 !==
|
|
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 !==
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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) => {
|