mirror of https://github.com/vuejs/core.git
112 lines
3.0 KiB
TypeScript
112 lines
3.0 KiB
TypeScript
|
import { isString } from '@vue/shared'
|
||
|
import { DOMNodeTypes, isComment } from './hydration'
|
||
|
|
||
|
/**
|
||
|
* 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 = any> = (
|
||
|
options?: Options,
|
||
|
) => HydrationStrategy
|
||
|
|
||
|
export const hydrateOnIdle: HydrationStrategyFactory = () => hydrate => {
|
||
|
const id = requestIdleCallback(hydrate)
|
||
|
return () => cancelIdleCallback(id)
|
||
|
}
|
||
|
|
||
|
export const hydrateOnVisible: HydrationStrategyFactory<string | number> =
|
||
|
(margin = 0) =>
|
||
|
(hydrate, forEach) => {
|
||
|
const ob = new IntersectionObserver(
|
||
|
entries => {
|
||
|
for (const e of entries) {
|
||
|
if (!e.isIntersecting) continue
|
||
|
ob.disconnect()
|
||
|
hydrate()
|
||
|
break
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
rootMargin: isString(margin) ? margin : margin + 'px',
|
||
|
},
|
||
|
)
|
||
|
forEach(el => 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<
|
||
|
string | string[]
|
||
|
> =
|
||
|
(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) {
|
||
|
// fragment
|
||
|
if (isComment(node) && node.data === '[') {
|
||
|
let depth = 1
|
||
|
let next = node.nextSibling
|
||
|
while (next) {
|
||
|
if (next.nodeType === DOMNodeTypes.ELEMENT) {
|
||
|
cb(next as Element)
|
||
|
} else if (isComment(next)) {
|
||
|
if (next.data === ']') {
|
||
|
if (--depth === 0) break
|
||
|
} else if (next.data === '[') {
|
||
|
depth++
|
||
|
}
|
||
|
}
|
||
|
next = next.nextSibling
|
||
|
}
|
||
|
} else {
|
||
|
cb(node as Element)
|
||
|
}
|
||
|
}
|