diff --git a/src/ts/constants.ts b/src/ts/constants.ts index 69a9f1b..95fb38b 100644 --- a/src/ts/constants.ts +++ b/src/ts/constants.ts @@ -9,10 +9,6 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -// Singleton `WeakMap`s to store metadata for td/th elements, as well as the -// datagrids themselves for each `` -export const METADATA_MAP = new WeakMap(); - // Output runtime debug info like FPS. export const DEBUG = true; diff --git a/src/ts/events.ts b/src/ts/events.ts index 0c45e7d..e9ae1df 100644 --- a/src/ts/events.ts +++ b/src/ts/events.ts @@ -9,10 +9,10 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { METADATA_MAP } from "./constants"; import { RegularVirtualTableViewModel } from "./scroll_panel"; import { throttle_tag } from "./utils"; import { CellMetadata } from "./types"; +import { METADATA_MAP } from "./view_model"; /** * When enabled, override iOS overscroll behavior by emulating scroll position @@ -180,7 +180,7 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { } const metadata = METADATA_MAP.get(element); - if (is_resize) { + if (is_resize && metadata) { event.stopImmediatePropagation(); // Clear column size data @@ -215,7 +215,7 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { : this.table_model.body.cells) { for (const td of event.shiftKey ? row - : [row[metadata._virtual_x]]) { + : [row[metadata.virtual_x!]]) { if (!td) { continue; } @@ -250,7 +250,7 @@ export class RegularViewEventModel extends RegularVirtualTableViewModel { } const metadata = METADATA_MAP.get(element); - if (is_resize) { + if (is_resize && metadata) { this._on_resize_column( event, element as HTMLTableCellElement, diff --git a/src/ts/regular-table.ts b/src/ts/regular-table.ts index bf623ca..026686b 100644 --- a/src/ts/regular-table.ts +++ b/src/ts/regular-table.ts @@ -9,11 +9,16 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { METADATA_MAP } from "./constants"; import { RegularViewEventModel } from "./events"; import { RegularTableViewModel } from "./table"; -import { DataListener, SetDataListenerOptions } from "./types"; +import { + CellMetadata, + DataListener, + FPSRecord, + SetDataListenerOptions, +} from "./types"; import { get_draw_fps } from "./utils"; +import { METADATA_MAP } from "./view_model"; type VirtualMode = "both" | "horizontal" | "vertical" | "none"; @@ -55,26 +60,6 @@ export class RegularTableElement extends RegularViewEventModel { } } - /** - * Reset the viewport of this regular table. - */ - private _reset_viewport(): void { - this._start_row = undefined; - this._end_row = undefined; - this._start_col = undefined; - this._end_col = undefined; - } - - /** - * Reset the scroll position of this regular table back to the origin. - */ - private _reset_scroll(): void { - this._column_sizes.indices = []; - this.scrollTop = 0; - this.scrollLeft = 0; - this._reset_viewport(); - } - /** * Reset column autosizing, such that column sizes will be recalculated * on the next draw() call. @@ -202,30 +187,38 @@ export class RegularTableElement extends RegularViewEventModel { * coordinates-like object to refer to metadata by logical position. * @returns {MetaData} The metadata associated with the element. */ - getMeta(element: HTMLElement | any): any { - if (typeof element === "undefined") { + getMeta(element?: HTMLElement | CellMetadata): CellMetadata | undefined { + if (element === undefined) { return; } else if (element instanceof HTMLElement) { return METADATA_MAP.get(element); - } else if (element.row_header_x >= 0) { - if (element.row_header_x < this._view_cache.row_headers_length) { + } else if ( + "row_header_x" in element && + element.row_header_x && + element.row_header_x >= 0 + ) { + if (element.row_header_x! < this._view_cache.row_headers_length) { const td = this.table_model.body._fetch_cell( - element.y, - element.row_header_x, + element.y!, + element.row_header_x!, ); return this.getMeta(td); } - } else if (element.column_header_y >= 0) { + } else if ( + "column_header_y" in element && + element.column_header_y! >= 0 + ) { if ( - element.column_header_y < this._view_cache.column_headers_length + element.column_header_y! < + this._view_cache.column_headers_length ) { - const td = this.table_model.body._fetch_cell( - element.column_header_y, - element.y, + const td = this.table_model.header._fetch_cell( + element.column_header_y!, + element.x!, ); return this.getMeta(td); } - } else { + } else if ("dx" in element) { return this.getMeta( this.table_model.body._fetch_cell( element.dy, @@ -253,13 +246,7 @@ export class RegularTableElement extends RegularViewEventModel { * @returns {Performance} Performance data aggregated since the last * call to `getDrawFPS()`. */ - getDrawFPS(): { - avg: number; - real_fps: number; - virtual_fps: number; - num_frames: number; - elapsed: number; - } { + getDrawFPS(): FPSRecord { return get_draw_fps(); } @@ -362,3 +349,27 @@ export class RegularTableElement extends RegularViewEventModel { if (document.createElement("regular-table").constructor === HTMLElement) { window.customElements.define("regular-table", RegularTableElement); } + +// Custom Elements extensions +declare global { + namespace JSX { + interface IntrinsicElements { + "regular-table": RegularTableElement; + } + } +} + +declare global { + interface Document { + createElement( + tagName: "regular-table", + options?: ElementCreationOptions, + ): RegularTableElement; + querySelector(selectors: string): E | null; + querySelector(selectors: "regular-table"): RegularTableElement | null; + } + + interface CustomElementRegistry { + get(tagName: "regular-table"): typeof RegularTableElement; + } +} diff --git a/src/ts/table.ts b/src/ts/table.ts index f66dd52..f6ae72d 100644 --- a/src/ts/table.ts +++ b/src/ts/table.ts @@ -257,9 +257,10 @@ abstract class RegularTableViewModelBase { x0: number, ): Promise { let missing_cidx = Math.max(dcidx + Math.floor(x0), 0); - viewport.start_col = missing_cidx; + const new_viewport = structuredClone(viewport); + new_viewport.start_col = missing_cidx; this._calculateViewportExtension( - viewport, + new_viewport, view_state, container_width, num_columns, @@ -268,10 +269,10 @@ abstract class RegularTableViewModelBase { ); const new_col = await view( - Math.floor(viewport.start_col), - Math.floor(viewport.start_row), - Math.ceil(viewport.end_col), - Math.ceil(viewport.end_row), + Math.floor(new_viewport.start_col), + Math.floor(new_viewport.start_row), + Math.ceil(new_viewport.end_col), + Math.ceil(new_viewport.end_row), ); let column_header_merge_depth: number | undefined; @@ -289,7 +290,7 @@ abstract class RegularTableViewModelBase { return { column_header_merge_depth, merge_headers }; } - viewport.end_col = viewport.start_col + new_col.data.length; + viewport.end_col = new_viewport.start_col + new_col.data.length; for (let i = 0; i < new_col.data.length; i++) { view_response.data[dcidx + i] = new_col.data[i]; if (new_col.metadata && view_response.metadata) { @@ -415,10 +416,6 @@ export class RegularTableViewModel extends RegularTableViewModelBase { } } - clearWidthStyles() { - this._columnWidthStyleSheet?.replaceSync(""); - } - /** * Updates column width styles for all columns using adoptedStyleSheets. * Generates CSS rules with :nth-child selectors for both auto-sized and diff --git a/src/ts/tbody.ts b/src/ts/tbody.ts index 4d46075..838ada9 100644 --- a/src/ts/tbody.ts +++ b/src/ts/tbody.ts @@ -12,6 +12,7 @@ import { BodyDrawResult, CellMetadata, + CellMetadataBuilder, CellScalar, ColumnState, ViewState, @@ -32,7 +33,7 @@ export class RegularBodyViewModel extends ViewModel { { column_name }: ColumnState, { ridx_offset }: ViewState, size_key: number, - ): { td: HTMLTableCellElement; metadata: CellMetadata } { + ): { td: HTMLTableCellElement; metadata: CellMetadataBuilder } { const td = this._get_cell(tagName, ridx, cidx); const metadata = this._get_or_create_metadata(td); metadata.y = ridx + Math.floor(ridx_offset); @@ -83,7 +84,7 @@ export class RegularBodyViewModel extends ViewModel { column_data_listener_metadata, } = column_state; let { row_height } = view_state; - let metadata: CellMetadata | undefined; + let metadata: CellMetadataBuilder | undefined; const ridx_offset: number[] = []; const tds: Array<{ td: HTMLTableCellElement; metadata: CellMetadata }> = []; @@ -103,7 +104,10 @@ export class RegularBodyViewModel extends ViewModel { for (const val of column_data) { let obj: - | { td: HTMLTableCellElement; metadata: CellMetadata } + | { + td: HTMLTableCellElement; + metadata: CellMetadataBuilder; + } | undefined; if (th) { const valArray = val as CellScalar[]; @@ -113,6 +117,7 @@ export class RegularBodyViewModel extends ViewModel { ridx - ridx_off_i, cidx_i, ); + const prev_row_metadata = this._get_or_create_metadata(prev_row); @@ -153,11 +158,13 @@ export class RegularBodyViewModel extends ViewModel { view_state, i, ); + const td = obj.td; const meta = obj.metadata; td.style.display = ""; td.removeAttribute("rowspan"); td.removeAttribute("colspan"); + meta.type = "row_header"; meta.row_header = valArray; meta.row_header_x = i; meta.y0 = y0_floor; @@ -168,7 +175,10 @@ export class RegularBodyViewModel extends ViewModel { } ridx_offset[i] = 1; cidx_offset[ridx] = 1; - tds[i] = obj; + tds[i] = obj as { + td: HTMLTableCellElement; + metadata: CellMetadata; + }; } } else { obj = this._draw_td( @@ -185,7 +195,8 @@ export class RegularBodyViewModel extends ViewModel { meta.user = column_data_listener_metadata[ridx]; } - meta.x = x_floor; + meta.type = "body"; + meta.x = x_floor || 0; meta.x1 = x1_ceil; meta.row_header = row_headers?.[ridx] || []; meta.y0 = y0_floor; @@ -197,7 +208,10 @@ export class RegularBodyViewModel extends ViewModel { meta.x0 = x0_floor; } - tds[0] = obj; + tds[0] = obj as { + td: HTMLTableCellElement; + metadata: CellMetadata; + }; } ridx++; diff --git a/src/ts/thead.ts b/src/ts/thead.ts index dd474a8..f5b3bf0 100644 --- a/src/ts/thead.ts +++ b/src/ts/thead.ts @@ -15,6 +15,9 @@ import { ColumnSizes, CellMetadata, CellScalar, + CellMetadataRowHeader, + CellMetadataColumnHeader, + CellMetadataBuilder, } from "./types"; /** @@ -25,7 +28,11 @@ import { * @class RegularHeaderViewModel */ export class RegularHeaderViewModel extends ViewModel { - private _group_header_cache: [CellMetadata, HTMLTableCellElement, number][]; + private _group_header_cache: [ + CellMetadataBuilder, + HTMLTableCellElement, + number, + ][]; private _offset_cache: number[]; constructor( @@ -65,7 +72,7 @@ export class RegularHeaderViewModel extends ViewModel { column: CellScalar[], column_name: unknown, th: HTMLTableCellElement, - ): CellMetadata { + ): CellMetadataBuilder { const metadata = this._get_or_create_metadata(th); metadata.column_header = column; metadata.value = column_name; @@ -77,7 +84,7 @@ export class RegularHeaderViewModel extends ViewModel { column_name: unknown, th: HTMLTableCellElement, size_key: number | number[], - ): CellMetadata { + ): CellMetadataBuilder { const metadata = this._get_or_create_metadata(th); metadata.column_header = column; metadata.value = column_name; @@ -123,7 +130,7 @@ export class RegularHeaderViewModel extends ViewModel { } let th: HTMLTableCellElement | undefined; - let metadata: CellMetadata | undefined; + let metadata: CellMetadataBuilder | undefined; let column_name: unknown; let output: HeaderDrawResult | undefined = undefined; column_header_merge_depth = @@ -172,7 +179,10 @@ export class RegularHeaderViewModel extends ViewModel { ); if (typeof output === "undefined") { - output = { th, metadata }; + output = { th, metadata } as { + th: HTMLTableCellElement; + metadata: CellMetadata; + }; } for (const [group_meta] of this._group_header_cache) { @@ -194,17 +204,24 @@ export class RegularHeaderViewModel extends ViewModel { metadata.x0 = Math.floor(x0); metadata.virtual_x = _virtual_x; if (colspan === 1) { + metadata.type = "corner"; metadata.row_header_x = Array.isArray(size_key) ? size_key[0] : size_key; } else { + metadata.type = "column_header"; delete metadata.row_header_x; } } } this._clean_rows(this._offset_cache.length); - output = output || { th: th!, metadata: metadata! }; + output = + output || + ({ th: th!, metadata: metadata! } as { + th: HTMLTableCellElement; + metadata: CellMetadata; + }); return output; } diff --git a/src/ts/types.ts b/src/ts/types.ts index bea4ea5..5ac62eb 100644 --- a/src/ts/types.ts +++ b/src/ts/types.ts @@ -91,25 +91,100 @@ export type DataListener = ( * header. */ -export interface CellMetadata { - column_header?: CellScalar[]; - row_header?: CellScalar[]; +export interface CellMetadataBuilder { + type: "row_header" | "column_header" | "corner" | "body"; + column_header: CellScalar[]; + row_header: CellScalar[]; value: unknown; - size_key?: number; - x?: number; + size_key: number; + virtual_x: number; column_header_y?: number; - x0?: number; - virtual_x?: number; row_header_x?: number; + x0: number; + y0: number; + y1: number; + x1: number; + x?: number; y?: number; - y0?: number; - y1?: number; - x1?: number; user?: unknown; dx?: number; dy?: number; } +export type CellMetadata = + | CellMetadataRowHeader + | CellMetadataColumnHeader + | CellMetadataBody + | CellMetadataCorner; + +export interface CellMetadataRowHeader { + type: "row_header"; + column_header: CellScalar[]; + row_header: CellScalar[]; + value: unknown; + size_key: number; + virtual_x: number; + row_header_x: number; + x0: number; + y0: number; + y1: number; + x1: number; + y: number; + user?: unknown; +} + +export interface CellMetadataColumnHeader { + type: "column_header"; + column_header: CellScalar[]; + row_header: CellScalar[]; + value: unknown; + size_key: number; + virtual_x: number; + column_header_y: number; + x0: number; + y0: number; + y1: number; + x1: number; + x: number; + user?: unknown; +} + +export interface CellMetadataCorner { + type: "corner"; + column_header: CellScalar[]; + row_header: CellScalar[]; + value: unknown; + size_key: number; + virtual_x: number; + row_header_x: number; + column_header_y: number; + x0: number; + y0: number; + y1: number; + x1: number; + x: number; + y: number; + user?: unknown; +} + +export interface CellMetadataBody { + type: "body"; + column_header: CellScalar[]; + row_header: CellScalar[]; + value: unknown; + size_key: number; + virtual_x: number; + x0: number; + y0: number; + y1: number; + x1: number; + x: number; + y: number; + user?: unknown; + dx: number; + dy: number; +} + /** * The `DataResponse` object describes a rectangular region of a virtual * data set, and some associated metadata. `` will use this diff --git a/src/ts/view_model.ts b/src/ts/view_model.ts index 87756ca..ab5337c 100644 --- a/src/ts/view_model.ts +++ b/src/ts/view_model.ts @@ -9,8 +9,11 @@ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { METADATA_MAP } from "./constants"; -import { CellMetadata, ColumnSizes } from "./types"; +import { CellMetadata, CellMetadataBuilder, ColumnSizes } from "./types"; + +// Singleton `WeakMap`s to store metadata for td/th elements, as well as the +// datagrids themselves for each `` +export const METADATA_MAP: WeakMap = new WeakMap(); /****************************************************************************** * @@ -79,15 +82,17 @@ export class ViewModel { _get_or_create_metadata( td: HTMLTableCellElement | undefined, - ): CellMetadata { + ): CellMetadataBuilder { if (!td) { - return { value: undefined }; + return { value: undefined } as CellMetadataBuilder; } + let metadata = METADATA_MAP.get(td); if (!metadata) { - metadata = { value: undefined }; + metadata = { value: undefined } as CellMetadata; METADATA_MAP.set(td, metadata); } + return metadata; } diff --git a/tests/getMeta.spec.js b/tests/getMeta.spec.js index 0fd6df0..c5360f3 100644 --- a/tests/getMeta.spec.js +++ b/tests/getMeta.spec.js @@ -26,6 +26,7 @@ test.describe("getMeta()", () => { }); expect(JSON.parse(meta)).toEqual({ + type: "body", column_header: ["Group 0", "Column 0"], row_header: ["Group 0", "Row 0"], dx: 0, @@ -49,7 +50,9 @@ test.describe("getMeta()", () => { el.getMeta(document.querySelector("tbody th")), ); }); + expect(JSON.parse(meta)).toEqual({ + type: "row_header", row_header: ["Group 0", "Row 0"], size_key: 0, virtual_x: 0, @@ -69,6 +72,7 @@ test.describe("getMeta()", () => { ); }); expect(JSON.parse(meta)).toEqual({ + type: "column_header", column_header: ["Group 0", "Column 0"], size_key: 3, virtual_x: 2, @@ -99,6 +103,7 @@ test.describe("getMeta()", () => { }); expect(JSON.parse(meta)).toEqual({ + type: "body", column_header: ["Group 10", "Column 16"], row_header: ["Group 0", "Row 0"], dx: 0, @@ -123,6 +128,7 @@ test.describe("getMeta()", () => { ); }); expect(JSON.parse(meta)).toEqual({ + type: "row_header", row_header: ["Group 0", "Row 0"], size_key: 0, virtual_x: 0, @@ -197,6 +203,7 @@ test.describe("getMeta()", () => { ); }); expect(JSON.parse(meta)).toEqual({ + type: "row_header", row_header: ["Group 50", "Row 52"], size_key: 0, virtual_x: 0, @@ -217,7 +224,9 @@ test.describe("getMeta()", () => { ), ); }); + expect(JSON.parse(meta)).toEqual({ + type: "column_header", column_header: ["Group 0", "Column 0"], size_key: 4, virtual_x: 2,