-
Notifications
You must be signed in to change notification settings - Fork 55
Expand file tree
/
Copy pathlazy-define.ts
More file actions
117 lines (103 loc) · 4 KB
/
lazy-define.ts
File metadata and controls
117 lines (103 loc) · 4 KB
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
type Strategy = (tagName: string) => Promise<void>
const dynamicElements = new Map<string, Set<() => void>>()
const ready = new Promise<void>(resolve => {
if (document.readyState !== 'loading') {
resolve()
} else {
document.addEventListener('readystatechange', () => resolve(), {once: true})
}
})
const firstInteraction = new Promise<void>(resolve => {
const controller = new AbortController()
controller.signal.addEventListener('abort', () => resolve())
const listenerOptions = {once: true, passive: true, signal: controller.signal}
const handler = () => controller.abort()
document.addEventListener('mousedown', handler, listenerOptions)
// eslint-disable-next-line github/require-passive-events
document.addEventListener('touchstart', handler, listenerOptions)
document.addEventListener('keydown', handler, listenerOptions)
document.addEventListener('pointerdown', handler, listenerOptions)
})
const visible = (tagName: string): Promise<void> =>
new Promise<void>(resolve => {
const observer = new IntersectionObserver(
entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
resolve()
observer.disconnect()
return
}
}
},
{
// Currently the threshold is set to 256px from the bottom of the viewport
// with a threshold of 0.1. This means the element will not load until about
// 2 keyboard-down-arrow presses away from being visible in the viewport,
// giving us some time to fetch it before the contents are made visible
rootMargin: '0px 0px 256px 0px',
threshold: 0.01
}
)
for (const el of document.querySelectorAll(tagName)) {
observer.observe(el)
}
})
const strategies: Record<string, Strategy> = {
ready: () => ready,
firstInteraction: () => firstInteraction,
visible
}
type ElementLike = Element | Document | ShadowRoot
const pendingElements = new Set<ElementLike>()
let scanTimer: number | null = null
function scan(element: ElementLike) {
pendingElements.add(element)
if (scanTimer != null) return
scanTimer = requestAnimationFrame(() => {
scanTimer = null
const elements = new Set(pendingElements)
pendingElements.clear()
if (!dynamicElements.size) {
return
}
outer: for (const el of elements) {
for (const tagName of dynamicElements.keys()) {
const child: Element | null = el instanceof Element && el.matches(tagName) ? el : el.querySelector(tagName)
if (customElements.get(tagName) || child) {
const strategyName = (child?.getAttribute('data-load-on') || 'ready') as keyof typeof strategies
const strategy = strategyName in strategies ? strategies[strategyName] : strategies.ready
// eslint-disable-next-line github/no-then
for (const cb of dynamicElements.get(tagName) || []) strategy(tagName).then(cb)
dynamicElements.delete(tagName)
if (!dynamicElements.size) break outer
}
}
}
})
}
let elementLoader: MutationObserver
export function lazyDefine(object: Record<string, () => void>): void
export function lazyDefine(tagName: string, callback: () => void): void
export function lazyDefine(tagNameOrObj: string | Record<string, () => void>, singleCallback?: () => void) {
if (typeof tagNameOrObj === 'string' && singleCallback) {
tagNameOrObj = {[tagNameOrObj]: singleCallback}
}
for (const [tagName, callback] of Object.entries(tagNameOrObj)) {
if (!dynamicElements.has(tagName)) dynamicElements.set(tagName, new Set<() => void>())
dynamicElements.get(tagName)!.add(callback)
}
observe(document)
}
export function observe(target: ElementLike): void {
elementLoader ||= new MutationObserver(mutations => {
if (!dynamicElements.size) return
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) scan(node)
}
}
})
scan(target)
elementLoader.observe(target, {subtree: true, childList: true})
}