From 4e2028e1c1dab81e3c607e354a996b94ae3da7a0 Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 22 Sep 2021 18:21:36 +0200 Subject: [PATCH 1/6] #9 prepared onDisappearing hook in cell renderers --- example/src/app/app.component.ts | 26 +++++++++--------- .../src/app/renderer/debug-cell-renderer.ts | 13 ++++++++- src/cell/model/cell-model.ts | 3 ++- src/cell/range/cell-range-util.ts | 23 +++++++++++++--- src/renderer/canvas/canvas-renderer.ts | 27 ++++++++++++++++++- .../cell/checkbox/checkbox-cell-renderer.ts | 8 ++++++ .../cell/header/row-column-header-renderer.ts | 16 +++++++++-- .../canvas/cell/image/image-cell-renderer.ts | 15 ++++++++++- .../cell/loading/loading-cell-renderer.ts | 15 ++++++++++- .../canvas/cell/text/text-cell-renderer.ts | 15 ++++++++++- src/renderer/cell/cell-renderer.ts | 6 +++++ 11 files changed, 142 insertions(+), 25 deletions(-) diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index 65f2e4b..d9ecffc 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -308,7 +308,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { startColumn: column, endColumn: column }, - rendererName: "row-column-header", + rendererName: RowColumnHeaderRenderer.NAME, value: null }; } @@ -336,7 +336,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { startColumn: column, endColumn: column }, - rendererName: "row-column-header", + rendererName: RowColumnHeaderRenderer.NAME, value: null }; } @@ -382,7 +382,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { const cellModel = AppComponent.initializeCellModel(); this.engine = new TableEngine(this.tableContainer.nativeElement, cellModel); - this.engine.getOptions().misc.debug = true; // Enable debug mode + this.engine.getOptions().misc.debug = false; // Enable/Disable debug mode // Setup row/column resizing this.engine.getOptions().renderer.canvas.rowColumnResizing.allowResizing = true; @@ -537,10 +537,10 @@ export class AppComponent implements AfterViewInit, OnDestroy { startColumn: 5, endColumn: 8 }, - rendererName: "loading", + rendererName: LoadingCellRenderer.NAME, value: { - cellRenderer: "image", + cellRenderer: ImageCellRenderer.NAME, promiseSupplier: async () => { return new Promise(resolve => setTimeout(() => resolve({ src: "assets/sloth.svg" @@ -550,7 +550,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { }, { range: CellRange.fromSingleRowColumn(25, 4), - rendererName: "text", + rendererName: TextCellRenderer.NAME, value: { text: "This is a cell for which we enabled line wrapping since this text is pretty long and will not fit into the cells available space.", options: { @@ -561,8 +561,8 @@ export class AppComponent implements AfterViewInit, OnDestroy { } as ITextCellRendererValue }, { - range: CellRange.fromSingleRowColumn(1000, 1000), - rendererName: "text", + range: CellRange.fromSingleRowColumn(200, 150), + rendererName: TextCellRenderer.NAME, value: "Last cell with more text than normally" }, { @@ -572,14 +572,14 @@ export class AppComponent implements AfterViewInit, OnDestroy { startColumn: 9, endColumn: 11 }, - rendererName: "debug", + rendererName: DebugCellRenderer.NAME, value: null }, { range: CellRange.fromSingleRowColumn(10, 2), - rendererName: "loading", + rendererName: LoadingCellRenderer.NAME, value: { - cellRenderer: "text", + cellRenderer: TextCellRenderer.NAME, promiseSupplier: async () => { return new Promise(resolve => setTimeout(() => resolve("Done 😀"), 2000)) }, @@ -604,9 +604,9 @@ export class AppComponent implements AfterViewInit, OnDestroy { (row, column) => row * column, (row, column) => { if (row === 0 || column === 0) { - return "row-column-header"; + return RowColumnHeaderRenderer.NAME; } else { - return "text"; + return TextCellRenderer.NAME; } }, (row) => 25, diff --git a/example/src/app/renderer/debug-cell-renderer.ts b/example/src/app/renderer/debug-cell-renderer.ts index 22658c2..62859b7 100644 --- a/example/src/app/renderer/debug-cell-renderer.ts +++ b/example/src/app/renderer/debug-cell-renderer.ts @@ -10,6 +10,9 @@ import {VerticalAlignment} from "../../../../src/util/alignment/vertical-alignme import {HorizontalAlignment} from "../../../../src/util/alignment/horizontal-alignment"; export class DebugCellRenderer implements ICanvasCellRenderer { + + public static readonly NAME: string = "debug"; + /** * Event listeners on cells rendered with this cell renderer. */ @@ -72,7 +75,7 @@ export class DebugCellRenderer implements ICanvasCellRenderer { } public getName(): string { - return "debug"; + return DebugCellRenderer.NAME; } public initialize(engine: TableEngine): void { @@ -83,4 +86,12 @@ export class DebugCellRenderer implements ICanvasCellRenderer { ctx.fillText(cell.value ?? "[No event yet]", Math.round(bounds.left + bounds.width / 2), Math.round(bounds.top + bounds.height / 2)); } + /** + * Called when the passed cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + public onDisappearing(cell: ICell): void { + // Do nothing + } + } diff --git a/src/cell/model/cell-model.ts b/src/cell/model/cell-model.ts index 591b2e5..10edb14 100644 --- a/src/cell/model/cell-model.ts +++ b/src/cell/model/cell-model.ts @@ -4,6 +4,7 @@ import {CellRangeUtil} from "../range/cell-range-util"; import {IRectangle} from "../../util/rect"; import {ICellModel} from "./cell-model.interface"; import {IBorder} from "../../border/border"; +import {TextCellRenderer} from "../../renderer/canvas/cell/text/text-cell-renderer"; /** * Model managing cells and their position and size in the table. @@ -13,7 +14,7 @@ export class CellModel implements ICellModel { /** * The default cell renderer name to use. */ - private static readonly DEFAULT_CELL_RENDERER_NAME: string = "text"; + private static readonly DEFAULT_CELL_RENDERER_NAME: string = TextCellRenderer.NAME; /** * The default row size. diff --git a/src/cell/range/cell-range-util.ts b/src/cell/range/cell-range-util.ts index 318d7f9..0f70379 100644 --- a/src/cell/range/cell-range-util.ts +++ b/src/cell/range/cell-range-util.ts @@ -25,6 +25,21 @@ export class CellRangeUtil { && range.endColumn <= containedIn.endColumn; } + /** + * Check whether the two given cell ranges overlap. + * @param a first cell range + * @param b second cell range + */ + public static overlap(a: ICellRange, b: ICellRange): boolean { + const cantOverlapVertically: boolean = a.startRow > b.endRow || a.endRow < b.startRow; + if (cantOverlapVertically) { + return false; + } + + const cantOverlapHorizontally: boolean = a.startColumn > b.endColumn || a.endColumn < b.startColumn; + return !cantOverlapHorizontally; + } + /** * Apply the exclusive or (XOR) operation on two cell ranges. * @param a first cell range @@ -37,7 +52,7 @@ export class CellRangeUtil { if (a.startRow !== b.startRow) { result.push({ startRow: Math.min(a.startRow, b.startRow), - endRow: Math.max(a.startRow, b.startRow), + endRow: Math.max(a.startRow, b.startRow) - 1, // Exclusive! startColumn: Math.min(a.startColumn, b.startColumn), endColumn: Math.max(a.endColumn, b.endColumn) }); @@ -46,7 +61,7 @@ export class CellRangeUtil { // Cut bottom rows in necessary if (a.endRow !== b.endRow) { result.push({ - startRow: Math.min(a.endRow, b.endRow), + startRow: Math.min(a.endRow, b.endRow) + 1, // Exclusive! endRow: Math.max(a.endRow, b.endRow), startColumn: Math.min(a.startColumn, b.startColumn), endColumn: Math.max(a.endColumn, b.endColumn) @@ -59,7 +74,7 @@ export class CellRangeUtil { startRow: Math.max(a.startRow, b.startRow), endRow: Math.min(a.endRow, b.endRow), startColumn: Math.min(a.startColumn, b.startColumn), - endColumn: Math.max(a.startColumn, b.startColumn) + endColumn: Math.max(a.startColumn, b.startColumn) - 1 // Exclusive! }); } @@ -68,7 +83,7 @@ export class CellRangeUtil { result.push({ startRow: Math.max(a.startRow, b.startRow), endRow: Math.min(a.endRow, b.endRow), - startColumn: Math.min(a.endColumn, b.endColumn), + startColumn: Math.min(a.endColumn, b.endColumn) + 1, // Exclusive! endColumn: Math.max(a.endColumn, b.endColumn) }); } diff --git a/src/renderer/canvas/canvas-renderer.ts b/src/renderer/canvas/canvas-renderer.ts index 67d500a..2a64365 100644 --- a/src/renderer/canvas/canvas-renderer.ts +++ b/src/renderer/canvas/canvas-renderer.ts @@ -2748,6 +2748,19 @@ export class CanvasRenderer implements ITableEngineRenderer { this._cellRendererLookup.set(renderer.getName(), renderer as ICanvasCellRenderer); } + /** + * Get a cell renderer by its name. + * @param rendererName name of the renderer to get + */ + private _getCellRendererForName(rendererName: string): ICanvasCellRenderer { + const cellRenderer: ICanvasCellRenderer = this._cellRendererLookup.get(rendererName); + if (!cellRenderer) { + throw new Error(`Could not find cell renderer for name '${rendererName}'`); + } + + return cellRenderer; + } + /** * Cleanup the cell viewport caches for cells that are out of the current viewport bounds. * @param oldCells the former rendered cells @@ -2773,7 +2786,19 @@ export class CanvasRenderer implements ITableEngineRenderer { const cells: ICell[] = this._cellModel.getCells(range); for (const cell of cells) { - cell.viewportCache = undefined; // Clearing cache property + // For merged cells make sure that the cell is completely disappeared from the viewport before clearing + const isMergedCell: boolean = !CellRangeUtil.isSingleRowColumnRange(cell.range); + if (isMergedCell) { + const isNotCompletelyInvisible: boolean = CellRangeUtil.overlap(cell.range, newRange); + if (isNotCompletelyInvisible) { + continue; + } + } + + // Clearing viewport cache property since the cell is no more visible + this._getCellRendererForName(cell.rendererName).onDisappearing(cell); + + cell.viewportCache = undefined; } } } diff --git a/src/renderer/canvas/cell/checkbox/checkbox-cell-renderer.ts b/src/renderer/canvas/cell/checkbox/checkbox-cell-renderer.ts index 3d5c17c..4a81791 100644 --- a/src/renderer/canvas/cell/checkbox/checkbox-cell-renderer.ts +++ b/src/renderer/canvas/cell/checkbox/checkbox-cell-renderer.ts @@ -150,6 +150,14 @@ export class CheckboxCellRenderer implements ICanvasCellRenderer { this._engine = engine; } + /** + * Called when the passed cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + public onDisappearing(cell: ICell): void { + // Do nothing + } + public render(ctx: CanvasRenderingContext2D, cell: ICell, bounds: IRectangle): void { let isContextSaved: boolean = false; diff --git a/src/renderer/canvas/cell/header/row-column-header-renderer.ts b/src/renderer/canvas/cell/header/row-column-header-renderer.ts index 8218bca..3941540 100644 --- a/src/renderer/canvas/cell/header/row-column-header-renderer.ts +++ b/src/renderer/canvas/cell/header/row-column-header-renderer.ts @@ -11,6 +11,11 @@ import {ICellRendererEventListener} from "../../../cell/event/cell-renderer-even */ export class RowColumnHeaderRenderer implements ICanvasCellRenderer { + /** + * Name of the cell renderer. + */ + public static readonly NAME: string = "row-column-header"; + /** * Available letters in the alphabet to use for generating column names. */ @@ -86,7 +91,7 @@ export class RowColumnHeaderRenderer implements ICanvasCellRenderer { * This must be unique. */ public getName(): string { - return "row-column-header"; + return RowColumnHeaderRenderer.NAME; } /** @@ -96,7 +101,6 @@ export class RowColumnHeaderRenderer implements ICanvasCellRenderer { * @param context of the current rendering cycle */ public before(ctx: CanvasRenderingContext2D, context: IRenderContext): void { - // TODO Make those things customizable (Renderer options?) ctx.textBaseline = "middle"; ctx.textAlign = "center"; } @@ -238,4 +242,12 @@ export class RowColumnHeaderRenderer implements ICanvasCellRenderer { return ""; } + /** + * Called when the passed cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + public onDisappearing(cell: ICell): void { + // Do nothing + } + } diff --git a/src/renderer/canvas/cell/image/image-cell-renderer.ts b/src/renderer/canvas/cell/image/image-cell-renderer.ts index c848690..95f0176 100644 --- a/src/renderer/canvas/cell/image/image-cell-renderer.ts +++ b/src/renderer/canvas/cell/image/image-cell-renderer.ts @@ -10,6 +10,11 @@ import {ICellRendererEventListener} from "../../../cell/event/cell-renderer-even */ export class ImageCellRenderer implements ICanvasCellRenderer { + /** + * Name of the cell renderer. + */ + public static readonly NAME: string = "image"; + /** * Cache for already loaded images. */ @@ -34,7 +39,7 @@ export class ImageCellRenderer implements ICanvasCellRenderer { * This must be unique. */ public getName(): string { - return "image"; + return ImageCellRenderer.NAME; } /** @@ -212,6 +217,14 @@ export class ImageCellRenderer implements ICanvasCellRenderer { } } + /** + * Called when the passed cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + public onDisappearing(cell: ICell): void { + // Do nothing + } + } /** diff --git a/src/renderer/canvas/cell/loading/loading-cell-renderer.ts b/src/renderer/canvas/cell/loading/loading-cell-renderer.ts index b197c9f..647606e 100644 --- a/src/renderer/canvas/cell/loading/loading-cell-renderer.ts +++ b/src/renderer/canvas/cell/loading/loading-cell-renderer.ts @@ -10,6 +10,11 @@ import {ICellRendererEventListener} from "../../../cell/event/cell-renderer-even */ export class LoadingCellRenderer implements ICanvasCellRenderer { + /** + * Name of the cell renderer. + */ + public static readonly NAME: string = "loading"; + /** * Duration of one full animation in milliseconds. */ @@ -79,7 +84,7 @@ export class LoadingCellRenderer implements ICanvasCellRenderer { * This must be unique. */ public getName(): string { - return "loading"; + return LoadingCellRenderer.NAME; } /** @@ -198,6 +203,14 @@ export class LoadingCellRenderer implements ICanvasCellRenderer { return ""; } + /** + * Called when the passed cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + public onDisappearing(cell: ICell): void { + // Do nothing + } + } /** diff --git a/src/renderer/canvas/cell/text/text-cell-renderer.ts b/src/renderer/canvas/cell/text/text-cell-renderer.ts index cbd02bf..2075896 100644 --- a/src/renderer/canvas/cell/text/text-cell-renderer.ts +++ b/src/renderer/canvas/cell/text/text-cell-renderer.ts @@ -21,6 +21,11 @@ import {HorizontalAlignment} from "../../../../util/alignment/horizontal-alignme */ export class TextCellRenderer implements ICanvasCellRenderer { + /** + * Name of the cell renderer. + */ + public static readonly NAME: string = "text"; + /** * Max duration of two mouse up events to be detected as double click (in milliseconds). */ @@ -166,7 +171,7 @@ export class TextCellRenderer implements ICanvasCellRenderer { * This must be unique. */ public getName(): string { - return "text"; + return TextCellRenderer.NAME; } /** @@ -412,6 +417,14 @@ export class TextCellRenderer implements ICanvasCellRenderer { return true; } + /** + * Called when the passed cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + public onDisappearing(cell: ICell): void { + // Do nothing + } + } /** diff --git a/src/renderer/cell/cell-renderer.ts b/src/renderer/cell/cell-renderer.ts index 89d0961..563a5c8 100644 --- a/src/renderer/cell/cell-renderer.ts +++ b/src/renderer/cell/cell-renderer.ts @@ -42,4 +42,10 @@ export interface ICellRenderer { */ getCopyValue(cell: ICell): string; + /** + * Called when a cell is disappearing from the visible area (viewport). + * @param cell that is disappearing + */ + onDisappearing(cell: ICell): void; + } From a4fcb82b5278dcd350fd4e58799187678c80ad82 Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 22 Sep 2021 20:00:21 +0200 Subject: [PATCH 2/6] #9 implemented HTML/DOM cell renderer --- example/src/app/app.component.ts | 42 ++++++ src/overlay/overlay-manager.ts | 6 + src/renderer/canvas/canvas-renderer.ts | 88 +++++++++--- .../canvas/cell/dom/dom-cell-renderer.ts | 126 ++++++++++++++++++ .../cell/text/text-cell-renderer-options.ts | 8 ++ .../canvas/cell/text/text-cell-renderer.ts | 14 +- 6 files changed, 258 insertions(+), 26 deletions(-) create mode 100644 src/renderer/canvas/cell/dom/dom-cell-renderer.ts diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index d9ecffc..e6b1796 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -34,6 +34,7 @@ import {ICell} from "../../../src/cell/cell"; import {DebugCellRenderer} from "./renderer/debug-cell-renderer"; import {CheckboxCellRenderer} from "../../../src/renderer/canvas/cell/checkbox/checkbox-cell-renderer"; import {ICheckboxCellRendererValue} from "../../../src/renderer/canvas/cell/checkbox/checkbox-cell-renderer-value"; +import {DOMCellRenderer} from "../../../src/renderer/canvas/cell/dom/dom-cell-renderer"; @Component({ selector: "app-root", @@ -475,6 +476,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { this.engine.registerCellRenderer(new ImageCellRenderer()); this.engine.registerCellRenderer(new LoadingCellRenderer()); this.engine.registerCellRenderer(new CheckboxCellRenderer()); + this.engine.registerCellRenderer(new DOMCellRenderer()); // Set an example border this.engine.getBorderModel().setBorder({ @@ -599,6 +601,46 @@ export class AppComponent implements AfterViewInit, OnDestroy { checked: false, label: "with Label" } as ICheckboxCellRendererValue + }, + { + range: { + startRow: 30, + endRow: 33, + startColumn: 3, + endColumn: 3 + }, + rendererName: TextCellRenderer.NAME, + value: { + text: "HTML/DOM cell renderer:", + options: { + horizontalAlignment: HorizontalAlignment.RIGHT, + verticalAlignment: VerticalAlignment.MIDDLE, + fontSize: 18, + useLineWrapping: true, + editable: true + } + } as ITextCellRendererValue + }, + { + range: { + startRow: 30, + endRow: 40, + startColumn: 4, + endColumn: 5 + }, + rendererName: DOMCellRenderer.NAME, + value: ` +
+

The Table-Engine is able to render HTML in a cell

+

+
    +
  • Or
  • +
  • a
  • +
  • list!
  • +
+

Nevertheless use these types of renderers sparingly, as they may result in poor performance

+
+` } ], (row, column) => row * column, diff --git a/src/overlay/overlay-manager.ts b/src/overlay/overlay-manager.ts index 6b136e4..1deeb99 100644 --- a/src/overlay/overlay-manager.ts +++ b/src/overlay/overlay-manager.ts @@ -22,4 +22,10 @@ export interface IOverlayManager { */ getOverlays(): IOverlay[]; + /** + * Update the given overlay. + * @param overlay to update + */ + updateOverlay(overlay: IOverlay): void; + } diff --git a/src/renderer/canvas/canvas-renderer.ts b/src/renderer/canvas/canvas-renderer.ts index 2a64365..23ed1ae 100644 --- a/src/renderer/canvas/canvas-renderer.ts +++ b/src/renderer/canvas/canvas-renderer.ts @@ -292,6 +292,12 @@ export class CanvasRenderer implements ITableEngineRenderer { */ private _updateOverlaysAfterRenderCycle: boolean = false; + /** + * List of overlays to update after the next rendering cycle. + * If this list is empty, all available overlays will be updated. + */ + private _overlaysToUpdateAfterRenderCycle: IOverlay[] = []; + /** * Currently focused cell position (if any). */ @@ -1467,13 +1473,22 @@ export class CanvasRenderer implements ITableEngineRenderer { const newBounds: DOMRect = this._container.getBoundingClientRect(); // Re-size scroll bar offsets as well - const fixedRowsHeight: number = !!this._lastRenderingContext.cells.fixedRowCells ? this._lastRenderingContext.cells.fixedRowCells.viewPortBounds.height : 0; - const fixedColumnsWidth: number = !!this._lastRenderingContext.cells.fixedColumnCells ? this._lastRenderingContext.cells.fixedColumnCells.viewPortBounds.width : 0; + const fixedRowsHeight: number = !!this._lastRenderingContext && !!this._lastRenderingContext.cells.fixedRowCells + ? this._lastRenderingContext.cells.fixedRowCells.viewPortBounds.height + : 0; + const fixedColumnsWidth: number = !!this._lastRenderingContext && !!this._lastRenderingContext.cells.fixedColumnCells + ? this._lastRenderingContext.cells.fixedColumnCells.viewPortBounds.width + : 0; const tableHeight: number = this._cellModel.getHeight() - fixedRowsHeight; const tableWidth: number = this._cellModel.getWidth() - fixedColumnsWidth; - const oldViewPort: IRectangle = this._lastRenderingContext.cells.nonFixedCells.viewPortBounds; + const oldViewPort: IRectangle = !!this._lastRenderingContext ? this._lastRenderingContext.cells.nonFixedCells.viewPortBounds : { + top: 0, + left: 0, + width: 0, + height: 0 + }; if (tableWidth > newBounds.width) { const oldMaxOffset = (tableWidth - oldViewPort.width); @@ -1688,14 +1703,13 @@ export class CanvasRenderer implements ITableEngineRenderer { */ private _updateFocusedCell(position: IInitialPosition | null): void { // Check if position changed - const changed: boolean = ( - (!this._focusedCellPosition && !!position) - || (!!this._focusedCellPosition && !position) - ) || ( - this._focusedCellPosition.row !== position.row - || this._focusedCellPosition.column !== position.column - ); + const oldRow: number = !!this._focusedCellPosition ? this._focusedCellPosition.row : -1; + const oldColumn: number = !!this._focusedCellPosition ? this._focusedCellPosition.column : -1; + const newRow: number = !!position ? position.row : -1; + const newColumn: number = !!position ? position.column : -1; + + const changed: boolean = newRow !== oldRow || newColumn !== oldColumn; if (!changed) { return; } @@ -1821,6 +1835,7 @@ export class CanvasRenderer implements ITableEngineRenderer { this._zoom = newZoom; this._updateOverlaysAfterRenderCycle = true; + this._repaintScheduler.next(); } } @@ -3263,13 +3278,31 @@ export class CanvasRenderer implements ITableEngineRenderer { this._layoutOverlay(overlay); } + /** + * Update the given overlay. + * @param overlay to update + */ + public updateOverlay(overlay: IOverlay): void { + // Schedule an overlay update for the passed overlay + this._updateOverlaysAfterRenderCycle = true; + this._overlaysToUpdateAfterRenderCycle.push(overlay); + } + /** * Update all overlays. */ private _updateOverlays(): void { - for (const overlay of this._overlays) { + const toUpdate: IOverlay[] = this._overlaysToUpdateAfterRenderCycle.length > 0 + ? this._overlaysToUpdateAfterRenderCycle + : this.getOverlays(); + + for (const overlay of toUpdate) { this._layoutOverlay(overlay); } + + if (this._overlaysToUpdateAfterRenderCycle.length > 0) { + this._overlaysToUpdateAfterRenderCycle.length = 0; + } } /** @@ -3285,7 +3318,8 @@ export class CanvasRenderer implements ITableEngineRenderer { let top: number = overlay.bounds.top; let height: number = overlay.bounds.height; - if (overlay.bounds.top < fixedRowsHeight) { + const isInFixedRows: boolean = overlay.bounds.top < fixedRowsHeight; + if (isInFixedRows) { // Overlaps with fixed rows const remaining: number = fixedRowsHeight - overlay.bounds.top; if (overlay.bounds.height > remaining) { @@ -3296,15 +3330,15 @@ export class CanvasRenderer implements ITableEngineRenderer { // Is only in scrollable area if (top - this._scrollOffset.y < fixedRowsHeight) { height -= fixedRowsHeight - (top - this._scrollOffset.y); - top = fixedRowsHeight; - } else { - top = top - this._scrollOffset.y; } + + top = top - this._scrollOffset.y; } let left: number = overlay.bounds.left; let width: number = overlay.bounds.width; - if (overlay.bounds.left < fixedColumnsWidth) { + const isInFixedColumns: boolean = overlay.bounds.left < fixedColumnsWidth; + if (isInFixedColumns) { // Overlaps with fixed columns const remaining: number = fixedColumnsWidth - overlay.bounds.left; if (overlay.bounds.width > remaining) { @@ -3315,21 +3349,33 @@ export class CanvasRenderer implements ITableEngineRenderer { // Is only in scrollable area if (left - this._scrollOffset.x < fixedColumnsWidth) { width -= fixedColumnsWidth - (left - this._scrollOffset.x); - left = fixedColumnsWidth; - } else { - left = left - this._scrollOffset.x; } + + left = left - this._scrollOffset.x; } const isVisible: boolean = height > 0 && top < viewPortHeight && width > 0 && left < viewPortWidth; if (isVisible) { overlay.element.style.display = "block"; + overlay.element.style.overflow = "hidden"; overlay.element.style.left = `${left * this._zoom}px`; overlay.element.style.top = `${top * this._zoom}px`; - overlay.element.style.width = `${width * this._zoom}px`; - overlay.element.style.height = `${height * this._zoom}px`; + overlay.element.style.width = `${overlay.bounds.width * this._zoom}px`; + overlay.element.style.height = `${overlay.bounds.height * this._zoom}px`; + + if (width < overlay.bounds.width || height < overlay.bounds.height) { + // Add clipping + const leftClipping: number = overlay.bounds.width - width; + const topClipping: number = overlay.bounds.height - height; + + overlay.element.style.clipPath = `inset(${topClipping * this._zoom}px 0 0 ${leftClipping * this._zoom}px)`; + } else { + if (overlay.element.style.clipPath.length > 0) { + overlay.element.style.clipPath = ""; + } + } } else { overlay.element.style.display = "none"; } diff --git a/src/renderer/canvas/cell/dom/dom-cell-renderer.ts b/src/renderer/canvas/cell/dom/dom-cell-renderer.ts new file mode 100644 index 0000000..b142081 --- /dev/null +++ b/src/renderer/canvas/cell/dom/dom-cell-renderer.ts @@ -0,0 +1,126 @@ +import {ICanvasCellRenderer} from "../canvas-cell-renderer"; +import {IRectangle} from "../../../../util/rect"; +import {ICell} from "../../../../cell/cell"; +import {TableEngine} from "../../../../table-engine"; +import {ICellRendererEventListener} from "../../../cell/event/cell-renderer-event-listener"; +import {IRenderContext} from "../../canvas-renderer"; +import {IOverlay} from "../../../../overlay/overlay"; + +/** + * Cell renderer for rendering HTML/DOM inside a cell. + */ +export class DOMCellRenderer implements ICanvasCellRenderer { + + /** + * Name of the cell renderer. + */ + public static readonly NAME: string = "dom"; + + /** + * Reference to the table engine. + */ + private _engine: TableEngine; + + public after(ctx: CanvasRenderingContext2D): void { + // Nothing to do after rendering cells with this renderer + } + + public before(ctx: CanvasRenderingContext2D, context: IRenderContext): void { + // Nothing to do before rendering cells with this renderer + } + + public cleanup(): void { + // Nothing to cleanup + } + + public getCopyValue(cell: ICell): string { + const cache: IDOMCellRendererViewportCache = DOMCellRenderer._cache(cell); + + return !!cache.overlay ? cache.overlay.element.innerHTML : ""; + } + + public getEventListener(): ICellRendererEventListener | null { + return null; + } + + public getName(): string { + return DOMCellRenderer.NAME; + } + + public initialize(engine: TableEngine): void { + this._engine = engine; + } + + public onDisappearing(cell: ICell): void { + // Remove DOM element (overlay) again + const cache: IDOMCellRendererViewportCache = DOMCellRenderer._cache(cell); + if (!!cache.overlay) { + this._engine.getOverlayManager().removeOverlay(cache.overlay); + } + } + + public render(ctx: CanvasRenderingContext2D, cell: ICell, bounds: IRectangle): void { + // Render nothing on the canvas, instead use an overlay to display the DOM element + const cache: IDOMCellRendererViewportCache = DOMCellRenderer._cache(cell); + if (!cache.overlay) { + // Create overlay + let domElement: HTMLElement; + if (cell.value instanceof HTMLElement) { + domElement = cell.value as HTMLElement; + } else { + domElement = document.createElement("div"); + domElement.innerHTML = `${cell.value}`; + } + + const overlay: IOverlay = { + element: domElement, + bounds: this._engine.getCellModel().getBounds(cell.range) + }; + this._engine.getOverlayManager().addOverlay(overlay); + + // Save overlay for later in the viewport cache + cache.overlay = overlay; + } else { + // Check whether cell dimensions changed -> Overlay update is needed + const oldBounds: IRectangle = cache.overlay.bounds; + const newBounds: IRectangle = this._engine.getCellModel().getBounds(cell.range); + + const boundsChanged: boolean = newBounds.left !== oldBounds.left + || newBounds.top !== oldBounds.top + || newBounds.width !== oldBounds.width + || newBounds.height !== oldBounds.height; + if (boundsChanged) { + cache.overlay.bounds = newBounds; + this._engine.getOverlayManager().updateOverlay(cache.overlay); + } + } + } + + /** + * Get the cache for the given cell. + * @param cell to get cache for + */ + private static _cache(cell: ICell): IDOMCellRendererViewportCache { + if (!!cell.viewportCache) { + return cell.viewportCache as IDOMCellRendererViewportCache; + } else { + const cache: IDOMCellRendererViewportCache = {}; + cell.viewportCache = cache; + + return cache; + } + } + +} + +/** + * Viewport cache of the DOM cell renderer. + */ +interface IDOMCellRendererViewportCache { + + /** + * The overlay showing the DOM element for a cell. + */ + overlay?: IOverlay; + +} diff --git a/src/renderer/canvas/cell/text/text-cell-renderer-options.ts b/src/renderer/canvas/cell/text/text-cell-renderer-options.ts index 9a39638..05504ea 100644 --- a/src/renderer/canvas/cell/text/text-cell-renderer-options.ts +++ b/src/renderer/canvas/cell/text/text-cell-renderer-options.ts @@ -2,6 +2,7 @@ import {IColor} from "../../../../util/color"; import {HorizontalAlignment} from "../../../../util/alignment/horizontal-alignment"; import {VerticalAlignment} from "../../../../util/alignment/vertical-alignment"; import {Colors} from "../../../../util/colors"; +import {ICell} from "../../../../cell/cell"; /** * The default font family in use. @@ -38,6 +39,13 @@ export const DEFAULT_LINE_HEIGHT: number = 16; */ export interface ITextCellRendererOptions { + /** + * Callback called when the value of the text cell renderer has been edited. + * @param cell that contains the changed value + * @returns whether the change is to be accepted or declined + */ + onChange?: (cell: ICell, oldValue: string, newValue: string) => boolean; + /** * Font family name. */ diff --git a/src/renderer/canvas/cell/text/text-cell-renderer.ts b/src/renderer/canvas/cell/text/text-cell-renderer.ts index 2075896..88d08b7 100644 --- a/src/renderer/canvas/cell/text/text-cell-renderer.ts +++ b/src/renderer/canvas/cell/text/text-cell-renderer.ts @@ -135,10 +135,14 @@ export class TextCellRenderer implements ICanvasCellRenderer { editor.removeEventListener("keydown", keyDownListener); // Save changes - if (!!specialValue) { - specialValue.text = editor.value; - } else { - event.cell.value = editor.value; + const callback = !!specialValue && !!specialValue.options.onChange ? specialValue.options.onChange : this._defaultOptions.onChange; + const acceptChange: boolean = !!callback ? callback(event.cell, editValue, editor.value) : true; + if (acceptChange) { + if (!!specialValue) { + specialValue.text = editor.value; + } else { + event.cell.value = editor.value; + } } // Invalidate viewport cache for cell @@ -373,7 +377,7 @@ export class TextCellRenderer implements ICanvasCellRenderer { if (lineCount === 1) { return offset; } else { - return offset - (lineHeight * lineCount) / 2; + return offset - (lineHeight * (lineCount - 1)) / 2; } case VerticalAlignment.TOP: return bounds.top; From a2c5583f624f8e58d68ff478f493d68712f51892 Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 22 Sep 2021 20:00:41 +0200 Subject: [PATCH 3/6] Pushing version to 0.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdcf1e3..41bf4a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "table-engine", - "version": "0.1.0", + "version": "0.1.1", "description": "Library to visualize huge tables in web environments", "files": [ "lib/**/*" From 92c0b3e5917b0978d2d5334d119798ed4c1087da Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 22 Sep 2021 20:01:38 +0200 Subject: [PATCH 4/6] Changing back demo application to 1 million cells --- example/src/app/app.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index e6b1796..b30b0d8 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -563,7 +563,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { } as ITextCellRendererValue }, { - range: CellRange.fromSingleRowColumn(200, 150), + range: CellRange.fromSingleRowColumn(1000, 1000), rendererName: TextCellRenderer.NAME, value: "Last cell with more text than normally" }, From d0c69884125885ddea7b48eb3c646b992086924a Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 22 Sep 2021 20:07:24 +0200 Subject: [PATCH 5/6] Made DOM/HTML cell renderer label in demo non-editable --- example/src/app/app.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index b30b0d8..cdbaa16 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -617,7 +617,7 @@ export class AppComponent implements AfterViewInit, OnDestroy { verticalAlignment: VerticalAlignment.MIDDLE, fontSize: 18, useLineWrapping: true, - editable: true + editable: false } } as ITextCellRendererValue }, From 69529a68d74dbcc502ccf574b15bae1c45720d5d Mon Sep 17 00:00:00 2001 From: bennyboer Date: Wed, 22 Sep 2021 20:10:40 +0200 Subject: [PATCH 6/6] Preventing user select in demo --- example/src/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/example/src/styles.scss b/example/src/styles.scss index 8562583..370b9ca 100644 --- a/example/src/styles.scss +++ b/example/src/styles.scss @@ -1,5 +1,6 @@ html, body { height: 100%; + user-select: none; } body {