import { noop, safeOnDestroy, clamp } from '../../internal/helpers/index.js'; export function debounceCallback(cb, delay) { let debounceTimer = 0; safeOnDestroy(() => { clearTimeout(debounceTimer); }); return () => { window.clearTimeout(debounceTimer); debounceTimer = window.setTimeout(cb, delay); }; } export function resizeObserver(node, handleResize) { let animationFrame = 0; const observer = new ResizeObserver(() => { cancelAnimationFrame(animationFrame); animationFrame = requestAnimationFrame(handleResize); }); observer.observe(node); return () => { window.cancelAnimationFrame(animationFrame); observer.unobserve(node); }; } // Custom scroll handler to avoid scroll-linked effects // https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Scroll-linked_effects export function addUnlinkedScrollListener(node, handler = noop) { let prevPosition = { left: node.scrollLeft, top: node.scrollTop }; let rAF = 0; (function loop() { const position = { left: node.scrollLeft, top: node.scrollTop }; const isHorizontalScroll = prevPosition.left !== position.left; const isVerticalScroll = prevPosition.top !== position.top; if (isHorizontalScroll || isVerticalScroll) handler(); prevPosition = position; rAF = window.requestAnimationFrame(loop); })(); return () => window.cancelAnimationFrame(rAF); } export function isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos) { return scrollPos > 0 && scrollPos < maxScrollPos; } // https://github.com/tmcw-up-for-adoption/simple-linear-scale/blob/master/index.js function linearScale(input, output) { return (value) => { if (input[0] === input[1] || output[0] === output[1]) return output[0]; const ratio = (output[1] - output[0]) / (input[1] - input[0]); return output[0] + ratio * (value - input[0]); }; } export function toInt(value) { return value ? parseInt(value, 10) : 0; } export function getThumbRatio(viewportSize, contentSize) { const ratio = viewportSize / contentSize; return isNaN(ratio) ? 0 : ratio; } export function getThumbSize(sizes) { const ratio = getThumbRatio(sizes.viewport, sizes.content); const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd; const thumbSize = (sizes.scrollbar.size - scrollbarPadding) * ratio; // minimum of 18 matches macOS minimum return Math.max(thumbSize, 18); } export function getScrollPositionFromPointer(pointerPos, pointerOffset, sizes, dir = 'ltr') { const thumbSizePx = getThumbSize(sizes); const thumbCenter = thumbSizePx / 2; const offset = pointerOffset || thumbCenter; const thumbOffsetFromEnd = thumbSizePx - offset; const minPointerPos = sizes.scrollbar.paddingStart + offset; const maxPointerPos = sizes.scrollbar.size - sizes.scrollbar.paddingEnd - thumbOffsetFromEnd; const maxScrollPos = sizes.content - sizes.viewport; const scrollRange = dir === 'ltr' ? [0, maxScrollPos] : [maxScrollPos * -1, 0]; const interpolate = linearScale([minPointerPos, maxPointerPos], scrollRange); return interpolate(pointerPos); } export function getThumbOffsetFromScroll(scrollPos, sizes, dir = 'ltr') { const thumbSizePx = getThumbSize(sizes); const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd; const scrollbar = sizes.scrollbar.size - scrollbarPadding; const maxScrollPos = sizes.content - sizes.viewport; const maxThumbPos = scrollbar - thumbSizePx; const [scrollClampMin, scrollClampMax] = dir === 'ltr' ? [0, maxScrollPos] : [maxScrollPos * -1, 0]; const scrollWithoutMomentum = clamp(scrollClampMin, scrollPos, scrollClampMax); const interpolate = linearScale([0, maxScrollPos], [0, maxThumbPos]); return interpolate(scrollWithoutMomentum); }