@vyckr/tachyon 1.1.8 → 1.1.10
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/bun.lock +3 -3
- package/components/clicker.html +30 -0
- package/package.json +2 -2
- package/routes/HTML +131 -6
- package/src/client/dev.html +0 -1
- package/src/client/prod.html +0 -1
- package/src/client/render.ts +417 -0
- package/src/client/template.js +66 -0
- package/src/client/yon.ts +141 -146
- package/src/serve.ts +34 -34
- package/components/counter.html +0 -13
- package/src/client/render.js +0 -278
- /package/src/client/{hmr.js → hmr.ts} +0 -0
package/bun.lock
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"jsdom": "^26.0.0",
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@types/bun": "
|
|
11
|
+
"@types/bun": "~1.2.8",
|
|
12
12
|
"@types/deno": "^2.0.0",
|
|
13
13
|
"@types/jsdom": "^21.1.7",
|
|
14
14
|
"@types/node": "^20.4.2",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="],
|
|
30
30
|
|
|
31
|
-
"@types/bun": ["@types/bun@1.2.
|
|
31
|
+
"@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="],
|
|
32
32
|
|
|
33
33
|
"@types/deno": ["@types/deno@2.0.0", "", {}, "sha512-O9/jRVlq93kqfkl4sYR5N7+Pz4ukzXVIbMnE/VgvpauNHsvjQ9iBVnJ3X0gAvMa2khcoFD8DSO7mQVCuiuDMPg=="],
|
|
34
34
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
|
46
46
|
|
|
47
|
-
"bun-types": ["bun-types@1.2.
|
|
47
|
+
"bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="],
|
|
48
48
|
|
|
49
49
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
|
50
50
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let title = "Default Title"
|
|
3
|
+
let description = "Default description for this component"
|
|
4
|
+
let clicks = 0
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="card component-card">
|
|
8
|
+
<h3 class="component-title">{title}</h3>
|
|
9
|
+
<p>{description}</p>
|
|
10
|
+
<button @click="clicks++">Clicked {clicks} times</button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
.component-card {
|
|
15
|
+
border-left: 4px solid #2563eb;
|
|
16
|
+
background-color: #f0f7ff;
|
|
17
|
+
}
|
|
18
|
+
.component-title {
|
|
19
|
+
color: #2563eb;
|
|
20
|
+
margin-top: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.card {
|
|
24
|
+
background-color: white;
|
|
25
|
+
border-radius: 8px;
|
|
26
|
+
padding: 1.5rem;
|
|
27
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
28
|
+
margin-bottom: 1rem;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vyckr/tachyon",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"author": "Chidelma",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Chidelma/Tachyon.git"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/bun": "
|
|
10
|
+
"@types/bun": "~1.2.8",
|
|
11
11
|
"@types/deno": "^2.0.0",
|
|
12
12
|
"@types/jsdom": "^21.1.7",
|
|
13
13
|
"@types/node": "^20.4.2"
|
package/routes/HTML
CHANGED
|
@@ -1,6 +1,131 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<script>
|
|
2
|
+
document.title = "Home"
|
|
3
|
+
|
|
4
|
+
let count = 0
|
|
5
|
+
let showInfo = false
|
|
6
|
+
let newTodo = ''
|
|
7
|
+
let todos = [
|
|
8
|
+
{ text: "Learn Yon framework", completed: false },
|
|
9
|
+
{ text: "Build an application", completed: false }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
let componentTitle = "Component Test"
|
|
13
|
+
let componentDesc = "This tests custom components in Yon"
|
|
14
|
+
|
|
15
|
+
function addTodo() {
|
|
16
|
+
if (newTodo) {
|
|
17
|
+
todos = [...todos, { text: newTodo, completed: false }]
|
|
18
|
+
newTodo = ''
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toggleTodo(index) {
|
|
23
|
+
todos = todos.map((todo, i) =>
|
|
24
|
+
i === index ? { ...todo, completed: !todo.completed } : todo
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function removeTodo(index) {
|
|
29
|
+
todos = todos.filter((_, i) => i !== index)
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<h1>Yon Framework Test</h1>
|
|
34
|
+
|
|
35
|
+
<ty-clicker :clicks="count"/>
|
|
36
|
+
|
|
37
|
+
<div class="card">
|
|
38
|
+
<h2>Counter Example</h2>
|
|
39
|
+
<p>Count: <span>{count}</span></p>
|
|
40
|
+
<button @click="count++">Increment</button>
|
|
41
|
+
<button @click="count--">Decrement</button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<h2>Todo List</h2>
|
|
45
|
+
<input type="text" placeholder="Add new todo" :value="newTodo" />
|
|
46
|
+
<button @click="addTodo()">Add</button>
|
|
47
|
+
|
|
48
|
+
<ty-loop :for="let i = 0; i < todos.length; i++">
|
|
49
|
+
<div class="todo-item">
|
|
50
|
+
<input type="checkbox" :checked="todos[i].completed" @change="toggleTodo(i)" />
|
|
51
|
+
<span :class="todos[i].completed ? 'completed' : ''">{todos[i].text}</span>
|
|
52
|
+
<button @click="removeTodo(i)" style="margin-left: auto; background-color: #ef4444;">Remove</button>
|
|
53
|
+
</div>
|
|
54
|
+
</ty-loop>
|
|
55
|
+
|
|
56
|
+
<ty-logic :if="showInfo">
|
|
57
|
+
<div class="card">
|
|
58
|
+
<h2>Framework Info</h2>
|
|
59
|
+
<p>This is testing the conditional rendering feature of the Yon framework.</p>
|
|
60
|
+
<button @click="showInfo = false">Hide Info</button>
|
|
61
|
+
</div>
|
|
62
|
+
</ty-logic>
|
|
63
|
+
<ty-logic :else>
|
|
64
|
+
<div class="card">
|
|
65
|
+
<button @click="showInfo = true">Show Info</button>
|
|
66
|
+
</div>
|
|
67
|
+
</ty-logic>
|
|
68
|
+
|
|
69
|
+
<ty-clicker :clicks="count"/>
|
|
70
|
+
|
|
71
|
+
<style>
|
|
72
|
+
body {
|
|
73
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
74
|
+
line-height: 1.5;
|
|
75
|
+
max-width: 800px;
|
|
76
|
+
margin: 0 auto;
|
|
77
|
+
padding: 1rem;
|
|
78
|
+
color: #333;
|
|
79
|
+
background-color: #f5f5f5;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.card {
|
|
83
|
+
background-color: white;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
padding: 1.5rem;
|
|
86
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
87
|
+
margin-bottom: 1rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
h1 {
|
|
91
|
+
color: #2563eb;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
button {
|
|
95
|
+
background-color: #2563eb;
|
|
96
|
+
color: white;
|
|
97
|
+
border: none;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
padding: 0.5rem 1rem;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
font-size: 1rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
button:hover {
|
|
105
|
+
background-color: #1d4ed8;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
input {
|
|
109
|
+
padding: 0.5rem;
|
|
110
|
+
border: 1px solid #ccc;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
margin-right: 0.5rem;
|
|
113
|
+
font-size: 1rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.todo-item {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
padding: 0.5rem;
|
|
120
|
+
border-bottom: 1px solid #eee;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.todo-item:last-child {
|
|
124
|
+
border-bottom: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.completed {
|
|
128
|
+
text-decoration: line-through;
|
|
129
|
+
opacity: 0.7;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
package/src/client/dev.html
CHANGED
package/src/client/prod.html
CHANGED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
let render: Function
|
|
2
|
+
const routes = new Map<string, Record<string, number>>()
|
|
3
|
+
let params: any[] = []
|
|
4
|
+
const slugs: Record<string, any> = {}
|
|
5
|
+
let previousRender: string
|
|
6
|
+
let madeRequest: boolean = false
|
|
7
|
+
let elementId: string | null;
|
|
8
|
+
let selectionStart: number | null;
|
|
9
|
+
const parser = new DOMParser()
|
|
10
|
+
let firstRender = routes.size === 0
|
|
11
|
+
const elementEvents: Record<string, EventListener> = {}
|
|
12
|
+
|
|
13
|
+
if(firstRender) {
|
|
14
|
+
|
|
15
|
+
fetch('/routes.json')
|
|
16
|
+
.then(res => res.json())
|
|
17
|
+
.then(data => {
|
|
18
|
+
for (const [path, slugs] of Object.entries(data)) {
|
|
19
|
+
routes.set(path, slugs as Record<string, number>)
|
|
20
|
+
}
|
|
21
|
+
setPageTemplate(window.location.pathname)
|
|
22
|
+
firstRender = false
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mergeBodyHTML(html: string) {
|
|
27
|
+
|
|
28
|
+
if(html === previousRender) return
|
|
29
|
+
|
|
30
|
+
previousRender = html
|
|
31
|
+
|
|
32
|
+
// document.body.innerHTML = html
|
|
33
|
+
|
|
34
|
+
if(madeRequest) {
|
|
35
|
+
document.body.innerHTML = html
|
|
36
|
+
} else {
|
|
37
|
+
const nextDom = parser.parseFromString(html, 'text/html')
|
|
38
|
+
updateDOM(document.body, nextDom.body)
|
|
39
|
+
deleteDOM(document.body, nextDom.body)
|
|
40
|
+
insertDOM(document.body, nextDom.body)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addEvents()
|
|
44
|
+
|
|
45
|
+
if(elementId) {
|
|
46
|
+
|
|
47
|
+
const element = document.getElementById(elementId)
|
|
48
|
+
|
|
49
|
+
if(element) {
|
|
50
|
+
|
|
51
|
+
element.focus()
|
|
52
|
+
|
|
53
|
+
if(selectionStart && 'setSelectionRange' in element) {
|
|
54
|
+
|
|
55
|
+
(element as HTMLInputElement).setSelectionRange(selectionStart, selectionStart)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
elementId = null;
|
|
60
|
+
selectionStart = null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function addEvents(elements?: HTMLCollection) {
|
|
65
|
+
|
|
66
|
+
if(!elements) elements = document.body.children
|
|
67
|
+
|
|
68
|
+
for (const element of elements) {
|
|
69
|
+
|
|
70
|
+
const allEvents: string[] = []
|
|
71
|
+
|
|
72
|
+
for (const attribute of element.attributes) {
|
|
73
|
+
|
|
74
|
+
if (attribute.name.startsWith('@')) {
|
|
75
|
+
|
|
76
|
+
const event = attribute.name.substring(1)
|
|
77
|
+
|
|
78
|
+
allEvents.push(event)
|
|
79
|
+
|
|
80
|
+
if(elementEvents[`${element.id}_${event}`]) {
|
|
81
|
+
element.removeEventListener(event, elementEvents[`${element.id}_${event}`])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
elementEvents[`${element.id}_${event}`] = async function(e) {
|
|
85
|
+
|
|
86
|
+
elementId = element.id
|
|
87
|
+
|
|
88
|
+
if (e.target) {
|
|
89
|
+
selectionStart = (e.target as HTMLInputElement).selectionStart;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
mergeBodyHTML(await render(elementId))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
element.addEventListener(event, elementEvents[`${element.id}_${event}`])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if(attribute.name.endsWith('ed') && attribute.value === "false") element.removeAttribute(attribute.name)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if(element instanceof HTMLSelectElement || element instanceof HTMLInputElement) {
|
|
102
|
+
|
|
103
|
+
element.oninput = async (ev) => {
|
|
104
|
+
if (ev.target) {
|
|
105
|
+
|
|
106
|
+
elementId = element.id
|
|
107
|
+
|
|
108
|
+
if (ev.target) {
|
|
109
|
+
selectionStart = (ev.target as HTMLInputElement).selectionStart;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const evt = { value: (ev.target as any).value, defaultValue: (ev.target as any).defaultValue }
|
|
113
|
+
|
|
114
|
+
mergeBodyHTML(await render(elementId, evt))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addEvents(element.children)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function onClickEvent(ev: MouseEvent) {
|
|
124
|
+
const target = ev.target as HTMLAnchorElement;
|
|
125
|
+
if(target?.href) {
|
|
126
|
+
const url = new URL(target?.href)
|
|
127
|
+
if(url.origin !== location.origin) return
|
|
128
|
+
ev.preventDefault()
|
|
129
|
+
setPageTemplate(url.pathname)
|
|
130
|
+
} else mergeBodyHTML(await render())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setPageTemplate(pathname: string) {
|
|
134
|
+
|
|
135
|
+
let url;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
|
|
139
|
+
let handler = getHandler(pathname)
|
|
140
|
+
|
|
141
|
+
if(handler === '/') handler = ''
|
|
142
|
+
|
|
143
|
+
url = `${handler}/HTML.js`
|
|
144
|
+
|
|
145
|
+
} catch(err) {
|
|
146
|
+
url = `/404.js`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
import(`/pages${url}`).then(async module => {
|
|
150
|
+
window.history.replaceState({}, '', pathname)
|
|
151
|
+
render = await module.default()
|
|
152
|
+
madeRequest = true
|
|
153
|
+
mergeBodyHTML(await render())
|
|
154
|
+
madeRequest = false
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
function getHandler(pathname: string) {
|
|
160
|
+
|
|
161
|
+
let handler;
|
|
162
|
+
|
|
163
|
+
if (pathname === '/') return pathname
|
|
164
|
+
|
|
165
|
+
const paths = pathname.split('/').slice(1);
|
|
166
|
+
|
|
167
|
+
let bestMatchKey = '';
|
|
168
|
+
let bestMatchLength = -1;
|
|
169
|
+
|
|
170
|
+
for (const [routeKey] of routes) {
|
|
171
|
+
|
|
172
|
+
const routeSegs = routeKey.split('/')
|
|
173
|
+
|
|
174
|
+
const isMatch = pathsMatch(routeSegs, paths.slice(0, routeSegs.length));
|
|
175
|
+
|
|
176
|
+
if (isMatch && routeSegs.length > bestMatchLength) {
|
|
177
|
+
bestMatchKey = routeKey;
|
|
178
|
+
bestMatchLength = routeSegs.length;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (bestMatchKey) {
|
|
183
|
+
|
|
184
|
+
handler = bestMatchKey
|
|
185
|
+
|
|
186
|
+
params = parseParams(paths.slice(bestMatchLength))
|
|
187
|
+
|
|
188
|
+
const slugMap = routes.get(bestMatchKey) ?? {}
|
|
189
|
+
|
|
190
|
+
Object.entries(slugMap).forEach(([key, idx]) => {
|
|
191
|
+
key = key.replace(':', '')
|
|
192
|
+
slugs[key] = paths[idx]
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!handler) throw new Error(`Route ${pathname} not found`, { cause: 404 });
|
|
197
|
+
|
|
198
|
+
return handler
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
function pathsMatch(routeSegs: string[], pathSegs: string[]) {
|
|
203
|
+
|
|
204
|
+
if (routeSegs.length !== pathSegs.length) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const slugs = routes.get(routeSegs.join('/')) || {}
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < routeSegs.length; i++) {
|
|
211
|
+
if (!slugs[routeSegs[i]] && routeSegs[i] !== pathSegs[i]) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
function parseParams(input: string[]) {
|
|
221
|
+
|
|
222
|
+
const params = []
|
|
223
|
+
|
|
224
|
+
for(const param of input) {
|
|
225
|
+
|
|
226
|
+
const num = Number(param)
|
|
227
|
+
|
|
228
|
+
if(!Number.isNaN(num)) params.push(num)
|
|
229
|
+
|
|
230
|
+
else if(param === 'true') params.push(true)
|
|
231
|
+
|
|
232
|
+
else if(param === 'false') params.push(false)
|
|
233
|
+
|
|
234
|
+
else if(param === 'null') params.push(null)
|
|
235
|
+
|
|
236
|
+
else if(param === 'undefined') params.push(undefined)
|
|
237
|
+
|
|
238
|
+
else params.push(param)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return params
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Object.keys(window).forEach(key => {
|
|
245
|
+
if(/^on/.test(key)) {
|
|
246
|
+
document.addEventListener(key.slice(2), async ev => {
|
|
247
|
+
|
|
248
|
+
switch(ev.type) {
|
|
249
|
+
case 'click':
|
|
250
|
+
await onClickEvent(ev as MouseEvent)
|
|
251
|
+
break
|
|
252
|
+
case 'popstate':
|
|
253
|
+
setPageTemplate(window.location.pathname)
|
|
254
|
+
break
|
|
255
|
+
default:
|
|
256
|
+
mergeBodyHTML(await render())
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Updates existing nodes in oldDOM with properties from newDOM
|
|
267
|
+
* @param {Node} oldDOM - The current DOM
|
|
268
|
+
* @param {Node} newDOM - The desired DOM state
|
|
269
|
+
*/
|
|
270
|
+
function updateDOM(oldDOM: Node, newDOM: Node): void {
|
|
271
|
+
// Skip if nodes are not of the same type
|
|
272
|
+
if (!areNodesOfSameType(oldDOM, newDOM)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If it's a text node, update the content
|
|
277
|
+
if (oldDOM.nodeType === Node.TEXT_NODE && newDOM.nodeType === Node.TEXT_NODE) {
|
|
278
|
+
if (oldDOM.textContent !== newDOM.textContent) {
|
|
279
|
+
oldDOM.textContent = newDOM.textContent;
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If it's an element node, update attributes
|
|
285
|
+
if (oldDOM.nodeType === Node.ELEMENT_NODE && newDOM.nodeType === Node.ELEMENT_NODE) {
|
|
286
|
+
// Type assertion since we've already checked nodeType
|
|
287
|
+
const oldElement = oldDOM as Element;
|
|
288
|
+
const newElement = newDOM as Element;
|
|
289
|
+
|
|
290
|
+
// Update attributes
|
|
291
|
+
updateAttributes(oldElement, newElement);
|
|
292
|
+
|
|
293
|
+
// Recursively update children that exist in both DOMs
|
|
294
|
+
const oldChildren = Array.from(oldDOM.childNodes);
|
|
295
|
+
const newChildren = Array.from(newDOM.childNodes);
|
|
296
|
+
|
|
297
|
+
const minLength = Math.min(oldChildren.length, newChildren.length);
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < minLength; i++) {
|
|
300
|
+
updateDOM(oldChildren[i], newChildren[i]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Deletes nodes from oldDOM that don't exist in newDOM
|
|
307
|
+
* @param {Node} oldDOM - The current DOM
|
|
308
|
+
* @param {Node} newDOM - The desired DOM state
|
|
309
|
+
*/
|
|
310
|
+
function deleteDOM(oldDOM: Node, newDOM: Node): void {
|
|
311
|
+
// If nodes are not of the same type, skip (will be handled by insertDOM)
|
|
312
|
+
if (!areNodesOfSameType(oldDOM, newDOM)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// If it's an element node, check children
|
|
317
|
+
if (oldDOM.nodeType === Node.ELEMENT_NODE && newDOM.nodeType === Node.ELEMENT_NODE) {
|
|
318
|
+
const oldChildren = Array.from(oldDOM.childNodes);
|
|
319
|
+
const newChildren = Array.from(newDOM.childNodes);
|
|
320
|
+
|
|
321
|
+
// Remove children that exist in oldDOM but not in newDOM
|
|
322
|
+
// Start from the end to avoid index shifting issues
|
|
323
|
+
for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
|
|
324
|
+
oldDOM.removeChild(oldChildren[i]);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Recursively delete for remaining children
|
|
328
|
+
const minLength = Math.min(oldChildren.length, newChildren.length);
|
|
329
|
+
for (let i = 0; i < minLength; i++) {
|
|
330
|
+
deleteDOM(oldChildren[i], newChildren[i]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Inserts nodes from newDOM that don't exist in oldDOM
|
|
337
|
+
* @param {Node} oldDOM - The current DOM
|
|
338
|
+
* @param {Node} newDOM - The desired DOM state
|
|
339
|
+
*/
|
|
340
|
+
function insertDOM(oldDOM: Node, newDOM: Node): void {
|
|
341
|
+
// If nodes are not of the same type, replace the entire node
|
|
342
|
+
if (!areNodesOfSameType(oldDOM, newDOM)) {
|
|
343
|
+
if (oldDOM.parentNode) {
|
|
344
|
+
oldDOM.parentNode.replaceChild(newDOM.cloneNode(true), oldDOM);
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// If it's an element node, check children
|
|
350
|
+
if (oldDOM.nodeType === Node.ELEMENT_NODE && newDOM.nodeType === Node.ELEMENT_NODE) {
|
|
351
|
+
const oldChildren = Array.from(oldDOM.childNodes);
|
|
352
|
+
const newChildren = Array.from(newDOM.childNodes);
|
|
353
|
+
|
|
354
|
+
// Add children that exist in newDOM but not in oldDOM
|
|
355
|
+
for (let i = oldChildren.length; i < newChildren.length; i++) {
|
|
356
|
+
oldDOM.appendChild(newChildren[i].cloneNode(true));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Recursively insert for existing children
|
|
360
|
+
const minLength = Math.min(oldChildren.length, newChildren.length);
|
|
361
|
+
for (let i = 0; i < minLength; i++) {
|
|
362
|
+
insertDOM(oldChildren[i], newChildren[i]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Helper function to update attributes of an element
|
|
369
|
+
* @param {Element} oldElement - The element to update
|
|
370
|
+
* @param {Element} newElement - The element with the desired attributes
|
|
371
|
+
*/
|
|
372
|
+
function updateAttributes(oldElement: Element, newElement: Element): void {
|
|
373
|
+
// Remove attributes not in newElement
|
|
374
|
+
for (const attr of Array.from(oldElement.attributes)) {
|
|
375
|
+
if (!newElement.hasAttribute(attr.name)) {
|
|
376
|
+
oldElement.removeAttribute(attr.name);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Add or update attributes from newElement
|
|
381
|
+
for (const attr of Array.from(newElement.attributes)) {
|
|
382
|
+
// Use strict equality to ensure empty strings are properly handled
|
|
383
|
+
if (!attr.name.startsWith('@') &&
|
|
384
|
+
(oldElement.getAttribute(attr.name) !== attr.value ||
|
|
385
|
+
(!oldElement.hasAttribute(attr.name) && attr.value === ""))) {
|
|
386
|
+
if(oldElement.children.length === 0) {
|
|
387
|
+
oldElement.outerHTML = newElement.outerHTML
|
|
388
|
+
} else oldElement.setAttribute(attr.name, attr.value)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Helper function to check if two nodes are of the same type
|
|
394
|
+
* @param {Node} oldNode - The old node
|
|
395
|
+
* @param {Node} newNode - The new node
|
|
396
|
+
* @returns {boolean} - Whether the nodes are of the same type
|
|
397
|
+
*/
|
|
398
|
+
function areNodesOfSameType(oldNode: Node, newNode: Node): boolean {
|
|
399
|
+
// Check if both nodes are defined
|
|
400
|
+
if (!oldNode || !newNode) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check if node types match
|
|
405
|
+
if (oldNode.nodeType !== newNode.nodeType) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// For element nodes, check if tag names match
|
|
410
|
+
if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
|
|
411
|
+
const oldElement = oldNode as Element;
|
|
412
|
+
const newElement = newNode as Element;
|
|
413
|
+
return oldElement.tagName === newElement.tagName;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return true;
|
|
417
|
+
}
|