UNPKG

@1771technologies/lytenyte-pro

Version:

Blazingly fast headless React data grid with 100s of features.

687 lines (686 loc) 32.1 kB
import {} from "../+types.js"; import { useRef } from "react"; import { makeColumnView } from "./helpers/column-view.js"; import { computeBounds, computeColumnPositions, computeRowPositions, makeLayoutState, updateFull, } from "@1771technologies/lytenyte-shared"; import { computed, effect, makeAtom, signal } from "@1771technologies/lytenyte-core/yinternal"; import { makeRowLayout } from "./helpers/row-layout/row-layout.js"; import { equal, rangesOverlap } from "@1771technologies/lytenyte-shared"; import { makeColumnLayout } from "./helpers/column-layout.js"; import { emptyRowDataSource } from "./helpers/empty-row-data-source.js"; import { getFullWidthCallback } from "./helpers/get-full-width-callback.js"; import { getSpanFn } from "./helpers/get-span-callback.js"; import { makeColumnField } from "./api/column-field.js"; import { makeSortForColumn } from "./api/sort-for-column.js"; import { columnAddRowGroup } from "./helpers/column-add-row-group.js"; import { makeEventListeners } from "./api/event-listeners.js"; import { makeScrollIntoView } from "./api/scroll-into-view.js"; import { makeColumnByIndex } from "./api/column-from-index.js"; import { makeColumnIndex } from "./api/column-index.js"; import { makeRowGroupColumnIndex } from "./api/row-group-column-index.js"; import { makeRowGroupIsExpanded } from "./api/row-group-is-expanded.js"; import { makeRowGroupToggle } from "./api/row-group-toggle.js"; import { makeRowGroupApplyExpansions } from "./api/row-group-apply-expansions.js"; import { makeEditBegin } from "./api/edit-begin.js"; import { makeEditIsCellActive } from "./api/edit-is-cell-active.js"; import { makeEditEnd } from "./api/edit-end.js"; import { makeEditUpdate } from "./api/edit-update.js"; import { makeRowById } from "./api/row-by-id.js"; import { makeRowByIndex } from "./api/row-by-index.js"; import { makeRowDetailIsExpanded } from "./api/row-detail-is-expanded.js"; import { makeRowDetailToggle } from "./api/row-detail-toggle.js"; import { makeRowDetailRenderedHeight } from "./api/row-detail-rendered-height.js"; import { columnHandleMarker } from "./helpers/column-marker.js"; import { makeRowSelect } from "./api/row-select.js"; import { makeRowSelectAll } from "./api/row-select-all.js"; import { makeRowSelected } from "./api/row-selected.js"; import { makeRowHandleSelect } from "./api/row-handle-select.js"; import { makeUseRowDrag } from "./api/use-row-drag.js"; import { makeColumnById } from "./api/column-by-id.js"; import { makeColumnResize } from "./api/column-resize.js"; import { makeColumnUpdate } from "./api/column-update.js"; import { makeColumnMove } from "./api/column-move.js"; import { makeColumnGroupToggle } from "./api/column-group-toggle.js"; import { makeColumnAutosize } from "./api/column-autosize.js"; import { makeExportCsv, makeExportCsvFile } from "./api/export-csv.js"; import { makeExportDataRect } from "./api/export-data-rect.js"; import { makeDialogFrameClose, makeDialogFrameOpen } from "./api/dialog-frame.js"; import { makePopoverFrameClose, makePopoverFrameOpen } from "./api/popover-frame.js"; import { makePositionFromElement } from "./api/position-from-element.js"; import { splitCellSelectionRect, } from "../cell-selection/split-cell-selection-rect.js"; import { boundSelectionRect } from "../cell-selection/bound-selection-rect.js"; import { makeCellRoot } from "./api/cell-root.js"; import { splitOnPivot } from "../cell-selection/split-on-pivot.js"; import { makeRowStore } from "@1771technologies/lytenyte-core/yinternal"; const EMPTY_POSITION_ARRAY = new Uint32Array(); const DEFAULT_HEADER_HEIGHT = 40; const COLUMN_GROUP_JOIN_DELIMITER = "-->"; export function makeLyteNyte(p) { /** * Primitive ATOMS and State */ const rtl = signal(p.rtl ?? false); const columns = signal(p.columns ?? []); const base = signal(p.columnBase ?? {}); const gridId = signal(p.gridId); const headerHeight = signal(p.headerHeight ?? DEFAULT_HEADER_HEIGHT); const headerGroupHeight = signal(p.headerGroupHeight ?? DEFAULT_HEADER_HEIGHT); const columnSizeToFit = signal(p.columnSizeToFit ?? false); const viewport = signal(null); const viewportHeightInner = signal(0); const viewportHeightOuter = signal(0); const viewportWidthInner = signal(0); const viewportWidthOuter = signal(0); const columnGroupJoinDelimiter = signal(p.columnGroupJoinDelimiter ?? COLUMN_GROUP_JOIN_DELIMITER); const columnGroupExpansions = signal(p.columnGroupExpansions ?? {}); const columnGroupDefaultExpansion = signal(p.columnGroupDefaultExpansion ?? true); const rowDataSource = signal(emptyRowDataSource); const rowHeight = signal(40); if (typeof p.rowHeight === "function") rowHeight.set(() => p.rowHeight); else rowHeight.set(p.rowHeight ?? 40); const rowAutoHeightGuess = signal(p.rowAutoHeightGuess ?? 40); const rowScanDistance = signal(p.rowScanDistance ?? 100); const colScanDistance = signal(p.colScanDistance ?? 100); const rowOverscanTop = signal(p.rowOverscanTop ?? 10); const rowOverscanBottom = signal(p.rowOverscanBottom ?? 10); const colOverScanStart = signal(p.colOverscanStart ?? 2); const colOverscanEnd = signal(p.colOverscanEnd ?? 2); const xScroll = signal(0); const yScroll = signal(0); const cellRenderers = signal(p.cellRenderers ?? {}); const rowFullWidthPredicate = signal({ fn: p.rowFullWidthPredicate ?? null }); const rowFullWidthRenderer = signal({ fn: p.rowFullWidthRenderer ?? (() => "Not defined") }); const sortModel = signal(p.sortModel ?? []); const filterModel = signal(p.filterModel ?? {}); const filterInModel = signal(p.filterInModel ?? {}); const rowGroupModel = signal(p.rowGroupModel ?? []); const aggModel = signal(p.aggModel ?? {}); const rowGroupDisplayMode = signal(p.rowGroupDisplayMode ?? "single-column"); const rowGroupDefaultExpansion = signal(p.rowGroupDefaultExpansion ?? false); const rowGroupExpansions = signal(p.rowGroupExpansions ?? {}); const rowGroupColumn = signal(p.rowGroupColumn ?? {}); const headerCellRenderers = signal(p.headerCellRenderers ?? {}); const floatingCellRenderers = signal(p.floatingCellRenderers ?? {}); const floatingRowEnabled = signal(p.floatingRowEnabled ?? false); const floatingRowHeight = signal(p.floatingRowHeight ?? 40); const editRenderers = signal(p.editRenderers ?? {}); const editRowValidatorFn = signal({ fn: p.editRowValidatorFn ?? (() => true) }); const editClickActivator = signal(p.editClickActivator ?? "single"); const editCellMode = signal(p.editCellMode ?? "readonly"); const columnMarker = signal(p.columnMarker ?? {}); const columnMarkerEnabled = signal(p.columnMarkerEnabled ?? false); const columnDoubleClickToAutosize = signal(p.columnDoubleClickToAutosize ?? true); const rowDetailHeight = signal(p.rowDetailHeight ?? 300); const rowDetailAutoHeightGuess = signal(p.rowDetailAutoHeightGuess ?? 300); const rowDetailExpansions = signal(p.rowDetailExpansions ?? new Set()); const rowDetailRenderer = signal({ fn: p.rowDetailRenderer ?? (() => "Not defined") }); const rowSelectedIds = signal(p.rowSelectedIds ?? new Set()); const rowSelectionMode = signal(p.rowSelectionMode ?? "none"); const rowSelectChildren = signal(p.rowSelectChildren ?? false); const rowSelectionActivator = signal(p.rowSelectionActivator ?? "single-click"); const virtualizeRows = signal(p.virtualizeRows ?? true); const virtualizeCols = signal(p.virtualizeCols ?? true); const quickSearch = signal(p.quickSearch ?? null); const quickSearchSensitivity = signal(p.quickSearchSensitivity ?? "case-insensitive"); const columnPivotMode = signal(p.columnPivotMode ?? false); const columnPivotModel = signal(p.columnPivotModel ?? { columns: [], filters: {}, filtersIn: {}, rows: [], sorts: [], values: [], }); const columnPivotColumns = signal([]); const columnPivotColumnGroupExpansions = signal({}); const columnPivotRowGroupExpansions = signal({}); const cellSelections = signal(p.cellSelections ?? []); const cellSelectionMode = signal(p.cellSelectionMode ?? "none"); const internal__rowGroupColumnState = signal({}); const internal_cellSelectionPivot = signal(null); const internal_cellSelectionAdditive = signal(null); const internal_cellSelectionSplits = computed(() => { const selections = cellSelections(); // @ts-expect-error the $() is defined but not typed. const topCount = rowDataStore.rowTopCount.$(); // @ts-expect-error the $() is defined but not typed. const centerCount = rowDataStore.rowCenterCount.$(); const meta = columnMeta(); const p = internal_cellSelectionPivot(); const splits = selections.flatMap((rect) => { return splitCellSelectionRect({ rect, rowTopCount: topCount, rowCenterCount: centerCount, colStartCount: meta.columnVisibleStartCount, colCenterCount: meta.columnVisibleCenterCount, }); }); const firstWithinPivot = splits.findIndex((c) => p && p.rowStart >= c.rowStart && p.rowEnd <= c.rowEnd && p.columnStart >= c.columnStart && p.columnEnd <= c.columnEnd); if (firstWithinPivot !== -1) { const pivotSplits = splitOnPivot(splits[firstWithinPivot], p); if (pivotSplits) splits.splice(firstWithinPivot, 1, ...pivotSplits); else splits.splice(firstWithinPivot, 1); } return splits; }); const dialogFrame = signal(p.dialogFrames ?? {}); const popoverFrame = signal(p.popoverFrames ?? {}); const internal_rowSelectionPivot = signal(null); const internal_rowSelectionLastWasDeselect = signal(false); const internal_draggingHeader = signal(null); const internal_rowAutoHeightCache = signal({}); const internal_rowDetailHeightCache = signal({}); const internal_focusActive = signal(null); const internal_focusPrevCol = signal(null); const internal_focusPrevRow = signal(null); const internal_editActivePosition = signal(null); const internal_editData = signal(null); const internal_editValidation = signal(true); const internal_dialogFrames = signal({}); const internal_popoverFrames = signal({}); const columnCount$ = computed(() => { const view = columnView(); return view.visibleColumns.length; }); const layoutState = makeLayoutState(0); const layoutState$ = computed(() => { // @ts-expect-error the $() is defined but not typed. rowDataStore.rowBottomCount.$(); // @ts-expect-error the $() is defined but not typed. rowDataStore.rowCenterCount.$(); // @ts-expect-error the $() is defined but not typed. rowDataStore.rowTopCount.$(); rowDataSource(); rowDetailExpansions(); rowGroupModel(); sortModel(); filterModel(); aggModel(); filterInModel(); columnPivotModel(); columns(); columnPivotColumns(); // @ts-expect-error the $() is defined but not typed. const rowCount = rowDataStore.rowCount.$(); const columnCount = columnCount$(); if (rowCount > layoutState.computed.length || columnCount != layoutState.base.length) { Object.assign(layoutState, makeLayoutState(columnCount, rowCount + 2000)); } else { layoutState.computed.fill(0); layoutState.special.fill(0); layoutState.lookup.clear(); } return { layout: { ...layoutState }, cache: new Map() }; }); /** * VIEW BOUNDS */ const bounds = computed(() => { const vpWidth = viewportWidthInner(); const vpHeight = viewportHeightInner(); const scrollTop = yScroll(); const scrollLeft = xScroll(); const xPos = xPositions(); const yPos = yPositions(); // @ts-expect-error the $() is defined but not typed. const topCount = rowDataStore.rowTopCount.$(); // @ts-expect-error the $() is defined but not typed. const bottomCount = rowDataStore.rowBottomCount.$(); const start = startCount(); const end = endCount(); const bounds = computeBounds({ viewportWidth: vpWidth, viewportHeight: vpHeight, scrollTop, scrollLeft, xPositions: xPos, yPositions: yPos, topCount, bottomCount, startCount: start, endCount: end, rowOverscanTop: rowOverscanTop(), rowOverscanBottom: rowOverscanBottom(), colOverscanStart: colOverScanStart(), colOverscanEnd: colOverscanEnd(), }); return bounds; }, { dirty: (prev, next) => !equal(prev, next) }); const internal_colBounds = computed(() => { const b = bounds(); return [b.colCenterStart, b.colCenterEnd]; }, { dirty: (l, r) => !equal(l, r) }); /** * COLUMN VIEW * Compute the column layout. This impacts both the row layout and header layout. Column layout * is impacted by the column definitions, the group expansions, the row group display mode. */ const columnView = computed(() => { const groupState = internal__rowGroupColumnState(); const pivotMode = columnPivotMode(); let cols; if (pivotMode) { const model = columnPivotModel(); cols = columnAddRowGroup({ columns: columnPivotColumns(), rowGroupDisplayMode: "single-column", rowGroupModel: model.rows.filter((c) => c.active ?? true).map((c) => c.field), rowGroupTemplate: rowGroupColumn(), rowGroupColumnState: groupState, }); } else { cols = columnAddRowGroup({ columns: columns(), rowGroupDisplayMode: rowGroupDisplayMode(), rowGroupModel: rowGroupModel(), rowGroupTemplate: rowGroupColumn(), rowGroupColumnState: groupState, }); } const colsWithMarker = columnHandleMarker({ columns: cols, marker: columnMarker(), markerEnabled: columnMarkerEnabled(), }); const view = makeColumnView({ columns: colsWithMarker, base: base(), groupExpansionDefault: columnGroupDefaultExpansion(), groupExpansions: columnGroupExpansions(), groupJoinDelimiter: columnGroupJoinDelimiter(), }); return view; }); /** * COMPUTE ATOMS */ const headerLayoutFull = computed(() => { const view = columnView(); const layout = makeColumnLayout(view, floatingRowEnabled()); return { maxCol: view.maxCol, maxRow: view.maxRow, layout }; }); const headerPosition$ = computed(() => { const focus = internal_focusActive(); return focus?.kind === "header-cell" || focus?.kind === "floating-cell" || focus?.kind === "header-group-cell" ? focus : null; }); const headerLayout = computed(() => { const view = columnView(); const layout = headerLayoutFull(); const spanBounds = bounds(); const headerPos = headerPosition$(); const filtered = layout.layout.map((row) => { return row.filter((col) => { if (col.colPin) return true; if (headerPos) { if (rangesOverlap(col.colStart, col.colEnd, headerPos.colIndex, headerPos.colIndex + 1)) return true; } return col.colStart >= spanBounds.colCenterStart && col.colStart < spanBounds.colCenterEnd; }); }); const dragged = internal_draggingHeader(); // When dragging a group column, we may end up removing that group column when joining // the group with other columns in the same group (if they are separated). We want to ensure // the group remains mounted until after the drag. Hence we check if there would've been a merge // and add the dragged element to the layout. Once the drag ends the element will be removed. The // isHiddenMove is important for this. if (dragged) { const row = dragged.rowStart; const has = filtered[row].findIndex((c) => c.kind === "group" && c.idOccurrence === dragged.idOccurrence); if (has === -1) { filtered[row].push({ ...dragged, isHiddenMove: true }); } } return { maxCol: view.maxCol, maxRow: view.maxRow, layout: filtered }; }); const columnGroupMeta = computed(() => columnView().meta); const columnMeta = computed(() => { const view = columnView(); return { columnLookup: view.lookup, columnsVisible: view.visibleColumns, columnVisibleCenterCount: view.centerCount, columnVisibleEndCount: view.endCount, columnVisibleStartCount: view.startCount, }; }); const xPositions = computed(() => { if (!viewport()) return EMPTY_POSITION_ARRAY; const view = columnView(); return computeColumnPositions(view.visibleColumns, base(), viewportWidthInner(), columnSizeToFit()); }); const widthTotal = computed(() => xPositions().at(-1)); const headerHeightTotal = computed(() => { const groupHHeight = headerGroupHeight(); const hHeight = headerHeight(); const view = columnView(); const floating = floatingRowEnabled(); const floatingHeight = floatingRowHeight(); return (view.maxRow - 1) * groupHHeight + hHeight + (floating ? floatingHeight : 0); }); const startCount = computed(() => columnView().startCount); const endCount = computed(() => columnView().endCount); /** ROWS */ const rowDataStore = makeRowStore({ getRow: (r) => { return rowDataSource().rowByIndex(r); }, }); const yPositions = computed(() => { if (!viewport()) return EMPTY_POSITION_ARRAY; // These can change the positions sortModel(); filterModel(); rowGroupModel(); columnPivotModel(); // @ts-expect-error the $() is defined but not typed. const rowCount = rowDataStore.rowCount.$(); const innerHeight = viewportHeightInner(); const detailExpansions = rowDetailExpansions(); internal_rowDetailHeightCache(); const rds = rowDataSource(); const rows = Object.fromEntries([...detailExpansions] .map((x) => rds.rowToIndex(x)) .filter((x) => x != null) .map((x) => [x, api.rowByIndex(x)])); const headerHeight = headerHeightTotal(); const x = computeRowPositions(rowCount, rowHeight(), rowAutoHeightGuess(), internal_rowAutoHeightCache(), (i) => { const row = rows[i]; if (!row || !api.rowDetailIsExpanded(row)) return 0; return api.rowDetailRenderedHeight(row); }, innerHeight - headerHeight); return x; }); const heightTotal = computed(() => yPositions().at(-1)); const hasSpans = computed(() => { const visible = columnMeta().columnsVisible; return cellSelectionMode() !== "none" || !visible.every((c) => !(c.colSpan || c.rowSpan)); }); const rowView = computed(() => { if (!viewport()) return { bottom: [], center: [], rowBottomTotalHeight: 0, rowCenterTotalHeight: 0, rowFirstCenter: 0, rowFocusedIndex: 0, rowTopTotalHeight: 0, top: [], }; let n = bounds(); if (!virtualizeRows()) { n = { ...n, rowCenterStart: n.rowTopEnd, rowCenterEnd: n.rowCenterLast }; } if (!virtualizeCols()) { n = { ...n, colCenterStart: n.colStartEnd, colCenterEnd: n.colCenterLast }; } const rowScan = rowScanDistance(); const columns = columnMeta().columnsVisible; const rds = rowDataSource(); const { layout, cache } = layoutState$(); // @ts-expect-error the $() is defined but not typed. const topCount = rowDataStore.rowTopCount.$(); // @ts-expect-error the $() is defined but not typed. const botCount = rowDataStore.rowBottomCount.$(); // @ts-expect-error the $() is defined but not typed. const rowCount = rowDataStore.rowCount.$(); const expansions = rowDetailExpansions(); const fullWidthPredicate = rowFullWidthPredicate().fn; updateFull({ topCount, botCount, startCount: n.colStartEnd, endCount: n.colEndEnd - n.colEndStart, centerCount: n.colCenterLast - n.colStartEnd, computeColSpan: getSpanFn(rds, grid, columns, "col"), computeRowSpan: getSpanFn(rds, grid, columns, "row"), isFullWidth: fullWidthPredicate ? getFullWidthCallback(rds, fullWidthPredicate, grid) : null, isRowCutoff: (r) => { const row = rds.rowByIndex(r); return !row || row.kind === "branch" || expansions.has(row.id); }, rowScanDistance: rowScan, rowStart: n.rowCenterStart, rowEnd: n.rowCenterEnd, rowMax: n.rowCenterLast, ...layout, }); const yPos = yPositions(); const botStart = rowCount - botCount; const focus = internal_focusActive(); const view = makeRowLayout({ view: n, viewCache: cache, layout, rds: rowDataStore, columns, focus: internal_focusActive(), }); const topHeight = yPos[topCount]; const botHeight = yPos.at(-1) - yPos[botStart]; const centerHeight = yPos.at(-1) - topHeight - botHeight; return { ...view, rowFocusedIndex: focus?.kind === "cell" || focus?.kind === "full-width" ? focus.rowIndex < topCount || focus.rowIndex >= rowCount - botCount ? null : focus.rowIndex : null, rowTopTotalHeight: topHeight, rowBottomTotalHeight: botHeight, rowCenterTotalHeight: centerHeight, rowFirstCenter: n.rowCenterStart, }; }); /** * STATE VIEW AND API */ const gridView = computed(() => { return { header: headerLayout(), rows: rowView(), }; }); const state = { rtl: makeAtom(rtl), columns: makeAtom(columns), columnMeta: makeAtom(columnMeta), columnBase: makeAtom(base), columnGroupExpansions: makeAtom(columnGroupExpansions), columnGroupDefaultExpansion: makeAtom(columnGroupDefaultExpansion), columnGroupJoinDelimiter: makeAtom(columnGroupJoinDelimiter), columnGroupMeta: makeAtom(columnGroupMeta), columnSizeToFit: makeAtom(columnSizeToFit), gridId: makeAtom(gridId), widthTotal: makeAtom(widthTotal), heightTotal: makeAtom(heightTotal), headerHeight: makeAtom(headerHeight), headerGroupHeight: makeAtom(headerGroupHeight), viewport: makeAtom(viewport), viewportHeightInner: makeAtom(viewportHeightInner), viewportHeightOuter: makeAtom(viewportHeightOuter), viewportWidthInner: makeAtom(viewportWidthInner), viewportWidthOuter: makeAtom(viewportWidthOuter), xPositions: makeAtom(xPositions), yPositions: makeAtom(yPositions), rowDataStore, rowDataSource: makeAtom(rowDataSource), rowAutoHeightGuess: makeAtom(rowAutoHeightGuess), rowHeight: makeAtom(rowHeight), rowScanDistance: makeAtom(rowScanDistance), colScanDistance: makeAtom(colScanDistance), colOverscanStart: makeAtom(colOverScanStart), colOverscanEnd: makeAtom(colOverscanEnd), rowOverscanTop: makeAtom(rowOverscanTop), rowOverscanBottom: makeAtom(rowOverscanBottom), rowFullWidthPredicate: makeAtom(rowFullWidthPredicate), rowFullWidthRenderer: makeAtom(rowFullWidthRenderer), cellRenderers: makeAtom(cellRenderers), sortModel: makeAtom(sortModel), filterModel: makeAtom(filterModel), filterInModel: makeAtom(filterInModel), rowGroupModel: makeAtom(rowGroupModel), aggModel: makeAtom(aggModel), rowGroupColumn: makeAtom(rowGroupColumn), rowGroupDefaultExpansion: makeAtom(rowGroupDefaultExpansion), rowGroupDisplayMode: makeAtom(rowGroupDisplayMode), rowGroupExpansions: makeAtom(rowGroupExpansions), headerCellRenderers: makeAtom(headerCellRenderers), floatingCellRenderers: makeAtom(floatingCellRenderers), floatingRowEnabled: makeAtom(floatingRowEnabled), floatingRowHeight: makeAtom(floatingRowHeight), editRenderers: makeAtom(editRenderers), editCellMode: makeAtom(editCellMode), editClickActivator: makeAtom(editClickActivator), editRowValidatorFn: makeAtom(editRowValidatorFn), editActivePosition: makeAtom(computed(() => internal_editActivePosition())), columnMarker: makeAtom(columnMarker), columnMarkerEnabled: makeAtom(columnMarkerEnabled), columnDoubleClickToAutosize: makeAtom(columnDoubleClickToAutosize), rowDetailExpansions: makeAtom(rowDetailExpansions), rowDetailHeight: makeAtom(rowDetailHeight), rowDetailRenderer: makeAtom(rowDetailRenderer), rowDetailAutoHeightGuess: makeAtom(rowDetailAutoHeightGuess), rowSelectedIds: makeAtom(rowSelectedIds), rowSelectionMode: makeAtom(rowSelectionMode), rowSelectionPivot: makeAtom(internal_rowSelectionPivot), rowSelectionActivator: makeAtom(rowSelectionActivator), rowSelectChildren: makeAtom(rowSelectChildren), viewBounds: makeAtom(bounds), virtualizeRows: makeAtom(virtualizeRows), virtualizeCols: makeAtom(virtualizeCols), quickSearch: makeAtom(quickSearch), quickSearchSensitivity: makeAtom(quickSearchSensitivity), columnPivotMode: makeAtom(columnPivotMode), columnPivotColumns: makeAtom(columnPivotColumns), columnPivotModel: makeAtom(columnPivotModel), columnPivotColumnGroupExpansions: makeAtom(columnPivotColumnGroupExpansions), columnPivotRowGroupExpansions: makeAtom(columnPivotRowGroupExpansions), dialogFrames: makeAtom(dialogFrame), popoverFrames: makeAtom(popoverFrame), cellSelections: makeAtom(cellSelections), cellSelectionMode: makeAtom(cellSelectionMode), }; const api = {}; const grid = { state, view: makeAtom(gridView), api }; const listeners = makeEventListeners(); Object.assign(api, { cellRoot: makeCellRoot(grid), columnField: makeColumnField(grid), columnByIndex: makeColumnByIndex(grid), columnIndex: makeColumnIndex(grid), columnById: makeColumnById(grid), columnResize: makeColumnResize(grid), columnUpdate: makeColumnUpdate(grid), columnMove: makeColumnMove(grid), sortForColumn: makeSortForColumn(grid), rowIsGroup: (r) => r.kind === "branch", rowIsLeaf: (r) => r.kind === "leaf", eventAddListener: listeners.eventAddListener, eventRemoveListener: listeners.eventRemoveListener, eventFire: listeners.eventFire, rowGroupColumnIndex: makeRowGroupColumnIndex(grid), rowGroupIsExpanded: makeRowGroupIsExpanded(grid), rowGroupToggle: makeRowGroupToggle(grid), rowGroupApplyExpansions: makeRowGroupApplyExpansions(grid), scrollIntoView: makeScrollIntoView(grid), editBegin: makeEditBegin(grid), editEnd: makeEditEnd(grid), editIsCellActive: makeEditIsCellActive(grid), editUpdate: makeEditUpdate(grid), rowById: makeRowById(grid), rowByIndex: makeRowByIndex(grid), rowDetailIsExpanded: makeRowDetailIsExpanded(grid), rowDetailRenderedHeight: makeRowDetailRenderedHeight(grid), rowDetailToggle: makeRowDetailToggle(grid), rowSelect: makeRowSelect(grid), rowSelectAll: makeRowSelectAll(grid), rowSelected: makeRowSelected(grid), rowHandleSelect: makeRowHandleSelect(grid), columnToggleGroup: makeColumnGroupToggle(grid), columnAutosize: makeColumnAutosize(grid), useRowDrag: makeUseRowDrag(grid), exportCsv: makeExportCsv(grid), exportCsvFile: makeExportCsvFile(grid), exportDataRect: makeExportDataRect(grid), dialogFrameClose: makeDialogFrameClose(grid), dialogFrameOpen: makeDialogFrameOpen(grid), popoverFrameClose: makePopoverFrameClose(grid), popoverFrameOpen: makePopoverFrameOpen(grid), positionFromElement: makePositionFromElement(grid), }); Object.assign(grid, { internal: { headerCols: makeAtom(computed(() => columnView().maxCol)), headerRows: makeAtom(computed(() => columnView().maxRow)), headerHeightTotal: makeAtom(headerHeightTotal), xScroll: makeAtom(xScroll), yScroll: makeAtom(yScroll), layout: layoutState, colBounds: makeAtom(internal_colBounds), hasSpans: makeAtom(hasSpans), focusActive: makeAtom(internal_focusActive), focusPrevColIndex: makeAtom(internal_focusPrevCol), focusPrevRowIndex: makeAtom(internal_focusPrevRow), editActivePos: makeAtom(internal_editActivePosition), editData: makeAtom(internal_editData), editValidation: makeAtom(internal_editValidation), rowAutoHeightCache: makeAtom(internal_rowAutoHeightCache), rowDetailAutoHeightCache: makeAtom(internal_rowDetailHeightCache), rowSelectedIds: rowSelectedIds, rowSelectionPivot: makeAtom(internal_rowSelectionPivot), rowSelectionLastWasDeselect: makeAtom(internal_rowSelectionLastWasDeselect), draggingHeader: makeAtom(internal_draggingHeader), rowGroupColumnState: makeAtom(internal__rowGroupColumnState), dialogFrames: makeAtom(internal_dialogFrames), popoverFrames: makeAtom(internal_popoverFrames), cellSelectionPivot: makeAtom(internal_cellSelectionPivot), cellSelectionAdditiveRects: makeAtom(internal_cellSelectionAdditive), cellSelectionIsDeselect: makeAtom(signal(false)), cellSelectionSplits: makeAtom(internal_cellSelectionSplits), }, }); effect(() => { rowDataSource().init(grid); }); rowDataSource.set(p.rowDataSource ?? emptyRowDataSource); // Ensure cell selections are bounded. effect(() => { // @ts-expect-error the $() is defined but not typed. rowDataStore.rowCount.$(); columnMeta(); const selections = cellSelections(); const safeSelections = selections.map((c) => boundSelectionRect(grid, c)); if (equal(selections, safeSelections)) return; cellSelections.set(selections); }); return grid; } export function useLyteNyte(p) { const m = useRef(null); if (!m.current) { m.current = makeLyteNyte(p); } return m.current; }