routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+176
@@ -0,0 +1,176 @@
|
||||
// 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, args = {}) {
|
||||
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
|
||||
Reference in New Issue
Block a user