UNPKG

instantsearch-ui-components

Version:

Common UI components for InstantSearch.

481 lines (470 loc) 19.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createStickToBottom = createStickToBottom; var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /* !--------------------------------------------------------------------------------------------- * Copyright (c) StackBlitz. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var DEFAULT_SPRING_ANIMATION = { /** * A value from 0 to 1, on how much to damp the animation. * 0 means no damping, 1 means full damping. * * @default 0.7 */ damping: 0.7, /** * The stiffness of how fast/slow the animation gets up to speed. * * @default 0.05 */ stiffness: 0.05, /** * The inertial mass associated with the animation. * Higher numbers make the animation slower. * * @default 1.25 */ mass: 1.25 }; var STICK_TO_BOTTOM_OFFSET_PX = 70; var SIXTY_FPS_INTERVAL_MS = 1000 / 60; var RETAIN_ANIMATION_DURATION_MS = 350; var mouseDown = false; if (typeof window !== 'undefined') { var _window$document, _window$document2, _window$document3; (_window$document = window.document) === null || _window$document === void 0 ? void 0 : _window$document.addEventListener('mousedown', function () { mouseDown = true; }); (_window$document2 = window.document) === null || _window$document2 === void 0 ? void 0 : _window$document2.addEventListener('mouseup', function () { mouseDown = false; }); (_window$document3 = window.document) === null || _window$document3 === void 0 ? void 0 : _window$document3.addEventListener('click', function () { mouseDown = false; }); } function createStickToBottom(_ref) { var useCallback = _ref.useCallback, useEffect = _ref.useEffect, useMemo = _ref.useMemo, useRef = _ref.useRef, useState = _ref.useState; return function useStickToBottom() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var _useState = useState(false), _useState2 = (0, _slicedToArray2.default)(_useState, 2), escapedFromLock = _useState2[0], updateEscapedFromLock = _useState2[1]; var _useState3 = useState(options.initial !== false), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), isAtBottom = _useState4[0], updateIsAtBottom = _useState4[1]; var _useState5 = useState(false), _useState6 = (0, _slicedToArray2.default)(_useState5, 2), isNearBottom = _useState6[0], setIsNearBottom = _useState6[1]; var optionsRef = useRef(null); optionsRef.current = options; // Create refs early so they can be used in other hooks var scrollRef = useRef(null); var contentRef = useRef(null); var isSelecting = useCallback(function () { var _scrollRef$current; if (!mouseDown) { return false; } if (typeof window === 'undefined') { return false; } var selection = window.getSelection(); if (!selection || !selection.rangeCount) { return false; } var range = selection.getRangeAt(0); return range.commonAncestorContainer.contains(scrollRef.current) || ((_scrollRef$current = scrollRef.current) === null || _scrollRef$current === void 0 ? void 0 : _scrollRef$current.contains(range.commonAncestorContainer)); }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: state is intentionally stable var state = useMemo(function () { var lastCalculation; return { escapedFromLock: escapedFromLock, isAtBottom: isAtBottom, resizeDifference: 0, accumulated: 0, velocity: 0, get scrollTop() { var _scrollRef$current$sc, _scrollRef$current2; return (_scrollRef$current$sc = (_scrollRef$current2 = scrollRef.current) === null || _scrollRef$current2 === void 0 ? void 0 : _scrollRef$current2.scrollTop) !== null && _scrollRef$current$sc !== void 0 ? _scrollRef$current$sc : 0; }, set scrollTop(scrollTop) { if (scrollRef.current) { scrollRef.current.scrollTop = scrollTop; state.ignoreScrollToTop = scrollRef.current.scrollTop; } }, get targetScrollTop() { if (!scrollRef.current || !contentRef.current) { return 0; } return scrollRef.current.scrollHeight - 1 - scrollRef.current.clientHeight; }, get calculatedTargetScrollTop() { var _lastCalculation; if (!scrollRef.current || !contentRef.current) { return 0; } var targetScrollTop = this.targetScrollTop; if (!optionsRef.current.targetScrollTop) { return targetScrollTop; } if (((_lastCalculation = lastCalculation) === null || _lastCalculation === void 0 ? void 0 : _lastCalculation.targetScrollTop) === targetScrollTop) { return lastCalculation.calculatedScrollTop; } var calculatedScrollTop = Math.max(Math.min(optionsRef.current.targetScrollTop(targetScrollTop, { scrollElement: scrollRef.current, contentElement: contentRef.current }), targetScrollTop), 0); lastCalculation = { targetScrollTop: targetScrollTop, calculatedScrollTop: calculatedScrollTop }; requestAnimationFrame(function () { lastCalculation = undefined; }); return calculatedScrollTop; }, get scrollDifference() { return this.calculatedTargetScrollTop - this.scrollTop; }, get isNearBottom() { return this.scrollDifference <= STICK_TO_BOTTOM_OFFSET_PX; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); var setIsAtBottom = useCallback(function (value) { state.isAtBottom = value; updateIsAtBottom(value); }, [state]); var setEscapedFromLock = useCallback(function (value) { state.escapedFromLock = value; updateEscapedFromLock(value); }, [state]); var scrollToBottom = useCallback(function () { var _state$animation2; var scrollOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (typeof scrollOptions === 'string') { scrollOptions = { animation: scrollOptions }; } if (!scrollOptions.preserveScrollPosition) { setIsAtBottom(true); } var waitElapsed = Date.now() + (Number(scrollOptions.wait) || 0); var behavior = mergeAnimations(optionsRef.current, scrollOptions.animation); var _scrollOptions = scrollOptions, _scrollOptions$ignore = _scrollOptions.ignoreEscapes, ignoreEscapes = _scrollOptions$ignore === void 0 ? false : _scrollOptions$ignore; var durationElapsed; var startTarget = state.calculatedTargetScrollTop; if (scrollOptions.duration instanceof Promise) { scrollOptions.duration.then(function () { durationElapsed = Date.now(); }, function () { durationElapsed = Date.now(); }); } else { var _scrollOptions$durati; durationElapsed = waitElapsed + ((_scrollOptions$durati = scrollOptions.duration) !== null && _scrollOptions$durati !== void 0 ? _scrollOptions$durati : 0); } var next = function next() { var promise = new Promise(requestAnimationFrame).then(function () { var _state$lastTick; if (!state.isAtBottom) { state.animation = undefined; return false; } var scrollTop = state.scrollTop; var tick = performance.now(); var tickDelta = (tick - ((_state$lastTick = state.lastTick) !== null && _state$lastTick !== void 0 ? _state$lastTick : tick)) / SIXTY_FPS_INTERVAL_MS; state.animation || (state.animation = { behavior: behavior, promise: promise, ignoreEscapes: ignoreEscapes }); if (state.animation.behavior === behavior) { state.lastTick = tick; } if (isSelecting()) { return next(); } if (waitElapsed > Date.now()) { return next(); } if (scrollTop < Math.min(startTarget, state.calculatedTargetScrollTop)) { var _state$animation; if (((_state$animation = state.animation) === null || _state$animation === void 0 ? void 0 : _state$animation.behavior) === behavior) { if (behavior === 'instant') { state.scrollTop = state.calculatedTargetScrollTop; return next(); } state.velocity = (behavior.damping * state.velocity + behavior.stiffness * state.scrollDifference) / behavior.mass; state.accumulated += state.velocity * tickDelta; state.scrollTop += state.accumulated; if (state.scrollTop !== scrollTop) { state.accumulated = 0; } } return next(); } if (durationElapsed > Date.now()) { startTarget = state.calculatedTargetScrollTop; return next(); } state.animation = undefined; /** * If we're still below the target, then queue * up another scroll to the bottom with the last * requested animatino. */ if (state.scrollTop < state.calculatedTargetScrollTop) { return scrollToBottom({ animation: mergeAnimations(optionsRef.current, optionsRef.current.resize), ignoreEscapes: ignoreEscapes, duration: Math.max(0, durationElapsed - Date.now()) || undefined }); } return state.isAtBottom; }); return promise.then(function (result) { requestAnimationFrame(function () { if (!state.animation) { state.lastTick = undefined; state.velocity = 0; } }); return result; }); }; if (scrollOptions.wait !== true) { state.animation = undefined; } if (((_state$animation2 = state.animation) === null || _state$animation2 === void 0 ? void 0 : _state$animation2.behavior) === behavior) { return state.animation.promise; } return next(); }, [setIsAtBottom, isSelecting, state]); var stopScroll = useCallback(function () { setEscapedFromLock(true); setIsAtBottom(false); }, [setEscapedFromLock, setIsAtBottom]); var handleScroll = useCallback(function (_ref2) { var target = _ref2.target; if (target !== scrollRef.current) { return; } var scrollTop = state.scrollTop, ignoreScrollToTop = state.ignoreScrollToTop; var _state$lastScrollTop = state.lastScrollTop, lastScrollTop = _state$lastScrollTop === void 0 ? scrollTop : _state$lastScrollTop; state.lastScrollTop = scrollTop; state.ignoreScrollToTop = undefined; if (ignoreScrollToTop && ignoreScrollToTop > scrollTop) { /** * When the user scrolls up while the animation plays, the `scrollTop` may * not come in separate events; if this happens, to make sure `isScrollingUp` * is correct, set the lastScrollTop to the ignored event. */ lastScrollTop = ignoreScrollToTop; } setIsNearBottom(state.isNearBottom); /** * Scroll events may come before a ResizeObserver event, * so in order to ignore resize events correctly we use a * timeout. * * @see https://github.com/WICG/resize-observer/issues/25#issuecomment-248757228 */ setTimeout(function () { var _state$animation3; /** * When theres a resize difference ignore the resize event. */ if (state.resizeDifference || scrollTop === ignoreScrollToTop) { return; } if (isSelecting()) { setEscapedFromLock(true); setIsAtBottom(false); return; } var isScrollingDown = scrollTop > lastScrollTop; var isScrollingUp = scrollTop < lastScrollTop; if ((_state$animation3 = state.animation) !== null && _state$animation3 !== void 0 && _state$animation3.ignoreEscapes) { state.scrollTop = lastScrollTop; return; } if (isScrollingUp) { setEscapedFromLock(true); setIsAtBottom(false); } if (isScrollingDown) { setEscapedFromLock(false); } if (!state.escapedFromLock && state.isNearBottom) { setIsAtBottom(true); } }, 1); }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]); var handleWheel = useCallback(function (_ref3) { var _state$animation4; var target = _ref3.target, deltaY = _ref3.deltaY; var element = target; while (!['scroll', 'auto'].includes(getComputedStyle(element).overflow)) { if (!element.parentElement) { return; } element = element.parentElement; } /** * The browser may cancel the scrolling from the mouse wheel * if we update it from the animation in meantime. * To prevent this, always escape when the wheel is scrolled up. */ if (element === scrollRef.current && deltaY < 0 && scrollRef.current.scrollHeight > scrollRef.current.clientHeight && !((_state$animation4 = state.animation) !== null && _state$animation4 !== void 0 && _state$animation4.ignoreEscapes)) { setEscapedFromLock(true); setIsAtBottom(false); } }, [setEscapedFromLock, setIsAtBottom, state]); // Attach scroll and wheel event listeners useEffect(function () { var scroll = scrollRef.current; if (!scroll) { return undefined; } scroll.addEventListener('scroll', handleScroll, { passive: true }); scroll.addEventListener('wheel', handleWheel, { passive: true }); return function () { scroll.removeEventListener('scroll', handleScroll); scroll.removeEventListener('wheel', handleWheel); }; }, [handleScroll, handleWheel]); // Attach ResizeObserver to content element useEffect(function () { var content = contentRef.current; if (!content) { return undefined; } var previousHeight; var resizeObserver = new ResizeObserver(function (_ref4) { var _previousHeight; var _ref5 = (0, _slicedToArray2.default)(_ref4, 1), entry = _ref5[0]; var height = entry.contentRect.height; var difference = height - ((_previousHeight = previousHeight) !== null && _previousHeight !== void 0 ? _previousHeight : height); state.resizeDifference = difference; /** * Sometimes the browser can overscroll past the target, * so check for this and adjust appropriately. */ if (state.scrollTop > state.targetScrollTop) { state.scrollTop = state.targetScrollTop; } setIsNearBottom(state.isNearBottom); if (difference >= 0) { /** * If it's a positive resize, scroll to the bottom when * we're already at the bottom. */ var animation = mergeAnimations(optionsRef.current, previousHeight ? optionsRef.current.resize : optionsRef.current.initial); scrollToBottom({ animation: animation, wait: true, preserveScrollPosition: true, duration: animation === 'instant' ? undefined : RETAIN_ANIMATION_DURATION_MS }); } else if (state.isNearBottom) { /** * Else if it's a negative resize, check if we're near the bottom * if we are want to un-escape from the lock, because the resize * could have caused the container to be at the bottom. */ setEscapedFromLock(false); setIsAtBottom(true); } previousHeight = height; /** * Reset the resize difference after the scroll event * has fired. Requires a rAF to wait for the scroll event, * and a setTimeout to wait for the other timeout we have in * resizeObserver in case the scroll event happens after the * resize event. */ requestAnimationFrame(function () { setTimeout(function () { if (state.resizeDifference === difference) { state.resizeDifference = 0; } }, 1); }); }); resizeObserver.observe(content); state.resizeObserver = resizeObserver; return function () { resizeObserver.disconnect(); state.resizeObserver = undefined; }; }, [state, setIsNearBottom, setEscapedFromLock, setIsAtBottom, scrollToBottom]); return { contentRef: contentRef, scrollRef: scrollRef, scrollToBottom: scrollToBottom, stopScroll: stopScroll, isAtBottom: isAtBottom || isNearBottom, isNearBottom: isNearBottom, escapedFromLock: escapedFromLock, state: state }; }; } var animationCache = new Map(); function mergeAnimations() { var result = _objectSpread({}, DEFAULT_SPRING_ANIMATION); var instant = false; for (var _len = arguments.length, animations = new Array(_len), _key = 0; _key < _len; _key++) { animations[_key] = arguments[_key]; } animations.forEach(function (animation) { var _animation$damping, _animation$stiffness, _animation$mass; if (animation === 'instant') { instant = true; return; } if ((0, _typeof2.default)(animation) !== 'object') { return; } instant = false; result.damping = (_animation$damping = animation.damping) !== null && _animation$damping !== void 0 ? _animation$damping : result.damping; result.stiffness = (_animation$stiffness = animation.stiffness) !== null && _animation$stiffness !== void 0 ? _animation$stiffness : result.stiffness; result.mass = (_animation$mass = animation.mass) !== null && _animation$mass !== void 0 ? _animation$mass : result.mass; }); var key = JSON.stringify(result); if (!animationCache.has(key)) { animationCache.set(key, Object.freeze(result)); } return instant ? 'instant' : animationCache.get(key); }