| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- 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?: 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)
- }
- }
|