import * as React from 'react'; import ReactGA from "react-ga4"; import { Types, Maybe } from '../../common'; import { Filter } from '../'; import { State as AppState, Update as AppUpdate, Node } from '../../state'; import { Row, THead } from './'; import { Persistent, PersistentSet } from '../../persist'; import { viewport } from '../../utils'; const HEADER = 148; const TH_HEIGHT = 35; const TR_HEIGHT = 31; const ROW_MARGIN = 5; import './List.css'; interface ListProps { appState: Readonly; appUpdate: AppUpdate; pins: PersistentSet; sortBy: Persistent>; } // Helper for readability, used as `key` prop for each `Row` // of the `List`, so that we can maximize re-using DOM elements. type Key = number; export class List extends React.Component { public state = { filter: null, viewportHeight: viewport().height, }; private listStart = 0; private listEnd = 0; private relativeTop = -1; private nextKey: Key = 0; private previousKeys = new Map(); public componentDidMount() { ReactGA.send({ hitType: "pageview", page: "/list" }); this.onScroll(); window.addEventListener('resize', this.onResize); window.addEventListener('scroll', this.onScroll); } public componentWillUnmount() { window.removeEventListener('resize', this.onResize); window.removeEventListener('scroll', this.onScroll); } public render() { const { pins, sortBy, appState } = this.props; const { selectedColumns } = appState; const { filter } = this.state; let nodes = appState.nodes.sorted(); if (filter != null) { nodes = nodes.filter(filter); if (nodes.length === 0) { return (
¯\_(ツ)_/¯
Nothing matches
); } // With filter present, we can no longer guarantee that focus corresponds // to rendering view, so we put the whole list in focus appState.nodes.setFocus(0, nodes.length); } else { appState.nodes.setFocus(this.listStart, this.listEnd); } const height = TH_HEIGHT + nodes.length * TR_HEIGHT; const top = this.listStart * TR_HEIGHT; nodes = nodes.slice(this.listStart, this.listEnd); const keys = this.recalculateKeys(nodes); return (
{nodes.map((node, i) => ( ))}
); } // Get an array of keys for each `Node` in viewport in order. // // * If a `Node` was previously rendered, it will keep its `Key`. // // * If a `Node` is new to the viewport, it will get a `Key` of // another `Node` that was removed from the viewport, or a new one. private recalculateKeys(nodes: Array): Array { // First we find all keys for `Node`s which didn't change from // last render. const keptKeys: Array> = nodes.map(({ id }) => { const key = this.previousKeys.get(id); if (key != null) { this.previousKeys.delete(id); } return key; }); // Array of all unused keys const unusedKeys = Array.from(this.previousKeys.values()); let search = 0; // Clear the map so we can set new values this.previousKeys.clear(); // Filling in blanks and re-populate previousKeys return keptKeys.map((key: Maybe, i) => { const id = nodes[i].id; // `Node` was previously in viewport if (key != null) { this.previousKeys.set(id, key); return key; } // Recycle the next unused key if (search < unusedKeys.length) { const unused = unusedKeys[search++]; this.previousKeys.set(id, unused); return unused; } // No unused keys left, generate a new key const newKey = this.nextKey++; this.previousKeys.set(id, newKey); return newKey; }); } private onScroll = () => { const relativeTop = divisibleBy( window.scrollY - (HEADER + TR_HEIGHT), TR_HEIGHT * ROW_MARGIN ); if (this.relativeTop === relativeTop) { return; } this.relativeTop = relativeTop; const { viewportHeight } = this.state; const top = Math.max(relativeTop, 0); const height = relativeTop < 0 ? viewportHeight + relativeTop : viewportHeight; const listStart = Math.max(((top / TR_HEIGHT) | 0) - ROW_MARGIN, 0); const listEnd = listStart + ROW_MARGIN * 2 + Math.ceil(height / TR_HEIGHT); if (listStart !== this.listStart || listEnd !== this.listEnd) { this.listStart = listStart; this.listEnd = listEnd; this.props.appUpdate({}); } }; private onResize = () => { const viewportHeight = viewport().height; this.setState({ viewportHeight }); }; private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => { this.setState({ filter }); }; } function divisibleBy(n: number, dividor: number): number { return n - (n % dividor); }