@oruga-ui/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
147 lines (126 loc) • 5.01 kB
text/typescript
import { getCurrentScope, type MaybeRefOrGetter } from "vue";
import { isDefined } from "@/utils/helpers";
import { isClient } from "@/utils/ssr";
import { unrefElement } from "./unrefElement";
import { useDebounce } from "./useDebounce";
import { useEventListener, type EventTarget } from "./useEventListener";
/** Call a function when the scoll reaches the end or the start of an element.
* This is useful for infinite scroll lists.
* @param element - The element to listen for scroll events.
* @param options - Options for the infinite scroll.
* @param options.onScroll - Function to call on every scroll event.
* @param options.onEnd - Function to call when the scroll reaches the end.
* @param options.onStart - Function to call when the scroll reaches the start.
* @param options.debounce - Debounce time in milliseconds for the function call on scroll events.
* @returns A function to call to to manually check the scroll position.
*/
export function useScrollEvents(
element: MaybeRefOrGetter<EventTarget>,
options: {
onScroll?: () => void;
onScrollEnd?: () => void;
onScrollStart?: () => void;
debounce?: number;
},
): () => void {
if (!getCurrentScope())
throw new Error(
"The 'useScrollEvents' composable should be used inside a current EffectScope.",
);
const debouncedCheckScroll = useDebounce(
checkScroll,
options.debounce ?? 100,
);
if (isClient)
useEventListener(element, "scroll", debouncedCheckScroll, {
passive: true,
});
/** Check if the scroll list inside the dropdown reached the top or it's end. */
function checkScroll(): void {
const el = unrefElement(element);
if (!el) return;
if (options.onScroll) options.onScroll();
const trashhold = el.offsetTop;
if (el.clientHeight !== el.scrollHeight) {
if (
Math.ceil(el.scrollTop + el.clientHeight + trashhold) >=
el.scrollHeight
) {
if (options.onScrollEnd) options.onScrollEnd();
} else if (el.scrollTop <= trashhold) {
if (options.onScrollStart) options.onScrollStart();
}
}
}
return debouncedCheckScroll;
}
/**
* Given an element, returns the element who scrolls it.
*/
export function getScrollingParent(target: HTMLElement): HTMLElement | null {
if (target.style.position === "fixed" || !target)
return document.documentElement;
let isScrollingParent = false;
let nextParent = target.parentElement;
while (!isScrollingParent && isDefined(nextParent)) {
if (nextParent === document.documentElement) break;
const { overflow, overflowY } = getComputedStyle(nextParent);
const { scrollHeight, clientHeight } = nextParent; // Both rounded by nature
isScrollingParent =
/(auto|scroll)/.test(`${overflow}${overflowY}`) &&
scrollHeight > clientHeight;
/* ...found it, this one is returned */
if (isScrollingParent) break;
/* ...if not check the next one */
nextParent = nextParent.parentElement;
}
return nextParent;
}
/**
* Check if an element is visible in the browser viewport.
* @deprecated currently unused - can be deleted
*/
export function isElementInView(
elementRef: MaybeRefOrGetter<HTMLElement>,
): boolean {
const element = unrefElement(elementRef);
const bounding = element.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <=
(window.innerWidth || document.documentElement.clientWidth)
);
}
/**
* Check if an element is currently scrollable by comparing its clientHeight and scrollHeight.
* @deprecated currently unused - can be deleted
*/
export function isScrollable(
elementRef: MaybeRefOrGetter<HTMLElement>,
): boolean {
const element = unrefElement(elementRef);
return element && element.clientHeight < element.scrollHeight;
}
/**
* Ensure a given child element is within the parent's visible scroll area.
* If the child is not visible, scroll the parent to the child's position.
*/
export function scrollElementInView(
scrollableParent: MaybeRefOrGetter<HTMLElement>,
childElement: MaybeRefOrGetter<HTMLElement>,
): void {
const parent = unrefElement(scrollableParent);
const element = unrefElement(childElement);
const { offsetHeight, offsetTop } = element;
const { offsetHeight: parentOffsetHeight, scrollTop } = parent;
const isAbove = offsetTop < scrollTop;
const isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;
if (isAbove) {
parent.scrollTo(0, offsetTop);
} else if (isBelow) {
parent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight);
}
}