@bitstillery/mithril 3.0.0-AA
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/LICENSE +21 -0
- package/README.md +185 -0
- package/api/mount-redraw.ts +173 -0
- package/api/router-server.ts +88 -0
- package/api/router.ts +670 -0
- package/browser.ts +10 -0
- package/htm.ts +7 -0
- package/hyperscript.ts +8 -0
- package/index.ts +134 -0
- package/jsx.d.ts +16 -0
- package/mount.ts +10 -0
- package/package.json +66 -0
- package/pathname/build.ts +40 -0
- package/pathname/compileTemplate.ts +49 -0
- package/pathname/parse.ts +19 -0
- package/querystring/build.ts +22 -0
- package/querystring/parse.ts +50 -0
- package/redraw.ts +10 -0
- package/render/cachedAttrsIsStaticMap.ts +10 -0
- package/render/delayedRemoval.ts +1 -0
- package/render/domFor.ts +23 -0
- package/render/emptyAttrs.ts +2 -0
- package/render/fragment.ts +11 -0
- package/render/hyperscript.ts +125 -0
- package/render/hyperscriptVnode.ts +21 -0
- package/render/render.ts +1307 -0
- package/render/renderToString.ts +494 -0
- package/render/ssrState.ts +366 -0
- package/render/trust.ts +6 -0
- package/render/vnode.ts +131 -0
- package/route.ts +11 -0
- package/server/logger.ts +223 -0
- package/server/session.ts +87 -0
- package/server/ssr.ts +205 -0
- package/server/ssrLogger.ts +13 -0
- package/server.ts +101 -0
- package/signal.ts +320 -0
- package/ssrContext.ts +87 -0
- package/state.ts +1026 -0
- package/store.ts +457 -0
- package/util/censor.ts +46 -0
- package/util/decodeURIComponentSafe.ts +34 -0
- package/util/hasOwn.ts +2 -0
- package/util/next_tick.ts +45 -0
- package/util/ssr.ts +385 -0
- package/util/uri.ts +126 -0
- package/version.ts +3 -0
package/render/render.ts
ADDED
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
import {setCurrentComponent, clearCurrentComponent, clearComponentDependencies} from '../signal'
|
|
2
|
+
import {logHydrationError, resetHydrationErrorCount} from '../util/ssr'
|
|
3
|
+
import {logger} from '../server/logger'
|
|
4
|
+
|
|
5
|
+
import Vnode from './vnode'
|
|
6
|
+
import delayedRemoval from './delayedRemoval'
|
|
7
|
+
import domFor from './domFor'
|
|
8
|
+
import cachedAttrsIsStaticMap from './cachedAttrsIsStaticMap'
|
|
9
|
+
|
|
10
|
+
import type {Vnode as VnodeType, Children} from './vnode'
|
|
11
|
+
|
|
12
|
+
export default function renderFactory() {
|
|
13
|
+
const nameSpace: Record<string, string> = {
|
|
14
|
+
svg: 'http://www.w3.org/2000/svg',
|
|
15
|
+
math: 'http://www.w3.org/1998/Math/MathML',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let currentRedraw: (() => void) | undefined
|
|
19
|
+
let currentRender: any
|
|
20
|
+
// Track hydration mismatches for override mode
|
|
21
|
+
let hydrationMismatchCount = 0
|
|
22
|
+
const MAX_HYDRATION_MISMATCHES = 5
|
|
23
|
+
|
|
24
|
+
function getDocument(dom: Node): Document {
|
|
25
|
+
return dom.ownerDocument!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getNameSpace(vnode: any): string | undefined {
|
|
29
|
+
return (vnode.attrs && vnode.attrs.xmlns) || nameSpace[vnode.tag]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// sanity check to discourage people from doing `vnode.state = ...`
|
|
33
|
+
function checkState(vnode: any, original: any) {
|
|
34
|
+
if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Note: the hook is passed as the `this` argument to allow proxying the
|
|
38
|
+
// arguments without requiring a full array allocation to do so. It also
|
|
39
|
+
// takes advantage of the fact the current `vnode` is the first argument in
|
|
40
|
+
// all lifecycle methods.
|
|
41
|
+
function callHook(this: any, vnode: any, ...args: any[]) {
|
|
42
|
+
if (this == null || typeof this.apply !== 'function') {
|
|
43
|
+
const tagName = typeof vnode?.tag === 'function' ? vnode.tag?.name : vnode?.tag
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
`callHook: expected a function with .apply (e.g. component.view), got ${tagName ?? vnode?.tag}. Check that the component has a view.`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
const original = vnode.state
|
|
49
|
+
try {
|
|
50
|
+
return this.apply(original, [vnode, ...args])
|
|
51
|
+
} finally {
|
|
52
|
+
checkState(vnode, original)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when
|
|
57
|
+
// inside an iframe. Catch and swallow this error, and heavy-handidly return null.
|
|
58
|
+
function activeElement(dom: Node): Element | null {
|
|
59
|
+
try {
|
|
60
|
+
return getDocument(dom).activeElement
|
|
61
|
+
} catch (_e) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// create
|
|
66
|
+
function createNodes(
|
|
67
|
+
parent: Element | DocumentFragment,
|
|
68
|
+
vnodes: (VnodeType | null)[],
|
|
69
|
+
start: number,
|
|
70
|
+
end: number,
|
|
71
|
+
hooks: Array<() => void>,
|
|
72
|
+
nextSibling: Node | null,
|
|
73
|
+
ns: string | undefined,
|
|
74
|
+
isHydrating: boolean = false,
|
|
75
|
+
matchedNodes: Set<Node> | null = null,
|
|
76
|
+
) {
|
|
77
|
+
// Track which DOM nodes we've matched during hydration to avoid reusing the same node twice
|
|
78
|
+
// Create a new set if not provided and we're hydrating at the root level
|
|
79
|
+
const createdMatchedNodes = matchedNodes == null && isHydrating && nextSibling == null
|
|
80
|
+
if (createdMatchedNodes) {
|
|
81
|
+
matchedNodes = new Set<Node>()
|
|
82
|
+
}
|
|
83
|
+
for (let i = start; i < end; i++) {
|
|
84
|
+
const vnode = vnodes[i]
|
|
85
|
+
if (vnode != null) {
|
|
86
|
+
createNode(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// After creating/matching all nodes, remove any unmatched nodes that remain
|
|
90
|
+
// Only do this at the root level to avoid removing nodes that are part of matched subtrees
|
|
91
|
+
if (createdMatchedNodes && matchedNodes && parent.firstChild && nextSibling == null) {
|
|
92
|
+
let node: Node | null = parent.firstChild
|
|
93
|
+
while (node) {
|
|
94
|
+
const next: Node | null = node.nextSibling
|
|
95
|
+
if (!matchedNodes.has(node)) {
|
|
96
|
+
try {
|
|
97
|
+
parent.removeChild(node)
|
|
98
|
+
} catch (e) {
|
|
99
|
+
const error = e as Error
|
|
100
|
+
logHydrationError(
|
|
101
|
+
'removeChild (root level cleanup)',
|
|
102
|
+
null, // No vnode at root level
|
|
103
|
+
parent instanceof Element ? parent : null,
|
|
104
|
+
error,
|
|
105
|
+
{parent: parent instanceof Element ? parent : undefined, node, matchedNodes},
|
|
106
|
+
)
|
|
107
|
+
// Don't re-throw - we've already logged the error with all details
|
|
108
|
+
// Re-throwing causes the browser to log the DOMException stack trace
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
node = next
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function createNode(
|
|
116
|
+
parent: Element | DocumentFragment,
|
|
117
|
+
vnode: any,
|
|
118
|
+
hooks: Array<() => void>,
|
|
119
|
+
ns: string | undefined,
|
|
120
|
+
nextSibling: Node | null,
|
|
121
|
+
isHydrating: boolean = false,
|
|
122
|
+
matchedNodes: Set<Node> | null = null,
|
|
123
|
+
) {
|
|
124
|
+
const tag = vnode.tag
|
|
125
|
+
if (typeof tag === 'string') {
|
|
126
|
+
vnode.state = {}
|
|
127
|
+
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks, isHydrating)
|
|
128
|
+
switch (tag) {
|
|
129
|
+
case '#':
|
|
130
|
+
createText(parent, vnode, nextSibling, isHydrating, matchedNodes)
|
|
131
|
+
break
|
|
132
|
+
case '<':
|
|
133
|
+
createHTML(parent, vnode, ns, nextSibling)
|
|
134
|
+
break
|
|
135
|
+
case '[':
|
|
136
|
+
createFragment(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
|
|
137
|
+
break
|
|
138
|
+
default:
|
|
139
|
+
createElement(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
|
|
140
|
+
}
|
|
141
|
+
} else createComponent(parent, vnode, hooks, ns, nextSibling, isHydrating, matchedNodes)
|
|
142
|
+
}
|
|
143
|
+
function createText(
|
|
144
|
+
parent: Element | DocumentFragment,
|
|
145
|
+
vnode: any,
|
|
146
|
+
nextSibling: Node | null,
|
|
147
|
+
isHydrating: boolean = false,
|
|
148
|
+
matchedNodes: Set<Node> | null = null,
|
|
149
|
+
) {
|
|
150
|
+
let textNode: Text
|
|
151
|
+
if (isHydrating && parent.firstChild && nextSibling == null && matchedNodes) {
|
|
152
|
+
// During hydration, try to reuse existing text node
|
|
153
|
+
// Normalize text for comparison (trim whitespace differences)
|
|
154
|
+
const expectedText = String(vnode.children || '').trim()
|
|
155
|
+
let candidate: Node | null = parent.firstChild
|
|
156
|
+
while (candidate) {
|
|
157
|
+
if (candidate.nodeType === 3 && !matchedNodes.has(candidate)) {
|
|
158
|
+
const candidateText = candidate as Text
|
|
159
|
+
const candidateValue = candidateText.nodeValue || ''
|
|
160
|
+
// Exact match preferred, but also accept trimmed match for whitespace differences
|
|
161
|
+
if (candidateValue === String(vnode.children) || (expectedText && candidateValue.trim() === expectedText)) {
|
|
162
|
+
textNode = candidateText
|
|
163
|
+
matchedNodes.add(textNode)
|
|
164
|
+
// Update text content if there's a minor difference (whitespace normalization)
|
|
165
|
+
if (candidateValue !== String(vnode.children)) {
|
|
166
|
+
textNode.nodeValue = String(vnode.children)
|
|
167
|
+
}
|
|
168
|
+
// Don't remove/reinsert - just reuse the existing node in place
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
candidate = candidate.nextSibling
|
|
173
|
+
}
|
|
174
|
+
// If no matching text node found, create new one
|
|
175
|
+
if (!textNode!) {
|
|
176
|
+
textNode = getDocument(parent as Element).createTextNode(vnode.children)
|
|
177
|
+
insertDOM(parent, textNode, nextSibling)
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
textNode = getDocument(parent as Element).createTextNode(vnode.children)
|
|
181
|
+
insertDOM(parent, textNode, nextSibling)
|
|
182
|
+
}
|
|
183
|
+
vnode.dom = textNode
|
|
184
|
+
}
|
|
185
|
+
const possibleParents: Record<string, string> = {
|
|
186
|
+
caption: 'table',
|
|
187
|
+
thead: 'table',
|
|
188
|
+
tbody: 'table',
|
|
189
|
+
tfoot: 'table',
|
|
190
|
+
tr: 'tbody',
|
|
191
|
+
th: 'tr',
|
|
192
|
+
td: 'tr',
|
|
193
|
+
colgroup: 'table',
|
|
194
|
+
col: 'colgroup',
|
|
195
|
+
}
|
|
196
|
+
function createHTML(parent: Element | DocumentFragment, vnode: any, ns: string | undefined, nextSibling: Node | null) {
|
|
197
|
+
const match = vnode.children.match(/^\s*?<(\w+)/im) || []
|
|
198
|
+
// not using the proper parent makes the child element(s) vanish.
|
|
199
|
+
// var div = document.createElement("div")
|
|
200
|
+
// div.innerHTML = "<td>i</td><td>j</td>"
|
|
201
|
+
// console.log(div.innerHTML)
|
|
202
|
+
// --> "ij", no <td> in sight.
|
|
203
|
+
let temp = getDocument(parent as Element).createElement(possibleParents[match[1]] || 'div')
|
|
204
|
+
if (ns === 'http://www.w3.org/2000/svg') {
|
|
205
|
+
temp.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg">' + vnode.children + '</svg>'
|
|
206
|
+
temp = temp.firstChild as HTMLElement
|
|
207
|
+
} else {
|
|
208
|
+
temp.innerHTML = vnode.children
|
|
209
|
+
}
|
|
210
|
+
vnode.dom = temp.firstChild
|
|
211
|
+
vnode.domSize = temp.childNodes.length
|
|
212
|
+
const fragment = getDocument(parent as Element).createDocumentFragment()
|
|
213
|
+
let child: Node | null
|
|
214
|
+
while ((child = temp.firstChild) != null) {
|
|
215
|
+
fragment.appendChild(child)
|
|
216
|
+
}
|
|
217
|
+
insertDOM(parent, fragment, nextSibling)
|
|
218
|
+
}
|
|
219
|
+
function createFragment(
|
|
220
|
+
parent: Element | DocumentFragment,
|
|
221
|
+
vnode: any,
|
|
222
|
+
hooks: Array<() => void>,
|
|
223
|
+
ns: string | undefined,
|
|
224
|
+
nextSibling: Node | null,
|
|
225
|
+
isHydrating: boolean = false,
|
|
226
|
+
matchedNodes: Set<Node> | null = null,
|
|
227
|
+
) {
|
|
228
|
+
const fragment = getDocument(parent as Element).createDocumentFragment()
|
|
229
|
+
if (vnode.children != null) {
|
|
230
|
+
const children = vnode.children
|
|
231
|
+
createNodes(fragment, children, 0, children.length, hooks, null, ns, isHydrating, matchedNodes)
|
|
232
|
+
}
|
|
233
|
+
vnode.dom = fragment.firstChild
|
|
234
|
+
vnode.domSize = fragment.childNodes.length
|
|
235
|
+
insertDOM(parent, fragment, nextSibling)
|
|
236
|
+
}
|
|
237
|
+
function createElement(
|
|
238
|
+
parent: Element | DocumentFragment,
|
|
239
|
+
vnode: any,
|
|
240
|
+
hooks: Array<() => void>,
|
|
241
|
+
ns: string | undefined,
|
|
242
|
+
nextSibling: Node | null,
|
|
243
|
+
isHydrating: boolean = false,
|
|
244
|
+
matchedNodes: Set<Node> | null = null,
|
|
245
|
+
) {
|
|
246
|
+
const tag = vnode.tag
|
|
247
|
+
const attrs = vnode.attrs
|
|
248
|
+
const is = vnode.is
|
|
249
|
+
|
|
250
|
+
ns = getNameSpace(vnode) || ns
|
|
251
|
+
|
|
252
|
+
let element: Element
|
|
253
|
+
if (isHydrating && parent.firstChild && nextSibling == null && matchedNodes) {
|
|
254
|
+
// During hydration, try to reuse existing DOM node
|
|
255
|
+
// Only match if we're appending (nextSibling == null) to preserve order
|
|
256
|
+
// Find the first unmatched child element that matches the tag
|
|
257
|
+
// More lenient matching: skip text nodes and comments, allow tag name case differences
|
|
258
|
+
let candidate: Node | null = parent.firstChild
|
|
259
|
+
let fallbackCandidate: Element | null = null
|
|
260
|
+
while (candidate) {
|
|
261
|
+
// Skip text nodes, comments, and already matched nodes
|
|
262
|
+
if (candidate.nodeType === 1 && !matchedNodes.has(candidate)) {
|
|
263
|
+
const candidateEl = candidate as Element
|
|
264
|
+
// Case-insensitive tag matching (browsers normalize to uppercase for some tags)
|
|
265
|
+
// Use tagName if available, fallback to nodeName (for DOM mocks)
|
|
266
|
+
const candidateTag = (candidateEl as any).tagName || candidateEl.nodeName
|
|
267
|
+
if (candidateTag && candidateTag.toLowerCase() === tag.toLowerCase()) {
|
|
268
|
+
// Prefer exact match (is attribute matches if specified)
|
|
269
|
+
if (!is || candidateEl.getAttribute('is') === is) {
|
|
270
|
+
element = candidateEl
|
|
271
|
+
matchedNodes.add(element)
|
|
272
|
+
// Don't remove/reinsert - just reuse the existing node in place
|
|
273
|
+
break
|
|
274
|
+
}
|
|
275
|
+
// Keep track of first matching tag as fallback (even if is attribute doesn't match)
|
|
276
|
+
if (!fallbackCandidate) {
|
|
277
|
+
fallbackCandidate = candidateEl
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
candidate = candidate.nextSibling
|
|
282
|
+
}
|
|
283
|
+
// If no exact match found but we have a fallback, use it
|
|
284
|
+
// This handles cases where is attribute differs but tag matches
|
|
285
|
+
if (!element! && fallbackCandidate) {
|
|
286
|
+
element = fallbackCandidate
|
|
287
|
+
matchedNodes.add(element)
|
|
288
|
+
}
|
|
289
|
+
// If still no matching element found, create new one
|
|
290
|
+
if (!element!) {
|
|
291
|
+
element = ns
|
|
292
|
+
? is
|
|
293
|
+
? getDocument(parent as Element).createElementNS(ns, tag, {is: is} as any)
|
|
294
|
+
: getDocument(parent as Element).createElementNS(ns, tag)
|
|
295
|
+
: is
|
|
296
|
+
? getDocument(parent as Element).createElement(tag, {is: is} as any)
|
|
297
|
+
: getDocument(parent as Element).createElement(tag)
|
|
298
|
+
insertDOM(parent, element, nextSibling)
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
// Normal creation path
|
|
302
|
+
element = ns
|
|
303
|
+
? is
|
|
304
|
+
? getDocument(parent as Element).createElementNS(ns, tag, {is: is} as any)
|
|
305
|
+
: getDocument(parent as Element).createElementNS(ns, tag)
|
|
306
|
+
: is
|
|
307
|
+
? getDocument(parent as Element).createElement(tag, {is: is} as any)
|
|
308
|
+
: getDocument(parent as Element).createElement(tag)
|
|
309
|
+
insertDOM(parent, element, nextSibling)
|
|
310
|
+
}
|
|
311
|
+
vnode.dom = element
|
|
312
|
+
|
|
313
|
+
if (attrs != null) {
|
|
314
|
+
setAttrs(vnode, attrs, ns)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!maybeSetContentEditable(vnode)) {
|
|
318
|
+
if (vnode.children != null) {
|
|
319
|
+
const children = vnode.children
|
|
320
|
+
// During hydration, if we reused an element, it already has children
|
|
321
|
+
// Create a new matchedNodes set for this element's children to avoid duplicates
|
|
322
|
+
const childMatchedNodes = isHydrating && element.firstChild ? new Set<Node>() : null
|
|
323
|
+
createNodes(element, children, 0, children.length, hooks, null, ns, isHydrating, childMatchedNodes)
|
|
324
|
+
// After creating/matching children, remove any unmatched nodes that remain
|
|
325
|
+
// Only remove unmatched nodes if we actually matched some nodes (to avoid clearing everything)
|
|
326
|
+
if (isHydrating && childMatchedNodes && element.firstChild && childMatchedNodes.size > 0) {
|
|
327
|
+
let node: Node | null = element.firstChild
|
|
328
|
+
while (node) {
|
|
329
|
+
const next: Node | null = node.nextSibling
|
|
330
|
+
if (!childMatchedNodes.has(node)) {
|
|
331
|
+
// Verify node is still a child before attempting removal
|
|
332
|
+
if (element.contains && element.contains(node)) {
|
|
333
|
+
try {
|
|
334
|
+
element.removeChild(node)
|
|
335
|
+
hydrationMismatchCount++
|
|
336
|
+
} catch (e) {
|
|
337
|
+
const error = e as Error
|
|
338
|
+
// Check if node was already removed (not a child anymore)
|
|
339
|
+
if (!element.contains || !element.contains(node)) {
|
|
340
|
+
// Node already removed, skip silently
|
|
341
|
+
node = next
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
hydrationMismatchCount++
|
|
345
|
+
logHydrationError('removeChild (element children cleanup)', vnode, element, error, {
|
|
346
|
+
parent: element,
|
|
347
|
+
node,
|
|
348
|
+
matchedNodes: childMatchedNodes,
|
|
349
|
+
})
|
|
350
|
+
// Don't re-throw - we've already logged the error with all details
|
|
351
|
+
// Re-throwing causes the browser to log the DOMException stack trace
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Node not in parent, already removed - skip silently
|
|
355
|
+
}
|
|
356
|
+
node = next
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (vnode.tag === 'select' && attrs != null) setLateSelectAttrs(vnode, attrs)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function initComponent(vnode: any, hooks: Array<() => void>, isHydrating: boolean = false) {
|
|
364
|
+
let sentinel: any
|
|
365
|
+
if (typeof vnode.tag.view === 'function') {
|
|
366
|
+
vnode.state = Object.create(vnode.tag)
|
|
367
|
+
sentinel = vnode.state.view
|
|
368
|
+
if (sentinel.$$reentrantLock$$ != null) return
|
|
369
|
+
sentinel.$$reentrantLock$$ = true
|
|
370
|
+
} else {
|
|
371
|
+
vnode.state = void 0
|
|
372
|
+
sentinel = vnode.tag
|
|
373
|
+
if (sentinel.$$reentrantLock$$ != null) return
|
|
374
|
+
sentinel.$$reentrantLock$$ = true
|
|
375
|
+
vnode.state =
|
|
376
|
+
vnode.tag.prototype != null && typeof vnode.tag.prototype.view === 'function'
|
|
377
|
+
? new vnode.tag(vnode)
|
|
378
|
+
: vnode.tag(vnode)
|
|
379
|
+
}
|
|
380
|
+
initLifecycle(vnode.state, vnode, hooks, isHydrating)
|
|
381
|
+
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks, isHydrating)
|
|
382
|
+
|
|
383
|
+
// Track component for signal dependency tracking
|
|
384
|
+
// Store mapping from vnode.state to vnode.tag (component object) for redraw
|
|
385
|
+
if (vnode.state && vnode.tag && !isHydrating) {
|
|
386
|
+
;(globalThis as any).__mithrilStateToComponent = (globalThis as any).__mithrilStateToComponent || new WeakMap()
|
|
387
|
+
;(globalThis as any).__mithrilStateToComponent.set(vnode.state, vnode.tag)
|
|
388
|
+
}
|
|
389
|
+
// Always track component dependencies for signal tracking (even during hydration)
|
|
390
|
+
// This allows signals to know which components depend on them
|
|
391
|
+
// We only skip oninit during hydration, not signal tracking
|
|
392
|
+
// Only set currentComponent if vnode.state exists (it might be undefined for some component types)
|
|
393
|
+
if (vnode.state != null) {
|
|
394
|
+
setCurrentComponent(vnode.state)
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
|
|
398
|
+
} finally {
|
|
399
|
+
if (vnode.state != null) {
|
|
400
|
+
clearCurrentComponent()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (vnode.instance === vnode) throw Error('A view cannot return the vnode it received as argument')
|
|
404
|
+
sentinel.$$reentrantLock$$ = null
|
|
405
|
+
}
|
|
406
|
+
function createComponent(
|
|
407
|
+
parent: Element | DocumentFragment,
|
|
408
|
+
vnode: any,
|
|
409
|
+
hooks: Array<() => void>,
|
|
410
|
+
ns: string | undefined,
|
|
411
|
+
nextSibling: Node | null,
|
|
412
|
+
isHydrating: boolean = false,
|
|
413
|
+
matchedNodes: Set<Node> | null = null,
|
|
414
|
+
) {
|
|
415
|
+
initComponent(vnode, hooks, isHydrating)
|
|
416
|
+
if (vnode.instance != null) {
|
|
417
|
+
createNode(parent, vnode.instance, hooks, ns, nextSibling, isHydrating, matchedNodes)
|
|
418
|
+
vnode.dom = vnode.instance.dom
|
|
419
|
+
vnode.domSize = vnode.instance.domSize
|
|
420
|
+
|
|
421
|
+
// Store component's DOM element for fine-grained redraw (not during hydration)
|
|
422
|
+
if (vnode.state && vnode.dom && !isHydrating) {
|
|
423
|
+
;(globalThis as any).__mithrilStateToDom = (globalThis as any).__mithrilStateToDom || new WeakMap()
|
|
424
|
+
;(globalThis as any).__mithrilStateToDom.set(vnode.state, vnode.dom)
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
vnode.domSize = 0
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// update
|
|
432
|
+
function updateNodes(
|
|
433
|
+
parent: Element | DocumentFragment,
|
|
434
|
+
old: (VnodeType | null)[] | null,
|
|
435
|
+
vnodes: (VnodeType | null)[] | null,
|
|
436
|
+
hooks: Array<() => void>,
|
|
437
|
+
nextSibling: Node | null,
|
|
438
|
+
ns: string | undefined,
|
|
439
|
+
isHydrating: boolean = false,
|
|
440
|
+
) {
|
|
441
|
+
if (old === vnodes || (old == null && vnodes == null)) return
|
|
442
|
+
else if (old == null || old.length === 0)
|
|
443
|
+
createNodes(parent, vnodes!, 0, vnodes!.length, hooks, nextSibling, ns, isHydrating)
|
|
444
|
+
else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length)
|
|
445
|
+
else {
|
|
446
|
+
const isOldKeyed = old[0] != null && old[0]!.key != null
|
|
447
|
+
const isKeyed = vnodes[0] != null && vnodes[0]!.key != null
|
|
448
|
+
let start = 0,
|
|
449
|
+
oldStart = 0,
|
|
450
|
+
o: any,
|
|
451
|
+
v: any
|
|
452
|
+
if (isOldKeyed !== isKeyed) {
|
|
453
|
+
removeNodes(parent, old, 0, old.length)
|
|
454
|
+
createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns, isHydrating)
|
|
455
|
+
} else if (!isKeyed) {
|
|
456
|
+
// Don't index past the end of either list (causes deopts).
|
|
457
|
+
const commonLength = old.length < vnodes.length ? old.length : vnodes.length
|
|
458
|
+
// Rewind if necessary to the first non-null index on either side.
|
|
459
|
+
// We could alternatively either explicitly create or remove nodes when `start !== oldStart`
|
|
460
|
+
// but that would be optimizing for sparse lists which are more rare than dense ones.
|
|
461
|
+
while (oldStart < old.length && old[oldStart] == null) oldStart++
|
|
462
|
+
while (start < vnodes.length && vnodes[start] == null) start++
|
|
463
|
+
start = start < oldStart ? start : oldStart
|
|
464
|
+
for (; start < commonLength; start++) {
|
|
465
|
+
o = old[start]
|
|
466
|
+
v = vnodes[start]
|
|
467
|
+
if (o === v || (o == null && v == null)) continue
|
|
468
|
+
else if (o == null)
|
|
469
|
+
createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, old.length, nextSibling), isHydrating)
|
|
470
|
+
else if (v == null) removeNode(parent, o)
|
|
471
|
+
else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, old.length, nextSibling), ns, isHydrating)
|
|
472
|
+
}
|
|
473
|
+
if (old.length > commonLength) removeNodes(parent, old, start, old.length)
|
|
474
|
+
if (vnodes.length > commonLength)
|
|
475
|
+
createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns, isHydrating)
|
|
476
|
+
} else {
|
|
477
|
+
// keyed diff
|
|
478
|
+
let oldEnd = old.length - 1,
|
|
479
|
+
end = vnodes.length - 1,
|
|
480
|
+
oe: any,
|
|
481
|
+
ve: any,
|
|
482
|
+
topSibling: Node | null
|
|
483
|
+
|
|
484
|
+
// bottom-up
|
|
485
|
+
while (oldEnd >= oldStart && end >= start) {
|
|
486
|
+
oe = old[oldEnd]
|
|
487
|
+
ve = vnodes[end]
|
|
488
|
+
if (oe == null || ve == null || oe.key !== ve.key) break
|
|
489
|
+
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, isHydrating)
|
|
490
|
+
if (ve.dom != null) nextSibling = ve.dom
|
|
491
|
+
;(oldEnd--, end--)
|
|
492
|
+
}
|
|
493
|
+
// top-down
|
|
494
|
+
while (oldEnd >= oldStart && end >= start) {
|
|
495
|
+
o = old[oldStart]
|
|
496
|
+
v = vnodes[start]
|
|
497
|
+
if (o == null || v == null || o.key !== v.key) break
|
|
498
|
+
;(oldStart++, start++)
|
|
499
|
+
if (o !== v)
|
|
500
|
+
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, oldEnd + 1, nextSibling), ns, isHydrating)
|
|
501
|
+
}
|
|
502
|
+
// swaps and list reversals
|
|
503
|
+
while (oldEnd >= oldStart && end >= start) {
|
|
504
|
+
if (start === end) break
|
|
505
|
+
o = old[oldStart]
|
|
506
|
+
ve = vnodes[end]
|
|
507
|
+
oe = old[oldEnd]
|
|
508
|
+
v = vnodes[start]
|
|
509
|
+
if (o == null || ve == null || oe == null || v == null || o.key !== ve.key || oe.key !== v.key) break
|
|
510
|
+
topSibling = getNextSibling(old, oldStart, oldEnd, nextSibling)
|
|
511
|
+
moveDOM(parent, oe, topSibling)
|
|
512
|
+
if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns, isHydrating)
|
|
513
|
+
if (++start <= --end) moveDOM(parent, o, nextSibling)
|
|
514
|
+
if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns, isHydrating)
|
|
515
|
+
if (ve.dom != null) nextSibling = ve.dom
|
|
516
|
+
oldStart++
|
|
517
|
+
oldEnd--
|
|
518
|
+
oe = old[oldEnd]
|
|
519
|
+
ve = vnodes[end]
|
|
520
|
+
o = old[oldStart]
|
|
521
|
+
v = vnodes[start]
|
|
522
|
+
}
|
|
523
|
+
// bottom up once again
|
|
524
|
+
while (oldEnd >= oldStart && end >= start) {
|
|
525
|
+
oe = old[oldEnd]
|
|
526
|
+
ve = vnodes[end]
|
|
527
|
+
if (oe == null || ve == null || oe.key !== ve.key) break
|
|
528
|
+
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, isHydrating)
|
|
529
|
+
if (ve.dom != null) nextSibling = ve.dom
|
|
530
|
+
;(oldEnd--, end--)
|
|
531
|
+
oe = old[oldEnd]
|
|
532
|
+
ve = vnodes[end]
|
|
533
|
+
}
|
|
534
|
+
if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1)
|
|
535
|
+
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns, isHydrating)
|
|
536
|
+
else {
|
|
537
|
+
// inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
|
|
538
|
+
const originalNextSibling = nextSibling
|
|
539
|
+
let pos = 2147483647,
|
|
540
|
+
matched = 0
|
|
541
|
+
const oldIndices = Array.from({length: end - start + 1}, () => -1)
|
|
542
|
+
const map: Record<string, number> = Object.create(null)
|
|
543
|
+
for (let i = start; i <= end; i++) {
|
|
544
|
+
if (vnodes[i] != null) map[vnodes[i]!.key!] = i
|
|
545
|
+
}
|
|
546
|
+
for (let i = oldEnd; i >= oldStart; i--) {
|
|
547
|
+
oe = old[i]
|
|
548
|
+
if (oe == null) continue
|
|
549
|
+
const newIndex = map[oe.key!]
|
|
550
|
+
if (newIndex != null) {
|
|
551
|
+
pos = newIndex < pos ? newIndex : -1 // becomes -1 if nodes were re-ordered
|
|
552
|
+
oldIndices[newIndex - start] = i
|
|
553
|
+
ve = vnodes[newIndex]
|
|
554
|
+
old[i] = null
|
|
555
|
+
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns, isHydrating)
|
|
556
|
+
if (ve != null && ve.dom != null) nextSibling = ve.dom
|
|
557
|
+
matched++
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
nextSibling = originalNextSibling
|
|
561
|
+
if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1)
|
|
562
|
+
if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns, isHydrating)
|
|
563
|
+
else {
|
|
564
|
+
if (pos === -1) {
|
|
565
|
+
// the indices of the indices of the items that are part of the
|
|
566
|
+
// longest increasing subsequence in the oldIndices list
|
|
567
|
+
const lisIndices = makeLisIndices(oldIndices)
|
|
568
|
+
let li = lisIndices.length - 1
|
|
569
|
+
for (let i = end; i >= start; i--) {
|
|
570
|
+
ve = vnodes[i]
|
|
571
|
+
if (ve == null) continue
|
|
572
|
+
if (oldIndices[i - start] === -1) createNode(parent, ve, hooks, ns, nextSibling, isHydrating)
|
|
573
|
+
else {
|
|
574
|
+
if (lisIndices[li] === i - start) li--
|
|
575
|
+
else moveDOM(parent, ve, nextSibling)
|
|
576
|
+
}
|
|
577
|
+
if (ve.dom != null) nextSibling = ve.dom
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
for (let i = end; i >= start; i--) {
|
|
581
|
+
ve = vnodes[i]
|
|
582
|
+
if (ve == null) continue
|
|
583
|
+
if (oldIndices[i - start] === -1) createNode(parent, ve, hooks, ns, nextSibling, isHydrating)
|
|
584
|
+
if (ve.dom != null) nextSibling = ve.dom
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function updateNode(
|
|
593
|
+
parent: Element | DocumentFragment,
|
|
594
|
+
old: any,
|
|
595
|
+
vnode: any,
|
|
596
|
+
hooks: Array<() => void>,
|
|
597
|
+
nextSibling: Node | null,
|
|
598
|
+
ns: string | undefined,
|
|
599
|
+
isHydrating: boolean = false,
|
|
600
|
+
) {
|
|
601
|
+
const oldTag = old.tag,
|
|
602
|
+
tag = vnode.tag
|
|
603
|
+
// Key change: different keys mean different logical nodes → remove old, create new.
|
|
604
|
+
// Critical when a component returns a single keyed child (e.g. router resolving to DocLoader).
|
|
605
|
+
if (oldTag === tag && old.is === vnode.is && old.key === vnode.key) {
|
|
606
|
+
vnode.state = old.state
|
|
607
|
+
vnode.events = old.events
|
|
608
|
+
if (shouldNotUpdate(vnode, old)) return
|
|
609
|
+
if (typeof oldTag === 'string') {
|
|
610
|
+
if (vnode.attrs != null) {
|
|
611
|
+
updateLifecycle(vnode.attrs, vnode, hooks)
|
|
612
|
+
}
|
|
613
|
+
switch (oldTag) {
|
|
614
|
+
case '#':
|
|
615
|
+
updateText(old, vnode)
|
|
616
|
+
break
|
|
617
|
+
case '<':
|
|
618
|
+
updateHTML(parent, old, vnode, ns, nextSibling)
|
|
619
|
+
break
|
|
620
|
+
case '[':
|
|
621
|
+
updateFragment(parent, old, vnode, hooks, nextSibling, ns, isHydrating)
|
|
622
|
+
break
|
|
623
|
+
default:
|
|
624
|
+
updateElement(old, vnode, hooks, ns, isHydrating)
|
|
625
|
+
}
|
|
626
|
+
} else updateComponent(parent, old, vnode, hooks, nextSibling, ns, isHydrating)
|
|
627
|
+
} else {
|
|
628
|
+
removeNode(parent, old, vnode) // Pass new vnode for context
|
|
629
|
+
createNode(parent, vnode, hooks, ns, nextSibling, isHydrating)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function updateText(old: any, vnode: any) {
|
|
633
|
+
if (old.children.toString() !== vnode.children.toString()) {
|
|
634
|
+
old.dom.nodeValue = vnode.children
|
|
635
|
+
}
|
|
636
|
+
vnode.dom = old.dom
|
|
637
|
+
}
|
|
638
|
+
function updateHTML(
|
|
639
|
+
parent: Element | DocumentFragment,
|
|
640
|
+
old: any,
|
|
641
|
+
vnode: any,
|
|
642
|
+
ns: string | undefined,
|
|
643
|
+
nextSibling: Node | null,
|
|
644
|
+
) {
|
|
645
|
+
if (old.children !== vnode.children) {
|
|
646
|
+
removeDOM(parent, old)
|
|
647
|
+
createHTML(parent, vnode, ns, nextSibling)
|
|
648
|
+
} else {
|
|
649
|
+
vnode.dom = old.dom
|
|
650
|
+
vnode.domSize = old.domSize
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function updateFragment(
|
|
654
|
+
parent: Element | DocumentFragment,
|
|
655
|
+
old: any,
|
|
656
|
+
vnode: any,
|
|
657
|
+
hooks: Array<() => void>,
|
|
658
|
+
nextSibling: Node | null,
|
|
659
|
+
ns: string | undefined,
|
|
660
|
+
isHydrating: boolean = false,
|
|
661
|
+
) {
|
|
662
|
+
updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns, isHydrating)
|
|
663
|
+
let domSize = 0
|
|
664
|
+
const children = vnode.children
|
|
665
|
+
vnode.dom = null
|
|
666
|
+
if (children != null) {
|
|
667
|
+
for (let i = 0; i < children.length; i++) {
|
|
668
|
+
const child = children[i]
|
|
669
|
+
if (child != null && child.dom != null) {
|
|
670
|
+
if (vnode.dom == null) vnode.dom = child.dom
|
|
671
|
+
domSize += child.domSize || 1
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
vnode.domSize = domSize
|
|
676
|
+
}
|
|
677
|
+
function updateElement(old: any, vnode: any, hooks: Array<() => void>, ns: string | undefined, isHydrating: boolean = false) {
|
|
678
|
+
const element = (vnode.dom = old.dom)
|
|
679
|
+
ns = getNameSpace(vnode) || ns
|
|
680
|
+
|
|
681
|
+
if (old.attrs != vnode.attrs || (vnode.attrs != null && !cachedAttrsIsStaticMap.get(vnode.attrs))) {
|
|
682
|
+
updateAttrs(vnode, old.attrs, vnode.attrs, ns)
|
|
683
|
+
}
|
|
684
|
+
if (!maybeSetContentEditable(vnode)) {
|
|
685
|
+
updateNodes(element, old.children, vnode.children, hooks, null, ns, isHydrating)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function updateComponent(
|
|
689
|
+
parent: Element | DocumentFragment,
|
|
690
|
+
old: any,
|
|
691
|
+
vnode: any,
|
|
692
|
+
hooks: Array<() => void>,
|
|
693
|
+
nextSibling: Node | null,
|
|
694
|
+
ns: string | undefined,
|
|
695
|
+
isHydrating: boolean = false,
|
|
696
|
+
) {
|
|
697
|
+
// Track component for signal dependency tracking
|
|
698
|
+
// Store mapping from vnode.state to vnode.tag (component object) for redraw
|
|
699
|
+
if (vnode.state && vnode.tag && !isHydrating) {
|
|
700
|
+
;(globalThis as any).__mithrilStateToComponent = (globalThis as any).__mithrilStateToComponent || new WeakMap()
|
|
701
|
+
;(globalThis as any).__mithrilStateToComponent.set(vnode.state, vnode.tag)
|
|
702
|
+
}
|
|
703
|
+
// Always track component dependencies for signal tracking (even during hydration)
|
|
704
|
+
// This allows signals to know which components depend on them
|
|
705
|
+
// We only skip oninit during hydration, not signal tracking
|
|
706
|
+
// Only set currentComponent if vnode.state exists (it might be undefined for some component types)
|
|
707
|
+
if (vnode.state != null) {
|
|
708
|
+
setCurrentComponent(vnode.state)
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
|
|
712
|
+
} finally {
|
|
713
|
+
if (vnode.state != null) {
|
|
714
|
+
clearCurrentComponent()
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (vnode.instance === vnode) throw Error('A view cannot return the vnode it received as argument')
|
|
718
|
+
updateLifecycle(vnode.state, vnode, hooks)
|
|
719
|
+
if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
|
|
720
|
+
if (vnode.instance != null) {
|
|
721
|
+
if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling, isHydrating)
|
|
722
|
+
else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns, isHydrating)
|
|
723
|
+
vnode.dom = vnode.instance.dom
|
|
724
|
+
vnode.domSize = vnode.instance.domSize
|
|
725
|
+
|
|
726
|
+
// Store component's DOM element for fine-grained redraw (not during hydration)
|
|
727
|
+
if (vnode.state && vnode.dom && !isHydrating) {
|
|
728
|
+
;(globalThis as any).__mithrilStateToDom = (globalThis as any).__mithrilStateToDom || new WeakMap()
|
|
729
|
+
;(globalThis as any).__mithrilStateToDom.set(vnode.state, vnode.dom)
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
if (old.instance != null) removeNode(parent, old.instance)
|
|
733
|
+
vnode.domSize = 0
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Lifted from ivi https://github.com/ivijs/ivi/
|
|
737
|
+
// takes a list of unique numbers (-1 is special and can
|
|
738
|
+
// occur multiple times) and returns an array with the indices
|
|
739
|
+
// of the items that are part of the longest increasing
|
|
740
|
+
// subsequence
|
|
741
|
+
const lisTemp: number[] = []
|
|
742
|
+
function makeLisIndices(a: number[]): number[] {
|
|
743
|
+
const result = [0]
|
|
744
|
+
let u = 0,
|
|
745
|
+
v = 0
|
|
746
|
+
const il = (lisTemp.length = a.length)
|
|
747
|
+
for (let i = 0; i < il; i++) lisTemp[i] = a[i]
|
|
748
|
+
for (let i = 0; i < il; ++i) {
|
|
749
|
+
if (a[i] === -1) continue
|
|
750
|
+
const j = result[result.length - 1]
|
|
751
|
+
if (a[j] < a[i]) {
|
|
752
|
+
lisTemp[i] = j
|
|
753
|
+
result.push(i)
|
|
754
|
+
continue
|
|
755
|
+
}
|
|
756
|
+
u = 0
|
|
757
|
+
v = result.length - 1
|
|
758
|
+
while (u < v) {
|
|
759
|
+
// Fast integer average without overflow.
|
|
760
|
+
|
|
761
|
+
const c = (u >>> 1) + (v >>> 1) + (u & v & 1)
|
|
762
|
+
if (a[result[c]] < a[i]) {
|
|
763
|
+
u = c + 1
|
|
764
|
+
} else {
|
|
765
|
+
v = c
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (a[i] < a[result[u]]) {
|
|
769
|
+
if (u > 0) lisTemp[i] = result[u - 1]
|
|
770
|
+
result[u] = i
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
u = result.length
|
|
774
|
+
v = result[u - 1]
|
|
775
|
+
while (u-- > 0) {
|
|
776
|
+
result[u] = v
|
|
777
|
+
v = lisTemp[v]
|
|
778
|
+
}
|
|
779
|
+
lisTemp.length = 0
|
|
780
|
+
return result
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function getNextSibling(vnodes: (VnodeType | null)[], i: number, end: number, nextSibling: Node | null): Node | null {
|
|
784
|
+
for (; i < end; i++) {
|
|
785
|
+
if (vnodes[i] != null && vnodes[i]!.dom != null) return vnodes[i]!.dom!
|
|
786
|
+
}
|
|
787
|
+
return nextSibling
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove)
|
|
791
|
+
function moveDOM(parent: Element | DocumentFragment, vnode: any, nextSibling: Node | null) {
|
|
792
|
+
if (vnode.dom != null) {
|
|
793
|
+
let target: Node
|
|
794
|
+
if (vnode.domSize == null || vnode.domSize === 1) {
|
|
795
|
+
// don't allocate for the common case
|
|
796
|
+
target = vnode.dom
|
|
797
|
+
} else {
|
|
798
|
+
target = getDocument(parent as Element).createDocumentFragment()
|
|
799
|
+
for (const dom of domFor(vnode)) target.appendChild(dom)
|
|
800
|
+
}
|
|
801
|
+
insertDOM(parent, target, nextSibling)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function insertDOM(parent: Element | DocumentFragment, dom: Node, nextSibling: Node | null) {
|
|
806
|
+
if (nextSibling != null) parent.insertBefore(dom, nextSibling)
|
|
807
|
+
else parent.appendChild(dom)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function maybeSetContentEditable(vnode: any): boolean {
|
|
811
|
+
if (
|
|
812
|
+
vnode.attrs == null ||
|
|
813
|
+
(vnode.attrs.contenteditable == null && // attribute
|
|
814
|
+
vnode.attrs.contentEditable == null) // property
|
|
815
|
+
)
|
|
816
|
+
return false
|
|
817
|
+
const children = vnode.children
|
|
818
|
+
if (children != null && children.length === 1 && children[0].tag === '<') {
|
|
819
|
+
const content = children[0].children
|
|
820
|
+
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
|
|
821
|
+
} else if (children != null && children.length !== 0) throw new Error('Child node of a contenteditable must be trusted.')
|
|
822
|
+
return true
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// remove
|
|
826
|
+
function removeNodes(parent: Element | DocumentFragment, vnodes: (VnodeType | null)[], start: number, end: number) {
|
|
827
|
+
for (let i = start; i < end; i++) {
|
|
828
|
+
const vnode = vnodes[i]
|
|
829
|
+
if (vnode != null) removeNode(parent, vnode)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function tryBlockRemove(parent: Element | DocumentFragment, vnode: any, source: any, counter: {v: number}) {
|
|
833
|
+
const original = vnode.state
|
|
834
|
+
const result = callHook.call(source.onbeforeremove, vnode)
|
|
835
|
+
if (result == null) return
|
|
836
|
+
|
|
837
|
+
const generation = currentRender
|
|
838
|
+
for (const dom of domFor(vnode)) delayedRemoval.set(dom, generation)
|
|
839
|
+
counter.v++
|
|
840
|
+
|
|
841
|
+
Promise.resolve(result).finally(function () {
|
|
842
|
+
checkState(vnode, original)
|
|
843
|
+
tryResumeRemove(parent, vnode, counter)
|
|
844
|
+
})
|
|
845
|
+
}
|
|
846
|
+
function tryResumeRemove(parent: Element | DocumentFragment, vnode: any, counter: {v: number}, newVnode?: any) {
|
|
847
|
+
if (--counter.v === 0) {
|
|
848
|
+
onremove(vnode)
|
|
849
|
+
removeDOM(parent, vnode, newVnode)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function removeNode(parent: Element | DocumentFragment, vnode: any, newVnode?: any) {
|
|
853
|
+
const counter = {v: 1}
|
|
854
|
+
if (typeof vnode.tag !== 'string' && typeof vnode.state.onbeforeremove === 'function')
|
|
855
|
+
tryBlockRemove(parent, vnode, vnode.state, counter)
|
|
856
|
+
if (vnode.attrs && typeof vnode.attrs.onbeforeremove === 'function') tryBlockRemove(parent, vnode, vnode.attrs, counter)
|
|
857
|
+
tryResumeRemove(parent, vnode, counter, newVnode)
|
|
858
|
+
}
|
|
859
|
+
function removeDOM(parent: Element | DocumentFragment, vnode: any, newVnode?: any) {
|
|
860
|
+
if (vnode.dom == null) return
|
|
861
|
+
if (vnode.domSize == null || vnode.domSize === 1) {
|
|
862
|
+
// Check if node is still a child before attempting removal
|
|
863
|
+
const node = vnode.dom
|
|
864
|
+
if (parent.contains && parent.contains(node)) {
|
|
865
|
+
// Verify node is actually a direct or indirect child
|
|
866
|
+
try {
|
|
867
|
+
parent.removeChild(node)
|
|
868
|
+
} catch (e) {
|
|
869
|
+
const error = e as Error
|
|
870
|
+
// Check if node was already removed (not a child anymore)
|
|
871
|
+
if (!parent.contains || !parent.contains(node)) {
|
|
872
|
+
// Node already removed, skip silently
|
|
873
|
+
return
|
|
874
|
+
}
|
|
875
|
+
logHydrationError('removeDOM (single node)', vnode, parent instanceof Element ? parent : null, error, {
|
|
876
|
+
parent: parent instanceof Element ? parent : undefined,
|
|
877
|
+
node: vnode.dom,
|
|
878
|
+
oldVnode: vnode,
|
|
879
|
+
newVnode: newVnode,
|
|
880
|
+
})
|
|
881
|
+
// Don't re-throw - we've already logged the error with all details
|
|
882
|
+
// Re-throwing causes the browser to log the DOMException stack trace
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Node not in parent, already removed - skip silently
|
|
886
|
+
} else {
|
|
887
|
+
for (const dom of domFor(vnode)) {
|
|
888
|
+
// Check if node is still a child before attempting removal
|
|
889
|
+
if (parent.contains && parent.contains(dom)) {
|
|
890
|
+
try {
|
|
891
|
+
parent.removeChild(dom)
|
|
892
|
+
} catch (e) {
|
|
893
|
+
const error = e as Error
|
|
894
|
+
// Check if node was already removed (not a child anymore)
|
|
895
|
+
if (!parent.contains || !parent.contains(dom)) {
|
|
896
|
+
// Node already removed, skip silently
|
|
897
|
+
continue
|
|
898
|
+
}
|
|
899
|
+
logHydrationError('removeDOM (multiple nodes)', vnode, parent instanceof Element ? parent : null, error, {
|
|
900
|
+
parent: parent instanceof Element ? parent : undefined,
|
|
901
|
+
node: dom,
|
|
902
|
+
oldVnode: vnode,
|
|
903
|
+
newVnode: newVnode,
|
|
904
|
+
})
|
|
905
|
+
// Don't re-throw - we've already logged the error with all details
|
|
906
|
+
// Re-throwing causes the browser to log the DOMException stack trace
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// Node not in parent, already removed - skip silently
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function onremove(vnode: any) {
|
|
915
|
+
// Clean up signal dependencies when component is removed
|
|
916
|
+
if (typeof vnode.tag !== 'string' && vnode.state != null) {
|
|
917
|
+
clearComponentDependencies(vnode.state)
|
|
918
|
+
}
|
|
919
|
+
if (typeof vnode.tag !== 'string' && typeof vnode.state.onremove === 'function')
|
|
920
|
+
callHook.call(vnode.state.onremove, vnode)
|
|
921
|
+
if (vnode.attrs && typeof vnode.attrs.onremove === 'function') callHook.call(vnode.attrs.onremove, vnode)
|
|
922
|
+
if (typeof vnode.tag !== 'string') {
|
|
923
|
+
if (vnode.instance != null) onremove(vnode.instance)
|
|
924
|
+
} else {
|
|
925
|
+
if (vnode.events != null) vnode.events._ = null
|
|
926
|
+
const children = vnode.children
|
|
927
|
+
if (Array.isArray(children)) {
|
|
928
|
+
for (let i = 0; i < children.length; i++) {
|
|
929
|
+
const child = children[i]
|
|
930
|
+
if (child != null) onremove(child)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// attrs
|
|
937
|
+
function setAttrs(vnode: any, attrs: Record<string, any>, ns: string | undefined) {
|
|
938
|
+
for (const key in attrs) {
|
|
939
|
+
setAttr(vnode, key, null, attrs[key], ns)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function setAttr(vnode: any, key: string, old: any, value: any, ns: string | undefined) {
|
|
943
|
+
if (
|
|
944
|
+
key === 'key' ||
|
|
945
|
+
value == null ||
|
|
946
|
+
isLifecycleMethod(key) ||
|
|
947
|
+
(old === value && !isFormAttribute(vnode, key) && typeof value !== 'object')
|
|
948
|
+
)
|
|
949
|
+
return
|
|
950
|
+
if (key[0] === 'o' && key[1] === 'n') return updateEvent(vnode, key, value)
|
|
951
|
+
if (key.slice(0, 6) === 'xlink:') vnode.dom.setAttributeNS('http://www.w3.org/1999/xlink', key.slice(6), value)
|
|
952
|
+
else if (key === 'style') updateStyle(vnode.dom, old, value)
|
|
953
|
+
else if (hasPropertyKey(vnode, key, ns)) {
|
|
954
|
+
if (key === 'value') {
|
|
955
|
+
// Only do the coercion if we're actually going to check the value.
|
|
956
|
+
// setting input[value] to same value by typing on focused element moves cursor to end in Chrome
|
|
957
|
+
// setting input[type=file][value] to same value causes an error to be generated if it's non-empty
|
|
958
|
+
// minlength/maxlength validation isn't performed on script-set values(#2256)
|
|
959
|
+
if ((vnode.tag === 'input' || vnode.tag === 'textarea') && vnode.dom.value === '' + value) return
|
|
960
|
+
// setting select[value] to same value while having select open blinks select dropdown in Chrome
|
|
961
|
+
if (vnode.tag === 'select' && old !== null && vnode.dom.value === '' + value) return
|
|
962
|
+
// setting option[value] to same value while having select open blinks select dropdown in Chrome
|
|
963
|
+
if (vnode.tag === 'option' && old !== null && vnode.dom.value === '' + value) return
|
|
964
|
+
// setting input[type=file][value] to different value is an error if it's non-empty
|
|
965
|
+
// Not ideal, but it at least works around the most common source of uncaught exceptions for now.
|
|
966
|
+
if (vnode.tag === 'input' && vnode.attrs.type === 'file' && '' + value !== '') {
|
|
967
|
+
console.error('`value` is read-only on file inputs!')
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur.
|
|
972
|
+
if (vnode.tag === 'input' && key === 'type') vnode.dom.setAttribute(key, value)
|
|
973
|
+
else vnode.dom[key] = value
|
|
974
|
+
} else {
|
|
975
|
+
if (typeof value === 'boolean') {
|
|
976
|
+
if (value) vnode.dom.setAttribute(key, '')
|
|
977
|
+
else vnode.dom.removeAttribute(key)
|
|
978
|
+
} else vnode.dom.setAttribute(key === 'className' ? 'class' : key, value)
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function removeAttr(vnode: any, key: string, old: any, ns: string | undefined) {
|
|
982
|
+
if (key === 'key' || old == null || isLifecycleMethod(key)) return
|
|
983
|
+
if (key[0] === 'o' && key[1] === 'n') updateEvent(vnode, key, undefined)
|
|
984
|
+
else if (key === 'style') updateStyle(vnode.dom, old, null)
|
|
985
|
+
else if (
|
|
986
|
+
hasPropertyKey(vnode, key, ns) &&
|
|
987
|
+
key !== 'className' &&
|
|
988
|
+
key !== 'title' && // creates "null" as title
|
|
989
|
+
!(
|
|
990
|
+
key === 'value' &&
|
|
991
|
+
(vnode.tag === 'option' ||
|
|
992
|
+
(vnode.tag === 'select' && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom)))
|
|
993
|
+
) &&
|
|
994
|
+
!(vnode.tag === 'input' && key === 'type')
|
|
995
|
+
) {
|
|
996
|
+
vnode.dom[key] = null
|
|
997
|
+
} else {
|
|
998
|
+
const nsLastIndex = key.indexOf(':')
|
|
999
|
+
if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1)
|
|
1000
|
+
if (old !== false) vnode.dom.removeAttribute(key === 'className' ? 'class' : key)
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function setLateSelectAttrs(vnode: any, attrs: Record<string, any>) {
|
|
1004
|
+
if ('value' in attrs) {
|
|
1005
|
+
if (attrs.value === null) {
|
|
1006
|
+
if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null
|
|
1007
|
+
} else {
|
|
1008
|
+
const normalized = '' + attrs.value
|
|
1009
|
+
if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) {
|
|
1010
|
+
vnode.dom.value = normalized
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if ('selectedIndex' in attrs) setAttr(vnode, 'selectedIndex', null, attrs.selectedIndex, undefined)
|
|
1015
|
+
}
|
|
1016
|
+
function updateAttrs(vnode: any, old: Record<string, any> | null, attrs: Record<string, any> | null, ns: string | undefined) {
|
|
1017
|
+
// Some attributes may NOT be case-sensitive (e.g. data-***),
|
|
1018
|
+
// so removal should be done first to prevent accidental removal for newly setting values.
|
|
1019
|
+
let val: any
|
|
1020
|
+
if (old != null) {
|
|
1021
|
+
if (old === attrs && !cachedAttrsIsStaticMap.has(attrs!)) {
|
|
1022
|
+
// Diagnostic: log element tag and attrs to identify the source
|
|
1023
|
+
console.warn(
|
|
1024
|
+
"Don't reuse attrs object, use new object for every redraw, this will throw in next major",
|
|
1025
|
+
'element:',
|
|
1026
|
+
vnode.tag,
|
|
1027
|
+
'attrs keys:',
|
|
1028
|
+
attrs != null ? Object.keys(attrs) : [],
|
|
1029
|
+
'dom:',
|
|
1030
|
+
vnode.dom,
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
for (const key in old) {
|
|
1034
|
+
if ((val = old[key]) != null && (attrs == null || attrs[key] == null)) {
|
|
1035
|
+
removeAttr(vnode, key, val, ns)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (attrs != null) {
|
|
1040
|
+
for (const key in attrs) {
|
|
1041
|
+
setAttr(vnode, key, old && old[key], attrs[key], ns)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function isFormAttribute(vnode: any, attr: string): boolean {
|
|
1046
|
+
return (
|
|
1047
|
+
attr === 'value' ||
|
|
1048
|
+
attr === 'checked' ||
|
|
1049
|
+
attr === 'selectedIndex' ||
|
|
1050
|
+
(attr === 'selected' &&
|
|
1051
|
+
(vnode.dom === activeElement(vnode.dom) ||
|
|
1052
|
+
(vnode.tag === 'option' && vnode.dom.parentNode === activeElement(vnode.dom))))
|
|
1053
|
+
)
|
|
1054
|
+
}
|
|
1055
|
+
function isLifecycleMethod(attr: string): boolean {
|
|
1056
|
+
return (
|
|
1057
|
+
attr === 'oninit' ||
|
|
1058
|
+
attr === 'oncreate' ||
|
|
1059
|
+
attr === 'onupdate' ||
|
|
1060
|
+
attr === 'onremove' ||
|
|
1061
|
+
attr === 'onbeforeremove' ||
|
|
1062
|
+
attr === 'onbeforeupdate'
|
|
1063
|
+
)
|
|
1064
|
+
}
|
|
1065
|
+
function hasPropertyKey(vnode: any, key: string, ns: string | undefined): boolean {
|
|
1066
|
+
// Filter out namespaced keys
|
|
1067
|
+
return (
|
|
1068
|
+
ns === undefined &&
|
|
1069
|
+
// If it's a custom element, just keep it.
|
|
1070
|
+
(vnode.tag.indexOf('-') > -1 ||
|
|
1071
|
+
vnode.is ||
|
|
1072
|
+
// If it's a normal element, let's try to avoid a few browser bugs.
|
|
1073
|
+
(key !== 'href' && key !== 'list' && key !== 'form' && key !== 'width' && key !== 'height')) && // && key !== "type"
|
|
1074
|
+
// Defer the property check until *after* we check everything.
|
|
1075
|
+
key in vnode.dom
|
|
1076
|
+
)
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// style
|
|
1080
|
+
function updateStyle(element: HTMLElement, old: any, style: any) {
|
|
1081
|
+
if (old === style) {
|
|
1082
|
+
// Styles are equivalent, do nothing.
|
|
1083
|
+
} else if (style == null) {
|
|
1084
|
+
// New style is missing, just clear it.
|
|
1085
|
+
element.style.cssText = ''
|
|
1086
|
+
} else if (typeof style !== 'object') {
|
|
1087
|
+
// New style is a string, let engine deal with patching.
|
|
1088
|
+
element.style.cssText = style
|
|
1089
|
+
} else if (old == null || typeof old !== 'object') {
|
|
1090
|
+
// `old` is missing or a string, `style` is an object.
|
|
1091
|
+
element.style.cssText = ''
|
|
1092
|
+
// Add new style properties
|
|
1093
|
+
for (const key in style) {
|
|
1094
|
+
const value = style[key]
|
|
1095
|
+
if (value != null) {
|
|
1096
|
+
if (key.includes('-')) element.style.setProperty(key, String(value))
|
|
1097
|
+
else (element.style as any)[key] = String(value)
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
// Both old & new are (different) objects.
|
|
1102
|
+
// Remove style properties that no longer exist
|
|
1103
|
+
// Style properties may have two cases(dash-case and camelCase),
|
|
1104
|
+
// so removal should be done first to prevent accidental removal for newly setting values.
|
|
1105
|
+
for (const key in old) {
|
|
1106
|
+
if (old[key] != null && style[key] == null) {
|
|
1107
|
+
if (key.includes('-')) element.style.removeProperty(key)
|
|
1108
|
+
else (element.style as any)[key] = ''
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// Update style properties that have changed
|
|
1112
|
+
for (const key in style) {
|
|
1113
|
+
let value = style[key]
|
|
1114
|
+
if (value != null && (value = String(value)) !== String(old[key])) {
|
|
1115
|
+
if (key.includes('-')) element.style.setProperty(key, value)
|
|
1116
|
+
else (element.style as any)[key] = value
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Here's an explanation of how this works:
|
|
1123
|
+
// 1. The event names are always (by design) prefixed by `on`.
|
|
1124
|
+
// 2. The EventListener interface accepts either a function or an object
|
|
1125
|
+
// with a `handleEvent` method.
|
|
1126
|
+
// 3. The object does not inherit from `Object.prototype`, to avoid
|
|
1127
|
+
// any potential interference with that (e.g. setters).
|
|
1128
|
+
// 4. The event name is remapped to the handler before calling it.
|
|
1129
|
+
// 5. In function-based event handlers, `ev.target === this`. We replicate
|
|
1130
|
+
// that below.
|
|
1131
|
+
// 6. In function-based event handlers, `return false` prevents the default
|
|
1132
|
+
// action and stops event propagation. We replicate that below.
|
|
1133
|
+
function EventDict(this: any) {
|
|
1134
|
+
// Save this, so the current redraw is correctly tracked.
|
|
1135
|
+
this._ = currentRedraw
|
|
1136
|
+
}
|
|
1137
|
+
EventDict.prototype = Object.create(null)
|
|
1138
|
+
EventDict.prototype.handleEvent = function (ev: any) {
|
|
1139
|
+
const handler = this['on' + ev.type]
|
|
1140
|
+
let result: any
|
|
1141
|
+
if (typeof handler === 'function') result = handler.call(ev.currentTarget, ev)
|
|
1142
|
+
else if (typeof handler.handleEvent === 'function') handler.handleEvent(ev)
|
|
1143
|
+
const self = this
|
|
1144
|
+
if (self._ != null) {
|
|
1145
|
+
if (ev.redraw !== false) (0, self._)()
|
|
1146
|
+
if (result != null && typeof result.then === 'function') {
|
|
1147
|
+
Promise.resolve(result).then(function () {
|
|
1148
|
+
if (self._ != null && ev.redraw !== false) (0, self._)()
|
|
1149
|
+
})
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (result === false) {
|
|
1153
|
+
ev.preventDefault()
|
|
1154
|
+
ev.stopPropagation()
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// event
|
|
1159
|
+
function updateEvent(vnode: any, key: string, value: any) {
|
|
1160
|
+
if (vnode.events != null) {
|
|
1161
|
+
vnode.events._ = currentRedraw
|
|
1162
|
+
if (vnode.events[key] === value) return
|
|
1163
|
+
if (value != null && (typeof value === 'function' || typeof value === 'object')) {
|
|
1164
|
+
if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false)
|
|
1165
|
+
vnode.events[key] = value
|
|
1166
|
+
} else {
|
|
1167
|
+
if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false)
|
|
1168
|
+
vnode.events[key] = undefined
|
|
1169
|
+
}
|
|
1170
|
+
} else if (value != null && (typeof value === 'function' || typeof value === 'object')) {
|
|
1171
|
+
vnode.events = new (EventDict as any)()
|
|
1172
|
+
vnode.dom.addEventListener(key.slice(2), vnode.events, false)
|
|
1173
|
+
vnode.events[key] = value
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// lifecycle
|
|
1178
|
+
function initLifecycle(source: any, vnode: any, hooks: Array<() => void>, isHydrating: boolean = false) {
|
|
1179
|
+
// Always call oninit, but pass context so components can make intelligent decisions
|
|
1180
|
+
// Components can check context.isSSR or context.isHydrating to conditionally load data
|
|
1181
|
+
if (typeof source.oninit === 'function') {
|
|
1182
|
+
const context = {
|
|
1183
|
+
isSSR: false,
|
|
1184
|
+
isHydrating: isHydrating,
|
|
1185
|
+
}
|
|
1186
|
+
const result = callHook.call(source.oninit, vnode, context)
|
|
1187
|
+
// Auto-redraw when async oninit completes (client-side only)
|
|
1188
|
+
if (result != null && typeof result.then === 'function' && currentRedraw != null) {
|
|
1189
|
+
Promise.resolve(result).then(function () {
|
|
1190
|
+
if (currentRedraw != null) {
|
|
1191
|
+
// @ts-expect-error - Comma operator intentionally used to call without 'this' binding
|
|
1192
|
+
;(0, currentRedraw)()
|
|
1193
|
+
}
|
|
1194
|
+
})
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof source.oncreate === 'function') hooks.push(callHook.bind(source.oncreate, vnode))
|
|
1198
|
+
}
|
|
1199
|
+
function updateLifecycle(source: any, vnode: any, hooks: Array<() => void>) {
|
|
1200
|
+
if (typeof source.onupdate === 'function') hooks.push(callHook.bind(source.onupdate, vnode))
|
|
1201
|
+
}
|
|
1202
|
+
function shouldNotUpdate(vnode: any, old: any): boolean {
|
|
1203
|
+
updateCheck: {
|
|
1204
|
+
if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === 'function') {
|
|
1205
|
+
const force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
|
|
1206
|
+
if (force !== undefined && !force) break updateCheck
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof vnode.tag !== 'string' && typeof vnode.state.onbeforeupdate === 'function') {
|
|
1209
|
+
const force = callHook.call(vnode.state.onbeforeupdate, vnode, old)
|
|
1210
|
+
if (force !== undefined && !force) break updateCheck
|
|
1211
|
+
}
|
|
1212
|
+
return false
|
|
1213
|
+
}
|
|
1214
|
+
vnode.dom = old.dom
|
|
1215
|
+
vnode.domSize = old.domSize
|
|
1216
|
+
vnode.instance = old.instance
|
|
1217
|
+
// One would think having the actual latest attributes would be ideal,
|
|
1218
|
+
// but it doesn't let us properly diff based on our current internal
|
|
1219
|
+
// representation. We have to save not only the old DOM info, but also
|
|
1220
|
+
// the attributes used to create it, as we diff *that*, not against the
|
|
1221
|
+
// DOM directly (with a few exceptions in `setAttr`). And, of course, we
|
|
1222
|
+
// need to save the children and text as they are conceptually not
|
|
1223
|
+
// unlike special "attributes" internally.
|
|
1224
|
+
vnode.attrs = old.attrs
|
|
1225
|
+
vnode.children = old.children
|
|
1226
|
+
vnode.text = old.text
|
|
1227
|
+
return true
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
let currentDOM: Element | null = null
|
|
1231
|
+
|
|
1232
|
+
return function (dom: Element, vnodes: Children | VnodeType | null, redraw?: () => void) {
|
|
1233
|
+
if (!dom) throw new TypeError('DOM element being rendered to does not exist.')
|
|
1234
|
+
if (currentDOM != null && dom.contains(currentDOM)) {
|
|
1235
|
+
throw new TypeError('Node is currently being rendered to and thus is locked.')
|
|
1236
|
+
}
|
|
1237
|
+
const prevRedraw = currentRedraw
|
|
1238
|
+
const prevDOM = currentDOM
|
|
1239
|
+
const hooks: Array<() => void> = []
|
|
1240
|
+
const active = activeElement(dom)
|
|
1241
|
+
const namespace = dom.namespaceURI
|
|
1242
|
+
|
|
1243
|
+
currentDOM = dom
|
|
1244
|
+
currentRedraw = typeof redraw === 'function' ? redraw : undefined
|
|
1245
|
+
currentRender = {}
|
|
1246
|
+
// Reset hydration error counter and mismatch count at start of each render cycle
|
|
1247
|
+
resetHydrationErrorCount()
|
|
1248
|
+
hydrationMismatchCount = 0
|
|
1249
|
+
try {
|
|
1250
|
+
// Detect hydration: DOM has children but no vnodes tracked
|
|
1251
|
+
// Only check children for Element nodes (DocumentFragment doesn't have children property)
|
|
1252
|
+
let isHydrating =
|
|
1253
|
+
(dom as any).vnodes == null &&
|
|
1254
|
+
dom.nodeType === 1 && // Element node
|
|
1255
|
+
'children' in dom &&
|
|
1256
|
+
(dom as Element).children.length > 0
|
|
1257
|
+
|
|
1258
|
+
// First time rendering into a node clears it out (unless hydrating)
|
|
1259
|
+
if (!isHydrating && (dom as any).vnodes == null) dom.textContent = ''
|
|
1260
|
+
const normalized = (Vnode as any).normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
|
|
1261
|
+
updateNodes(
|
|
1262
|
+
dom,
|
|
1263
|
+
(dom as any).vnodes,
|
|
1264
|
+
normalized,
|
|
1265
|
+
hooks,
|
|
1266
|
+
null,
|
|
1267
|
+
(namespace === 'http://www.w3.org/1999/xhtml' ? undefined : namespace) as string | undefined,
|
|
1268
|
+
isHydrating,
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
// Check if we've exceeded mismatch threshold after processing nodes
|
|
1272
|
+
// If so, clear and re-render from scratch (client VDOM wins)
|
|
1273
|
+
if (isHydrating && hydrationMismatchCount > MAX_HYDRATION_MISMATCHES) {
|
|
1274
|
+
logger.warn('hydration mismatch threshold exceeded. clearing parent and re-rendering from client vdom.', {
|
|
1275
|
+
mismatchCount: hydrationMismatchCount,
|
|
1276
|
+
threshold: MAX_HYDRATION_MISMATCHES,
|
|
1277
|
+
})
|
|
1278
|
+
dom.textContent = ''
|
|
1279
|
+
hydrationMismatchCount = 0
|
|
1280
|
+
// Clear old vnodes and re-render without hydration flag
|
|
1281
|
+
;(dom as any).vnodes = null
|
|
1282
|
+
// Re-render with fresh hooks array (hooks from first render are discarded)
|
|
1283
|
+
const overrideHooks: Array<() => void> = []
|
|
1284
|
+
updateNodes(
|
|
1285
|
+
dom,
|
|
1286
|
+
null,
|
|
1287
|
+
normalized,
|
|
1288
|
+
overrideHooks,
|
|
1289
|
+
null,
|
|
1290
|
+
(namespace === 'http://www.w3.org/1999/xhtml' ? undefined : namespace) as string | undefined,
|
|
1291
|
+
false,
|
|
1292
|
+
)
|
|
1293
|
+
// Execute hooks from override render
|
|
1294
|
+
for (let i = 0; i < overrideHooks.length; i++) overrideHooks[i]()
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
;(dom as any).vnodes = normalized
|
|
1298
|
+
// `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
|
|
1299
|
+
if (active != null && activeElement(dom) !== active && typeof (active as any).focus === 'function')
|
|
1300
|
+
(active as any).focus()
|
|
1301
|
+
for (let i = 0; i < hooks.length; i++) hooks[i]()
|
|
1302
|
+
} finally {
|
|
1303
|
+
currentRedraw = prevRedraw
|
|
1304
|
+
currentDOM = prevDOM
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|