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)
 | 
						|
  }
 | 
						|
}
 |