UNPKG

vuetify

Version:

Vue Material Component Framework

177 lines (167 loc) 6.33 kB
// Utilities import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { clamp, consoleWarn, propsFactory } from "../util/index.js"; // Types // Composables export const makeScrollProps = propsFactory({ scrollTarget: { type: String }, scrollThreshold: { type: [String, Number], default: 300 } }, 'scroll'); export function useScroll(props) { let args = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; const { canScroll, layoutSize } = args; let previousScroll = 0; let previousScrollHeight = 0; const target = ref(null); const currentScroll = shallowRef(0); const savedScroll = shallowRef(0); const currentThreshold = shallowRef(0); const isScrollActive = shallowRef(false); const isScrollingUp = shallowRef(false); const isAtBottom = shallowRef(false); const reachedBottomWhileScrollingDown = shallowRef(false); const hasEnoughScrollableSpace = shallowRef(true); const scrollThreshold = computed(() => { return Number(props.scrollThreshold); }); /** * 1: at top * 0: at threshold */ const scrollRatio = computed(() => { return clamp((scrollThreshold.value - currentScroll.value) / scrollThreshold.value || 0); }); function getScrollMetrics(targetEl) { const clientHeight = 'window' in targetEl ? window.innerHeight : targetEl.clientHeight; const scrollHeight = 'window' in targetEl ? document.documentElement.scrollHeight : targetEl.scrollHeight; return { clientHeight, scrollHeight }; } function checkScrollableSpace() { const targetEl = target.value; if (!targetEl) return; const { clientHeight, scrollHeight } = getScrollMetrics(targetEl); const maxScrollableDistance = scrollHeight - clientHeight; // When the scroll-hide element (like AppBar) hides, it causes the page to grow // We need extra scrollable space beyond the threshold to prevent bouncing // Add the element's height to the required minimum distance const elementHeight = layoutSize?.value || 0; const minRequiredDistance = scrollThreshold.value + elementHeight; // Only enable scroll-hide if there's enough scrollable space hasEnoughScrollableSpace.value = maxScrollableDistance > minRequiredDistance; } function onResize() { checkScrollableSpace(); } function onScroll() { const targetEl = target.value; if (!targetEl || canScroll && !canScroll.value) return; previousScroll = currentScroll.value; currentScroll.value = 'window' in targetEl ? targetEl.pageYOffset : targetEl.scrollTop; const currentScrollHeight = targetEl instanceof Window ? document.documentElement.scrollHeight : targetEl.scrollHeight; if (previousScrollHeight !== currentScrollHeight) { // If page is growing (content loading), recalculate scrollable space // If page is shrinking (likely due to navbar animation), don't recalculate if (currentScrollHeight > previousScrollHeight) { checkScrollableSpace(); } previousScrollHeight = currentScrollHeight; } isScrollingUp.value = currentScroll.value < previousScroll; currentThreshold.value = Math.abs(currentScroll.value - scrollThreshold.value); // Detect if at bottom of page const { clientHeight, scrollHeight } = getScrollMetrics(targetEl); const atBottom = currentScroll.value + clientHeight >= scrollHeight - 5; // Track when bottom is reached during downward scroll // Only set flag if ALL conditions are met: // 1. Scrolled past threshold (navbar is hiding) // 2. Page has enough scrollable space for scroll-hide // This prevents activation on short pages or edge cases if (!isScrollingUp.value && atBottom && currentScroll.value >= scrollThreshold.value && hasEnoughScrollableSpace.value) { reachedBottomWhileScrollingDown.value = true; } // Reset the flag when: // 1. Scrolling up away from bottom (with small tolerance for touchpad/momentum scrolling) // 2. Scroll position jumped significantly (e.g., navigation, scroll restoration) // 3. Scroll is at the very top (page navigation resets to top) const scrollJumped = Math.abs(currentScroll.value - previousScroll) > 100; const atTop = currentScroll.value <= 5; const scrolledUpSignificantly = isScrollingUp.value && previousScroll - currentScroll.value > 1; if (scrolledUpSignificantly && !atBottom || scrollJumped && currentScroll.value < scrollThreshold.value || atTop) { reachedBottomWhileScrollingDown.value = false; } // Update state isAtBottom.value = atBottom; } watch(isScrollingUp, () => { savedScroll.value = savedScroll.value || currentScroll.value; }); watch(isScrollActive, () => { savedScroll.value = 0; }); onMounted(() => { watch(() => props.scrollTarget, scrollTarget => { const newTarget = scrollTarget ? document.querySelector(scrollTarget) : window; if (!newTarget) { consoleWarn(`Unable to locate element with identifier ${scrollTarget}`); return; } if (newTarget === target.value) return; target.value?.removeEventListener('scroll', onScroll); target.value = newTarget; target.value.addEventListener('scroll', onScroll, { passive: true }); // Check scrollable space when target is set Promise.resolve().then(() => { checkScrollableSpace(); }); }, { immediate: true }); // Listen to window resize to recalculate scrollable space window.addEventListener('resize', onResize, { passive: true }); }); onBeforeUnmount(() => { target.value?.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onResize); }); // Do we need this? If yes - seems that // there's no need to expose onScroll canScroll && watch(canScroll, onScroll, { immediate: true }); return { scrollThreshold, currentScroll, currentThreshold, isScrollActive, scrollRatio, // required only for testing // probably can be removed // later (2 chars chlng) isScrollingUp, savedScroll, isAtBottom, reachedBottomWhileScrollingDown, hasEnoughScrollableSpace }; } //# sourceMappingURL=scroll.js.map