diff --git a/README.md b/README.md index 9361486..8888dad 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ available. - [Hierarchial/Group Headers](#hierarchialgroup-headers) - [`async` Data Models](#async-data-models) - [`.addStyleListener()` and `getMeta()` Styling](#addstylelistener-and-getmeta-styling) - - [`.invalidate()`](#invalidate) - [`.addEventListener()` Interaction](#addeventlistener-interaction) - [Scrolling](#scrolling) - [Pivots, Filters, Sorts, and Column Expressions with `perspective`](#pivots-filters-sorts-and-column-expressions-with-perspective) @@ -483,28 +482,6 @@ function style_th(th) { } ``` -### `.invalidate()` - -To prevent DOM renders, `` conserves DOM calls like `offsetWidth` -to an internal cache. When a `` or ``'s `width` is modified within a -callback to `.addStyleListener()`, you must indicate to `` that -its dimensions have changed in order to invalidate this cache, or you may not -end up with enough rendered columns to fill the screen! - -A call to `invalidate()` that does not need new columns only imparts a small -runtime overhead to re-calculate virtual width per async draw iteration, but -should be used conservatively if possible. Calling `invalidate()` outside of a -callback to `.addStyleListener()` will throw an `Error`. - -```javascript -table.addStyleListener(() => { - for (const th of table.querySelectorAll("tbody th")) { - th.style.maxWidth = "20px"; - } - table.invalidate(); -}); -``` - ## `.addEventListener()` Interaction `` is a normal `HTMLElement`! Use the `regular-table` API in diff --git a/features/row_mouse_selection.js b/features/row_mouse_selection.js index 41c18d9..f614a43 100644 --- a/features/row_mouse_selection.js +++ b/features/row_mouse_selection.js @@ -174,16 +174,14 @@ const createRowRangeSelection = (table, rowSelection, dl) => { rowSelection.y1, lastSelection.y1, ); + const y1 = Math.max( rowSelection.y0, lastSelection.y0, rowSelection.y1, lastSelection.y1, ); - const row_header_x = Math.min( - rowSelection.row_header_x, - lastSelection.row_header_x, - ); + rowSelection.y0 = y0; rowSelection.y1 = y1; } diff --git a/package.json b/package.json index f4ea415..d2e28e5 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "lint": "prettier --check src tests features/*.js examples/**/*.js examples/*.html", "start": "http-server", "test": "pnpm exec playwright test", - "benchmark": "node benchmarks/benchmark.mjs", + "bench": "node benchmarks/benchmark.mjs", "changelog": "docker run -it --rm -e CHANGELOG_GITHUB_TOKEN=$GITHUB_TOKEN -v \"$(pwd)\":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u finos -p regular-table", "watch": "npm-run-all -p watch:*" }, diff --git a/src/less/container.less b/src/less/container.less index d263616..1e39049 100644 --- a/src/less/container.less +++ b/src/less/container.less @@ -40,12 +40,6 @@ div.rt-scroll-table-clip { height: 100%; } -div.rt-tree-container { - display: flex; - align-items: center; - height: 100%; -} - slot { position: absolute; overflow: hidden; diff --git a/src/less/material.less b/src/less/material.less index 39421a4..08a0fcc 100644 --- a/src/less/material.less +++ b/src/less/material.less @@ -56,35 +56,10 @@ regular-table { max-width: 0px; } - thead tr:last-child .rt-float, - tbody .rt-float { - text-align: right; - } - - thead .rt-integer, - tbody .rt-integer { - text-align: right; - } - tbody th { text-align: left; } - span.rt-tree-container { - display: flex; - align-items: center; - height: 100%; - } - - thead .rt-string, - tbody .rt-string, - thead .rt-date, - tbody .rt-date, - thead .rt-datetime, - tbody .rt-datetime { - text-align: left; - } - // frozen rows thead tr:last-child th { @@ -95,13 +70,6 @@ regular-table { position: relative; } - tr th span.rt-tree-group { - margin-left: 5px; - margin-right: 15px; - border-left: 1px solid #eee; - height: 100%; - } - td, th { white-space: nowrap; @@ -133,65 +101,11 @@ regular-table { outline: none; } - span.rt-row-header-icon { - color: #aaa; - padding-right: 4px; - font-family: "Material Icons"; - } - - span.rt-column-header-icon { - font-size: 10px; - padding-left: 3px; - display: inline-block; - width: 10px; - font-family: "Material Icons"; - } - - span.rt-row-header-icon:hover { - color: #1a7da1; - text-shadow: 0px 0px 3px #1a7da1; - } - - .rt-selected td { - background-color: #eee; - } - .rt-cell-clip { overflow: hidden; text-overflow: ellipsis; } - // OPTIONAL zebra striping - - // @zebra-stripe-color: rgb(238,238,238); - - // tbody tr:nth-child(even) td:not(.rt-group-header) { - // background: @zebra-stripe-color; - // } - - // tbody tr:nth-child(even) span.rt-group-name { - // background: @zebra-stripe-color; - // } - - td span.rt-group-name, - th span.rt-group-name { - margin-right: -5px; - padding-right: 5px; - padding-left: 8px; - flex: 1; - height: 100%; - } - - th span.rt-group-name { - text-align: left; - } - - td th span.rt-group-leaf, - th span.rt-group-leaf { - margin-left: 16px; - height: 100%; - } - .rt-column-resize { height: 100%; width: 10px; diff --git a/src/ts/events.ts b/src/ts/events.ts index 78b337b..0c45e7d 100644 --- a/src/ts/events.ts +++ b/src/ts/events.ts @@ -56,7 +56,7 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { */ async _on_scroll(event: Event) { event.stopPropagation(); - await this.draw({ invalid_viewport: false }); + await this.draw({ invalid_viewport: false, cache: true }); this.dispatchEvent(new CustomEvent("regular-table-scroll")); } @@ -224,7 +224,7 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { } } - await this.draw(); + await this.draw({ cache: true }); } } @@ -316,7 +316,7 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { this._column_sizes.indices[size_key || 0] !== override_width; this._column_sizes.indices[size_key || 0] = override_width; if (should_redraw) { - this.draw(); + this.draw({ cache: true }); } }; @@ -349,7 +349,11 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { // If the column is smaller, new columns may need to be fetched, so // redraw, else just update the DOM widths and clipping classes. if (diff < 0) { - await this.draw({ preserve_width: true, throttle: false }); + await this.draw({ + preserve_width: true, + throttle: false, + cache: true, + }); } else { // Update column width styles via adoptedStyleSheets this.table_model.updateColumnWidthStyles( diff --git a/src/ts/regular-table.ts b/src/ts/regular-table.ts index 7c83071..bf623ca 100644 --- a/src/ts/regular-table.ts +++ b/src/ts/regular-table.ts @@ -183,23 +183,6 @@ export class RegularTableElement extends RegularViewEventModel { ); } - /** - * When called within the execution scope of a function registered to this - * `` as a `StyleListener`, invalidate this draw's - * dimensions and attempt to draw more columns. Useful if your - * `StyleListener` changes a cells dimensions, otherwise `` - * may not draw enough columns to fill the screen. - */ - invalidate(): void { - if (!this._is_styling) { - throw new Error( - "Cannot call `invalidate()` outside of a `StyleListener`", - ); - } - /** */ - this._invalidated = true; - } - /** * Returns the `MetaData` object associated with a `` or ``. When * your `StyleListener` is invoked, use this method to look up additional diff --git a/src/ts/scroll_panel.ts b/src/ts/scroll_panel.ts index 53a7ec9..ee0e3f6 100644 --- a/src/ts/scroll_panel.ts +++ b/src/ts/scroll_panel.ts @@ -284,27 +284,23 @@ export class RegularVirtualTableViewModel extends HTMLElement { */ protected _update_sub_cell_offset( viewport: Viewport, - last_cells?: CellTuple[], + last_cell?: CellTuple, ): void { const y_offset = (this._column_sizes.row_height || 20) * (viewport.start_row % 1) || 0; - if ( - (this.table_model._row_headers_length || 0) + - Math.floor(viewport.start_col) >= - this._column_sizes.indices.length - ) { - return; - } - const x_offset_index = (this.table_model._row_headers_length || 0) + Math.floor(viewport.start_col); + if (this._column_sizes.indices[x_offset_index] === undefined) { + this._column_sizes.indices[x_offset_index] = + last_cell?.[0]?.offsetWidth || 0; + } + const x_offset = - (this._column_sizes.indices[x_offset_index] || - (last_cells?.[0]?.[0]?.offsetWidth ?? 0)) * + this._column_sizes.indices[x_offset_index]! * (viewport.start_col % 1) || 0; this._sub_cell_rule.style.setProperty(CLIP_X, `${x_offset}px`); @@ -424,6 +420,7 @@ export class RegularVirtualTableViewModel extends HTMLElement { diff / (this._column_sizes.indices[start_col + scroll_index_offset - 1] || 60); + return Math.max(0, start_col - 1); } @@ -510,6 +507,10 @@ async function internal_draw( options: DrawOptions, ): Promise { const __debug_start_time__ = DEBUG && performance.now(); + if (!options.cache) { + this.table_model._resetDimState(); + } + const { invalid_viewport = true, preserve_width = false } = options; const { num_columns, @@ -517,7 +518,8 @@ async function internal_draw( num_row_headers, num_column_headers, row_height, - } = await this._view_cache.view(0, 0, 0, 0); + } = await this.table_model._getDimState(this._view_cache); + this._column_sizes.row_height = row_height || this._column_sizes.row_height; if (num_row_headers !== undefined) { this._view_cache.row_headers_length = num_row_headers; @@ -530,8 +532,10 @@ async function internal_draw( // Cache virtual mode checks and default values const is_non_vertical = this._virtual_mode === "none" || this._virtual_mode === "horizontal"; + const is_non_horizontal = this._virtual_mode === "none" || this._virtual_mode === "vertical"; + const safe_num_rows = num_rows || 0; const safe_num_columns = num_columns || 0; this._container_size = { @@ -549,48 +553,30 @@ async function internal_draw( const { invalid_row, invalid_column } = this._validate_viewport(viewport); const invalid_schema_or_column = this._invalid_schema || invalid_column; if (invalid_schema_or_column || invalid_row || invalid_viewport) { - let autosize_cells: CellTuple[] = []; let first_iteration = true; - for await (let last_cells of this.table_model.draw( + await this.table_model.draw( this._container_size, this._view_cache, this._selected_id, preserve_width, viewport, safe_num_columns, - )) { - if (last_cells !== undefined) { - autosize_cells.push(...last_cells); - } - - this.table_model.updateColumnWidthStyles( - viewport, - this._view_cache.row_headers_length, - ); - - // We want to perform this before the next event loop so there - // is no scroll jitter, but only on the first iteration as - // subsequent viewports are incorrect. - if (first_iteration) { - this._update_sub_cell_offset(viewport, autosize_cells); - first_iteration = false; - } - - this._is_styling = true; - for (const callback of this._style_callbacks) { - await callback({ detail: this as RegularTableElement }); - } - - this._is_styling = false; - if (!this._invalidated && last_cells !== undefined) { - break; - } + async (last_cells) => { + // We want to perform this before the next event loop so there + // is no scroll jitter, but only on the first iteration as + // subsequent viewports are incorrect. + if (first_iteration) { + this._update_sub_cell_offset(viewport, last_cells); + first_iteration = false; + } - this._invalidated = false; - } + for (const callback of this._style_callbacks) { + await callback({ detail: this as RegularTableElement }); + } + }, + ); const old_height = this._column_sizes.row_height; - this.table_model.autosize_cells(autosize_cells, row_height); this.table_model.header.reset_header_cache(); if (old_height !== this._column_sizes.row_height) { this._update_virtual_panel_height(safe_num_rows); diff --git a/src/ts/table.ts b/src/ts/table.ts index 010ecb6..f66dd52 100644 --- a/src/ts/table.ts +++ b/src/ts/table.ts @@ -256,7 +256,7 @@ abstract class RegularTableViewModelBase { _virtual_x: number, x0: number, ): Promise { - let missing_cidx = Math.max(viewport.end_col, 0); + let missing_cidx = Math.max(dcidx + Math.floor(x0), 0); viewport.start_col = missing_cidx; this._calculateViewportExtension( viewport, @@ -314,6 +314,9 @@ abstract class RegularTableViewModelBase { ): void { this.body.clean({ ridx: cont_body?.ridx || 0, cidx: _virtual_x }); this.header.clean(); + } + + protected _carriageReturn() { this.body._span_factory.reset(); this.header._span_factory.reset(); } @@ -331,6 +334,8 @@ export class RegularTableViewModel extends RegularTableViewModelBase { public table: HTMLTableElement; private _columnWidthStyleSheet?: CSSStyleSheet; private _lastColumnWidthCss?: string; + private _lastDataResponse?: DataResponse; + private _lastViewport?: Viewport; constructor( table_clip: HTMLElement, @@ -532,24 +537,38 @@ export class RegularTableViewModel extends RegularTableViewModelBase { this._lastColumnWidthCss = css; } - async *draw( + async _getDimState(view_cache: ViewCache): Promise { + if (!this._lastDataResponse) { + return await view_cache.view(0, 0, 0, 0); + } + + return this._lastDataResponse!; + } + + _resetDimState() { + this._lastDataResponse = undefined; + this._lastViewport = undefined; + } + + async draw( container_size: { width: number; height: number }, view_cache: ViewCache, selected_id: number | undefined, preserve_width: boolean, viewport: Viewport, num_columns: number, - ): AsyncGenerator { + style_callback: (last_cells: CellTuple) => Promise, + ): Promise { const { width: container_width, height: container_height } = container_size; // Fetch and prepare initial data - const view_response = await view_cache.view( + const view_response = (this._lastDataResponse = await view_cache.view( Math.floor(viewport.start_col), Math.floor(viewport.start_row), Math.ceil(viewport.end_col), Math.ceil(viewport.end_row), - ); + )); let { column_header_merge_depth, merge_headers = "both" } = view_response; @@ -621,7 +640,12 @@ export class RegularTableViewModel extends RegularTableViewModelBase { // Fetch missing columns if needed if (!view_response.data[dcidx]) { // Style the partially-renderd rows so there is no FOUT - yield undefined; + this.updateColumnWidthStyles( + viewport, + view_cache.row_headers_length, + ); + + await style_callback(last_cells[this._row_headers_length]); const fetch_result = await this._fetchMissingColumns( viewport, view, @@ -645,7 +669,21 @@ export class RegularTableViewModel extends RegularTableViewModelBase { if (!view_response.data[dcidx]) { this._cleanupAfterDraw(cont_body, _virtual_x); - yield last_cells; + this._carriageReturn(); + this.updateColumnWidthStyles( + viewport, + view_cache.row_headers_length, + ); + + await style_callback( + last_cells[this._row_headers_length], + ); + + this.autosize_cells( + last_cells, + this._column_sizes.row_height, + ); + return; } } @@ -700,22 +738,41 @@ export class RegularTableViewModel extends RegularTableViewModelBase { // Check if viewport filled if (this._isViewportFilled(view_state, container_width)) { this._cleanupAfterDraw(cont_body, _virtual_x); - yield last_cells; + this.updateColumnWidthStyles( + viewport, + view_cache.row_headers_length, + ); + + await style_callback(last_cells[this._row_headers_length]); // Recalculate after style listeners view_state.viewport_width = 0; - for (const [td] of last_cells) { - view_state.viewport_width += td.offsetWidth; + this.autosize_cells( + last_cells, + this._column_sizes.row_height, + ); + + for (let i = 0; i < last_cells.length; i++) { + view_state.viewport_width += + this._column_sizes.indices[Math.floor(x0) + i] || 0; } if (this._isViewportFilled(view_state, container_width)) { + this._carriageReturn(); return; } } } this._cleanupAfterDraw(cont_body, _virtual_x); - yield last_cells; + this._carriageReturn(); + this.updateColumnWidthStyles( + viewport, + view_cache.row_headers_length, + ); + + await style_callback(last_cells[this._row_headers_length]); + this.autosize_cells(last_cells, this._column_sizes.row_height); } finally { this._cleanupAfterDraw(cont_body, _virtual_x); } diff --git a/src/ts/thead.ts b/src/ts/thead.ts index 2410090..dd474a8 100644 --- a/src/ts/thead.ts +++ b/src/ts/thead.ts @@ -197,6 +197,8 @@ export class RegularHeaderViewModel extends ViewModel { metadata.row_header_x = Array.isArray(size_key) ? size_key[0] : size_key; + } else { + delete metadata.row_header_x; } } } diff --git a/src/ts/types.ts b/src/ts/types.ts index b0a2d7d..bea4ea5 100644 --- a/src/ts/types.ts +++ b/src/ts/types.ts @@ -260,6 +260,7 @@ export interface DrawOptions { invalid_viewport?: boolean; preserve_width?: boolean; throttle?: boolean; + cache?: boolean; } /** diff --git a/tests/addStyleListener.spec.js b/tests/addStyleListener.spec.js index 5e331bc..8613f73 100644 --- a/tests/addStyleListener.spec.js +++ b/tests/addStyleListener.spec.js @@ -271,47 +271,6 @@ test.describe("addStyleListener() and invalidate()", () => { }); }); - test.describe("invalidate()", () => { - test("can be called within a style listener", async ({ page }) => { - const table = page.locator("regular-table"); - const result = await table.evaluate(async (el) => { - let invalidateCalled = false; - el.addStyleListener(() => { - if (!invalidateCalled) { - try { - el.invalidate(); - invalidateCalled = true; - } catch (e) { - invalidateCalled = false; - } - } - }); - await el.draw(); - return invalidateCalled; - }); - - expect(result).toBe(true); - }); - - test("throws error when called outside style listener", async ({ - page, - }) => { - const table = page.locator("regular-table"); - const result = await table.evaluate(async (el) => { - try { - el.invalidate(); - return false; - } catch (e) { - return e.message.includes( - "Cannot call `invalidate()` outside of a `StyleListener`", - ); - } - }); - - expect(result).toBe(true); - }); - }); - test.describe("integration scenarios", () => { test("style listener with invalidate applies striped rows", async ({ page, diff --git a/tests/getMeta.spec.js b/tests/getMeta.spec.js index fd14683..0fd6df0 100644 --- a/tests/getMeta.spec.js +++ b/tests/getMeta.spec.js @@ -90,11 +90,14 @@ test.describe("getMeta()", () => { test("for {x: 0, y: 0}", async ({ page }) => { const table = page.locator("regular-table"); - const meta = await table.evaluate((el) => { + const meta = await table.evaluate(async (el) => { + await el.draw(); + await el.draw(); return JSON.stringify( el.getMeta(document.querySelector("td")), ); }); + expect(JSON.parse(meta)).toEqual({ column_header: ["Group 10", "Column 16"], row_header: ["Group 0", "Row 0"], @@ -105,7 +108,7 @@ test.describe("getMeta()", () => { value: "16", x: 16, x0: 16, - x1: 22, + x1: 24, y: 0, y0: 0, y1: 8, diff --git a/tests/setDataListener.spec.js b/tests/setDataListener.spec.js index b64a670..e7a8429 100644 --- a/tests/setDataListener.spec.js +++ b/tests/setDataListener.spec.js @@ -56,9 +56,9 @@ test.describe("setDataListener()", () => { test("receives correct viewport coordinates", async ({ page }) => { const table = page.locator("regular-table"); const callbackArgs = await table.evaluate(async (el) => { - let capturedArgs = null; + let capturedArgs = []; const testListener = (x0, y0, x1, y1) => { - capturedArgs = { x0, y0, x1, y1 }; + capturedArgs.push({ x0, y0, x1, y1 }); return { num_rows: 100, num_columns: 100, @@ -73,11 +73,12 @@ test.describe("setDataListener()", () => { return capturedArgs; }); - expect(callbackArgs).toBeTruthy(); - expect(callbackArgs.x0).toBe(22); - expect(callbackArgs.y0).toBe(0); - expect(callbackArgs.x1).toBeGreaterThan(0); - expect(callbackArgs.y1).toBeGreaterThan(0); + expect(callbackArgs.length).toBe(3); + expect(callbackArgs).toStrictEqual([ + { x0: 0, x1: 0, y0: 0, y1: 0 }, + { x0: 0, x1: 22, y0: 0, y1: 38 }, + { x0: 0, x1: 5, y0: 0, y1: 38 }, + ]); }); test("updates when table is scrolled", async ({ page }) => {