go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/static/_nextjs/src/components/displayValue.tsx (about) 1 /** 2 * Copyright (c) 2024 - Present. Will Charczuk. All rights reserved. 3 * Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 4 */ 5 import * as api from "../api/nodes"; 6 import { Cell, CellRenderer, Column, CoordinateData, Rect, Region, SelectionModes, Table2, Utils } from "@blueprintjs/table"; 7 import { RefObject, useCallback, useEffect, useRef, useState } from "react"; 8 import { Viewport } from "reactflow"; 9 10 export interface DisplayValueProps { 11 stabilizationNum?: number; 12 viewport?: Viewport; 13 refreshedAt?: Date; 14 valueType: string; 15 value: any; 16 nodeType: string; 17 } 18 19 export function DisplayValue(props: DisplayValueProps) { 20 const haveStabilized = props.stabilizationNum !== undefined && props.stabilizationNum !== 1; 21 const valueIsSet = props.value !== undefined && props.value != null; 22 const varIsLoading = !valueIsSet 23 const nonVarIsLoading = !haveStabilized || !valueIsSet 24 const isLoading = (props.nodeType === 'var' || props.nodeType === 'table') ? varIsLoading : nonVarIsLoading; 25 26 if (isLoading) { 27 return (<div className="display-value"><div className="bp5-skeleton">Loading ...</div></div>) 28 } 29 if (props.valueType === 'svg') { 30 return (<div className="display-value"><div className="value-type-svg" dangerouslySetInnerHTML={{ __html: String(props.value.main) }}></div></div>) 31 } 32 if (props.valueType === 'table') { 33 return (<DisplayTable {...props} />) 34 } 35 if (props.valueType === 'any') { 36 return (<pre className="display-value bp5-code-block">{JSON.stringify(props.value, null, 2)}</pre>) 37 } 38 if (props.valueType.startsWith('[]')) { 39 return (<DisplayList {...props} />) 40 } 41 return (<pre className="display-value bp5-code-block">{String(props.value)}</pre>) 42 } 43 44 export function correctTableViewport(tableRef: RefObject<Table2>, props: DisplayValueProps): () => void { 45 return () => { 46 if (!tableRef.current) { 47 return 48 } 49 if (!tableRef.current.locator) { 50 return 51 } 52 if (!props.viewport) { 53 return 54 } 55 56 const { zoom } = props.viewport; 57 58 function convertPointToColumn(clientX: number, useMidpoint?: boolean): number { 59 const tableRect = this.getTableRect(); 60 if (this.grid === undefined || !tableRect.containsX(clientX)) { 61 return -1; 62 } 63 const gridX = this.toGridX(clientX) / zoom; 64 const limit = useMidpoint ? this.grid.numCols : this.grid.numCols - 1; 65 const lookupFn = useMidpoint ? this.convertCellMidpointToClientX : this.convertCellIndexToClientX; 66 return Utils.binarySearch(gridX, limit, lookupFn); 67 } 68 69 function convertPointToRow(clientY: number, useMidpoint?: boolean): number { 70 const tableRect = this.getTableRect(); 71 if (this.grid === undefined || !tableRect.containsY(clientY)) { 72 return -1; 73 } 74 const gridY = this.toGridY(clientY) / zoom; 75 const limit = useMidpoint ? this.grid.numRows : this.grid.numRows - 1; 76 const lookupFn = useMidpoint ? this.convertCellMidpointToClientY : this.convertCellIndexToClientY; 77 return Utils.binarySearch(gridY, limit, lookupFn); 78 } 79 80 function convertPointToCell(clientX: number, clientY: number) { 81 const gridX = this.toGridX(clientX) / zoom; 82 const gridY = this.toGridY(clientY) / zoom; 83 const col = Utils.binarySearch(gridX, this.grid!.numCols - 1, this.convertCellIndexToClientX); 84 const row = Utils.binarySearch(gridY, this.grid!.numRows - 1, this.convertCellIndexToClientY); 85 return { col, row }; 86 } 87 88 tableRef.current.locator.convertPointToColumn = convertPointToColumn.bind(tableRef.current.locator); 89 tableRef.current.locator.convertPointToRow = convertPointToRow.bind(tableRef.current.locator); 90 tableRef.current.locator.convertPointToCell = convertPointToCell.bind(tableRef.current.locator); 91 } 92 } 93 94 interface RowIndices { 95 rowIndexStart: number; 96 rowIndexEnd: number; 97 } 98 interface ColumnIndices { 99 columnIndexStart: number; 100 columnIndexEnd: number; 101 } 102 103 export function DisplayList(props: DisplayValueProps) { 104 const tableRef = useRef<Table2>(null) 105 const list = props.value as any[]; 106 const [visibleRegion, setVisibleRegion] = useState<api.TableRange | undefined>() 107 108 useEffect(correctTableViewport(tableRef, props), [tableRef.current, props.viewport]) 109 110 const atRow = (rowIndex: number): any => { 111 // const rowOffset = visibleRegion !== undefined ? visibleRegion.top : 0; 112 // const effectiveRowIndex = rowIndex - rowOffset; 113 if (list && list.length > rowIndex) { 114 return list[rowIndex] 115 } 116 return null 117 } 118 119 const renderCell = useCallback((rowIndex: number) => { 120 return (<Cell>{atRow(rowIndex)}</Cell>) 121 }, [props]) 122 123 const getCellClipboardData = useCallback((row: number, col: number, cellRenderer: CellRenderer) => { 124 return list[row] 125 }, [props]) 126 127 const onVisibleCellsChange = useCallback((rowIndices: RowIndices, columnIndices: ColumnIndices) => { 128 const newRegion = { 129 top: rowIndices.rowIndexStart, 130 left: columnIndices.columnIndexStart, 131 bottom: rowIndices.rowIndexEnd, 132 right: columnIndices.columnIndexEnd, 133 } 134 setVisibleRegion(newRegion) 135 }, [props]) 136 137 return ( 138 <div className="display-value"> 139 <div className="display-table-outer"> 140 <div className="layout-boundary"> 141 <Table2 142 ref={tableRef} 143 className="display-table" 144 numRows={list ? list.length : 0} 145 cellRendererDependencies={[list, props.stabilizationNum, props.refreshedAt]} 146 enableColumnHeader={false} 147 enableColumnInteractionBar={false} 148 enableColumnResizing={false} 149 enableColumnReordering={false} 150 enableMultipleSelection={true} 151 selectionModes={SelectionModes.ALL} 152 getCellClipboardData={getCellClipboardData} 153 onVisibleCellsChange={onVisibleCellsChange} 154 > 155 <Column id={0} cellRenderer={renderCell} name="" /> 156 </Table2> 157 </div> 158 </div> 159 </div> 160 ) 161 } 162 163 export function DisplayTable(props: DisplayValueProps) { 164 const tableRef = useRef<Table2>(null) 165 const selectionRef = useRef<any>(null) 166 167 const data = props.value as api.Table 168 const [visibleRegion, setVisibleRegion] = useState<api.TableRange | undefined>() 169 170 useEffect(correctTableViewport(tableRef, props), [tableRef.current, props.viewport]) 171 172 const atRow = (rowIndex: number, columnIndex: number): any => { 173 //const rowOffset = visibleRegion !== undefined ? visibleRegion.top : 0; 174 //const effectiveRowIndex = rowIndex - rowOffset; 175 //const columnOffset = visibleRegion !== undefined ? visibleRegion.left : 0; 176 //const effectiveColumnIndex = columnIndex - columnOffset; 177 return api.tableRowCol(data, rowIndex, columnIndex) 178 } 179 180 const renderCell = (rowIndex: number, columnIndex: number) => { 181 const value = atRow(rowIndex, columnIndex) 182 if (value === undefined) { 183 return (<Cell></Cell>) 184 } 185 return (<Cell key={`${rowIndex}-${columnIndex}`}>{value}</Cell>) 186 } 187 188 const onSelection = (selection) => { 189 selectionRef.current = selection; 190 } 191 const getCellClipboardData = (row: number, col: number, cellRenderer: CellRenderer) => { 192 return atRow(row, col); 193 } 194 195 const onVisibleCellsChange = useCallback((rowIndices: RowIndices, columnIndices: ColumnIndices) => { 196 const newRegion = { 197 top: rowIndices.rowIndexStart, 198 left: columnIndices.columnIndexStart, 199 bottom: rowIndices.rowIndexEnd, 200 right: columnIndices.columnIndexEnd, 201 } 202 setVisibleRegion(newRegion) 203 }, [props]) 204 205 if (!data || !data.columns || data.columns.length === 0) { 206 return (<div className="display-value"><div className="bp5-skeleton">Loading ...</div></div>) 207 } 208 209 return ( 210 <div className="display-value"> 211 <div className="display-table-outer"> 212 <div className="layout-boundary"> 213 <Table2 214 ref={tableRef} 215 numRows={api.tableMaxRows(data)} 216 className="display-table" 217 cellRendererDependencies={[data, props.stabilizationNum, props.refreshedAt]} 218 selectionModes={SelectionModes.ALL} 219 getCellClipboardData={getCellClipboardData} 220 onSelection={onSelection} 221 onVisibleCellsChange={onVisibleCellsChange} 222 > 223 {data && data.columns && data.columns.length > 0 ? data.columns.map((c, i) => ( 224 <Column id={i} key={i} name={c.name} cellRenderer={renderCell} /> 225 )) : (<Column />)} 226 </Table2> 227 </div> 228 </div> 229 </div> 230 ) 231 }