UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

270 lines 11.9 kB
"use strict"; /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useTableRedraw = exports.RedrawContext = exports.DataSourceRendererVirtual = void 0; const react_1 = __importStar(require("react")); const react_virtual_1 = require("react-virtual"); const observe_rect_1 = __importDefault(require("@reach/observe-rect")); // how fast we update if updates are low-prio (e.g. out of window and not super significant) const LOW_PRIO_UPDATE = 1000; //ms const HIGH_PRIO_UPDATE = 40; // 25fps const SMALL_DATASET = 1000; // what we consider a small dataset, for which we keep all updates snappy var UpdatePrio; (function (UpdatePrio) { UpdatePrio[UpdatePrio["NONE"] = 0] = "NONE"; UpdatePrio[UpdatePrio["LOW"] = 1] = "LOW"; UpdatePrio[UpdatePrio["HIGH"] = 2] = "HIGH"; })(UpdatePrio || (UpdatePrio = {})); /** * This component is UI agnostic, and just takes care of virtualizing the provided dataSource, and render it as efficiently a possibible, * de priorizing off screen updates etc. */ exports.DataSourceRendererVirtual = (0, react_1.memo)(function DataSourceRendererVirtual({ dataView, defaultRowHeight, useFixedRowHeight, context, itemRenderer, autoScroll, onKeyDown, virtualizerRef, onRangeChange, onUpdateAutoScroll, emptyRenderer, }) { /** * Virtualization */ // render scheduling const renderPending = (0, react_1.useRef)(UpdatePrio.NONE); const lastRender = (0, react_1.useRef)(Date.now()); const [, setForceUpdate] = (0, react_1.useState)(0); const forceHeightRecalculation = (0, react_1.useRef)(0); const parentRef = react_1.default.useRef(null); const isUnitTest = useInUnitTest(); const virtualizer = (0, react_virtual_1.useVirtual)({ size: dataView.size, parentRef, useObserver: isUnitTest ? () => ({ height: 500, width: 1000 }) : undefined, // eslint-disable-next-line estimateSize: (0, react_1.useCallback)(() => defaultRowHeight, [forceHeightRecalculation.current, defaultRowHeight]), // TODO: optimise by using setting a keyExtractor if DataSource is keyed overscan: 0, }); if (virtualizerRef) { virtualizerRef.current = virtualizer; } const redraw = (0, react_1.useCallback)(() => { forceHeightRecalculation.current++; setForceUpdate((x) => x + 1); }, []); (0, react_1.useEffect)(function subscribeToDataSource() { const forceUpdate = () => { if (unmounted) { return; } timeoutHandle = undefined; setForceUpdate((x) => x + 1); }; let unmounted = false; let timeoutHandle = undefined; function rerender(prio, invalidateHeights = false) { if (invalidateHeights && !useFixedRowHeight) { // the height of some existing rows might have changed forceHeightRecalculation.current++; } if (isUnitTest) { // test environment, update immediately forceUpdate(); return; } if (renderPending.current >= prio) { // already scheduled an update with equal or higher prio return; } renderPending.current = Math.max(renderPending.current, prio); if (prio === UpdatePrio.LOW) { // Possible optimization: make DEBOUNCE depend on how big the relative change is, and how far from the current window if (!timeoutHandle) { timeoutHandle = setTimeout(forceUpdate, LOW_PRIO_UPDATE); } } else { // High, drop low prio timeout if (timeoutHandle) { clearTimeout(timeoutHandle); timeoutHandle = undefined; } if (lastRender.current < Date.now() - HIGH_PRIO_UPDATE) { forceUpdate(); // trigger render now } else { // debounced timeoutHandle = setTimeout(forceUpdate, HIGH_PRIO_UPDATE); } } } const unsubscribe = dataView.addListener((event) => { switch (event.type) { case 'reset': rerender(UpdatePrio.HIGH, true); break; case 'shift': if (dataView.size < SMALL_DATASET) { rerender(UpdatePrio.HIGH, false); } else if (event.location === 'in' || // to support smooth tailing we want to render on records directly at the end of the window immediately as well (event.location === 'after' && event.delta > 0 && event.index === dataView.windowEnd)) { rerender(UpdatePrio.HIGH, false); } else { // optimization: we don't want to listen to every count change, especially after window // and in some cases before window rerender(UpdatePrio.LOW, false); } break; case 'update': // in visible range, so let's force update rerender(UpdatePrio.HIGH, true); break; } }); return () => { unmounted = true; unsubscribe(); }; }, [setForceUpdate, useFixedRowHeight, isUnitTest, dataView]); (0, react_1.useEffect)(() => { // initial virtualization is incorrect because the parent ref is not yet set, so trigger render after mount setForceUpdate((x) => x + 1); }, [setForceUpdate]); (0, react_1.useLayoutEffect)(function updateWindow() { const start = virtualizer.virtualItems[0]?.index ?? 0; const end = start + virtualizer.virtualItems.length; if (start !== dataView.windowStart && !autoScroll) { onRangeChange?.(start, end, dataView.size, parentRef.current?.scrollTop ?? 0); } dataView.setWindow(start, end); }); /** * Scrolling */ const onScroll = (0, react_1.useCallback)(() => { const elem = parentRef.current; if (!elem) { return; } const fromEnd = elem.scrollHeight - elem.scrollTop - elem.clientHeight; if (autoScroll && fromEnd > 1) { onUpdateAutoScroll?.(false); } else if (!autoScroll && fromEnd < 1) { onUpdateAutoScroll?.(true); } }, [onUpdateAutoScroll, autoScroll]); (0, react_1.useLayoutEffect)(function scrollToEnd() { if (autoScroll) { virtualizer.scrollToIndex(dataView.size - 1, /* smooth is not typed by react-virtual, but passed on to the DOM as it should*/ { align: 'end', behavior: 'smooth', }); } }); /** * Render finalization */ renderPending.current = UpdatePrio.NONE; lastRender.current = Date.now(); /** * Observer parent height */ (0, react_1.useEffect)(function redrawOnResize() { if (!parentRef.current) { return; } let lastWidth = 0; const observer = (0, observe_rect_1.default)(parentRef.current, (rect) => { if (lastWidth !== rect.width) { lastWidth = rect.width; redraw(); } }); observer.observe(); return () => observer.unobserve(); }, [redraw]); /** * Rendering */ return (react_1.default.createElement(exports.RedrawContext.Provider, { value: redraw }, react_1.default.createElement("div", { ref: parentRef, onScroll: onScroll, style: tableContainerStyle }, virtualizer.virtualItems.length === 0 ? emptyRenderer?.(dataView) : null, react_1.default.createElement("div", { style: { ...tableWindowStyle, height: virtualizer.totalSize, }, onKeyDown: onKeyDown, tabIndex: 0 }, virtualizer.virtualItems.map((virtualRow) => { const value = dataView.get(virtualRow.index); if (value === undefined) { console.error(`DataSourceRendererVirtual -> unexpected out-of-bound value. Data view has ${dataView.size} items. Requested ${virtualRow.index}. Virtuaslizer has ${virtualizer.virtualItems.length} items.`); throw new Error(`DataSourceRendererVirtual -> unexpected out-of-bound value. Data view has ${dataView.size} items. Requested ${virtualRow.index}. Virtuaslizer has ${virtualizer.virtualItems.length} items.`); } // the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always. // Also all row containers are renderd as part of same component to have 'less react' framework code in between*/} return (react_1.default.createElement("div", { key: virtualRow.index, style: { position: 'absolute', top: 0, left: 0, width: '100%', height: useFixedRowHeight ? virtualRow.size : undefined, transform: `translateY(${virtualRow.start}px)`, }, ref: useFixedRowHeight ? undefined : virtualRow.measureRef }, itemRenderer(value, virtualRow.index, context))); }))))); }); const tableContainerStyle = { overflowY: 'auto', overflowX: 'hidden', display: 'flex', // because: https://stackoverflow.com/questions/37386244/what-does-flex-1-mean flex: `1 1 0`, }; const tableWindowStyle = { position: 'relative', width: '100%', }; exports.RedrawContext = (0, react_1.createContext)(undefined); function useTableRedraw() { return (0, react_1.useContext)(exports.RedrawContext); } exports.useTableRedraw = useTableRedraw; function useInUnitTest() { // N.B. Not reusing flipper-common here, since data-source is published as separate package return typeof process !== 'undefined' && process?.env?.NODE_ENV === 'test'; } //# sourceMappingURL=DataSourceRendererVirtual.js.map