import { computed, readonly, ref, watch } from 'vue'; import type { Ref } from 'vue'; import { toArray } from '@robonen/stdlib'; import type { ConfigurableWindow } from '@/types'; import { defaultWindow } from '@/types'; import type { MaybeComputedElementRef } from '@/composables/component/unrefElement'; import { unrefElement } from '@/composables/component/unrefElement'; import { useSupported } from '@/composables/utilities/useSupported'; import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose'; export interface UseResizeObserverOptions extends ConfigurableWindow { /** * The box model to observe * * @default 'content-box' */ box?: ResizeObserverBoxOptions; /** * Start observing immediately once the target is resolved * * @default true */ immediate?: boolean; } export type ResizeObserverCallback = ( entries: readonly ResizeObserverEntry[], observer: ResizeObserver, ) => void; export interface UseResizeObserverReturn { /** * Whether `ResizeObserver` is supported in the current environment */ isSupported: Readonly>; /** * Whether the observer is currently active */ isActive: Readonly>; /** * Temporarily stop observing (disconnects the observer) while keeping the * target watcher alive, so observing can be resumed later */ pause: () => void; /** * Resume observing after a `pause` */ resume: () => void; /** * Permanently stop observing and tear down the target watcher */ stop: () => void; } /** * @name useResizeObserver * @category Elements * @description Reports changes to the dimensions of an element via `ResizeObserver`. * Accepts a single target or an array of (reactive) targets. The observer is * recreated only when the resolved elements change, and can be paused/resumed. * * @param {MaybeComputedElementRef | MaybeComputedElementRef[]} target Element(s) to observe * @param {ResizeObserverCallback} callback Invoked with the observer entries * @param {UseResizeObserverOptions} [options={}] Options * @returns {UseResizeObserverReturn} `isSupported`, `isActive`, `pause`, `resume`, and `stop` * * @example * useResizeObserver(el, ([entry]) => { * console.log(entry.contentRect.width); * }); * * @example * const { pause, resume } = useResizeObserver([el1, el2], (entries) => { * // react to multiple targets * }, { box: 'border-box' }); * * @since 0.0.14 */ export function useResizeObserver( target: MaybeComputedElementRef | MaybeComputedElementRef[], callback: ResizeObserverCallback, options: UseResizeObserverOptions = {}, ): UseResizeObserverReturn { const { window = defaultWindow, box, immediate = true } = options; const isSupported = useSupported(() => window && 'ResizeObserver' in window); // Cache the observer options object so it is not rebuilt on every observe call const observerOptions: ResizeObserverOptions | undefined = box ? { box } : undefined; const isActive = ref(immediate); let observer: ResizeObserver | undefined; const targets = computed(() => { return toArray(target).map(el => unrefElement(el)).filter((el): el is Element => Boolean(el)); }); const cleanup = () => { if (observer) { observer.disconnect(); observer = undefined; } }; const stopWatch = watch( () => [targets.value, isActive.value] as const, ([els, active]) => { cleanup(); if (!active || !isSupported.value || !window || !els.length) return; observer = new ResizeObserver(callback); for (const el of els) observer.observe(el, observerOptions); }, { immediate: true, flush: 'post' }, ); const resume = () => { isActive.value = true; }; const pause = () => { cleanup(); isActive.value = false; }; const stop = () => { cleanup(); stopWatch(); }; tryOnScopeDispose(stop); return { isSupported, isActive: readonly(isActive), pause, resume, stop, }; }