mirror of https://github.com/vuejs/core.git
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
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)
|
|
}
|
|
}
|