forked from github/turbo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
view.ts
129 lines (109 loc) · 3.5 KB
/
view.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { ReloadReason } from "./native/browser_adapter"
import { Renderer } from "./renderer"
import { Snapshot } from "./snapshot"
import { Position } from "./types"
import { getAnchor } from "./url"
export interface ViewDelegate<S extends Snapshot> {
allowsImmediateRender(snapshot: S, resume: (value: any) => void): boolean
preloadOnLoadLinksForView(element: Element): void
viewRenderedSnapshot(snapshot: S, isPreview: boolean): void
viewInvalidated(reason: ReloadReason): void
}
export abstract class View<
E extends Element,
S extends Snapshot<E> = Snapshot<E>,
R extends Renderer<E, S> = Renderer<E, S>,
D extends ViewDelegate<S> = ViewDelegate<S>
> {
readonly delegate: D
readonly element: E
renderer?: R
abstract readonly snapshot: S
renderPromise?: Promise<void>
private resolveRenderPromise = (_value: any) => {}
private resolveInterceptionPromise = (_value: any) => {}
constructor(delegate: D, element: E) {
this.delegate = delegate
this.element = element
}
// Scrolling
scrollToAnchor(anchor: string | undefined) {
const element = this.snapshot.getElementForAnchor(anchor)
if (element) {
this.scrollToElement(element)
this.focusElement(element)
} else {
this.scrollToPosition({ x: 0, y: 0 })
}
}
scrollToAnchorFromLocation(location: URL) {
this.scrollToAnchor(getAnchor(location))
}
scrollToElement(element: Element) {
element.scrollIntoView()
}
focusElement(element: Element) {
if (element instanceof HTMLElement) {
if (element.hasAttribute("tabindex")) {
element.focus()
} else {
element.setAttribute("tabindex", "-1")
element.focus()
element.removeAttribute("tabindex")
}
}
}
scrollToPosition({ x, y }: Position) {
this.scrollRoot.scrollTo(x, y)
}
scrollToTop() {
this.scrollToPosition({ x: 0, y: 0 })
}
get scrollRoot(): { scrollTo(x: number, y: number): void } {
return window
}
// Rendering
async render(renderer: R) {
const { isPreview, shouldRender, newSnapshot: snapshot } = renderer
if (shouldRender) {
try {
this.renderPromise = new Promise((resolve) => (this.resolveRenderPromise = resolve))
this.renderer = renderer
await this.prepareToRenderSnapshot(renderer)
const renderInterception = new Promise((resolve) => (this.resolveInterceptionPromise = resolve))
const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise)
if (!immediateRender) await renderInterception
await this.renderSnapshot(renderer)
this.delegate.viewRenderedSnapshot(snapshot, isPreview)
this.delegate.preloadOnLoadLinksForView(this.element)
this.finishRenderingSnapshot(renderer)
} finally {
delete this.renderer
this.resolveRenderPromise(undefined)
delete this.renderPromise
}
} else {
this.invalidate(renderer.reloadReason)
}
}
invalidate(reason: ReloadReason) {
this.delegate.viewInvalidated(reason)
}
async prepareToRenderSnapshot(renderer: R) {
this.markAsPreview(renderer.isPreview)
await renderer.prepareToRender()
}
markAsPreview(isPreview: boolean) {
if (isPreview) {
this.element.setAttribute("data-turbo-preview", "")
} else {
this.element.removeAttribute("data-turbo-preview")
}
}
async renderSnapshot(renderer: R) {
await renderer.render()
}
finishRenderingSnapshot(renderer: R) {
renderer.finishRendering()
}
}