diff --git a/package.json b/package.json index 1eb25b7..556b542 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@krjakbrjak/virtualtable", - "version": "1.1.6", + "version": "1.1.7", "description": "", "repository": { "type": "git", diff --git a/src/VirtualTable.tsx b/src/VirtualTable.tsx index 00880eb..fef4cbc 100644 --- a/src/VirtualTable.tsx +++ b/src/VirtualTable.tsx @@ -9,7 +9,7 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; -import { slideItems } from './helpers/collections'; +import { slideItems, Page } from './helpers/collections'; import './base.css'; @@ -17,14 +17,26 @@ import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection'; import { Style, DataSource } from './helpers/types'; import { Container, Row, Col } from 'react-bootstrap'; +/** + * Represent the rectangular. + */ +interface Rect { + x: number; + y: number; + height: number; + width: number; +} + interface State { - scrollTop: number, - itemHeight: number, - itemCount: number, - items: Array, - offset: number, + ready: boolean; + scrollTop: number; + itemHeight: number; + itemCount: number; + page: Page; + offset: number; selected: number; hovered: number; + rect: Rect; } interface Action { @@ -39,6 +51,31 @@ interface Args { style?: Style; } +function get_initial_state(): State { + return { + ready: false, + scrollTop: 0, + itemHeight: 0, + itemCount: 0, + page: { + items: [], + offset: 0, + }, + offset: 0, + selected: -1, + hovered: -1, + rect: { + x: 0, + y: 0, + height: 0, + width: 0, + } + } +} +function calculatePageCount(pageHeight: number, itemHeight: number) { + return 2 * Math.floor(pageHeight / itemHeight); +} + /** * Reducer function for managing state changes. * @@ -50,13 +87,9 @@ interface Args { function reducer(state: State, action: Action): State { switch (action.type) { case 'scroll': - return { ...state, ...action.data }; case 'render': - return { ...state, ...action.data }; case 'loaded': - return { ...state, ...action.data }; case 'click': - return { ...state, ...action.data }; case 'hover': return { ...state, ...action.data }; default: @@ -79,16 +112,6 @@ function reducer(state: State, action: Action): State { * @property {DataSource} fetcher A datasource to fetch the data. */ -/** - * Represent the rectangular. - */ -interface Rect { - x: number; - y: number; - height: number; - width: number; -} - /** * @description VirtualTable component. * @@ -99,89 +122,9 @@ interface Rect { */ export default function VirtualTable({ height, renderer, fetcher, style }: Args): JSX.Element { const ref = useRef(null); + const invisible = useRef(null); const [collection, setCollection] = useState>(() => new LazyPaginatedCollection(1, fetcher)); - const [rect, setRect] = useState({ - x: 0, - y: 0, - height: 0, - width: 0, - }); - - useEffect(() => { - setCollection(new LazyPaginatedCollection(collection.pageSize() ? collection.pageSize() : 1, fetcher)); - }, [fetcher]); - - const [state, dispatch] = useReducer(reducer, { - scrollTop: 0, - itemHeight: 0, - itemCount: 0, - items: [], - offset: 0, - selected: -1, - hovered: -1, - }); - - const [currentOffset, setCurrentOffset] = useState(0); - - const calculatePageCount = () => 2 * Math.floor(height / state.itemHeight); - - useEffect(() => { - const handler = () => { - if (ref && ref.current) { - setRect(ref.current.getBoundingClientRect()); - } - }; - window.addEventListener('resize', handler); - return function cleanup() { - window.removeEventListener('resize', handler, true); - } - }, []); - - useEffect(() => { - if (collection) { - collection.slice(0, collection.pageSize()).then((result) => { - dispatch({ - type: 'loaded', - data: { - scrollTop: 0, - itemHeight: 0, - items: [], - offset: 0, - selected: -1, - hovered: -1, - ...result, - itemCount: collection.count(), - }, - }); - }); - } - }, [collection]); - - useEffect(() => { - if (state.itemHeight) { - const offset = Math.floor(state.scrollTop / state.itemHeight); - const c = calculatePageCount(); - if (c !== collection.pageSize()) { - setCurrentOffset(0); - setCollection(new LazyPaginatedCollection(c, fetcher)); - } else { - setCurrentOffset(offset); - collection.slice(offset, collection.pageSize()).then((result) => { - if (currentOffset !== result.offset) { - dispatch({ - type: 'loaded', - data: { - ...result, - itemCount: collection.count(), - }, - }); - } - }); - } - } - }, [ - state, - ]); + const [state, dispatch] = useReducer(reducer, get_initial_state()); const generate = (offset: number, d: Array) => { const ret = []; @@ -201,26 +144,118 @@ export default function VirtualTable({ height, renderer, fetcher, style }: return ret; }; + + // A callback to update the table view in case of resize event. + const handler = () => { + let itemHeight = state.itemHeight; + let rect = state.rect; + if (invisible && invisible.current) { + itemHeight = invisible.current.clientHeight; + } + if (ref && ref.current) { + rect = ref.current.getBoundingClientRect(); + } + + // Update the size of the widget and the size of the items + dispatch({ + type: 'render', + data: { + rect, + itemHeight, + scrollTop: 0, + selected: -1, + hovered: -1, + page: { + items: [], + offset: 0, + } + }, + }); + + // If the item's height is already known, then update the lazy collection + // and re-fetch the items. + if (itemHeight) { + const new_collection = new LazyPaginatedCollection(calculatePageCount(rect.height, itemHeight), fetcher); + new_collection.slice(0, new_collection.pageSize()).then((result) => { + dispatch({ + type: 'loaded', + data: { + page: result, + itemCount: new_collection.count(), + }, + }); + setCollection(new_collection); + }); + } + }; + + // Effect that updates the lazy collection in case fetcher gets updated useEffect(() => { - if (ref.current) { - ref.current.scrollTop = state.scrollTop % state.itemHeight; - if (ref.current.children && ref.current.children.length) { - if (ref.current.children[0].clientHeight !== state.itemHeight) { - setRect(ref.current.getBoundingClientRect()); + setCollection(new LazyPaginatedCollection(collection.pageSize() ? collection.pageSize() : 1, fetcher)); + }, [fetcher]); + + // Effect to fetch the first item (to draw a fake item to get the true size if the item) + // and the total number of items. + useEffect(() => { + collection.slice(0, collection.pageSize()).then((result) => { + dispatch({ + type: 'loaded', + data: { + ready: true, + page: result, + itemCount: collection.count(), + }, + }); + }); + + window.addEventListener('resize', handler); + return function cleanup() { + window.removeEventListener('resize', handler, true); + } + }, []); + + // Effect to run on all state updates. + useEffect(() => { + if (state.ready) { + if (state.itemHeight) { + const offset = Math.floor(state.scrollTop / state.itemHeight); + const c = calculatePageCount(height, state.itemHeight); + if (c === collection.pageSize() && state.offset !== offset) { + // Update the offset first and then start fetching the necessary items. + // This ensures a non-interruptive user experience, where all the + // required data is already available. dispatch({ - type: 'render', + type: 'loaded', data: { - itemHeight: ref.current.children[0].clientHeight, + offset, }, }); + collection.slice(offset, collection.pageSize()).then((result) => { + if (state.offset !== result.offset) { + dispatch({ + type: 'loaded', + data: { + page: result, + itemCount: collection.count(), + }, + }); + } + }); } + } else { + handler(); } } + }, [state]); + + // Effect to run on each render to make sure that the scrolltop of + // the item container is up-to-date. + useEffect(() => { + if (ref.current) { + ref.current.scrollTop = state.scrollTop % state.itemHeight; + } }); - if (state.items.length === 0) { - return
; - } return ( @@ -233,18 +268,23 @@ export default function VirtualTable({ height, renderer, fetcher, style }: height, }} > - {generate(currentOffset, slideItems(currentOffset, { - items: state.items, - offset: state.offset, - }))} + {state.ready && state.itemHeight === 0 && +
+ {renderer(state.page.items[0], '')} +
} + {state.itemHeight !== 0 && generate(state.offset, slideItems(state.offset, state.page))}
{ const index = Math.floor((e.clientY + state.scrollTop - ref.current.getBoundingClientRect().top) / state.itemHeight); diff --git a/src/helpers/collections.ts b/src/helpers/collections.ts index 32ec9c2..70c7cc2 100644 --- a/src/helpers/collections.ts +++ b/src/helpers/collections.ts @@ -2,7 +2,7 @@ * Represent a page. * @typedef {Object} Slice */ -interface Page { +export interface Page { /** * Page items */