@jhubbardsf/svelte-inview
Version:
A Svelte action that monitors an element enters or leaves the viewport or a parent element. Performant and efficient thanks to using Intersection Observer under the hood.
129 lines (111 loc) • 3.64 kB
text/typescript
import { tick } from 'svelte';
import type { ActionReturn } from 'svelte/action';
import type {
ObserverEventDetails,
Options,
Position,
ScrollDirection,
Event,
LifecycleEventDetails,
} from './types';
const defaultOptions: Options = {
root: null,
rootMargin: '0px',
threshold: 0,
unobserveOnEnter: false,
};
const createEvent = <T = ObserverEventDetails>(
name: Event,
detail: T
): CustomEvent<T> => new CustomEvent(name, { detail });
interface Attributes {
// Backwards compatibility
'on:inview_change'?: (e: CustomEvent<ObserverEventDetails>) => void;
'on:inview_enter'?: (e: CustomEvent<ObserverEventDetails>) => void;
'on:inview_leave'?: (e: CustomEvent<ObserverEventDetails>) => void;
'on:inview_init'?: (e: CustomEvent<LifecycleEventDetails>) => void;
// Svelte5 Syntax
oninview_change?: (e: CustomEvent<ObserverEventDetails>) => void;
oninview_enter?: (e: CustomEvent<ObserverEventDetails>) => void;
oninview_leave?: (e: CustomEvent<ObserverEventDetails>) => void;
oninview_init?: (e: CustomEvent<LifecycleEventDetails>) => void;
}
export function inview(
node: HTMLElement,
options: Options = {}
): ActionReturn<Options, Attributes> {
const { root, rootMargin, threshold, unobserveOnEnter }: Options = {
...defaultOptions,
...options,
};
let prevPos: Position = {
x: undefined,
y: undefined,
};
let scrollDirection: ScrollDirection = {
vertical: undefined,
horizontal: undefined,
};
if (typeof IntersectionObserver !== 'undefined' && node) {
const observer = new IntersectionObserver(
(entries, _observer) => {
entries.forEach((singleEntry) => {
if (prevPos.y > singleEntry.boundingClientRect.y) {
scrollDirection.vertical = 'up';
} else {
scrollDirection.vertical = 'down';
}
if (prevPos.x > singleEntry.boundingClientRect.x) {
scrollDirection.horizontal = 'left';
} else {
scrollDirection.horizontal = 'right';
}
prevPos = {
y: singleEntry.boundingClientRect.y,
x: singleEntry.boundingClientRect.x,
};
const detail: ObserverEventDetails = {
inView: singleEntry.isIntersecting,
entry: singleEntry,
scrollDirection,
node,
observer: _observer,
};
node.dispatchEvent(createEvent('inview_change', detail));
//@ts-expect-error only for backward compatibility
node.dispatchEvent(createEvent('change', detail));
if (singleEntry.isIntersecting) {
node.dispatchEvent(createEvent('inview_enter', detail));
//@ts-expect-error only for backward compatibility
node.dispatchEvent(createEvent('enter', detail));
unobserveOnEnter && _observer.unobserve(node);
} else {
node.dispatchEvent(createEvent('inview_leave', detail));
//@ts-expect-error only for backward compatibility
node.dispatchEvent(createEvent('leave', detail));
}
});
},
{
root,
rootMargin,
threshold,
}
);
tick().then(() => {
node.dispatchEvent(
createEvent<LifecycleEventDetails>('inview_init', { observer, node })
);
node.dispatchEvent(
//@ts-expect-error only for backward compatibility
createEvent<LifecycleEventDetails>('init', { observer, node })
);
});
observer.observe(node);
return {
destroy() {
observer.unobserve(node);
},
};
}
}