UNPKG

react-scoll

Version:

infinite scroller, record scroll position, like twitter's timeline

191 lines (181 loc) 7.54 kB
import React, { createContext, useState, useRef, useEffect, forwardRef, useContext, useMemo } from 'react'; const ScrollContext = createContext({ items: [], setItems() { }, index: 0, setIndex() { }, scrollTop: 0, setScrollTop() { }, page: 1, setPage() { }, isControl: false, isComplete: false, setIsComplete() { }, loading: false, setLoading() { }, }); const packItems = (items = [], startIndex = 0) => { return items.map((item, i) => ({ index: startIndex + i, data: item })); }; const Provider = ({ children, source }) => { const [items, setItems] = useState([]); const [index, setIndex] = useState(0); const [page, setPage] = useState(1); const [scrollTop, setScrollTop] = useState(0); const [isControl, setIsControl] = useState(false); const [isComplete, setIsComplete] = useState(false); const [loading, setLoading] = useState(false); const lastItemsLength = useRef(0); useEffect(() => { if (!source) return; setItems(packItems(source)); setLoading(false); setIsControl(true); if (lastItemsLength.current === source.length) { setIsComplete(true); } else { lastItemsLength.current = source.length; setIsComplete(false); } }, [source]); return (React.createElement(ScrollContext.Provider, { value: { items, setItems, index, setIndex, scrollTop, setScrollTop, page, setPage, isControl, isComplete, setIsComplete, loading, setLoading, } }, React.Children.only(children))); }; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } const Scroller = (_a, outRef) => { var { averageHeight = 350, element: Element, length: customLength, style = {}, onFetch, upperRender } = _a, divProps = __rest(_a, ["averageHeight", "element", "length", "style", "onFetch", "upperRender"]); const [mounted, setMounted] = useState(false); const [length, setLength] = useState(0); const [topCount, setTopCount] = useState(0); const lastScrollTop = useRef(0); const contanierRef = useRef(); const upperRenderRef = useRef(null); const { items, setItems, index, setIndex, scrollTop, setScrollTop, page, setPage, isControl, isComplete, setIsComplete, loading, setLoading, } = useContext(ScrollContext); const canFetchRef = useRef(!items.length); const activeItems = useMemo(() => items.slice(index, index + length), [items, index, length]); const upperHolderHeight = useMemo(() => averageHeight * index, [ index, averageHeight, ]); const underHolderHight = useMemo(() => { const v = averageHeight * (items.length - index - length); return v >= 0 ? v : 0; }, [averageHeight, items.length, index, length]); // restore scroll position useEffect(() => { if (mounted) return; if (contanierRef.current) { contanierRef.current.scrollTop = scrollTop; } setMounted(true); }, [scrollTop, mounted]); // guess item count, visible count eg. useEffect(() => { if (customLength) { setLength(customLength); setTopCount(customLength * 0.2); return; } if (items.length) { const len = Math.floor(items.length * 0.7); setLength(len); setTopCount(Math.floor(len * 0.2)); } }, [customLength, items.length, page]); useEffect(() => { if (!canFetchRef.current) return; setLoading(true); const push = (data) => { setItems((state) => { const list = packItems(data, state.length); return page === 1 ? list : [...state, ...list]; }); if (!data.length) { setIsComplete(true); } setLoading(false); }; if (onFetch) { isControl ? onFetch() : onFetch({ page, push }); canFetchRef.current = false; } }, [isControl, onFetch, page, setIsComplete, setItems, setLoading]); useEffect(() => { if (!contanierRef.current) return; const cd = contanierRef.current; const onScroll = () => { if (!cd) return; const direction = cd.scrollTop - lastScrollTop.current > 0; if (direction && !loading && !isComplete && cd.scrollTop + 3000 > cd.scrollHeight - cd.clientHeight) { setPage((state) => state + 1); canFetchRef.current = true; } const upperRenderHeight = upperRenderRef.current ? upperRenderRef.current.clientHeight : 0; const startIndex = Math.floor((cd.scrollTop - topCount * averageHeight - upperRenderHeight) / averageHeight); setIndex(startIndex >= 0 ? startIndex : 0); lastScrollTop.current = cd.scrollTop; setScrollTop(cd.scrollTop); }; cd.addEventListener('scroll', onScroll); return () => cd.removeEventListener('scroll', onScroll); }, [averageHeight, loading, topCount, setIndex, setScrollTop, setPage, isComplete]); return (React.createElement("div", Object.assign({}, divProps, { ref: (n) => { contanierRef.current = n; outRef && (outRef.current = n); }, style: Object.assign({ height: '100vh', overflow: 'auto' }, style) }), React.createElement("div", { ref: upperRenderRef }, upperRender && upperRender()), React.createElement("div", { style: { height: upperHolderHeight } }), activeItems.map((item) => React.createElement(Element, Object.assign({ key: item.index }, item.data))), React.createElement("div", { style: { height: underHolderHight, } }))); }; var scroller = forwardRef(Scroller); export { Provider, scroller as Scroller };