150 lines
3.9 KiB
TypeScript
150 lines
3.9 KiB
TypeScript
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<Ref<boolean>>;
|
|
|
|
/**
|
|
* Whether the observer is currently active
|
|
*/
|
|
isActive: Readonly<Ref<boolean>>;
|
|
|
|
/**
|
|
* 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,
|
|
};
|
|
}
|