From dbf3cd0a7f3d27b759edb5acb9d55d4ec290d4a3 Mon Sep 17 00:00:00 2001 From: Sterling Camden Date: Wed, 31 Aug 2022 20:20:20 -0700 Subject: [PATCH] feat(editor): brand new react-y editor api also fix grid creation and destroy and use with much simpler ref implementation, also prettierrc --- src/lib/components/__tests__/grid.test.tsx | 115 ++++++-------- src/lib/components/grid.tsx | 166 +++++++++++++++------ src/lib/components/index.tsx | 2 +- 3 files changed, 167 insertions(+), 116 deletions(-) diff --git a/src/lib/components/__tests__/grid.test.tsx b/src/lib/components/__tests__/grid.test.tsx index 6878b0f..454bf2a 100644 --- a/src/lib/components/__tests__/grid.test.tsx +++ b/src/lib/components/__tests__/grid.test.tsx @@ -7,7 +7,7 @@ import _merge = require('lodash/merge'); const mockReactDomRender = jest.fn(); jest.mock('react-dom', () => ({ - render: mockReactDomRender + render: mockReactDomRender, })); const mockDataSet = jest.fn(); @@ -23,15 +23,15 @@ const makeDescriptor = (): Partial => { const mockDim = () => ({ converters: { data: { - get: jest.fn(makeDescriptor) - } + get: jest.fn(makeDescriptor), + }, }, rowColModel: { clear: jest.fn(), add: jest.fn(), create: jest.fn(makeDescriptor), - numHeaders: jest.fn() - } + numHeaders: jest.fn(), + }, }); const mockRowDim = _merge({}, mockDim(), { rowColModel: { row: jest.fn() } }); const mockColDim = _merge({}, mockDim(), { rowColModel: { col: jest.fn() } }); @@ -43,17 +43,22 @@ const mockGridCreate = jest.fn((o: any) => ({ cols: mockColDim, colModel: { createBuilder: (render: any, update: any): any => ({ - render, update - }) + render, + update, + }), }, rowModel: { createBuilder: (render: any, update: any): any => ({ - render, update - }) + render, + update, + }), }, dataModel: { - handleCachedDataChange: mockDataSetDirty - } + handleCachedDataChange: mockDataSetDirty, + }, + eventLoop: { + bind: () => () => {}, + }, })); (grid.create as any) = mockGridCreate; @@ -78,9 +83,7 @@ beforeEach(() => { }); it('should create a container for the grid', () => { - const reactGrid = mount( - - ); + const reactGrid = mount(); const gridContainer = reactGrid.children().getDOMNode().firstElementChild as HTMLElement; expect(gridContainer).toBeDefined(); if (gridContainer) { @@ -93,34 +96,26 @@ it('should create a container for the grid', () => { }); it('should keep the container in the DOM on subsequent updates', () => { - const reactGrid = mount( - - ); + const reactGrid = mount(); expect(reactGrid.children().getDOMNode().firstElementChild).toBeDefined(); reactGrid.setProps({ rows: [{}], cols: [] }); expect(reactGrid.children().getDOMNode().firstElementChild).toBeDefined(); }); it('should create a grid with opts', () => { - const reactGrid = mount( - - ); + const reactGrid = mount(); expect(mockGridCreate).toHaveBeenCalledWith({ snapToCell: true }); }); it('should build a grid with the container', () => { - const reactGrid = mount( - - ); + const reactGrid = mount(); expect(mockGridBuild).toHaveBeenCalledWith((reactGrid.instance() as ReactGrid).gridContainer.current); }); it('should add the supplied rows and cols to the grid', () => { const rows = [{ header: true }, { height: 4 }]; const cols = [{ fixed: true }, { width: 4 }]; - const reactGrid = mount( - - ); + const reactGrid = mount(); expect(mockRowDim.rowColModel.add.mock.calls[0][0]).toMatchObject(rows); expect(mockColDim.rowColModel.add.mock.calls[0][0]).toMatchObject(cols); }); @@ -128,14 +123,12 @@ it('should add the supplied rows and cols to the grid', () => { it('should not call add if the supplied rows and cols havent changed functionally', () => { const rows = [{ header: true }, { height: 4 }]; const cols = [{ fixed: true }, { width: 4 }]; - const reactGrid = mount( - - ); + const reactGrid = mount(); mockRowDim.rowColModel.add.mockClear(); mockColDim.rowColModel.add.mockClear(); reactGrid.setProps({ rows: JSON.parse(JSON.stringify(rows)), - cols: JSON.parse(JSON.stringify(cols)) + cols: JSON.parse(JSON.stringify(cols)), }); expect(mockRowDim.rowColModel.add).not.toHaveBeenCalled(); expect(mockColDim.rowColModel.add).not.toHaveBeenCalled(); @@ -144,32 +137,27 @@ it('should not call add if the supplied rows and cols havent changed functionall it('should not call add if the supplied rows and cols havent changed ref', () => { const rows = [{ header: true }, { height: 4 }]; const cols = [{ fixed: true }, { width: 4 }]; - const reactGrid = mount( - - ); + const reactGrid = mount(); mockRowDim.rowColModel.add.mockClear(); mockColDim.rowColModel.add.mockClear(); reactGrid.setProps({ rows, - cols + cols, }); expect(mockRowDim.rowColModel.add).not.toHaveBeenCalled(); expect(mockColDim.rowColModel.add).not.toHaveBeenCalled(); }); it('should supply data to the grid', () => { - const dataRow1 = [{ value: undefined, formatted: '1' }, { value: undefined, formatted: '2' }]; - const dataRow2 = [{ value: undefined, formatted: '3' }, { value: undefined, formatted: '4' }]; - const reactGrid = mount( - - ); + const dataRow1 = [ + { value: undefined, formatted: '1' }, + { value: undefined, formatted: '2' }, + ]; + const dataRow2 = [ + { value: undefined, formatted: '3' }, + { value: undefined, formatted: '4' }, + ]; + const reactGrid = mount(); expect(mockRowDim.converters.data.get).toHaveBeenCalledWith(0); expect(mockRowDim.converters.data.get).toHaveBeenCalledWith(1); expect(mockDataSet).toHaveBeenCalledWith(dataRow1); @@ -177,20 +165,17 @@ it('should supply data to the grid', () => { }); it('should re-supply data to the grid IFF the ref has changed', () => { - const dataRow1 = [{ value: undefined, formatted: '1' }, { value: undefined, formatted: '2' }]; - const dataRow2 = [{ value: undefined, formatted: '3' }, { value: undefined, formatted: '4' }]; + const dataRow1 = [ + { value: undefined, formatted: '1' }, + { value: undefined, formatted: '2' }, + ]; + const dataRow2 = [ + { value: undefined, formatted: '3' }, + { value: undefined, formatted: '4' }, + ]; const rows = [{}, {}]; const cols = [{}, {}]; - const reactGrid = mount( - - ); + const reactGrid = mount(); mockRowDim.converters.data.get.mockClear(); mockDataSet.mockClear(); const data = [dataRow1, dataRow2]; @@ -211,9 +196,7 @@ it('should use a colBuilder to supply React rendered content to the grid via cel const cols = [{ fixed: true }, { width: 4 }]; const a = ; const cellRenderer = jest.fn().mockReturnValue(a); - const reactGrid = mount( - - ); + const reactGrid = mount(); const gridCols = mockColDim.rowColModel.add.mock.calls[0][0]; const cellRendererBuilder = gridCols[0].builder; const rendered = cellRendererBuilder.render(); @@ -229,9 +212,7 @@ it('should use a rowBuilder to supply React rendered content to the grid via hea const cols = [{ fixed: true }, { width: 4 }]; const a = ; const cellRenderer = jest.fn().mockReturnValue(a); - const reactGrid = mount( - - ); + const reactGrid = mount(); const gridRows = mockRowDim.rowColModel.add.mock.calls[0][0]; const cellRendererBuilder = gridRows[0].builder; const rendered = cellRendererBuilder.render(); @@ -248,9 +229,7 @@ it('should not call headerCellRenderer prop if its not a header', () => { const a = ; const cellRenderer = jest.fn().mockReturnValue(a); mockRowDim.rowColModel.numHeaders.mockReturnValue(1); - const reactGrid = mount( - - ); + const reactGrid = mount(); const gridRows = mockRowDim.rowColModel.add.mock.calls[0][0]; const cellRendererBuilder = gridRows[0].builder; const rendered = cellRendererBuilder.render(); @@ -259,4 +238,4 @@ it('should not call headerCellRenderer prop if its not a header', () => { expect(cellRendererBuilder.update(rendered, { virtualRow: 1, virtualCol: 1, data: { formatted: 'poo' } })).toBe(undefined); expect(cellRenderer).not.toHaveBeenCalled(); expect(mockReactDomRender).not.toHaveBeenCalled(); -}); \ No newline at end of file +}); diff --git a/src/lib/components/grid.tsx b/src/lib/components/grid.tsx index 374b4c6..c9d5a5c 100644 --- a/src/lib/components/grid.tsx +++ b/src/lib/components/grid.tsx @@ -10,6 +10,7 @@ import { Grid, IBuilderUpdateContext, IColDescriptor, + IEditDecorator, IGridDataChange, IGridDataResult, IGridDimension, @@ -17,19 +18,35 @@ import { IRowColBuilder, IRowColDescriptor, IRowDescriptor, - RowModel + RowModel, } from 'grid'; +export type ReactRowCol = Omit & + Partial<{ + editOptions: Omit, 'getEditor'> & Partial, 'getEditor'>>; + }>; +export type ReactRowColDescriptor = ReactRowCol; +export type IReactColDescriptor = ReactRowCol; +export type IReactRowDescriptor = ReactRowCol; +export type GridEditState = { editing: boolean; row: number; col: number; typedText?: string }; export interface IGridProps extends IGridOpts { - rows: Array>; - cols: Array>; + rows: Array>; + cols: Array>; data?: Array>>; cellRenderer?(context: IBuilderUpdateContext): ReactElement | string | undefined; headerCellRenderer?(context: IBuilderUpdateContext): ReactElement | string | undefined; setData?(changes: Array>): Array> | undefined; + className?: string; + setGrid?(grid: Grid): void; + setEditState?(opts: GridEditState): void; + editor?: React.ReactElement; + saveEdit?: () => Promise | undefined>; } -export interface IGridState { } +export interface IGridState { + editState?: GridEditState; + editorContainer?: HTMLDivElement; +} export class ReactGrid extends Component { grid: Grid | undefined; @@ -46,57 +63,115 @@ export class ReactGrid extends Component { const { rows, cols, data, ...gridOpts } = this.props; const grid = create(gridOpts); this.grid = grid; + const origSet = grid.dataModel.set; grid.dataModel.set = (rowOrData: number | Array>, c?: number, datum?: string | string[]) => { const dataChanges = !Array.isArray(rowOrData) - ? [{ - row: rowOrData, - col: c as number, - value: datum - }] + ? [ + { + row: rowOrData, + col: c as number, + value: datum, + }, + ] : rowOrData; - const newChanges = this.props.setData && this.props.setData(dataChanges) || dataChanges; + const newChanges = (this.props.setData && this.props.setData(dataChanges)) || dataChanges; origSet.call(grid.dataModel, newChanges); }; - return grid + return grid; } + setEditState = (editState: GridEditState) => { + this.setState({ + editState: editState, + }); + this.props.setEditState?.(editState); + }; ensureGridContainerInDOM() { - const grid = this.grid || this.createGrid(); + if (this.grid) { + return this.grid; + } + const grid = this.createGrid(); if (this.gridContainer.current) { grid.build(this.gridContainer.current); + grid.eventLoop.bind('grid-edit', () => { + if (!grid.editModel.editing && this.state.editState) { + this.setEditState({ ...this.state.editState, editing: false, typedText: undefined }); + } + }); } else { - console.error('grid ref didnt exist at mount') + console.error('grid ref didnt exist at mount'); } + this.props.setGrid?.(grid); return grid; } createDiscriptors( - descriptorObjects: Array>, + descriptorObjects: Array>, dim: IGridDimension, - ): { baseDescriptors: IRowColDescriptor[], needsExpandedDescriptors: IRowColDescriptor[] } { + grid: Grid + ): { baseDescriptors: IRowColDescriptor[]; needsExpandedDescriptors: IRowColDescriptor[] } { + const isCol = grid.cols === dim; let needsExpandedDescriptors: IRowColDescriptor[] = []; const baseDescriptors = descriptorObjects.map((newDescriptor) => { const descriptor = dim.rowColModel.create(); if (newDescriptor.children) { - - const { baseDescriptors: children, needsExpandedDescriptors: childNeedsExpanded } = - this.createDiscriptors(newDescriptor.children, dim); + const { baseDescriptors: children, needsExpandedDescriptors: childNeedsExpanded } = this.createDiscriptors( + newDescriptor.children, + dim, + grid + ); descriptor.children = children; - needsExpandedDescriptors = [ - ...needsExpandedDescriptors, - ...childNeedsExpanded, - ...newDescriptor.expanded ? [descriptor] : [], - ]; + needsExpandedDescriptors = [...needsExpandedDescriptors, ...childNeedsExpanded, ...(newDescriptor.expanded ? [descriptor] : [])]; } - Object.assign(descriptor, _.omit(newDescriptor, 'children')); + if (isCol && newDescriptor.editOptions && !newDescriptor.editOptions.getEditor) { + const editOptions = { + ...newDescriptor.editOptions, + getEditor: (row: number) => { + if (!grid) { + return {}; + } + const col = (descriptor.index && dim.converters.virtual.toData(descriptor.index)) || -1; + const editState = { + editing: true, + row, + col, + }; + this.setEditState(editState); + + const decorator: IEditDecorator = grid.decorators.create(-1, -1, 50, 150); + decorator.render = () => { + const typedText = decorator.typedText?.(); + this.setEditState({ ...editState, typedText }); + if (decorator.boundingBox) { + decorator.boundingBox.style.zIndex = '100'; + } + const div = document.createElement('div'); + this.setState({ editorContainer: div }); + div.style.pointerEvents = 'all'; + + return div; + }; + return { + decorator, + save: async () => { + return this.props.saveEdit?.(); + }, + // isInMe: (e) => targetIsElement(e.target) && isWithinElementWithClass(e.target, ['modal-backdrop', 'modal-dialog']), + }; + }, + }; + descriptor.editOptions = editOptions; + } + Object.assign(descriptor, _.omit(newDescriptor, 'children', 'editOptions')); if ((dim.rowColModel as ColModel).col !== undefined) { descriptor.builder = newDescriptor.builder || this.cellRendererBuilder; } if ((dim.rowColModel as RowModel).row !== undefined && newDescriptor.header) { descriptor.builder = newDescriptor.builder || this.headerCellRendererBuilder; } + return descriptor; }); @@ -106,21 +181,19 @@ export class ReactGrid extends Component { }; } - reflectNewRowsOrCols( - nextDescriptors: Array>, - dim: IGridDimension - ) { + reflectNewRowsOrCols(nextDescriptors: Array>, dim: IGridDimension, grid: Grid) { dim.rowColModel.clear(true); - const { baseDescriptors, needsExpandedDescriptors } = this.createDiscriptors(nextDescriptors, dim); - const newDescriptors = - dim.rowColModel.add(baseDescriptors); - needsExpandedDescriptors.forEach((d) => d.expanded = true); + const { baseDescriptors, needsExpandedDescriptors } = this.createDiscriptors(nextDescriptors, dim, grid); + const newDescriptors = dim.rowColModel.add(baseDescriptors); + needsExpandedDescriptors.forEach((d) => (d.expanded = true)); return newDescriptors; } - descriptorsChanged(d1: Array>, d2: Array>) { - return d1.length !== d2.length || // different lengths short cut - d1.some((descriptor, index) => JSON.stringify(d2[index]) !== JSON.stringify(descriptor)); + descriptorsChanged(d1: Array>, d2: Array>) { + return ( + d1.length !== d2.length || // different lengths short cut + d1.some((descriptor, index) => JSON.stringify(d2[index]) !== JSON.stringify(descriptor)) + ); } handleNewData(data: Array>> | undefined, grid: Grid) { @@ -133,7 +206,7 @@ export class ReactGrid extends Component { } componentDidMount() { - console.log('mount') + console.log('mount'); const grid = this.ensureGridContainerInDOM(); this.cellRendererBuilder = grid.colModel.createBuilder( () => document.createElement('div'), @@ -162,24 +235,24 @@ export class ReactGrid extends Component { return element; } ); - this.reflectNewRowsOrCols(this.props.rows, grid.rows); - this.reflectNewRowsOrCols(this.props.cols, grid.cols); + this.reflectNewRowsOrCols(this.props.rows, grid.rows, grid); + this.reflectNewRowsOrCols(this.props.cols, grid.cols, grid); this.handleNewData(this.props.data, grid); } // we return false from should update but react may ignore our hint in the future componentDidUpdate(prevProps: IGridProps) { - console.log('update') + console.log('update'); const something = 'a string you cant ignore'; const grid = this.ensureGridContainerInDOM(); const nextProps = this.props; if (this.descriptorsChanged(prevProps.rows, nextProps.rows)) { - this.reflectNewRowsOrCols(nextProps.rows, grid.rows); + this.reflectNewRowsOrCols(nextProps.rows, grid.rows, grid); } if (this.descriptorsChanged(prevProps.cols, nextProps.cols)) { - this.reflectNewRowsOrCols(nextProps.cols, grid.cols); + this.reflectNewRowsOrCols(nextProps.cols, grid.cols, grid); } if (prevProps.data !== nextProps.data) { @@ -188,8 +261,8 @@ export class ReactGrid extends Component { } componentWillUnmount() { - console.log('unmount') - if(this.grid){ + console.log('unmount'); + if (this.grid) { this.grid.destroy(); this.grid.destroyed = true; this.grid = undefined; @@ -197,11 +270,10 @@ export class ReactGrid extends Component { } render() { - - return ( -
+
+ {this.props.editor && this.state.editorContainer && ReactDOM.createPortal(this.props.editor, this.state.editorContainer)}
); } @@ -213,4 +285,4 @@ const gridStyle = { left: '0', height: '100%', width: '100%', -} as const \ No newline at end of file +} as const; diff --git a/src/lib/components/index.tsx b/src/lib/components/index.tsx index 817263d..d24d1bd 100644 --- a/src/lib/components/index.tsx +++ b/src/lib/components/index.tsx @@ -1 +1 @@ -export { ReactGrid } from './grid'; \ No newline at end of file +export * from './grid';