Files
tools/vue/toolkit/src/composables/elements/useIntersectionObserver/index.ts
T
robonen 59e995d0b5 feat(vue): expand @robonen/vue composable collection
Composables, tests, category barrels, and README for @robonen/vue.
2026-06-08 15:51:16 +07:00

151 lines
4.1 KiB
TypeScript

import { computed, readonly, ref, toValue, watch } from 'vue';
import type { MaybeRefOrGetter, Ref } from 'vue';
import { noop, toArray } from '@robonen/stdlib';
import type { ConfigurableWindow } from '@/types';
import { defaultWindow } from '@/types';
import type { MaybeComputedElementRef, MaybeElement } from '@/composables/component/unrefElement';
import { unrefElement } from '@/composables/component/unrefElement';
import { useSupported } from '@/composables/utilities/useSupported';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
export interface UseIntersectionObserverOptions extends ConfigurableWindow {
/**
* The element or document used as the viewport for checking visibility
*/
root?: MaybeComputedElementRef | Document;
/**
* Margin around the root. Reactive — pass a ref or getter to update it.
*
* @default '0px'
*/
rootMargin?: MaybeRefOrGetter<string>;
/**
* Threshold(s) at which to trigger the callback. Reactive — pass a ref or
* getter to update it.
*
* @default 0
*/
threshold?: MaybeRefOrGetter<number | number[]>;
/**
* Start observing immediately
*
* @default true
*/
immediate?: boolean;
}
export interface UseIntersectionObserverReturn {
isSupported: Readonly<Ref<boolean>>;
isActive: Readonly<Ref<boolean>>;
pause: () => void;
resume: () => void;
stop: () => void;
}
/**
* @name useIntersectionObserver
* @category Elements
* @description Detect when an element enters or leaves the viewport via
* `IntersectionObserver`. Accepts a single target, an array of targets, or a
* ref/getter resolving to either, plus reactive `rootMargin` and `threshold`.
*
* @param {MaybeComputedElementRef | MaybeComputedElementRef[] | MaybeRefOrGetter<MaybeElement[]>} target Element(s) to observe
* @param {IntersectionObserverCallback} callback Invoked with the observer entries
* @param {UseIntersectionObserverOptions} [options={}] Options
* @returns {UseIntersectionObserverReturn} Observer controls
*
* @example
* useIntersectionObserver(el, ([{ isIntersecting }]) => {
* visible.value = isIntersecting;
* });
*
* @since 0.0.15
*/
export function useIntersectionObserver(
target: MaybeComputedElementRef | MaybeComputedElementRef[] | MaybeRefOrGetter<MaybeElement[]>,
callback: IntersectionObserverCallback,
options: UseIntersectionObserverOptions = {},
): UseIntersectionObserverReturn {
const {
root,
rootMargin = '0px',
threshold = 0,
window = defaultWindow,
immediate = true,
} = options;
const isSupported = useSupported(() => window && 'IntersectionObserver' in window);
const targets = computed(() => {
const value = toValue(target) as MaybeElement | MaybeElement[];
return toArray(value as MaybeElement)
.map(el => unrefElement(el))
.filter((el): el is Element => Boolean(el));
});
const isActive = ref(immediate);
let cleanup = noop;
const stopWatch = isSupported.value
? watch(
() => [
targets.value,
unrefElement(root as MaybeComputedElementRef),
toValue(rootMargin),
toValue(threshold),
isActive.value,
] as const,
([els, rootEl, margin, thresh, active]) => {
cleanup();
if (!active || !els.length)
return;
const observer = new IntersectionObserver(callback, {
root: (rootEl as Element | null) ?? (root as Document | undefined),
rootMargin: margin,
threshold: thresh,
});
for (const el of els)
observer.observe(el);
cleanup = () => {
observer.disconnect();
cleanup = noop;
};
},
{ immediate: true, flush: 'post' },
)
: noop;
const resume = (): void => {
isActive.value = true;
};
const pause = (): void => {
cleanup();
isActive.value = false;
};
const stop = (): void => {
cleanup();
stopWatch();
isActive.value = false;
};
tryOnScopeDispose(stop);
return {
isSupported,
isActive: readonly(isActive),
pause,
resume,
stop,
};
}