react-scoll
Version:
infinite scroller, record scroll position, like twitter's timeline
191 lines (181 loc) • 7.54 kB
JavaScript
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 };