-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
/
Copy pathhydrationStrategies.ts
139 lines (130 loc) · 3.96 KB
/
hydrationStrategies.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
130
131
132
133
134
135
136
137
138
139
import { getGlobalThis, isString } from '@vue/shared'
import { DOMNodeTypes, isComment } from './hydration'
// Polyfills for Safari support
// see https://caniuse.com/requestidlecallback
const requestIdleCallback: Window['requestIdleCallback'] =
getGlobalThis().requestIdleCallback || (cb => setTimeout(cb, 1))
const cancelIdleCallback: Window['cancelIdleCallback'] =
getGlobalThis().cancelIdleCallback || (id => clearTimeout(id))
/**
* A lazy hydration strategy for async components.
* @param hydrate - call this to perform the actual hydration.
* @param forEachElement - iterate through the root elements of the component's
* non-hydrated DOM, accounting for possible fragments.
* @returns a teardown function to be called if the async component is unmounted
* before it is hydrated. This can be used to e.g. remove DOM event
* listeners.
*/
export type HydrationStrategy = (
hydrate: () => void,
forEachElement: (cb: (el: Element) => any) => void,
) => (() => void) | void
export type HydrationStrategyFactory<Options> = (
options?: Options,
) => HydrationStrategy
export const hydrateOnIdle: HydrationStrategyFactory<number> =
(timeout = 10000) =>
hydrate => {
const id = requestIdleCallback(hydrate, { timeout })
return () => cancelIdleCallback(id)
}
function elementIsVisibleInViewport(el: Element) {
const { top, left, bottom, right } = el.getBoundingClientRect()
// eslint-disable-next-line no-restricted-globals
const { innerHeight, innerWidth } = window
return (
((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) &&
((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
)
}
export const hydrateOnVisible: HydrationStrategyFactory<
IntersectionObserverInit
> = opts => (hydrate, forEach) => {
const ob = new IntersectionObserver(entries => {
for (const e of entries) {
if (!e.isIntersecting) continue
ob.disconnect()
hydrate()
break
}
}, opts)
forEach(el => {
if (!(el instanceof Element)) return
if (elementIsVisibleInViewport(el)) {
hydrate()
ob.disconnect()
return false
}
ob.observe(el)
})
return () => ob.disconnect()
}
export const hydrateOnMediaQuery: HydrationStrategyFactory<string> =
query => hydrate => {
if (query) {
const mql = matchMedia(query)
if (mql.matches) {
hydrate()
} else {
mql.addEventListener('change', hydrate, { once: true })
return () => mql.removeEventListener('change', hydrate)
}
}
}
export const hydrateOnInteraction: HydrationStrategyFactory<
keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>
> =
(interactions = []) =>
(hydrate, forEach) => {
if (isString(interactions)) interactions = [interactions]
let hasHydrated = false
const doHydrate = (e: Event) => {
if (!hasHydrated) {
hasHydrated = true
teardown()
hydrate()
// replay event
e.target!.dispatchEvent(new (e.constructor as any)(e.type, e))
}
}
const teardown = () => {
forEach(el => {
for (const i of interactions) {
el.removeEventListener(i, doHydrate)
}
})
}
forEach(el => {
for (const i of interactions) {
el.addEventListener(i, doHydrate, { once: true })
}
})
return teardown
}
export function forEachElement(
node: Node,
cb: (el: Element) => void | false,
): void {
// fragment
if (isComment(node) && node.data === '[') {
let depth = 1
let next = node.nextSibling
while (next) {
if (next.nodeType === DOMNodeTypes.ELEMENT) {
const result = cb(next as Element)
if (result === false) {
break
}
} else if (isComment(next)) {
if (next.data === ']') {
if (--depth === 0) break
} else if (next.data === '[') {
depth++
}
}
next = next.nextSibling
}
} else {
cb(node as Element)
}
}