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?: Options, ) => HydrationStrategy export const hydrateOnIdle: HydrationStrategyFactory = (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 = 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 > = (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) } }