From 6a2d28b2971a6015b8dee0bbb283b4336ae04b8d Mon Sep 17 00:00:00 2001 From: Vladimir Kruzhkov Date: Mon, 15 Jul 2024 00:36:03 +0300 Subject: [PATCH] updated naming, added comonents for vertical/horizontal lists and grid --- build_examples/demo.js | 265 ++++++++++++------ package.json | 4 +- readme.md | 403 +++++++++++++++++++++++++-- src/examples/demo.tsx | 71 +++-- src/fixed-grid.tsx | 42 +++ src/fixed-list-x.tsx | 38 +++ src/{simple.tsx => fixed-list-y.tsx} | 16 +- src/index.tsx | 156 ++++++++--- 8 files changed, 828 insertions(+), 167 deletions(-) create mode 100644 src/fixed-grid.tsx create mode 100644 src/fixed-list-x.tsx rename src/{simple.tsx => fixed-list-y.tsx} (65%) diff --git a/build_examples/demo.js b/build_examples/demo.js index 5cc3338..9b7e030 100644 --- a/build_examples/demo.js +++ b/build_examples/demo.js @@ -32,51 +32,116 @@ const react_1 = __importStar(require("react")); const client_1 = __importDefault(require("react-dom/client")); const __1 = require(".."); const utils_1 = require("../utils"); -const simple_1 = require("../simple"); -const Item = ({ item }) => { - return (0, jsx_runtime_1.jsx)("div", { style: { height: '40px' }, children: item }); -}; -function ListWithHook({ items }) { +const fixed_list_y_1 = require("../fixed-list-y"); +const fixed_grid_1 = require("../fixed-grid"); +const itemsLine = Array.from({ length: 300 }).map((_, i) => `item ${i}`); +const itemsGrid = Array.from({ length: 300 }).map((_, iy) => Array.from({ length: 300 }).map((_, ix) => `item ${ix} ${iy}`)); +function ListWithHookExample() { + const items = itemsLine; const containerRef = (0, react_1.useRef)(undefined); const itemHeight = 40; - const rendered = (0, __1.useVirtualOverflowV)({ + const { renderedItems, updateViewRect } = (0, __1.useVirtualOverflowY)({ containerRef, itemHeight, - itemsLength: items.length, + itemsLengthY: items.length, overscanItemsCount: 3, calcVisibleRect: utils_1.virtualOverflowUtils.calcVisibleRectOverflowed, - renderItem: (itemIndex, offsetTop, item = items[itemIndex]) => ((0, jsx_runtime_1.jsx)("div", { style: { position: 'absolute', top: `${offsetTop}px` }, children: (0, jsx_runtime_1.jsx)(Item, { item: item }) }, item)), + renderItem: (itemIndex, offsetTop, item = items[itemIndex]) => ((0, jsx_runtime_1.jsx)("div", { style: { position: 'absolute', top: `${offsetTop}px` }, children: (0, jsx_runtime_1.jsx)("div", { style: { height: '40px' }, children: item }) }, item)), }); - return ((0, jsx_runtime_1.jsx)("div", { style: { overflowY: 'scroll', height: '300px', background: 'lightgreen' }, children: (0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: { position: 'relative', height: `${itemHeight * items.length}px` }, children: rendered }) })); + (0, react_1.useEffect)(() => { + setInterval(() => updateViewRect(), 8); + }, []); + return ((0, jsx_runtime_1.jsx)("div", { style: { overflowY: 'scroll', height: '300px', background: 'lightgreen' }, children: (0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: { position: 'relative', height: `${itemHeight * items.length}px` }, children: renderedItems }) })); +} +function VerticalListExample() { + const items = itemsLine; + return ((0, jsx_runtime_1.jsx)("div", { style: { overflowY: 'scroll', height: '300px', background: 'lightgreen' }, children: (0, jsx_runtime_1.jsx)(fixed_list_y_1.VirtualListY, { items: items, itemHeight: 40, itemKey: x => x, renderItem: item => (0, jsx_runtime_1.jsx)("div", { style: { height: '40px' }, children: item }) }) })); } -function ListSimple(props) { - const items = props.items; - return ((0, jsx_runtime_1.jsx)("div", { style: { overflowY: 'scroll', height: '300px', background: 'lightgreen' }, children: (0, jsx_runtime_1.jsx)(simple_1.SimpleVirtualListV, { items: items, itemHeight: 40, itemKey: x => x, renderItem: item => (0, jsx_runtime_1.jsx)("div", { style: { height: '40px' }, children: item }) }) })); +function GridExample() { + const items = itemsGrid; + return ((0, jsx_runtime_1.jsx)("div", { style: { overflowY: 'scroll', height: '300px', background: 'lightgreen' }, children: (0, jsx_runtime_1.jsx)(fixed_grid_1.VirtualGrid, { items: items, columnsNum: 300, itemWidth: 40, itemHeight: 80, itemKey: x => x, overscanItemsCount: 3, renderItem: item => (0, jsx_runtime_1.jsx)("div", { style: { width: '40px', height: '80px' }, children: item }) }) })); } -const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`); function App() { - const [, forceUpd] = react_1.default.useState(0); - const r = (0, react_1.useRef)(undefined); - return ((0, jsx_runtime_1.jsxs)("div", { ref: r, children: [(0, jsx_runtime_1.jsx)("div", { style: { height: '700px' } }), (0, jsx_runtime_1.jsxs)("div", { style: { overflowY: 'scroll', height: '600px', background: 'blue' }, children: [(0, jsx_runtime_1.jsx)("div", { style: { height: '700px' } }), (0, jsx_runtime_1.jsx)(ListSimple, { items: items })] }), (0, jsx_runtime_1.jsx)("div", { style: { height: '700px' } })] })); + return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { style: { + height: '700px', fontSize: '60px', + color: 'black' + }, children: "Scroll me down" }), (0, jsx_runtime_1.jsxs)("div", { style: { overflowY: 'scroll', height: '600px', background: 'blue' }, children: [(0, jsx_runtime_1.jsx)("div", { style: { + height: '700px', fontSize: '60px', + color: 'white' + }, children: "Scroll me down" }), (0, jsx_runtime_1.jsx)(VerticalListExample, {})] }), (0, jsx_runtime_1.jsx)("div", { style: { height: '700px' } })] })); } const rootElement = document.getElementById("demo"); const root = client_1.default.createRoot(rootElement); root.render((0, jsx_runtime_1.jsx)(react_1.default.StrictMode, { children: (0, jsx_runtime_1.jsx)(App, {}) })); -},{"..":2,"../simple":3,"../utils":4,"react":14,"react-dom/client":8,"react/jsx-runtime":15}],2:[function(require,module,exports){ +},{"..":4,"../fixed-grid":2,"../fixed-list-y":3,"../utils":5,"react":15,"react-dom/client":9,"react/jsx-runtime":16}],2:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VirtualGrid = void 0; +const jsx_runtime_1 = require("react/jsx-runtime"); +const react_1 = require("react"); +const _1 = require("."); +function VirtualGrid(props) { + const containerRef = (0, react_1.useRef)(undefined); + const { renderedItems } = (0, _1.useVirtualOverflowGrid)({ + containerRef, + itemsLengthY: props.items.length, + itemsLengthX: props.columnsNum, + itemWidth: props.itemWidth, + itemHeight: props.itemHeight, + overscanItemsCount: props.overscanItemsCount, + calcVisibleRect: props.calcVisibleRect, + renderItem: (itemIndexX, leftOffsetPx, itemIndexY, topOffsetPx) => { + const item = props.items[itemIndexY][itemIndexX]; + if (!item) + return null; + return ((0, jsx_runtime_1.jsx)("div", { style: { position: 'absolute', top: `${topOffsetPx}px`, left: `${leftOffsetPx}px` }, children: props.renderItem(item, itemIndexX, leftOffsetPx, itemIndexY, topOffsetPx) }, props.itemKey(item, itemIndexX, itemIndexY))); + } + }, [props.items, props.itemHeight]); + return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: { width: `${props.columnsNum * props.itemWidth}px`, height: `${props.items.length * props.itemHeight}px`, position: 'relative' }, children: renderedItems })); +} +exports.VirtualGrid = VirtualGrid; + +},{".":4,"react":15,"react/jsx-runtime":16}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.useVirtualOverflowV = exports.virtualOverflowCalcItemsV = exports.virtualOverflowCalcVisibleRect = void 0; +exports.VirtualListY = void 0; +const jsx_runtime_1 = require("react/jsx-runtime"); +const react_1 = require("react"); +const _1 = require("."); +function VirtualListY(props) { + const containerRef = (0, react_1.useRef)(undefined); + const { renderedItems } = (0, _1.useVirtualOverflowY)({ + containerRef, + itemsLengthY: props.items.length, + itemHeight: props.itemHeight, + overscanItemsCount: props.overscanItemsCount, + calcVisibleRect: props.calcVisibleRect, + renderItem: (itemIndex, topOffset) => { + const item = props.items[itemIndex]; + if (!item) + return null; + return ((0, jsx_runtime_1.jsx)("div", { style: { position: 'absolute', top: `${topOffset}px` }, children: props.renderItem(item, itemIndex, topOffset) }, props.itemKey(item, itemIndex))); + } + }, [props.items, props.itemHeight]); + return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: { height: `${props.items.length * props.itemHeight}px`, position: 'relative' }, children: renderedItems })); +} +exports.VirtualListY = VirtualListY; + +},{".":4,"react":15,"react/jsx-runtime":16}],4:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useVirtualOverflowGrid = exports.useVirtualOverflowX = exports.useVirtualOverflowY = exports.useCalcVirtualOverflow = exports.virtualOverflowCalcItems = exports.virtualOverflowCalcVisibleRect = void 0; const react_1 = require("react"); function debounceAnimationFrame(func) { let frameRequest = 0; - return [ - () => { + return { + requestFrame: () => { cancelAnimationFrame(frameRequest); frameRequest = requestAnimationFrame((frameTime) => func.call(undefined, frameTime)); }, - () => cancelAnimationFrame(frameRequest) - ]; + cancelFrame: () => cancelAnimationFrame(frameRequest) + }; } function virtualOverflowCalcVisibleRect(element) { const elementRect = element.getBoundingClientRect(); @@ -115,26 +180,47 @@ function virtualOverflowCalcVisibleRect(element) { }; } exports.virtualOverflowCalcVisibleRect = virtualOverflowCalcVisibleRect; -function virtualOverflowCalcItemsV(visibleRect, itemHeight, overscanItemsCount, itemsLength) { - let itemStart = Math.floor(visibleRect.contentOffsetTop / itemHeight); - let itemLen = Math.ceil(visibleRect.contentVisibleHeight / itemHeight); +// contentOffsetStart = visibleRect.contentOffsetTop +// contentVisibleSize = visibleRect.contentVisibleHeight +function virtualOverflowCalcItems(contentOffsetStart, contentVisibleSize, itemSize, overscanItemsCount, itemsLength) { + let itemStart = Math.floor(contentOffsetStart / itemSize); + let itemLen = Math.ceil(contentVisibleSize / itemSize); itemStart = Math.max(0, itemStart - overscanItemsCount); itemLen = Math.max(0, Math.min(itemsLength - itemStart, itemLen + overscanItemsCount + overscanItemsCount)); - return [itemStart, itemLen]; -} -exports.virtualOverflowCalcItemsV = virtualOverflowCalcItemsV; -function useVirtualOverflowV(params, deps = []) { - const { renderItem, containerRef, itemsLength, itemHeight, overscanItemsCount = 3, calcVisibleRect = virtualOverflowCalcVisibleRect } = params; - const [[itemStart, itemLength], setItemSlice] = (0, react_1.useState)([0, 0]); - (0, react_1.useLayoutEffect)(() => { + return { itemStart, itemLen }; +} +exports.virtualOverflowCalcItems = virtualOverflowCalcItems; +function useCalcVirtualOverflow(params, deps) { + const { containerRef, itemsLengthX, itemsLengthY, itemWidth, itemHeight, overscanItemsCount = 3, calcVisibleRect = virtualOverflowCalcVisibleRect } = params; + const [itemSlice, setItemSlice] = (0, react_1.useState)({ + topStartIndex: 0, + lengthY: 0, + leftStartIndex: 0, + lengthX: 0, + }); + const { requestFrame: updateViewRect, cancelFrame } = (0, react_1.useMemo)(() => debounceAnimationFrame((frameTime) => { if (!containerRef.current) - return () => { }; - const containerEl = containerRef.current; - const [updateViewRect, cancelFrame] = debounceAnimationFrame((frameTime) => { - const visibleRect = calcVisibleRect(containerEl, frameTime); - const itemSlicePos = virtualOverflowCalcItemsV(visibleRect, itemHeight, overscanItemsCount, itemsLength); - setItemSlice(itemSlicePos); - }); + return; + const visibleRect = calcVisibleRect(containerRef.current, frameTime); + const newItemSlice = { + topStartIndex: 0, + lengthY: 1, + leftStartIndex: 0, + lengthX: 1, + }; + if (itemHeight !== undefined && itemsLengthY !== undefined) { + const verticalSlice = virtualOverflowCalcItems(visibleRect.contentOffsetTop, visibleRect.contentVisibleHeight, itemHeight, overscanItemsCount, itemsLengthY); + newItemSlice.topStartIndex = verticalSlice.itemStart; + newItemSlice.lengthY = verticalSlice.itemLen; + } + if (itemWidth !== undefined && itemsLengthX !== undefined) { + const horizontalSlice = virtualOverflowCalcItems(visibleRect.contentOffsetLeft, visibleRect.contentVisibleWidth, itemWidth, overscanItemsCount, itemsLengthX); + newItemSlice.leftStartIndex = horizontalSlice.itemStart; + newItemSlice.lengthX = horizontalSlice.itemLen; + } + setItemSlice(newItemSlice); + }), [containerRef.current, itemsLengthX, itemsLengthY, itemWidth, itemHeight, ...deps]); + (0, react_1.useLayoutEffect)(() => { window.addEventListener('scroll', updateViewRect, { capture: true, passive: true }); window.addEventListener('resize', updateViewRect, { capture: true, passive: true }); window.addEventListener('orientationchange', updateViewRect, { capture: true, passive: true }); @@ -145,41 +231,52 @@ function useVirtualOverflowV(params, deps = []) { window.removeEventListener('resize', updateViewRect); window.removeEventListener('orientationchange', updateViewRect); }; - }, [containerRef.current, itemsLength, itemHeight, ...deps]); - const outLength = itemStart + itemLength; - const renderedItems = Array.from({ length: outLength }); - for (let i = itemStart; i < outLength; ++i) { - renderedItems[i - itemStart] = renderItem(i, i * itemHeight); + }, [containerRef.current, itemsLengthX, itemsLengthY, itemWidth, itemHeight, ...deps]); + return { itemSlice, updateViewRect }; +} +exports.useCalcVirtualOverflow = useCalcVirtualOverflow; +function utilRenderItems1D(itemStart, itemsLength, itemSize, renderItem) { + const itemEnd = itemStart + itemsLength; + const renderedItems = Array.from({ length: itemsLength }); + for (let i = itemStart; i < itemEnd; ++i) { + renderedItems[i - itemStart] = renderItem(i, i * itemSize); } return renderedItems; } -exports.useVirtualOverflowV = useVirtualOverflowV; - -},{"react":14}],3:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SimpleVirtualListV = void 0; -const jsx_runtime_1 = require("react/jsx-runtime"); -const react_1 = require("react"); -const _1 = require("."); -function SimpleVirtualListV(props) { - const containerRef = (0, react_1.useRef)(undefined); - const rendered = (0, _1.useVirtualOverflowV)({ - containerRef, - itemsLength: props.items.length, - itemHeight: props.itemHeight, - renderItem: (itemIndex, topOffset) => { - const item = props.items[itemIndex]; - if (!item) - return null; - return ((0, jsx_runtime_1.jsx)("div", { style: { position: 'absolute', top: `${topOffset}px` }, children: props.renderItem(item, itemIndex, topOffset) }, props.itemKey(item, itemIndex))); +function useVirtualOverflowY(params, deps = []) { + const { itemSlice, updateViewRect } = useCalcVirtualOverflow(params, deps); + return { + renderedItems: utilRenderItems1D(itemSlice.topStartIndex, itemSlice.lengthY, params.itemHeight, params.renderItem), + updateViewRect, + }; +} +exports.useVirtualOverflowY = useVirtualOverflowY; +function useVirtualOverflowX(params, deps = []) { + const { itemSlice, updateViewRect } = useCalcVirtualOverflow(params, deps); + return { + renderedItems: utilRenderItems1D(itemSlice.leftStartIndex, itemSlice.lengthX, params.itemWidth, params.renderItem), + updateViewRect, + }; +} +exports.useVirtualOverflowX = useVirtualOverflowX; +function useVirtualOverflowGrid(params, deps = []) { + const { itemSlice, updateViewRect } = useCalcVirtualOverflow(params, deps); + const renderedItems = Array.from({ length: itemSlice.lengthX * itemSlice.lengthY }); + for (let iy = 0; iy < itemSlice.lengthY; ++iy) { + for (let ix = 0; ix < itemSlice.lengthX; ++ix) { + const realXindex = itemSlice.leftStartIndex + ix; + const realYindex = itemSlice.topStartIndex + iy; + renderedItems[ix + iy * itemSlice.lengthX] = params.renderItem(realXindex, realXindex * params.itemWidth, realYindex, realYindex * params.itemHeight); } - }, [props.items, props.itemHeight]); - return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: { height: `${props.items.length * props.itemHeight}px`, position: 'relative' }, children: rendered })); + } + return { + renderedItems, + updateViewRect + }; } -exports.SimpleVirtualListV = SimpleVirtualListV; +exports.useVirtualOverflowGrid = useVirtualOverflowGrid; -},{".":2,"react":14,"react/jsx-runtime":15}],4:[function(require,module,exports){ +},{"react":15}],5:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.virtualOverflowUtils = void 0; @@ -268,7 +365,7 @@ var virtualOverflowUtils; virtualOverflowUtils.calcVisibleRectOverflowed = calcVisibleRectOverflowed; })(virtualOverflowUtils || (exports.virtualOverflowUtils = virtualOverflowUtils = {})); -},{}],5:[function(require,module,exports){ +},{}],6:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; @@ -454,7 +551,7 @@ process.chdir = function (dir) { }; process.umask = function() { return 0; }; -},{}],6:[function(require,module,exports){ +},{}],7:[function(require,module,exports){ (function (process){(function (){ /** * @license React @@ -30326,7 +30423,7 @@ if ( } }).call(this)}).call(this,require('_process')) -},{"_process":5,"react":14,"scheduler":18}],7:[function(require,module,exports){ +},{"_process":6,"react":15,"scheduler":19}],8:[function(require,module,exports){ /** * @license React * react-dom.production.min.js @@ -30651,7 +30748,7 @@ exports.hydrateRoot=function(a,b,c){if(!ol(a))throw Error(p(405));var d=null!=c& e);return new nl(b)};exports.render=function(a,b,c){if(!pl(b))throw Error(p(200));return sl(null,a,b,!1,c)};exports.unmountComponentAtNode=function(a){if(!pl(a))throw Error(p(40));return a._reactRootContainer?(Sk(function(){sl(null,null,a,!1,function(){a._reactRootContainer=null;a[uf]=null})}),!0):!1};exports.unstable_batchedUpdates=Rk; exports.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!pl(c))throw Error(p(200));if(null==a||void 0===a._reactInternals)throw Error(p(38));return sl(a,b,c,!1,d)};exports.version="18.2.0-next-9e3b772b8-20220608"; -},{"react":14,"scheduler":18}],8:[function(require,module,exports){ +},{"react":15,"scheduler":19}],9:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -30680,7 +30777,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"_process":5,"react-dom":9}],9:[function(require,module,exports){ +},{"_process":6,"react-dom":10}],10:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -30722,7 +30819,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/react-dom.development.js":6,"./cjs/react-dom.production.min.js":7,"_process":5}],10:[function(require,module,exports){ +},{"./cjs/react-dom.development.js":7,"./cjs/react-dom.production.min.js":8,"_process":6}],11:[function(require,module,exports){ (function (process){(function (){ /** * @license React @@ -32040,7 +32137,7 @@ exports.jsxs = jsxs; } }).call(this)}).call(this,require('_process')) -},{"_process":5,"react":14}],11:[function(require,module,exports){ +},{"_process":6,"react":15}],12:[function(require,module,exports){ /** * @license React * react-jsx-runtime.production.min.js @@ -32053,7 +32150,7 @@ exports.jsxs = jsxs; 'use strict';var f=require("react"),k=Symbol.for("react.element"),l=Symbol.for("react.fragment"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0}; function q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=""+g);void 0!==a.key&&(e=""+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q; -},{"react":14}],12:[function(require,module,exports){ +},{"react":15}],13:[function(require,module,exports){ (function (process){(function (){ /** * @license React @@ -34796,7 +34893,7 @@ if ( } }).call(this)}).call(this,require('_process')) -},{"_process":5}],13:[function(require,module,exports){ +},{"_process":6}],14:[function(require,module,exports){ /** * @license React * react.production.min.js @@ -34824,7 +34921,7 @@ exports.useCallback=function(a,b){return U.current.useCallback(a,b)};exports.use exports.useInsertionEffect=function(a,b){return U.current.useInsertionEffect(a,b)};exports.useLayoutEffect=function(a,b){return U.current.useLayoutEffect(a,b)};exports.useMemo=function(a,b){return U.current.useMemo(a,b)};exports.useReducer=function(a,b,e){return U.current.useReducer(a,b,e)};exports.useRef=function(a){return U.current.useRef(a)};exports.useState=function(a){return U.current.useState(a)};exports.useSyncExternalStore=function(a,b,e){return U.current.useSyncExternalStore(a,b,e)}; exports.useTransition=function(){return U.current.useTransition()};exports.version="18.2.0"; -},{}],14:[function(require,module,exports){ +},{}],15:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -34835,7 +34932,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/react.development.js":12,"./cjs/react.production.min.js":13,"_process":5}],15:[function(require,module,exports){ +},{"./cjs/react.development.js":13,"./cjs/react.production.min.js":14,"_process":6}],16:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -34846,7 +34943,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/react-jsx-runtime.development.js":10,"./cjs/react-jsx-runtime.production.min.js":11,"_process":5}],16:[function(require,module,exports){ +},{"./cjs/react-jsx-runtime.development.js":11,"./cjs/react-jsx-runtime.production.min.js":12,"_process":6}],17:[function(require,module,exports){ (function (process,setImmediate){(function (){ /** * @license React @@ -35484,7 +35581,7 @@ if ( } }).call(this)}).call(this,require('_process'),require("timers").setImmediate) -},{"_process":5,"timers":19}],17:[function(require,module,exports){ +},{"_process":6,"timers":20}],18:[function(require,module,exports){ (function (setImmediate){(function (){ /** * @license React @@ -35507,7 +35604,7 @@ exports.unstable_scheduleCallback=function(a,b,c){var d=exports.unstable_now();" exports.unstable_shouldYield=M;exports.unstable_wrapCallback=function(a){var b=y;return function(){var c=y;y=b;try{return a.apply(this,arguments)}finally{y=c}}}; }).call(this)}).call(this,require("timers").setImmediate) -},{"timers":19}],18:[function(require,module,exports){ +},{"timers":20}],19:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -35518,7 +35615,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/scheduler.development.js":16,"./cjs/scheduler.production.min.js":17,"_process":5}],19:[function(require,module,exports){ +},{"./cjs/scheduler.development.js":17,"./cjs/scheduler.production.min.js":18,"_process":6}],20:[function(require,module,exports){ (function (setImmediate,clearImmediate){(function (){ var nextTick = require('process/browser.js').nextTick; var apply = Function.prototype.apply; @@ -35597,4 +35694,4 @@ exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : delete immediateIds[id]; }; }).call(this)}).call(this,require("timers").setImmediate,require("timers").clearImmediate) -},{"process/browser.js":5,"timers":19}]},{},[1]); +},{"process/browser.js":6,"timers":20}]},{},[1]); diff --git a/package.json b/package.json index 4c57d3b..51b2c16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-virtual-overflow", - "version": "1.0.3", + "version": "1.1.0", "description": "virtual scroll without headache", "main": "lib/index.cjs.js", "module": "lib/index.esm.js", @@ -45,7 +45,7 @@ }, "size-limit": [ { - "path": "lib/index.esm.js", + "path": "lib/fixed-list-y.js", "limit": "1 kB", "gzip": true } diff --git a/readme.md b/readme.md index 9ef9d6f..a1ad8c4 100644 --- a/readme.md +++ b/readme.md @@ -10,14 +10,16 @@ Similar to [react-virtualized](https://github.com/bvaughn/react-virtualized), bu - No magical divs will wrap your list with position: absolute and height: 0 - No scroll syncing problems - No AutoWindow over AutoSize with VerticalSpecialList +- Full rendering controll - It just works -- <1kb gzipped +- ~0.5kb gzipped -Currently only vertical list supported with fixed item's height, but will add horizontal & grid soon +Currently only fixed item sizes supported, but will dynamic sizing later. -Components & hooks in this library will automatically find all containers with overflows and render only visible items. +Components & hooks in this library will automatically find all containers with overflows and render only visible items. +So you could stack and wrap your list in anyway you want, everything will work. -So you could stack/wrap/move your list in anyway you want, everything will work. +You also could use some parts of this library for example to calculate only visible on screen rect of element. ![](./important.jpg) @@ -27,10 +29,10 @@ npm i react-virtual-overflow [demo app code](src/examples/demo.tsx) -## Simple +## Simple example ```tsx -import { SimpleVirtualListV } from "react-virtual-overflow/lib/simple"; +import { VirtualListY } from "react-virtual-overflow/lib/fixed-list-y"; function MyApp() { const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`); @@ -43,7 +45,7 @@ function MyApp() { return (
- x} @@ -54,12 +56,12 @@ function MyApp() { } ``` -## Advanced +## Advanced example Advanced example with hook ```tsx -import { useVirtualOverflowV } from "react-virtual-overflow"; +import { useVirtualOverflowY } from "react-virtual-overflow"; function MyApp() { const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`); @@ -68,9 +70,9 @@ function MyApp() { const itemHeight = 40; - const renderedItems = useVirtualOverflowV({ + const { renderedItems } = useVirtualOverflowY({ containerRef, - itemsLength: items.length, + itemsLengthY: items.length, itemHeight, renderItem: (itemIndex, offsetTop) => { const item = items[itemIndex]; @@ -93,25 +95,164 @@ function MyApp() { } ``` -### useVirtualOverflowV +## Available hooks & components + +
+ +VirtualListY component + + +This component is used to render vertical list + +```tsx +import { VirtualListY } from "react-virtual-overflow/lib/fixed-list-y"; + +type VirtualListYProps = { + items: ItemT[], + itemHeight: number, + // used to calculate react key when rendering + itemKey: (item: ItemT, itemIndex: number) => string, + overscanItemsCount?: number, + renderItem: (item: ItemT, itemIndex: number, contentTopOffset: number) => React.ReactNode, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; + +function MyApp() { + const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`); + + const itemHeight = 40; + + const renderItem = (item) => ( +
{item}
+ ); + + return ( +
+ x} + renderItem={renderItem} + /> +
+ ); +} +``` + +
+ + +
+ +VirtualListX component + + +This component is used to render horizontal list + +```tsx +import { VirtualListX } from "react-virtual-overflow/lib/fixed-list-x"; + +type VirtualListXProps = { + items: ItemT[], + itemWidth: number, + itemKey: (item: ItemT, itemIndex: number) => string, + overscanItemsCount?: number, + renderItem: (item: ItemT, itemIndex: number, contentTopOffset: number) => React.ReactNode, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; + +function MyApp() { + const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`); + + const itemWidth = 40; + + const renderItem = (item) => ( +
{item}
+ ); + + return ( +
+ x} + renderItem={renderItem} + /> +
+ ); +} +``` + +
+ + +
+ +VirtualGrid component + + +This component is used to render grid + +```tsx +import { VirtualGrid } from "react-virtual-overflow/lib/fixed-grid"; + +type VirtualGridProps = { + items: ItemT[][], + columnsNum: number, + itemWidth: number, + itemHeight: number, + itemKey: (item: ItemT, itemIndexX: number, itemIndexY: number) => string, + overscanItemsCount?: number, + renderItem: (item: ItemT, itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => React.ReactNode, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; + +function GridExample() { + const items = itemsGrid; + + return ( +
+ x} + overscanItemsCount={3} + renderItem={item =>
{item}
} + /> +
+ ); +} +``` + +
+ + +
+ +Vertical list hook + + +`useVirtualOverflowY` hook that computes and renders vertical list It accepts this params: ```ts -type UseVirtualOverflowParamsV = { - // reference to you container with elements +type UseVirtualOverflowParamsY = { + // reference to container with elements (not scroll) containerRef: React.MutableRefObject; // total num of items - itemsLength: number; + itemsLengthY: number; // how to render each item - renderItem: (indexIndex: number, contentTopOffset: number) => React.ReactNode; + renderItem: (itemIndex: number, contentTopOffsetPx: number) => React.ReactNode; - // height of one item + // height of one item in pixels itemHeight: number; - // how much items should be rendered beyond visibleborder + // how much items should be rendered beyond visible border // default=3 overscanItemsCount?: number; @@ -120,6 +261,228 @@ type UseVirtualOverflowParamsV = { }; ``` +And returns: + +```ts +{ + renderedItems: React.Node[], + + // method that will force update calculations + updateViewRect: () => void, +} +``` + +
+ + +
+ +Horizontal list hook + + +`useVirtualOverflowX` hook that computes and renders horizontal list + +It accepts this params: + +```ts +type UseVirtualOverflowParamsX = { + // reference to container with elements (not scroll) + containerRef: React.MutableRefObject; + + // total num of items + itemsLengthX: number; + + // how to render each item + renderItem: (itemIndex: number, contentLeftOffsetPx: number) => React.ReactNode; + + // width of one item in pixels + itemWidth: number; + + // how much items should be rendered beyond visible border + // default=3 + overscanItemsCount?: number; + + // function to calculate visible rect (check utils for other options) + calcVisibleRect?: CalcVisibleRectFn; +}; +``` + +And returns: + +```ts +{ + renderedItems: React.Node[], + + // method that will force update calculations + updateViewRect: () => void, +} +``` + +
+ + +
+ +Grid hook + + +`useVirtualOverflowGrid` hook that computes and renders horizontal list + +It accepts this params: + +```ts +type UseVirtualOverflowParamsGrid = { + // reference to container with elements (not scroll) + containerRef: React.MutableRefObject; + + // total num of items horizontal + itemsLengthX: number; + + // total num of items vertical + itemsLengthY: number; + + // how to render each item + renderItem: (itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => React.ReactNode; + + // width of one item in pixels + itemWidth: number; + + // height of one item in pixels + itemHeight: number; + + // how much items should be rendered beyond visible border + // default=3 + overscanItemsCount?: number; + + // function to calculate visible rect (check utils for other options) + calcVisibleRect?: CalcVisibleRectFn; +}; +``` + +And returns: + +```ts +{ + renderedItems: React.Node[], + + // method that will force update calculations + updateViewRect: () => void, +} +``` + +
+ + +
+ +useCalcVirtualOverflow - universal hook for fixed list/grid + + +`useCalcVirtualOverflow` hook that computes visible rect at calculates slice of items that should be rendered + +It could be used if you want to render items manually, and you need only slice calculated + +It accepts this params: + +```ts +type UseVirtualOverflowParams = { + containerRef: React.MutableRefObject, + itemsLengthX?: number, + itemsLengthY?: number, + /** if undefined, then horizontal calculation will be skipped */ + itemWidth?: number, + /** if undefined, then vertical calculation will be skipped */ + itemHeight?: number, + /** default=3 */ + overscanItemsCount?: number, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn, +}; +``` + +And returns: + +```ts +{ + itemSlice: { + topStartIndex: number; + lengthY: number; + leftStartIndex: number; + lengthX: number; + }; + updateViewRect: () => void; +} +``` + +
+ + +
+ +Calculate visible on screen rect + + +`virtualOverflowCalcVisibleRect` method will calculate on screen visible rect of some element + +It accepts this params: + +```ts +function virtualOverflowCalcVisibleRect(element: HTMLElement): { + top: number; + left: number; + bottom: number; + right: number; + contentOffsetTop: number; + contentOffsetLeft: number; + contentVisibleHeight: number; + contentVisibleWidth: number; +}; +``` + +
+ + + +
+ +Slice calculation from visible rect + + +`virtualOverflowCalcItems` method will calculate slice of items from visible rect + +You can pass here horizontal and vertical values from "calcVisibleRect" method. + +This method is axis-agnostic, so you just first calculate vertical data by passing vertical coords of rect, and then (if you need) horizontal. + +```ts +function virtualOverflowCalcItems( + contentOffsetStartPx: number, + contentVisibleSizePx: number, + itemSize: number, + overscanItemsCount: number, + itemsLength: number +); + +// returns +{ + // index of starting item that should be rendered (including overscan) + itemStart: number, + // total count of items (including start & end overscan) + itemLen: number +}; + +// Example for vertical slice calculation: +const visibleRect = calcVisibleRect(containerRef.current); +const verticalSlice = virtualOverflowCalcItems( + visibleRect.contentOffsetTop, + visibleRect.contentVisibleHeight, + itemHeight, + overscanItemsCount, + itemsLengthY +); +``` + +
+ ### utils All methods here are inside `virtualOverflowUtils` namespace in `react-virtual-overflow/utils`. I will not write namespace here below for readability purposes. @@ -143,9 +506,9 @@ useLayoutEffect(() => { setParentsWithOverflow(stack); }, []); -const rendered = useVirtualOverflowV({ +const { renderedItems } = useVirtualOverflowY({ containerRef, - itemsLength, + itemsLengthY, itemHeight, calcVisibleRect: (el: HTMLElement) => { // calculate only by found overflows diff --git a/src/examples/demo.tsx b/src/examples/demo.tsx index 28085bc..3bfc065 100644 --- a/src/examples/demo.tsx +++ b/src/examples/demo.tsx @@ -1,46 +1,51 @@ -import React, { useRef } from "react"; +import React, { useEffect, useRef } from "react"; import ReactDOM from "react-dom/client"; -import { useVirtualOverflowV } from ".."; +import { useVirtualOverflowY } from ".."; import { virtualOverflowUtils } from "../utils"; -import { SimpleVirtualListV } from "../simple"; +import { VirtualListY } from "../fixed-list-y"; +import { VirtualGrid } from "../fixed-grid"; -const Item = ({ item }: any) => { - return
{item}
; -}; +const itemsLine = Array.from({ length: 300 }).map((_, i) => `item ${i}`); +const itemsGrid = Array.from({ length: 300 }).map((_, iy) => Array.from({ length: 300 }).map((_, ix) => `item ${ix} ${iy}`)); -function ListWithHook({ items }: any) { +function ListWithHookExample() { + const items = itemsLine; const containerRef = useRef(undefined!); const itemHeight = 40; - const rendered = useVirtualOverflowV({ + const { renderedItems, updateViewRect } = useVirtualOverflowY({ containerRef, itemHeight, - itemsLength: items.length, + itemsLengthY: items.length, overscanItemsCount: 3, calcVisibleRect: virtualOverflowUtils.calcVisibleRectOverflowed, renderItem: (itemIndex, offsetTop, item = items[itemIndex]) => (
- +
{item}
), }); + useEffect(() => { + setInterval(() => updateViewRect(), 8); + }, []); + return (
- {rendered} + {renderedItems}
); } -function ListSimple(props: any) { - const items = props.items as string[]; +function VerticalListExample() { + const items = itemsLine; return (
- x} @@ -50,19 +55,39 @@ function ListSimple(props: any) { ); } -const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`); - -function App() { - const [, forceUpd] = React.useState(0); +function GridExample() { + const items = itemsGrid; - const r = useRef(undefined!); + return ( +
+ x} + overscanItemsCount={3} + renderItem={item =>
{item}
} + /> +
+ ); +} +function App() { return ( -
-
+
+
Scroll me down
-
- +
+ Scroll me down +
+
diff --git a/src/fixed-grid.tsx b/src/fixed-grid.tsx new file mode 100644 index 0000000..657dbed --- /dev/null +++ b/src/fixed-grid.tsx @@ -0,0 +1,42 @@ +import { useRef } from "react"; +import { VirtualOverflowCalcVisibleRectFn, useVirtualOverflowGrid } from "."; + +export type VirtualGridProps = { + items: ItemT[][], + columnsNum: number, + itemWidth: number, + itemHeight: number, + itemKey: (item: ItemT, itemIndexX: number, itemIndexY: number) => string, + overscanItemsCount?: number, + renderItem: (item: ItemT, itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => React.ReactNode, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; + +export function VirtualGrid(props: VirtualGridProps) { + const containerRef = useRef(undefined!); + + const { renderedItems } = useVirtualOverflowGrid({ + containerRef, + itemsLengthY: props.items.length, + itemsLengthX: props.columnsNum, + itemWidth: props.itemWidth, + itemHeight: props.itemHeight, + overscanItemsCount: props.overscanItemsCount, + calcVisibleRect: props.calcVisibleRect, + renderItem: (itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => { + const item = props.items[itemIndexY][itemIndexX]; + if (!item) return null; + return ( +
+ {props.renderItem(item, itemIndexX, leftOffsetPx, itemIndexY, topOffsetPx)} +
+ ); + } + }, [props.items, props.itemHeight]); + + return ( +
+ {renderedItems} +
+ ); +} \ No newline at end of file diff --git a/src/fixed-list-x.tsx b/src/fixed-list-x.tsx new file mode 100644 index 0000000..336c2ea --- /dev/null +++ b/src/fixed-list-x.tsx @@ -0,0 +1,38 @@ +import { useRef } from "react"; +import { VirtualOverflowCalcVisibleRectFn, useVirtualOverflowX } from "."; + +export type VirtualListXProps = { + items: ItemT[], + itemWidth: number, + itemKey: (item: ItemT, itemIndex: number) => string, + overscanItemsCount?: number, + renderItem: (item: ItemT, itemIndex: number, contentTopOffset: number) => React.ReactNode, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; + +export function VirtualListX(props: VirtualListXProps) { + const containerRef = useRef(undefined!); + + const { renderedItems } = useVirtualOverflowX({ + containerRef, + itemsLengthX: props.items.length, + itemWidth: props.itemWidth, + overscanItemsCount: props.overscanItemsCount, + calcVisibleRect: props.calcVisibleRect, + renderItem: (itemIndex: number, topOffset: number) => { + const item = props.items[itemIndex]; + if (!item) return null; + return ( +
+ {props.renderItem(item, itemIndex, topOffset)} +
+ ); + } + }, [props.items, props.itemWidth]); + + return ( +
+ {renderedItems} +
+ ); +} \ No newline at end of file diff --git a/src/simple.tsx b/src/fixed-list-y.tsx similarity index 65% rename from src/simple.tsx rename to src/fixed-list-y.tsx index c6507c7..23ebcd2 100644 --- a/src/simple.tsx +++ b/src/fixed-list-y.tsx @@ -1,22 +1,24 @@ import { useRef } from "react"; -import { VirtualOverflowCalcVisibleRectFn, useVirtualOverflowV } from "."; +import { VirtualOverflowCalcVisibleRectFn, useVirtualOverflowY } from "."; -export type SimpleVirtualListVProps = { +export type VirtualListYProps = { items: ItemT[], itemHeight: number, itemKey: (item: ItemT, itemIndex: number) => string, overscanItemsCount?: number, - renderItem: (item: ItemT, indexIndex: number, contentTopOffset: number) => React.ReactNode, + renderItem: (item: ItemT, itemIndex: number, contentTopOffset: number) => React.ReactNode, calcVisibleRect?: VirtualOverflowCalcVisibleRectFn }; -export function SimpleVirtualListV(props: SimpleVirtualListVProps) { +export function VirtualListY(props: VirtualListYProps) { const containerRef = useRef(undefined!); - const rendered = useVirtualOverflowV({ + const { renderedItems } = useVirtualOverflowY({ containerRef, - itemsLength: props.items.length, + itemsLengthY: props.items.length, itemHeight: props.itemHeight, + overscanItemsCount: props.overscanItemsCount, + calcVisibleRect: props.calcVisibleRect, renderItem: (itemIndex: number, topOffset: number) => { const item = props.items[itemIndex]; if (!item) return null; @@ -30,7 +32,7 @@ export function SimpleVirtualListV(props: SimpleVirtualListVProps) return (
- {rendered} + {renderedItems}
); } \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index e36ca03..5f7720c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,15 @@ -import { useLayoutEffect, useState } from "react"; +import { useLayoutEffect, useMemo, useState } from "react"; function debounceAnimationFrame(func: (frameTime: number) => void) { let frameRequest = 0; - return [ - () => { + return { + requestFrame: () => { cancelAnimationFrame(frameRequest); frameRequest = requestAnimationFrame((frameTime) => func.call(undefined, frameTime)); }, - () => cancelAnimationFrame(frameRequest) - ] as const; + cancelFrame: () => cancelAnimationFrame(frameRequest) + }; } export type VirtualOverflowVisibleRect = { @@ -23,14 +23,49 @@ export type VirtualOverflowVisibleRect = { contentVisibleWidth: number; }; -export type VirtualOverflowCalcVisibleRectFn = (element: HTMLElement, frameIndex: number) => VirtualOverflowVisibleRect; +export type VirtualOverflowCalcVisibleRectFn = (element: HTMLElement, frameTime: number) => VirtualOverflowVisibleRect; +export type VirtualOverflowRenderItem1DFn = (itemIndex: number, offsetPx: number) => React.ReactNode; +export type VirtualOverflowRenderItem2DFn = (itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => React.ReactNode; + +export type UseCalcVirtualOverflowParams = { + containerRef: React.MutableRefObject, + itemsLengthX?: number, + itemsLengthY?: number, + /** if undefined, then horizontal calculation will be skipped */ + itemWidth?: number, + /** if undefined, then vertical calculation will be skipped */ + itemHeight?: number, + /** default=3 */ + overscanItemsCount?: number, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn, +}; + +export type UseVirtualOverflowParamsGrid = { + containerRef: React.MutableRefObject, + itemsLengthX: number, + itemsLengthY: number, + itemWidth: number, + itemHeight: number, + overscanItemsCount?: number, + renderItem: VirtualOverflowRenderItem2DFn, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; -export type UseVirtualOverflowParamsV = { +export type UseVirtualOverflowParamsY = { containerRef: React.MutableRefObject, - itemsLength: number, + itemsLengthY: number, itemHeight: number, overscanItemsCount?: number, - renderItem: (indexIndex: number, contentTopOffset: number) => React.ReactNode, + renderItem: VirtualOverflowRenderItem1DFn, + calcVisibleRect?: VirtualOverflowCalcVisibleRectFn +}; + +export type UseVirtualOverflowParamsX = { + containerRef: React.MutableRefObject, + itemsLengthX: number, + itemWidth: number, + overscanItemsCount?: number, + renderItem: VirtualOverflowRenderItem1DFn, calcVisibleRect?: VirtualOverflowCalcVisibleRectFn }; @@ -78,30 +113,50 @@ export function virtualOverflowCalcVisibleRect(element: HTMLElement) { }; } -export function virtualOverflowCalcItemsV(visibleRect: VirtualOverflowVisibleRect, itemHeight: number, overscanItemsCount: number, itemsLength: number): [number, number] { - let itemStart = Math.floor(visibleRect.contentOffsetTop / itemHeight); - let itemLen = Math.ceil(visibleRect.contentVisibleHeight / itemHeight); +// contentOffsetStart = visibleRect.contentOffsetTop +// contentVisibleSize = visibleRect.contentVisibleHeight +export function virtualOverflowCalcItems(contentOffsetStart: number, contentVisibleSize: number, itemSize: number, overscanItemsCount: number, itemsLength: number) { + let itemStart = Math.floor(contentOffsetStart / itemSize); + let itemLen = Math.ceil(contentVisibleSize / itemSize); itemStart = Math.max(0, itemStart - overscanItemsCount); itemLen = Math.max(0, Math.min(itemsLength - itemStart, itemLen + overscanItemsCount + overscanItemsCount)); - return [itemStart, itemLen]; + return { itemStart, itemLen }; } -export function useVirtualOverflowV(params: UseVirtualOverflowParamsV, deps: any[] = []) { - const { renderItem, containerRef, itemsLength, itemHeight, overscanItemsCount = 3, calcVisibleRect = virtualOverflowCalcVisibleRect } = params; - const [[itemStart, itemLength], setItemSlice] = useState([0, 0]); +export function useCalcVirtualOverflow(params: UseCalcVirtualOverflowParams, deps: any[]) { + const { containerRef, itemsLengthX, itemsLengthY, itemWidth, itemHeight, overscanItemsCount = 3, calcVisibleRect = virtualOverflowCalcVisibleRect } = params; + const [itemSlice, setItemSlice] = useState({ + topStartIndex: 0, + lengthY: 0, + leftStartIndex: 0, + lengthX: 0, + }); + + const { requestFrame: updateViewRect, cancelFrame } = useMemo(() => debounceAnimationFrame((frameTime) => { + if (!containerRef.current) return; + const visibleRect = calcVisibleRect(containerRef.current, frameTime); + const newItemSlice = { + topStartIndex: 0, + lengthY: 1, + leftStartIndex: 0, + lengthX: 1, + }; + if (itemHeight !== undefined && itemsLengthY !== undefined) { + const verticalSlice = virtualOverflowCalcItems(visibleRect.contentOffsetTop, visibleRect.contentVisibleHeight, itemHeight, overscanItemsCount, itemsLengthY); + newItemSlice.topStartIndex = verticalSlice.itemStart; + newItemSlice.lengthY = verticalSlice.itemLen; + } + if (itemWidth !== undefined && itemsLengthX !== undefined) { + const horizontalSlice = virtualOverflowCalcItems(visibleRect.contentOffsetLeft, visibleRect.contentVisibleWidth, itemWidth, overscanItemsCount, itemsLengthX); + newItemSlice.leftStartIndex = horizontalSlice.itemStart; + newItemSlice.lengthX = horizontalSlice.itemLen; + } + setItemSlice(newItemSlice); + }), [containerRef.current, itemsLengthX, itemsLengthY, itemWidth, itemHeight, ...deps]); useLayoutEffect(() => { - if (!containerRef.current) return () => { }; - const containerEl = containerRef.current; - - const [updateViewRect, cancelFrame] = debounceAnimationFrame((frameTime) => { - const visibleRect = calcVisibleRect(containerEl, frameTime); - const itemSlicePos = virtualOverflowCalcItemsV(visibleRect, itemHeight, overscanItemsCount, itemsLength); - setItemSlice(itemSlicePos); - }); - window.addEventListener('scroll', updateViewRect, { capture: true, passive: true }); window.addEventListener('resize', updateViewRect, { capture: true, passive: true }); window.addEventListener('orientationchange', updateViewRect, { capture: true, passive: true }); @@ -114,15 +169,54 @@ export function useVirtualOverflowV(params: UseVirtualOverflowParamsV, deps: any window.removeEventListener('resize', updateViewRect); window.removeEventListener('orientationchange', updateViewRect); }; - }, [containerRef.current, itemsLength, itemHeight, ...deps]); + }, [containerRef.current, itemsLengthX, itemsLengthY, itemWidth, itemHeight, ...deps]); - const outLength = itemStart + itemLength; + return { itemSlice, updateViewRect }; +} + +function utilRenderItems1D(itemStart: number, itemsLength: number, itemSize: number, renderItem: VirtualOverflowRenderItem1DFn) { + const itemEnd = itemStart + itemsLength; - const renderedItems: React.ReactNode[] = Array.from({ length: outLength }); + const renderedItems: React.ReactNode[] = Array.from({ length: itemsLength }); - for (let i = itemStart; i < outLength; ++i) { - renderedItems[i - itemStart] = renderItem(i, i * itemHeight); + for (let i = itemStart; i < itemEnd; ++i) { + renderedItems[i - itemStart] = renderItem(i, i * itemSize); } return renderedItems; -} \ No newline at end of file +} + +export function useVirtualOverflowY(params: UseVirtualOverflowParamsY, deps: any[] = []) { + const { itemSlice, updateViewRect } = useCalcVirtualOverflow(params, deps); + return { + renderedItems: utilRenderItems1D(itemSlice.topStartIndex, itemSlice.lengthY, params.itemHeight, params.renderItem), + updateViewRect, + }; +} + +export function useVirtualOverflowX(params: UseVirtualOverflowParamsX, deps: any[] = []) { + const { itemSlice, updateViewRect } = useCalcVirtualOverflow(params, deps); + return { + renderedItems: utilRenderItems1D(itemSlice.leftStartIndex, itemSlice.lengthX, params.itemWidth, params.renderItem), + updateViewRect, + }; +} + +export function useVirtualOverflowGrid(params: UseVirtualOverflowParamsGrid, deps: any[] = []) { + const { itemSlice, updateViewRect } = useCalcVirtualOverflow(params, deps); + + const renderedItems: React.ReactNode[] = Array.from({ length: itemSlice.lengthX * itemSlice.lengthY }); + + for (let iy = 0; iy < itemSlice.lengthY; ++iy) { + for (let ix = 0; ix < itemSlice.lengthX; ++ix) { + const realXindex = itemSlice.leftStartIndex + ix; + const realYindex = itemSlice.topStartIndex + iy; + renderedItems[ix + iy * itemSlice.lengthX] = params.renderItem(realXindex, realXindex * params.itemWidth, realYindex, realYindex * params.itemHeight); + } + } + + return { + renderedItems, + updateViewRect + }; +}