@remix-run/ui 0.1.1 → 0.1.2
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/README.md +24 -3
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +3 -3
- package/dist/components/accordion/accordion.js +0 -2
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +6 -0
- package/dist/components/accordion/demos/card.demo.js +58 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -0
- package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
- package/dist/components/accordion/demos/multiple.demo.js +19 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
- package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
- package/dist/components/accordion/demos/overview.demo.js +19 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
- package/dist/components/button/button.d.ts +82 -0
- package/dist/components/button/button.js +60 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/demos/aliases.demo.d.ts +6 -0
- package/dist/components/button/demos/aliases.demo.js +18 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -0
- package/dist/components/button/demos/basic.demo.d.ts +6 -0
- package/dist/components/button/demos/basic.demo.js +19 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -0
- package/dist/components/button/demos/states.demo.d.ts +6 -0
- package/dist/components/button/demos/states.demo.js +29 -0
- package/dist/components/button/demos/states.demo.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +3 -3
- package/dist/components/combobox/combobox.js +0 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
- package/dist/components/combobox/demos/overview.demo.js +153 -0
- package/dist/components/combobox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
- package/dist/components/listbox/demos/overview.demo.js +64 -0
- package/dist/components/listbox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +1 -6
- package/dist/components/listbox/listbox.js +21 -33
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
- package/dist/components/menu/demos/bubbling.demo.js +15 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +6 -0
- package/dist/components/menu/demos/overview.demo.js +32 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -0
- package/dist/components/menu/menu.d.ts +5 -14
- package/dist/components/menu/menu.js +23 -38
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/demos/overview.demo.d.ts +6 -0
- package/dist/components/popover/demos/overview.demo.js +64 -0
- package/dist/components/popover/demos/overview.demo.js.map +1 -0
- package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
- package/dist/components/select/demos/deconstructed.demo.js +53 -0
- package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
- package/dist/components/select/demos/overview.demo.d.ts +5 -0
- package/dist/components/select/demos/overview.demo.js +27 -0
- package/dist/components/select/demos/overview.demo.js.map +1 -0
- package/dist/components/select/select.d.ts +3 -3
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +3 -3
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/component.d.ts +8 -5
- package/dist/runtime/component.js +84 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +12 -0
- package/dist/runtime/core/attributes.js +85 -0
- package/dist/runtime/core/attributes.js.map +1 -0
- package/dist/runtime/core/children.d.ts +7 -0
- package/dist/runtime/core/children.js +30 -0
- package/dist/runtime/core/children.js.map +1 -0
- package/dist/runtime/core/props.d.ts +9 -0
- package/dist/runtime/core/props.js +169 -0
- package/dist/runtime/core/props.js.map +1 -0
- package/dist/runtime/core/vnode.d.ts +3 -0
- package/dist/runtime/core/vnode.js +39 -0
- package/dist/runtime/core/vnode.js.map +1 -0
- package/dist/runtime/create-element.js +2 -1
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +47 -14
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.d.ts +1 -2
- package/dist/runtime/diff-props.js +2 -181
- package/dist/runtime/diff-props.js.map +1 -1
- package/dist/runtime/dom.d.ts +4 -0
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +41 -47
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.js +2 -28
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/mixin.js +9 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +15 -4
- package/dist/runtime/mixins/on-mixin.js +9 -2
- package/dist/runtime/mixins/on-mixin.js.map +1 -1
- package/dist/runtime/navigation.js +9 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.js +433 -192
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/stream-protocol.d.ts +8 -0
- package/dist/runtime/stream-protocol.js +20 -0
- package/dist/runtime/stream-protocol.js.map +1 -0
- package/dist/runtime/to-vnode.js +5 -9
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +5 -4
- package/dist/runtime/vnode.js +2 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.js +76 -106
- package/dist/server/stream.js.map +1 -1
- package/dist/style/layers.d.ts +2 -0
- package/dist/style/layers.js +3 -0
- package/dist/style/layers.js.map +1 -0
- package/dist/style/stylesheet.d.ts +3 -1
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/README.md +314 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +8 -10
- package/src/components/accordion/demos/card.demo.tsx +113 -0
- package/src/components/accordion/demos/multiple.demo.tsx +56 -0
- package/src/components/accordion/demos/overview.demo.tsx +56 -0
- package/src/components/anchor/README.md +153 -0
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
- package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
- package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
- package/src/components/button/README.md +4 -4
- package/src/components/button/button.tsx +88 -2
- package/src/components/button/demos/aliases.demo.tsx +27 -0
- package/src/components/button/demos/basic.demo.tsx +30 -0
- package/src/components/button/demos/states.demo.tsx +52 -0
- package/src/components/combobox/combobox.tsx +8 -10
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +23 -32
- package/src/components/menu/README.md +96 -0
- package/src/components/menu/demos/bubbling.demo.tsx +34 -0
- package/src/components/menu/demos/overview.demo.tsx +76 -0
- package/src/components/menu/menu.tsx +35 -41
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/select/README.md +107 -0
- package/src/components/select/demos/deconstructed.demo.tsx +95 -0
- package/src/components/select/demos/overview.demo.tsx +51 -0
- package/src/components/select/select.tsx +28 -28
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +1 -1
- package/src/runtime/component.ts +94 -82
- package/src/runtime/core/attributes.ts +97 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +185 -0
- package/src/runtime/core/vnode.ts +48 -0
- package/src/runtime/create-element.ts +2 -1
- package/src/runtime/diff-dom.ts +59 -12
- package/src/runtime/diff-props.ts +2 -187
- package/src/runtime/dom.ts +4 -0
- package/src/runtime/frame.ts +71 -55
- package/src/runtime/jsx.ts +2 -29
- package/src/runtime/mixins/mixin.ts +9 -4
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/navigation.ts +10 -1
- package/src/runtime/reconcile.ts +540 -209
- package/src/runtime/run.ts +17 -1
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/to-vnode.ts +5 -8
- package/src/runtime/vnode.ts +6 -9
- package/src/server/README.md +88 -0
- package/src/server/stream.ts +99 -116
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +75 -10
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
package/src/runtime/diff-dom.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { invariant } from './invariant.ts'
|
|
2
2
|
import type { FrameContext } from './frame.ts'
|
|
3
3
|
|
|
4
|
+
type HydratedVirtualRootStartMarker = Comment & {
|
|
5
|
+
$rmx: {
|
|
6
|
+
dispose(): void
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
|
|
5
11
|
let parent = curr[0]?.parentNode ?? context.regionParent ?? null
|
|
6
12
|
invariant(parent, 'Parent node not found')
|
|
@@ -23,8 +29,7 @@ export function diffNodes(curr: Node[], next: Node[], context: FrameContext) {
|
|
|
23
29
|
parent.appendChild(n)
|
|
24
30
|
}
|
|
25
31
|
} else if (c && !n) {
|
|
26
|
-
|
|
27
|
-
parent.removeChild(c)
|
|
32
|
+
removeNode(c, parent, context)
|
|
28
33
|
} else if (c && n) {
|
|
29
34
|
// Skip hydrated client-entry boundary ranges; hydration pass re-renders
|
|
30
35
|
// roots with new props from incoming payload
|
|
@@ -71,7 +76,10 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
|
|
|
71
76
|
// Comment -> Comment
|
|
72
77
|
if (isCommentNode(current) && isCommentNode(next)) {
|
|
73
78
|
let newData = next.data
|
|
74
|
-
if (current.data !== newData)
|
|
79
|
+
if (current.data !== newData) {
|
|
80
|
+
if (isFrameStartMarker(current)) disposeFrameStartMarker(current, context)
|
|
81
|
+
current.data = newData
|
|
82
|
+
}
|
|
75
83
|
return
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -80,7 +88,10 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
|
|
|
80
88
|
// Different tags: replace
|
|
81
89
|
if (current.tagName !== next.tagName) {
|
|
82
90
|
let parent = current.parentNode
|
|
83
|
-
if (parent)
|
|
91
|
+
if (parent) {
|
|
92
|
+
parent.insertBefore(next, current)
|
|
93
|
+
removeNode(current, parent, context)
|
|
94
|
+
}
|
|
84
95
|
return
|
|
85
96
|
}
|
|
86
97
|
|
|
@@ -93,7 +104,10 @@ function diffNode(current: Node, next: Node, context: FrameContext): ChildNode |
|
|
|
93
104
|
|
|
94
105
|
// Type mismatch: replace
|
|
95
106
|
let parent = current.parentNode
|
|
96
|
-
if (parent)
|
|
107
|
+
if (parent) {
|
|
108
|
+
parent.insertBefore(next, current)
|
|
109
|
+
removeNode(current, parent, context)
|
|
110
|
+
}
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
function diffElementAttributes(current: Element, next: Element): void {
|
|
@@ -315,8 +329,7 @@ function diffElementChildren(current: Element, next: Element, context: FrameCont
|
|
|
315
329
|
for (let i = 0; i < currentChildren.length; i++) {
|
|
316
330
|
if (!used[i]) {
|
|
317
331
|
let nodeToRemove = currentChildren[i]
|
|
318
|
-
|
|
319
|
-
current.removeChild(currentChildren[i])
|
|
332
|
+
removeNode(nodeToRemove, current, context)
|
|
320
333
|
}
|
|
321
334
|
}
|
|
322
335
|
}
|
|
@@ -375,6 +388,32 @@ function isFrameStartMarker(node: Node): node is Comment {
|
|
|
375
388
|
return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
|
|
376
389
|
}
|
|
377
390
|
|
|
391
|
+
function removeNode(node: Node, parent: ParentNode, context: FrameContext): void {
|
|
392
|
+
disposeRemovedVirtualRoots(node)
|
|
393
|
+
disposeRemovedSubFrames(node, context)
|
|
394
|
+
|
|
395
|
+
if (node.parentNode === parent) {
|
|
396
|
+
parent.removeChild(node)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function disposeRemovedVirtualRoots(node: Node): void {
|
|
401
|
+
let stack: Node[] = [node]
|
|
402
|
+
while (stack.length > 0) {
|
|
403
|
+
let next = stack.pop()
|
|
404
|
+
if (!next) continue
|
|
405
|
+
|
|
406
|
+
if (isHydratedVirtualRootStartMarker(next)) {
|
|
407
|
+
next.$rmx.dispose()
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (let child of Array.from(next.childNodes)) {
|
|
412
|
+
stack.push(child)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
378
417
|
function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
|
|
379
418
|
let stack: Node[] = [node]
|
|
380
419
|
while (stack.length > 0) {
|
|
@@ -382,11 +421,7 @@ function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
|
|
|
382
421
|
if (!next) continue
|
|
383
422
|
|
|
384
423
|
if (isFrameStartMarker(next)) {
|
|
385
|
-
|
|
386
|
-
if (subFrame) {
|
|
387
|
-
subFrame.dispose()
|
|
388
|
-
context.frameInstances.delete(next)
|
|
389
|
-
}
|
|
424
|
+
disposeFrameStartMarker(next, context)
|
|
390
425
|
}
|
|
391
426
|
|
|
392
427
|
for (let child of Array.from(next.childNodes)) {
|
|
@@ -395,10 +430,22 @@ function disposeRemovedSubFrames(node: Node, context: FrameContext): void {
|
|
|
395
430
|
}
|
|
396
431
|
}
|
|
397
432
|
|
|
433
|
+
function disposeFrameStartMarker(marker: Comment, context: FrameContext): void {
|
|
434
|
+
let subFrame = context.frameInstances.get(marker)
|
|
435
|
+
if (subFrame) {
|
|
436
|
+
subFrame.dispose()
|
|
437
|
+
context.frameInstances.delete(marker)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
398
441
|
function isVirtualRootStartMarker(node: Node): node is Comment {
|
|
399
442
|
return isCommentNode(node) && node.data.trim().startsWith('rmx:h:')
|
|
400
443
|
}
|
|
401
444
|
|
|
445
|
+
function isHydratedVirtualRootStartMarker(node: Node): node is HydratedVirtualRootStartMarker {
|
|
446
|
+
return isVirtualRootStartMarker(node) && '$rmx' in node
|
|
447
|
+
}
|
|
448
|
+
|
|
402
449
|
function isVirtualRootEndMarker(node: Node): node is Comment {
|
|
403
450
|
return isCommentNode(node) && node.data.trim() === '/rmx:h'
|
|
404
451
|
}
|
|
@@ -1,200 +1,15 @@
|
|
|
1
1
|
import { invariant } from './invariant.ts'
|
|
2
|
-
import { createStyleManager
|
|
2
|
+
import { createStyleManager } from '../style/index.ts'
|
|
3
3
|
import type { StyleManager } from '../style/index.ts'
|
|
4
|
-
import type { ElementProps } from './jsx.ts'
|
|
5
|
-
import { normalizeSvgAttribute } from './svg-attributes.ts'
|
|
6
|
-
|
|
7
|
-
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
8
4
|
|
|
9
5
|
let globalStyleManager =
|
|
10
6
|
typeof window !== 'undefined' ? createStyleManager() : (null as unknown as StyleManager)
|
|
11
7
|
|
|
12
8
|
export { type StyleManager }
|
|
9
|
+
export { patchHostProps as diffHostProps } from './core/props.ts'
|
|
13
10
|
|
|
14
11
|
export let defaultStyleManager: StyleManager = globalStyleManager
|
|
15
12
|
|
|
16
|
-
// Preact excludes certain attributes from the property path due to browser quirks
|
|
17
|
-
const ATTRIBUTE_FALLBACK_NAMES = new Set([
|
|
18
|
-
'width',
|
|
19
|
-
'height',
|
|
20
|
-
'href',
|
|
21
|
-
'list',
|
|
22
|
-
'form',
|
|
23
|
-
'tabIndex',
|
|
24
|
-
'download',
|
|
25
|
-
'rowSpan',
|
|
26
|
-
'colSpan',
|
|
27
|
-
'role',
|
|
28
|
-
'popover',
|
|
29
|
-
])
|
|
30
|
-
|
|
31
|
-
// Determine if we should use the property path for a given name.
|
|
32
|
-
// Also acts as a type guard to allow bracket assignment without casts.
|
|
33
|
-
function canUseProperty(
|
|
34
|
-
dom: Element,
|
|
35
|
-
name: string,
|
|
36
|
-
isSvg: boolean,
|
|
37
|
-
): dom is Element & Record<string, unknown> {
|
|
38
|
-
if (isSvg) return false
|
|
39
|
-
if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false
|
|
40
|
-
return name in dom
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isFrameworkProp(name: string): boolean {
|
|
44
|
-
return (
|
|
45
|
-
name === 'children' ||
|
|
46
|
-
name === 'mix' ||
|
|
47
|
-
name === 'key' ||
|
|
48
|
-
name === 'animate' ||
|
|
49
|
-
name === 'innerHTML'
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// TODO: would rather actually diff el.style object directly instead of writing
|
|
54
|
-
// to the style attribute
|
|
55
|
-
function serializeStyleObject(style: Record<string, unknown>): string {
|
|
56
|
-
let parts: string[] = []
|
|
57
|
-
for (let [key, value] of Object.entries(style)) {
|
|
58
|
-
if (value == null) continue
|
|
59
|
-
if (typeof value === 'boolean') continue
|
|
60
|
-
if (typeof value === 'number' && !Number.isFinite(value)) continue
|
|
61
|
-
|
|
62
|
-
let cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
|
|
63
|
-
|
|
64
|
-
let cssValue = Array.isArray(value)
|
|
65
|
-
? (value as unknown[]).join(', ')
|
|
66
|
-
: normalizeCssValue(key, value)
|
|
67
|
-
|
|
68
|
-
parts.push(`${cssKey}: ${cssValue};`)
|
|
69
|
-
}
|
|
70
|
-
return parts.join(' ')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizePropName(name: string, isSvg: boolean): { ns?: string; attr: string } {
|
|
74
|
-
// aria-/data- pass through
|
|
75
|
-
if (name.startsWith('aria-') || name.startsWith('data-')) return { attr: name }
|
|
76
|
-
|
|
77
|
-
// DOM property -> HTML mappings
|
|
78
|
-
if (name === 'className') return { attr: 'class' }
|
|
79
|
-
if (!isSvg) {
|
|
80
|
-
if (name === 'htmlFor') return { attr: 'for' }
|
|
81
|
-
if (name === 'tabIndex') return { attr: 'tabindex' }
|
|
82
|
-
if (name === 'acceptCharset') return { attr: 'accept-charset' }
|
|
83
|
-
if (name === 'httpEquiv') return { attr: 'http-equiv' }
|
|
84
|
-
return { attr: name.toLowerCase() }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return normalizeSvgAttribute(name)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function toLocalName(attrName: string): string {
|
|
91
|
-
let separatorIndex = attrName.indexOf(':')
|
|
92
|
-
if (separatorIndex === -1) return attrName
|
|
93
|
-
return attrName.slice(separatorIndex + 1)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function clearRuntimePropertyOnRemoval(dom: Element & Record<string, unknown>, name: string): void {
|
|
97
|
-
try {
|
|
98
|
-
if (name === 'value' || name === 'defaultValue') {
|
|
99
|
-
dom[name] = ''
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
if (name === 'checked' || name === 'defaultChecked' || name === 'selected') {
|
|
103
|
-
dom[name] = false
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
if (name === 'selectedIndex') {
|
|
107
|
-
dom[name] = -1
|
|
108
|
-
}
|
|
109
|
-
} catch {}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getMergedClassName(props: ElementProps): string | undefined {
|
|
113
|
-
let classAttr = typeof props.class === 'string' ? props.class : ''
|
|
114
|
-
let className = typeof props.className === 'string' ? props.className : ''
|
|
115
|
-
let mergedClassName =
|
|
116
|
-
classAttr && className ? `${classAttr} ${className}` : classAttr || className
|
|
117
|
-
return mergedClassName || undefined
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function diffHostProps(curr: ElementProps, next: ElementProps, dom: Element) {
|
|
121
|
-
let isSvg = dom.namespaceURI === SVG_NS
|
|
122
|
-
let currClassName = getMergedClassName(curr)
|
|
123
|
-
let nextClassName = getMergedClassName(next)
|
|
124
|
-
|
|
125
|
-
if (currClassName !== nextClassName) {
|
|
126
|
-
if (nextClassName) {
|
|
127
|
-
dom.setAttribute('class', nextClassName)
|
|
128
|
-
} else {
|
|
129
|
-
dom.removeAttribute('class')
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Removals
|
|
134
|
-
for (let name in curr) {
|
|
135
|
-
if (isFrameworkProp(name)) continue
|
|
136
|
-
if (name === 'class' || name === 'className') continue
|
|
137
|
-
if (!(name in next) || next[name] == null) {
|
|
138
|
-
// Clear runtime state for form-like props where removing the attribute is not enough.
|
|
139
|
-
if (canUseProperty(dom, name, isSvg)) {
|
|
140
|
-
clearRuntimePropertyOnRemoval(dom, name)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let { ns, attr } = normalizePropName(name, isSvg)
|
|
144
|
-
if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
|
|
145
|
-
else dom.removeAttribute(attr)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Additions/updates
|
|
150
|
-
for (let name in next) {
|
|
151
|
-
if (isFrameworkProp(name)) continue
|
|
152
|
-
if (name === 'class' || name === 'className') continue
|
|
153
|
-
let nextValue = next[name]
|
|
154
|
-
if (nextValue == null) continue
|
|
155
|
-
let prevValue = curr[name]
|
|
156
|
-
if (prevValue !== nextValue) {
|
|
157
|
-
let { ns, attr } = normalizePropName(name, isSvg)
|
|
158
|
-
|
|
159
|
-
// Object style: serialize to attribute for now
|
|
160
|
-
if (
|
|
161
|
-
attr === 'style' &&
|
|
162
|
-
typeof nextValue === 'object' &&
|
|
163
|
-
nextValue &&
|
|
164
|
-
!Array.isArray(nextValue)
|
|
165
|
-
) {
|
|
166
|
-
dom.setAttribute('style', serializeStyleObject(nextValue))
|
|
167
|
-
continue
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Prefer property assignment when possible (HTML only, not SVG)
|
|
171
|
-
if (canUseProperty(dom, name, isSvg)) {
|
|
172
|
-
try {
|
|
173
|
-
dom[name] = nextValue == null ? '' : nextValue
|
|
174
|
-
continue
|
|
175
|
-
} catch {}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Attribute path
|
|
179
|
-
if (typeof nextValue === 'function') {
|
|
180
|
-
// Never serialize functions as attribute values
|
|
181
|
-
continue
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')
|
|
185
|
-
if (nextValue != null && (nextValue !== false || isAriaOrData)) {
|
|
186
|
-
// Special-case popover: true => presence only
|
|
187
|
-
let attrValue = name === 'popover' && nextValue === true ? '' : String(nextValue)
|
|
188
|
-
if (ns) dom.setAttributeNS(ns, attr, attrValue)
|
|
189
|
-
else dom.setAttribute(attr, attrValue)
|
|
190
|
-
} else {
|
|
191
|
-
if (ns) dom.removeAttributeNS(ns, toLocalName(attr))
|
|
192
|
-
else dom.removeAttribute(attr)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
13
|
/**
|
|
199
14
|
* Reset the global style state. For testing only - not exported from index.ts.
|
|
200
15
|
*/
|
package/src/runtime/dom.ts
CHANGED
|
@@ -3418,6 +3418,10 @@ export interface TemplateHTMLProps<eventTarget extends EventTarget = HTMLTemplat
|
|
|
3418
3418
|
*/
|
|
3419
3419
|
export interface TextareaHTMLProps<eventTarget extends EventTarget = HTMLTextAreaElement>
|
|
3420
3420
|
extends HTMLProps<eventTarget> {
|
|
3421
|
+
/** Textarea content comes from `value` or `defaultValue`. */
|
|
3422
|
+
children?: never
|
|
3423
|
+
/** Textarea content comes from `value` or `defaultValue`. */
|
|
3424
|
+
innerHTML?: never
|
|
3421
3425
|
/** The `autocomplete` HTML attribute. */
|
|
3422
3426
|
autocomplete?: Trackable<string | undefined>
|
|
3423
3427
|
/** The `autoComplete` HTML attribute. */
|
package/src/runtime/frame.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { Scheduler, VirtualRoot } from './vdom.ts'
|
|
|
8
8
|
import { createRangeRoot, createRoot } from './vdom.ts'
|
|
9
9
|
import { diffNodes } from './diff-dom.ts'
|
|
10
10
|
import { createStyleManager, type StyleManager } from '../style/index.ts'
|
|
11
|
+
import { findFlushMarker, type FlushKind } from './stream-protocol.ts'
|
|
11
12
|
|
|
12
13
|
type FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment
|
|
13
14
|
|
|
@@ -39,12 +40,31 @@ type FrameMarkerData = FrameData & {
|
|
|
39
40
|
type PendingClientEntries = Map<Comment, [Comment, RemixElement]>
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* Loads a client
|
|
43
|
+
* Loads a named client-entry export for hydration.
|
|
44
|
+
*
|
|
45
|
+
* @param moduleUrl Browser-resolvable URL for the module that contains the client entry.
|
|
46
|
+
* @param exportName Named export to read from the loaded module.
|
|
47
|
+
* @returns The exported component function, or a promise for it.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* run({
|
|
52
|
+
* async loadModule(moduleUrl, exportName) {
|
|
53
|
+
* let mod = await import(moduleUrl)
|
|
54
|
+
* return mod[exportName]
|
|
55
|
+
* },
|
|
56
|
+
* })
|
|
57
|
+
* ```
|
|
43
58
|
*/
|
|
44
59
|
export type LoadModule = (moduleUrl: string, exportName: string) => Promise<Function> | Function
|
|
45
60
|
|
|
46
61
|
/**
|
|
47
|
-
* Resolves
|
|
62
|
+
* Resolves content for a browser-loaded frame.
|
|
63
|
+
*
|
|
64
|
+
* @param src Source string from the `<Frame src>` prop.
|
|
65
|
+
* @param signal Abort signal for the active frame load or reload.
|
|
66
|
+
* @param target Optional name of the frame being reloaded.
|
|
67
|
+
* @returns HTML, a stream of HTML bytes, or Remix node content to render into the frame.
|
|
48
68
|
*/
|
|
49
69
|
export type ResolveFrame = (
|
|
50
70
|
src: string,
|
|
@@ -138,6 +158,7 @@ export type Frame = {
|
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
type RenderOptions = {
|
|
161
|
+
flushKind?: FlushKind
|
|
141
162
|
initialHydrationTracker?: InitialHydrationTracker
|
|
142
163
|
signal?: AbortSignal
|
|
143
164
|
}
|
|
@@ -211,9 +232,9 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
|
|
|
211
232
|
if (options?.signal?.aborted) return
|
|
212
233
|
|
|
213
234
|
if (content instanceof ReadableStream) {
|
|
214
|
-
await renderFrameStream(content, container.doc, async (html) => {
|
|
235
|
+
await renderFrameStream(content, container.doc, async (html, flushKind) => {
|
|
215
236
|
if (options?.signal?.aborted) return
|
|
216
|
-
await render(html, options)
|
|
237
|
+
await render(html, { ...options, flushKind })
|
|
217
238
|
})
|
|
218
239
|
return
|
|
219
240
|
}
|
|
@@ -237,39 +258,42 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
|
|
|
237
258
|
contentRoot = undefined
|
|
238
259
|
}
|
|
239
260
|
|
|
261
|
+
if (typeof content === 'string') {
|
|
262
|
+
let flushed = await consumeFlushBatches(content, async (html, flushKind) => {
|
|
263
|
+
await render(html, { ...options, flushKind })
|
|
264
|
+
})
|
|
265
|
+
if (flushed.applied) {
|
|
266
|
+
if (flushed.remainder !== '') {
|
|
267
|
+
await render(flushed.remainder, { ...options, flushKind: 'fragment' })
|
|
268
|
+
}
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
240
273
|
let htmlContent = typeof content === 'string' ? stripDoctypeMarkup(content) : undefined
|
|
241
274
|
|
|
242
275
|
let isFullDocumentReload =
|
|
243
276
|
container.root instanceof Document &&
|
|
244
277
|
htmlContent !== undefined &&
|
|
245
|
-
|
|
278
|
+
options?.flushKind === 'document'
|
|
246
279
|
|
|
247
280
|
if (isFullDocumentReload && htmlContent !== undefined) {
|
|
248
|
-
// Full-document reload should tear down existing hydrated roots and subframes
|
|
249
|
-
// before diffing fresh HTML, otherwise stale component instances can survive
|
|
250
|
-
// on detached DOM nodes.
|
|
251
|
-
let previousBodyNodes = Array.from(container.doc.body.childNodes)
|
|
252
|
-
removeVirtualRoots(previousBodyNodes)
|
|
253
|
-
disposeSubFrames(previousBodyNodes, context)
|
|
254
281
|
let parsed = new DOMParser().parseFromString(htmlContent, 'text/html')
|
|
255
282
|
mergeRmxDataFromDocument(context.data, parsed)
|
|
256
|
-
context.styleManager.
|
|
257
|
-
context.styleManager.adoptServerStyles(
|
|
283
|
+
context.styleManager.replaceServerStyles(
|
|
258
284
|
collectFrameServerStyleTags(createElementContainer(parsed)),
|
|
259
285
|
)
|
|
260
286
|
|
|
261
287
|
syncElementAttributes(container.doc.documentElement, parsed.documentElement)
|
|
262
|
-
syncElementAttributes(container.doc.head, parsed.head)
|
|
263
|
-
syncElementAttributes(container.doc.body, parsed.body)
|
|
264
288
|
|
|
265
|
-
diffNodes(
|
|
289
|
+
diffNodes([container.doc.head], [parsed.head], {
|
|
266
290
|
...context,
|
|
267
|
-
regionParent: container.doc.
|
|
291
|
+
regionParent: container.doc.documentElement,
|
|
268
292
|
regionTailRef: null,
|
|
269
293
|
})
|
|
270
|
-
diffNodes(
|
|
294
|
+
diffNodes([container.doc.body], [parsed.body], {
|
|
271
295
|
...context,
|
|
272
|
-
regionParent: container.doc.
|
|
296
|
+
regionParent: container.doc.documentElement,
|
|
273
297
|
regionTailRef: null,
|
|
274
298
|
})
|
|
275
299
|
|
|
@@ -282,8 +306,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
|
|
|
282
306
|
|
|
283
307
|
let fragment =
|
|
284
308
|
htmlContent !== undefined ? createFragmentFromString(container.doc, htmlContent) : content
|
|
285
|
-
context.styleManager.
|
|
286
|
-
context.styleManager.adoptServerStyles(
|
|
309
|
+
context.styleManager.replaceServerStyles(
|
|
287
310
|
collectFrameServerStyleTags(createElementContainer(fragment)),
|
|
288
311
|
)
|
|
289
312
|
removeEmptyHeads(fragment)
|
|
@@ -344,8 +367,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
|
|
|
344
367
|
async function hydrateInitial(): Promise<void> {
|
|
345
368
|
let initialHydrationTracker = createInitialHydrationTracker()
|
|
346
369
|
|
|
347
|
-
context.styleManager.
|
|
348
|
-
context.styleManager.adoptServerStyles(collectFrameServerStyleTags(container))
|
|
370
|
+
context.styleManager.replaceServerStyles(collectFrameServerStyleTags(container))
|
|
349
371
|
createSubFrames(container.childNodes, context)
|
|
350
372
|
scheduleHydrationInContainer(container, context, initialHydrationTracker)
|
|
351
373
|
|
|
@@ -986,13 +1008,12 @@ function extractTemplatesFromBuffer(
|
|
|
986
1008
|
async function renderFrameStream(
|
|
987
1009
|
stream: ReadableStream<Uint8Array>,
|
|
988
1010
|
doc: Document,
|
|
989
|
-
applyHtml: (html: string) => Promise<void>,
|
|
1011
|
+
applyHtml: (html: string, flushKind: FlushKind) => Promise<void>,
|
|
990
1012
|
): Promise<void> {
|
|
991
1013
|
let reader = stream.getReader()
|
|
992
1014
|
let decoder = new TextDecoder()
|
|
993
1015
|
let buffer = ''
|
|
994
1016
|
let html = ''
|
|
995
|
-
let appliedLength = 0
|
|
996
1017
|
let appliedOnce = false
|
|
997
1018
|
|
|
998
1019
|
try {
|
|
@@ -1006,13 +1027,9 @@ async function renderFrameStream(
|
|
|
1006
1027
|
|
|
1007
1028
|
if (parsed.html !== '') {
|
|
1008
1029
|
html += parsed.html
|
|
1009
|
-
let
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1013
|
-
await applyHtml(html)
|
|
1014
|
-
appliedLength = html.length
|
|
1015
|
-
appliedOnce = true
|
|
1030
|
+
let flushed = await consumeFlushBatches(html, applyHtml)
|
|
1031
|
+
appliedOnce = flushed.applied || appliedOnce
|
|
1032
|
+
html = flushed.remainder
|
|
1016
1033
|
}
|
|
1017
1034
|
}
|
|
1018
1035
|
|
|
@@ -1025,21 +1042,40 @@ async function renderFrameStream(
|
|
|
1025
1042
|
buffer = ''
|
|
1026
1043
|
}
|
|
1027
1044
|
|
|
1028
|
-
if (html !== ''
|
|
1029
|
-
await applyHtml(html)
|
|
1045
|
+
if (html !== '') {
|
|
1046
|
+
await applyHtml(html, 'fragment')
|
|
1030
1047
|
appliedOnce = true
|
|
1031
1048
|
}
|
|
1032
1049
|
|
|
1033
1050
|
// A frame stream can legitimately resolve to empty content. Ensure the
|
|
1034
1051
|
// existing frame region is cleared instead of treated as a no-op.
|
|
1035
1052
|
if (html === '' && !appliedOnce) {
|
|
1036
|
-
await applyHtml('')
|
|
1053
|
+
await applyHtml('', 'fragment')
|
|
1037
1054
|
}
|
|
1038
1055
|
} finally {
|
|
1039
1056
|
reader.releaseLock()
|
|
1040
1057
|
}
|
|
1041
1058
|
}
|
|
1042
1059
|
|
|
1060
|
+
async function consumeFlushBatches(
|
|
1061
|
+
html: string,
|
|
1062
|
+
applyHtml: (html: string, flushKind: FlushKind) => Promise<void>,
|
|
1063
|
+
): Promise<{ applied: boolean; remainder: string }> {
|
|
1064
|
+
let applied = false
|
|
1065
|
+
let cursor = 0
|
|
1066
|
+
let marker = findFlushMarker(html, cursor)
|
|
1067
|
+
|
|
1068
|
+
while (marker) {
|
|
1069
|
+
let batch = html.slice(cursor, marker.index)
|
|
1070
|
+
await applyHtml(batch, marker.kind)
|
|
1071
|
+
applied = true
|
|
1072
|
+
cursor = marker.endIndex
|
|
1073
|
+
marker = findFlushMarker(html, cursor)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return { applied, remainder: html.slice(cursor) }
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1043
1079
|
type FrameContainer = {
|
|
1044
1080
|
doc: Document
|
|
1045
1081
|
root: ParentNode
|
|
@@ -1104,11 +1140,6 @@ function isRemixNodeFrameContent(content: InternalFrameContent): content is Remi
|
|
|
1104
1140
|
)
|
|
1105
1141
|
}
|
|
1106
1142
|
|
|
1107
|
-
function isFullDocumentHtml(content: string): boolean {
|
|
1108
|
-
let trimmed = content.trimStart()
|
|
1109
|
-
return /^<!doctype html\b/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
1143
|
type HydrationMarker = {
|
|
1113
1144
|
id: string
|
|
1114
1145
|
start: Comment
|
|
@@ -1201,18 +1232,3 @@ function findEndMarker(
|
|
|
1201
1232
|
|
|
1202
1233
|
throw new Error('End marker not found')
|
|
1203
1234
|
}
|
|
1204
|
-
|
|
1205
|
-
function collectHtmlMarkerSummary(html: string): Record<string, number> {
|
|
1206
|
-
return {
|
|
1207
|
-
frameStarts: html.match(/<!--\s*rmx:f:/g)?.length ?? 0,
|
|
1208
|
-
frameEnds: html.match(/<!--\s*\/rmx:f\s*-->/g)?.length ?? 0,
|
|
1209
|
-
hydrationStarts: html.match(/<!--\s*rmx:h:/g)?.length ?? 0,
|
|
1210
|
-
hydrationEnds: html.match(/<!--\s*\/rmx:h\s*-->/g)?.length ?? 0,
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
function hasBalancedMarkerSummary(summary: Record<string, number>): boolean {
|
|
1215
|
-
return (
|
|
1216
|
-
summary.frameStarts === summary.frameEnds && summary.hydrationStarts === summary.hydrationEnds
|
|
1217
|
-
)
|
|
1218
|
-
}
|
package/src/runtime/jsx.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as dom from './dom.d.ts'
|
|
2
2
|
import type { Handle, RenderFn } from './component.ts'
|
|
3
|
+
import { createRemixElement } from './core/vnode.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Any valid element type accepted by JSX or {@link import('./create-element.ts').createElement}.
|
|
@@ -102,7 +103,7 @@ export type Props<T extends keyof JSX.IntrinsicElements> = NormalizeMixProp<
|
|
|
102
103
|
*/
|
|
103
104
|
export function jsx(type: ElementType, props: ElementProps, key?: string): RemixElement
|
|
104
105
|
export function jsx(type: ElementType, props: ElementProps, key?: string): RemixElement {
|
|
105
|
-
return
|
|
106
|
+
return createRemixElement(type, props, key)
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
export { jsx as jsxDEV, jsx as jsxs }
|
|
@@ -367,31 +368,3 @@ declare global {
|
|
|
367
368
|
IntrinsicHTMLElements {}
|
|
368
369
|
}
|
|
369
370
|
}
|
|
370
|
-
|
|
371
|
-
function normalizeElementProps(props: ElementProps | null | undefined): ElementProps {
|
|
372
|
-
if (!props) return {}
|
|
373
|
-
if (!('mix' in props)) return props
|
|
374
|
-
|
|
375
|
-
let { mix, ...rest } = props
|
|
376
|
-
let normalizedMix = normalizeMixValue(mix)
|
|
377
|
-
return normalizedMix === undefined ? rest : { ...rest, mix: normalizedMix }
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function normalizeMixValue(mix: unknown): unknown[] | undefined {
|
|
381
|
-
if (!mix) return undefined
|
|
382
|
-
|
|
383
|
-
let normalizedMix: unknown[] = []
|
|
384
|
-
flattenMixValue(mix, normalizedMix)
|
|
385
|
-
return normalizedMix.length === 0 ? undefined : normalizedMix
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function flattenMixValue(mix: unknown, out: unknown[]): void {
|
|
389
|
-
if (!mix) return
|
|
390
|
-
if (!Array.isArray(mix)) {
|
|
391
|
-
out.push(mix)
|
|
392
|
-
return
|
|
393
|
-
}
|
|
394
|
-
for (let item of mix) {
|
|
395
|
-
flattenMixValue(item, out)
|
|
396
|
-
}
|
|
397
|
-
}
|
|
@@ -598,7 +598,6 @@ class MixinHandleImpl
|
|
|
598
598
|
this.#activeScope = scope
|
|
599
599
|
if (!scope) return
|
|
600
600
|
if (this.#scopeTargets.has(scope)) return
|
|
601
|
-
this.#scopeSignals.set(scope, new AbortController())
|
|
602
601
|
this.#scopeTargets.set(scope, new TypedEventTarget<MixinHandleEventMap<Element>>())
|
|
603
602
|
this.#scopePhaseCounts.set(scope, { beforeUpdate: 0, commit: 0 })
|
|
604
603
|
}
|
|
@@ -616,8 +615,11 @@ class MixinHandleImpl
|
|
|
616
615
|
this.#decrementGlobalPhaseCount('beforeUpdate', scopePhaseCounts.beforeUpdate)
|
|
617
616
|
this.#decrementGlobalPhaseCount('commit', scopePhaseCounts.commit)
|
|
618
617
|
}
|
|
619
|
-
this.#scopeSignals.get(scope)
|
|
620
|
-
|
|
618
|
+
let controller = this.#scopeSignals.get(scope)
|
|
619
|
+
if (controller) {
|
|
620
|
+
controller.abort()
|
|
621
|
+
this.#scopeSignals.delete(scope)
|
|
622
|
+
}
|
|
621
623
|
this.#scopePhaseCounts.delete(scope)
|
|
622
624
|
this.#scopeTargets.delete(scope)
|
|
623
625
|
if (this.#activeScope === scope) {
|
|
@@ -646,7 +648,10 @@ class MixinHandleImpl
|
|
|
646
648
|
|
|
647
649
|
#getScopeSignal(scope: symbol) {
|
|
648
650
|
let controller = this.#scopeSignals.get(scope)
|
|
649
|
-
|
|
651
|
+
if (!controller) {
|
|
652
|
+
controller = new AbortController()
|
|
653
|
+
this.#scopeSignals.set(scope, controller)
|
|
654
|
+
}
|
|
650
655
|
return controller.signal
|
|
651
656
|
}
|
|
652
657
|
|