diff --git a/packages/viewer-d3fc/src/ts/plugin/plugin.ts b/packages/viewer-d3fc/src/ts/plugin/plugin.ts index ccca5254b0..8bc8c11400 100644 --- a/packages/viewer-d3fc/src/ts/plugin/plugin.ts +++ b/packages/viewer-d3fc/src/ts/plugin/plugin.ts @@ -524,7 +524,7 @@ class HTMLPerspectiveViewerD3fcPluginElement extends HTMLElement { * causes non-cleared redraws duplicate column labels when calculating column name * resize/repositions - see `treemapLabel.js`. */ - async resize(view) { + async resize(_view) { if (this.offsetParent !== null) { if (this._settings?.data !== undefined) { this._draw(); @@ -536,13 +536,13 @@ class HTMLPerspectiveViewerD3fcPluginElement extends HTMLElement { } } - async restyle(view, _end_col, _end_row) { + async restyle(view) { let settings = this._settings; if (settings) { delete settings["colorStyles"]; delete settings["textStyles"]; if (this.isConnected) { - initialiseStyles(this._container, this._settings); + initialiseStyles(this._container, settings); this.resize(view); } } diff --git a/packages/viewer-d3fc/src/ts/tooltip/selectionEvent.ts b/packages/viewer-d3fc/src/ts/tooltip/selectionEvent.ts index d9387082af..b789731c84 100644 --- a/packages/viewer-d3fc/src/ts/tooltip/selectionEvent.ts +++ b/packages/viewer-d3fc/src/ts/tooltip/selectionEvent.ts @@ -27,8 +27,13 @@ export const raiseEvent = (node, data, settings) => { [], [{ filter }], ); + + if (node.tagName !== "PERSPECTIVE-VIEWER") { + node = node.getRootNode().host.parentElement; + } + node.dispatchEvent( - new CustomEvent("perspective-select", { + new CustomEvent("perspective-global-filter", { bubbles: true, composed: true, detail, diff --git a/packages/viewer-datagrid/src/css/regular_table.css b/packages/viewer-datagrid/src/css/regular_table.css index f4886c16d8..064b6d93aa 100644 --- a/packages/viewer-datagrid/src/css/regular_table.css +++ b/packages/viewer-datagrid/src/css/regular_table.css @@ -97,34 +97,18 @@ perspective-viewer, cursor: pointer; } -.psp-row-selected, -:hover .psp-row-selected, -:hover th.psp-tree-leaf.psp-row-selected, -:hover th.psp-tree-label.psp-row-selected { - color: white !important; - background-color: var(--selected-row--background-color, #ea7319) !important; - border-color: var(--selected-row--background-color, #ea7319) !important; -} - regular-table.flat-group-rollup-mode.vertical-row-headers th.psp-tree-label:not(:last-of-type) { writing-mode: vertical-lr; } -.psp-row-selected.psp-tree-label:not(:hover):before { - color: white; -} - -regular-table:not(.flat-group-rollup-mode) - .psp-row-selected.psp-tree-label:not(:hover):before { - color: white; -} - -.psp-row-subselected, -:hover .psp-row-subselected, -:hover th.psp-tree-leaf.psp-row-subselected, -:hover th.psp-tree-label.psp-row-subselected { - background: rgba(234, 115, 25, 0.2) !important; +.psp-select-region-inactive, +:hover .psp-select-region-inactive, +:hover th.psp-tree-leaf.psp-select-region-inactive, +:hover th.psp-tree-label.psp-select-region-inactive { + background-color: var(--psp-inactive--color) !important; + color: var(--psp--background-color) !important; + border-color: var(--psp--background-color) !important; } .psp-error { @@ -265,6 +249,7 @@ tbody th:last-of-type { overflow: hidden; text-overflow: ellipsis; } + tbody th:empty { background-image: linear-gradient( to right, @@ -316,12 +301,68 @@ regular-table:not(.flat-group-rollup-mode) { .psp-align-right { text-align: right; } + +.psp-color-mode-bar { + padding: 0 2px; +} + +.psp-color-mode-label-bar { + position: relative; + padding: 0 2px; + + .psp-bar { + isolation: isolate; + position: unset; + } + + .psp-bar:after { + color: var(--psp-label-bar-bg); + content: var(--label); + mix-blend-mode: difference; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: inline-flex; + justify-content: flex-end; + align-items: center; + padding: 0 5px; + } +} + +.psp-label-bar { + inset: 0; + pointer-events: none; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0px; +} + +.psp-label-bar-fill { + position: absolute; + top: 10%; + height: 80%; + background: var(--psp-label-bar-color); + pointer-events: none; +} + +.psp-label-bar-text { + position: relative; + color: var(--psp-label-bar-bg); + mix-blend-mode: difference; + pointer-events: none; +} + .psp-align-left { text-align: left; } + .psp-positive:not(:focus) { color: var(--psp-datagrid--pos-cell--color); } + .psp-negative:not(:focus) { color: var(--psp-datagrid--neg-cell--color); } @@ -363,6 +404,7 @@ regular-table table { th.psp-header-leaf { border-bottom-width: 0px; + span { height: 23px; min-height: 23px; @@ -395,18 +437,18 @@ regular-table table { regular-table tbody tr:hover - td.psp-select-region:not(.psp-row-selected):not(.psp-row-subselected), + td.psp-select-region:not(.psp-select-region-inactive), regular-table tbody tr:hover + tr - td.psp-select-region:not(.psp-row-selected):not(.psp-row-subselected) { + td.psp-select-region:not(.psp-select-region-inactive) { border-color: var(--psp--background-color) !important; } regular-table tbody tr:hover { td.psp-select-region.psp-menu-open { - &:not(.psp-row-selected):not(.psp-row-subselected) { + &:not(.psp-select-region-inactive) { box-shadow: inset -2px 0px 0px var(--psp--background-color), inset 2px 0px 0px var(--psp--background-color); @@ -479,6 +521,7 @@ regular-table table tbody tr:hover + tr:after { rgba(0, 128, 255, 0.5) ); } + 100% { background-color: var( --pulse--background-color-end, @@ -494,6 +537,7 @@ regular-table table tbody tr:hover + tr:after { rgba(0, 128, 255, 0.5) ); } + 100% { background-color: var( --pulse--background-color-end, @@ -509,6 +553,7 @@ regular-table table tbody tr:hover + tr:after { rgba(255, 25, 0, 0.5) ); } + 100% { background-color: var( --pulse--background-color-end, @@ -524,6 +569,7 @@ regular-table table tbody tr:hover + tr:after { rgba(255, 25, 0, 0.5) ); } + 100% { background-color: var( --pulse--background-color-end, diff --git a/packages/viewer-datagrid/src/css/row-hover.css b/packages/viewer-datagrid/src/css/row-hover.css index c5c5c54a77..430fc65af9 100644 --- a/packages/viewer-datagrid/src/css/row-hover.css +++ b/packages/viewer-datagrid/src/css/row-hover.css @@ -14,10 +14,14 @@ regular-table { tbody { tr:hover - th.psp-tree-leaf:not(.psp-row-selected):not(.psp-row-subselected), + th.psp-tree-leaf:not(.psp-select-region):not( + .psp-select-region-inactive + ), tr:hover - th.psp-tree-label:not(.psp-row-selected):not(.psp-row-subselected), - tr:hover td:not(.psp-row-selected):not(.psp-row-subselected), + th.psp-tree-label:not(.psp-select-region):not( + .psp-select-region-inactive + ), + tr:hover td:not(.psp-select-region):not(.psp-select-region-inactive), tr:hover:after { border-color: var( --psp-datagrid--hover--border-color, @@ -36,7 +40,9 @@ regular-table { } tr:last-child:hover - td:not(.psp-row-selected):not(.psp-row-subselected).psp-menu-open { + td:not(.psp-select-region):not( + .psp-select-region-inactive + ).psp-menu-open { box-shadow: inset -2px 0px 0px var(--psp--color), inset 2px 0px 0px var(--psp--color), @@ -46,11 +52,17 @@ regular-table { tr:hover + tr - th.psp-tree-leaf:not(.psp-row-selected):not(.psp-row-subselected), + th.psp-tree-leaf:not(.psp-select-region):not( + .psp-select-region-inactive + ), tr:hover + tr - th.psp-tree-label:not(.psp-row-selected):not(.psp-row-subselected), - tr:hover + tr td:not(.psp-row-selected):not(.psp-row-subselected) { + th.psp-tree-label:not(.psp-select-region):not( + .psp-select-region-inactive + ), + tr:hover + + tr + td:not(.psp-select-region):not(.psp-select-region-inactive) { border-top-color: transparent; } @@ -72,6 +84,7 @@ regular-table { tr:hover { color: inherit; + th:first-child:not(:empty), th:first-child:empty + th:not(:empty), th:first-child:empty ~ th:empty + th:not(:empty), diff --git a/packages/viewer-datagrid/src/css/toolbar.css b/packages/viewer-datagrid/src/css/toolbar.css index ccab11e478..c8ccdbf680 100644 --- a/packages/viewer-datagrid/src/css/toolbar.css +++ b/packages/viewer-datagrid/src/css/toolbar.css @@ -94,6 +94,10 @@ -webkit-mask-image: var(--psp-toolbar-edit-mode-select-region--content); } +#edit_mode[data-edit-mode="SELECT_ROW_TREE"]:before { + -webkit-mask-image: var(--psp-toolbar-edit-mode-select-row-tree--content); +} + /* #edit_mode span:before { */ /* content: var(--edit-mode-toggle--content, "N/A"); */ /* } */ @@ -124,6 +128,13 @@ ); } +#edit_mode[data-edit-mode="SELECT_ROW_TREE"] span:before { + content: var( + --psp-label--edit-mode-select-row-tree--content, + "Tree Select" + ); +} + #scroll_lock span:before { content: var(--psp-label--scroll-lock-toggle--content, "Free Scroll"); } diff --git a/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts b/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts index 4a1e0913f1..fabcacfcf7 100644 --- a/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts +++ b/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts @@ -210,7 +210,7 @@ export class HTMLPerspectiveViewerDatagridPluginElement return out.trim(); } - async resize(): Promise { + async resize(_view: View): Promise { if (!this.isConnected || this.offsetParent == null) { return; } @@ -225,25 +225,22 @@ export class HTMLPerspectiveViewerDatagridPluginElement this.regular_table.clear(); } - async save(): Promise { + save(): any { return save.call(this); } - async restore( - token: DatagridPluginConfig, - columns_config?: ColumnsConfig, - ): Promise { + restore(token: DatagridPluginConfig, columns_config?: ColumnsConfig): void { return restore.call(this, token, columns_config ?? {}); } - async restyle(): Promise { + async restyle(view: View): Promise { // Get view from model if available, otherwise no-op if (this.model?._view) { - await this.draw(this.model._view); + await this.draw(view); } } - async delete(): Promise { + delete(): void { this.disconnectedCallback(); this._toolbar = undefined; if ((this.regular_table as any).table_model) { diff --git a/packages/viewer-datagrid/src/ts/custom_elements/toolbar.ts b/packages/viewer-datagrid/src/ts/custom_elements/toolbar.ts index 79e5454383..2a523adbfd 100644 --- a/packages/viewer-datagrid/src/ts/custom_elements/toolbar.ts +++ b/packages/viewer-datagrid/src/ts/custom_elements/toolbar.ts @@ -66,6 +66,7 @@ export class HTMLPerspectiveViewerDatagridToolbarElement extends HTMLElement { plugin._edit_button = this.shadowRoot!.querySelector( "#edit_mode", ) as HTMLElement; + plugin._edit_button.addEventListener("click", () => { toggle_edit_mode.call(plugin); plugin.regular_table.draw(); diff --git a/packages/viewer-datagrid/src/ts/data_listener/format_cell.ts b/packages/viewer-datagrid/src/ts/data_listener/format_cell.ts index 8057f2d1d0..cc24032083 100644 --- a/packages/viewer-datagrid/src/ts/data_listener/format_cell.ts +++ b/packages/viewer-datagrid/src/ts/data_listener/format_cell.ts @@ -44,7 +44,11 @@ export function format_cell( const plugin: ColumnConfig = plugins[title] || {}; const is_numeric = type === "integer" || type === "float"; - if (is_numeric && plugin?.number_fg_mode === "bar") { + if ( + is_numeric && + (plugin?.number_fg_mode === "bar" || + plugin?.number_fg_mode === "label-bar") + ) { const a = Math.max( 0, Math.min( @@ -54,15 +58,30 @@ export function format_cell( ), ); - const div = this._div_factory.get(); - const anchor = (val as number) >= 0 ? "left" : "right"; + const anchor = (val as number) >= 0 ? "" : "justify-self:flex-end;"; const pct = (a * 100).toFixed(2); - div.setAttribute( - "style", - `width:calc(${pct}% - 4px);position:absolute;${anchor}:2px;height:80%;top:10%;pointer-events:none;`, - ); - return div; + if (plugin.number_fg_mode === "bar") { + const div = this._div_factory.get(); + div.className = "psp-bar"; + div.setAttribute( + "style", + `${anchor}width:${pct}%;height:80%;top:10%;pointer-events:none;background:var(--psp-label-bar-color)`, + ); + + return div; + } else { + const formatter = FORMAT_CACHE.get(type, plugin); + const label = formatter ? formatter.format(val) : (val as string); + + const div = this._div_factory.get(); + div.className = "psp-bar"; + div.setAttribute( + "style", + `--label:"${label}";${anchor}width:${pct}%;height:80%;top:10%;pointer-events:none;background:var(--psp-label-bar-color)`, + ); + return div; + } } else if (plugin?.format === "link" && type === "string") { const anchor = document.createElement("a"); anchor.setAttribute("href", val as string); diff --git a/packages/viewer-datagrid/src/ts/event_handlers/click.ts b/packages/viewer-datagrid/src/ts/event_handlers/click.ts index a25c0e0e22..ebeee263db 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/click.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/click.ts @@ -15,78 +15,52 @@ import * as edit_keydown from "./keydown/edit_keydown.js"; import type { DatagridModel, PerspectiveViewerElement, - SelectedPosition, + SelectedPositionMap, } from "../types.js"; +import { isEditableMode } from "../types.js"; import { RegularTableElement } from "regular-table"; -import { HTMLPerspectiveViewerDatagridPluginElement } from "../custom_elements/datagrid.js"; -type SelectedPositionMap = Map; - -export function is_editable( - this: DatagridModel, - viewer: PerspectiveViewerElement, - allowed: boolean = false, -): boolean { - const has_pivots = - this._config.group_by.length === 0 && - this._config.split_by.length === 0; - const selectable = viewer.hasAttribute("selectable"); - const plugin = viewer.children[0] as - | HTMLPerspectiveViewerDatagridPluginElement - | undefined; - const editable = allowed || !!(plugin?._edit_mode === "EDIT"); - return has_pivots && !selectable && editable; -} - -export function keydownListener( - this: DatagridModel, +export function createKeydownListener( + model: DatagridModel, table: RegularTableElement, viewer: PerspectiveViewerElement, selected_position_map: SelectedPositionMap, - event: KeyboardEvent, -): void { - if (this._edit_mode === "EDIT") { - if (!is_editable.call(this, viewer)) { - return; - } +): EventListener { + return (event: Event): void => { + const keyEvent = event as KeyboardEvent; + if (model._edit_mode === "EDIT") { + if (!isEditableMode(model, viewer)) { + return; + } - edit_keydown.keydownListener.call( - this, - table, - viewer, - selected_position_map, - event, - ); - } else { - console.debug( - `Mode ${this._edit_mode} for "keydown" event not yet implemented`, - ); - } + edit_keydown.keydownListener( + model, + table, + viewer, + selected_position_map, + keyEvent, + ); + } else { + console.debug( + `Mode ${model._edit_mode} for "keydown" event not yet implemented`, + ); + } + }; } -export function clickListener( - this: DatagridModel, +export function createEditClickListener( + model: DatagridModel, table: RegularTableElement, viewer: PerspectiveViewerElement, - event: MouseEvent, -): void { - if (this._edit_mode === "EDIT") { - if (!is_editable.call(this, viewer)) { - return; - } +): EventListener { + return (event: Event): void => { + const mouseEvent = event as MouseEvent; + if (model._edit_mode === "EDIT") { + if (!isEditableMode(model, viewer)) { + return; + } - edit_click.clickListener.call(this, table, viewer, event); - } else if (this._edit_mode === "READ_ONLY") { - // No-op for read-only mode - } else if (this._edit_mode === "SELECT_COLUMN") { - // Not yet implemented - } else if (this._edit_mode === "SELECT_ROW") { - // Not yet implemented - } else if (this._edit_mode === "SELECT_REGION") { - // Not yet implemented - } else { - console.debug( - `Mode ${this._edit_mode} for "click" event not yet implemented`, - ); - } + edit_click.clickListener(model, table, viewer, mouseEvent); + } + }; } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/click/edit_click.ts b/packages/viewer-datagrid/src/ts/event_handlers/click/edit_click.ts index f83cc403a7..dd084b738e 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/click/edit_click.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/click/edit_click.ts @@ -56,21 +56,21 @@ export function write_cell( } export function clickListener( - this: DatagridModel, + model: DatagridModel, table: RegularTable, _viewer: PerspectiveViewerElement, event: MouseEvent, ): void { const meta = table.getMeta(event.target as HTMLElement); if (meta?.type === "body" || meta?.type === "column_header") { - const is_editable2 = this._is_editable[meta.x]; - const is_bool = get_psp_type(this, meta) === "boolean"; + const is_editable2 = model._is_editable[meta.x]; + const is_bool = get_psp_type(model, meta) === "boolean"; const is_null = (event.target as Element).classList.contains( "psp-null", ); if (is_editable2 && is_bool && !is_null) { - write_cell(table, this, event.target as HTMLElement); + write_cell(table, model, event.target as HTMLElement); } } } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/deselect_all.ts b/packages/viewer-datagrid/src/ts/event_handlers/deselect_all.ts deleted file mode 100644 index f24a0caa21..0000000000 --- a/packages/viewer-datagrid/src/ts/event_handlers/deselect_all.ts +++ /dev/null @@ -1,28 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { RegularTableElement } from "regular-table"; -import type { PerspectiveViewerElement } from "../types.js"; - -type SelectedRowsMap = Map; - -export async function deselect_all_listener( - regularTable: RegularTableElement, - _viewer: PerspectiveViewerElement, - selected_rows_map: SelectedRowsMap, -): Promise { - selected_rows_map.delete(regularTable); - for (const td of regularTable.querySelectorAll("td,th")) { - td.classList.toggle("psp-row-selected", false); - td.classList.toggle("psp-row-subselected", false); - } -} diff --git a/packages/viewer-datagrid/src/ts/event_handlers/dispatch_click.ts b/packages/viewer-datagrid/src/ts/event_handlers/dispatch_click.ts index c82ef347b2..d7a00e2114 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/dispatch_click.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/dispatch_click.ts @@ -18,25 +18,27 @@ import type { PerspectiveClickDetail, } from "../types.js"; -export async function dispatch_click_listener( - this: DatagridModel, +export function createDispatchClickListener( + model: DatagridModel, table: RegularTableElement, viewer: PerspectiveViewerElement, - event: MouseEvent, -): Promise { - const meta = table.getMeta(event.target as HTMLElement); - if (!meta || meta.type !== "body") return; - const { x, y } = meta; - const { row, column_names, config } = await getCellConfig(this, y, x); - viewer.dispatchEvent( - new CustomEvent("perspective-click", { - bubbles: true, - composed: true, - detail: { - row, - column_names, - config, - }, - }), - ); +): EventListener { + return async (event: Event): Promise => { + const mouseEvent = event as MouseEvent; + const meta = table.getMeta(mouseEvent.target as HTMLElement); + if (!meta || meta.type !== "body") return; + const { x, y } = meta; + const { row, column_names, config } = await getCellConfig(model, y, x); + viewer.dispatchEvent( + new CustomEvent("perspective-click", { + bubbles: true, + composed: true, + detail: { + row, + column_names, + config, + }, + }), + ); + }; } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/expand_collapse.ts b/packages/viewer-datagrid/src/ts/event_handlers/expand_collapse.ts index b04b46a83d..95f82d3769 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/expand_collapse.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/expand_collapse.ts @@ -13,7 +13,7 @@ import type { RegularTable, DatagridModel } from "../types.js"; export async function expandCollapseHandler( - this: DatagridModel, + model: DatagridModel, regularTable: RegularTable, event: MouseEvent, ): Promise { @@ -24,22 +24,22 @@ export async function expandCollapseHandler( ); if (event.shiftKey && is_collapse) { - this._view.set_depth( + model._view.set_depth( (meta.row_header as unknown[]).filter((x) => x !== undefined) .length - 2, ); } else if (event.shiftKey) { - this._view.set_depth( + model._view.set_depth( (meta.row_header as unknown[]).filter((x) => x !== undefined) .length - 1, ); } else if (is_collapse) { - this._view.collapse(meta.y); + model._view.collapse(meta.y); } else { - this._view.expand(meta.y); + model._view.expand(meta.y); } - this._num_rows = await this._view.num_rows(); - this._num_columns = await this._view.num_columns(); + model._num_rows = await model._view.num_rows(); + model._num_columns = await model._view.num_columns(); regularTable.draw(); } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/focus.ts b/packages/viewer-datagrid/src/ts/event_handlers/focus.ts index d6f4ba34b6..de4a72f848 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/focus.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/focus.ts @@ -10,54 +10,57 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { is_editable } from "./click.js"; import { write_cell } from "./click/edit_click.js"; import type { RegularTable, DatagridModel, PerspectiveViewerElement, SelectedPosition, + SelectedPositionMap, } from "../types.js"; +import { isEditableMode } from "../types.js"; -type SelectedPositionMap = Map; - -export function focusoutListener( - this: DatagridModel, +export function createFocusoutListener( + model: DatagridModel, table: RegularTable, viewer: PerspectiveViewerElement, selected_position_map: SelectedPositionMap, - event: FocusEvent, -): void { - if (is_editable.call(this, viewer) && selected_position_map.has(table)) { - const target = event.target as HTMLElement; - target.classList.remove("psp-error"); - const selectedPosition = selected_position_map.get(table)!; - selected_position_map.delete(table); - if (selectedPosition.content !== target.textContent) { - if (!write_cell(table, this, target)) { - target.textContent = selectedPosition.content || ""; - target.classList.add("psp-error"); - target.focus(); +): EventListener { + return (event: Event): void => { + const focusEvent = event as FocusEvent; + if (isEditableMode(model, viewer) && selected_position_map.has(table)) { + const target = focusEvent.target as HTMLElement; + target.classList.remove("psp-error"); + const selectedPosition = selected_position_map.get(table)!; + selected_position_map.delete(table); + if (selectedPosition.content !== target.textContent) { + if (!write_cell(table, model, target)) { + target.textContent = selectedPosition.content || ""; + target.classList.add("psp-error"); + target.focus(); + } } } - } + }; } -export function focusinListener( - this: DatagridModel, +export function createFocusinListener( + _model: DatagridModel, table: RegularTable, _viewer: PerspectiveViewerElement, selected_position_map: SelectedPositionMap, - event: FocusEvent, -): void { - const target = event.target as HTMLElement; - const meta = table.getMeta(target); - if (meta?.type === "body") { - const new_state: SelectedPosition = { - x: meta.x, - y: meta.y, - content: target.textContent || undefined, - }; - selected_position_map.set(table, new_state); - } +): EventListener { + return (event: Event): void => { + const focusEvent = event as FocusEvent; + const target = focusEvent.target as HTMLElement; + const meta = table.getMeta(target); + if (meta?.type === "body") { + const new_state: SelectedPosition = { + x: meta.x, + y: meta.y, + content: target.textContent || undefined, + }; + selected_position_map.set(table, new_state); + } + }; } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/header_click.ts b/packages/viewer-datagrid/src/ts/event_handlers/header_click.ts index 5a4eded6c0..2e2c0a3723 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/header_click.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/header_click.ts @@ -18,68 +18,110 @@ import type { PerspectiveViewerElement, } from "../types.js"; -export async function mousedown_listener( - this: DatagridModel, +export function createMousedownListener( + model: DatagridModel, regularTable: RegularTable, viewer: PerspectiveViewerElement, - event: MouseEvent, -): Promise { - if (event.which !== 1) { - return; - } - - let target = event.target as HTMLElement | null; - if (target?.tagName === "A") { - return; - } - - while (target && target.tagName !== "TD" && target.tagName !== "TH") { - target = target.parentElement; - if (!target || !regularTable.contains(target)) { +): EventListener { + return async (event: Event): Promise => { + const mouseEvent = event as MouseEvent; + if (mouseEvent.which !== 1) { return; } - } - - if (!target) return; - - if (target.classList.contains("psp-tree-label")) { - expandCollapseHandler.call(this, regularTable, event); - return; - } - - if (target.classList.contains("psp-menu-enabled")) { - const meta = regularTable.getMeta(target); - const column_name = meta?.column_header?.[this._config.split_by.length]; - await viewer.toggleColumnSettings(`${column_name}`); - } else if (target.classList.contains("psp-sort-enabled")) { - sortHandler.call(this, regularTable, viewer, event, target); - } + + let target = mouseEvent.target as HTMLElement | null; + if (target?.tagName === "A") { + return; + } + + while (target && target.tagName !== "TD" && target.tagName !== "TH") { + target = target.parentElement; + if (!target || !regularTable.contains(target)) { + return; + } + } + + if (!target) return; + + if (target.classList.contains("psp-tree-label")) { + if (model._edit_mode !== "SELECT_ROW_TREE") { + expandCollapseHandler(model, regularTable, mouseEvent); + } + + return; + } + + if (target.classList.contains("psp-menu-enabled")) { + const meta = regularTable.getMeta(target); + const column_name = + meta?.column_header?.[model._config.split_by.length]; + await viewer.toggleColumnSettings(`${column_name}`); + } else if (target.classList.contains("psp-sort-enabled")) { + sortHandler(model, regularTable, viewer, mouseEvent, target); + } + }; } -export function click_listener( +export function createDblclickListener( + model: DatagridModel, regularTable: RegularTable, - event: MouseEvent, -): void { - if (event.which !== 1) { - return; - } - - let target = event.target as HTMLElement | null; - while (target && target.tagName !== "TD" && target.tagName !== "TH") { - target = target.parentElement; - if (!target || !regularTable.contains(target)) { + viewer: PerspectiveViewerElement, +): EventListener { + return async (event: Event): Promise => { + const mouseEvent = event as MouseEvent; + if (mouseEvent.which !== 1) { + return; + } + + let target = mouseEvent.target as HTMLElement | null; + if (target?.tagName === "A") { return; } - } - - if (!target) return; - - if (target.classList.contains("psp-tree-label") && event.offsetX < 26) { - event.stopImmediatePropagation(); - } else if ( - target.classList.contains("psp-header-leaf") && - !target.classList.contains("psp-header-corner") - ) { - event.stopImmediatePropagation(); - } + + while (target && target.tagName !== "TD" && target.tagName !== "TH") { + target = target.parentElement; + if (!target || !regularTable.contains(target)) { + return; + } + } + + if (!target) return; + + if (target.classList.contains("psp-tree-label")) { + if (model._edit_mode === "SELECT_ROW_TREE") { + expandCollapseHandler(model, regularTable, mouseEvent); + } + } + }; +} + +export function createClickListener(regularTable: RegularTable): EventListener { + return (event: Event): void => { + const mouseEvent = event as MouseEvent; + if (mouseEvent.which !== 1) { + return; + } + + let target = mouseEvent.target as HTMLElement | null; + while (target && target.tagName !== "TD" && target.tagName !== "TH") { + target = target.parentElement; + if (!target || !regularTable.contains(target)) { + return; + } + } + + if (!target) return; + + if ( + target.classList.contains("psp-tree-label") && + mouseEvent.offsetX < 26 + ) { + mouseEvent.stopImmediatePropagation(); + } else if ( + target.classList.contains("psp-header-leaf") && + !target.classList.contains("psp-header-corner") + ) { + mouseEvent.stopImmediatePropagation(); + } + }; } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/keydown/edit_keydown.ts b/packages/viewer-datagrid/src/ts/event_handlers/keydown/edit_keydown.ts index cd0cddbc58..d56f7d8524 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/keydown/edit_keydown.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/keydown/edit_keydown.ts @@ -15,24 +15,28 @@ import type { RegularTable, DatagridModel, PerspectiveViewerElement, - SelectedPosition, + SelectedPositionMap, } from "../../types.js"; -type SelectedPositionMap = Map; - -type AsyncFunction = ( - this: DatagridModel, - ...args: T -) => Promise; +type AsyncMoveFunction = ( + model: DatagridModel, + table: RegularTable, + selected_position_map: SelectedPositionMap, + active_cell: HTMLElement, + dx: number, + dy: number, +) => Promise; -function lock( - body: AsyncFunction, -): AsyncFunction { +function lock(body: AsyncMoveFunction): AsyncMoveFunction { let lockPromise: Promise | undefined; return async function ( - this: DatagridModel, - ...args: T - ): Promise { + model: DatagridModel, + table: RegularTable, + selected_position_map: SelectedPositionMap, + active_cell: HTMLElement, + dx: number, + dy: number, + ): Promise { if (lockPromise) { await lockPromise; return; @@ -40,7 +44,14 @@ function lock( let resolve: () => void; lockPromise = new Promise((x) => (resolve = x)); - const result = await body.apply(this, args); + const result = await body( + model, + table, + selected_position_map, + active_cell, + dx, + dy, + ); lockPromise = undefined; resolve!(); return result; @@ -52,23 +63,23 @@ interface ContentEditableElement extends HTMLElement { selectionStart?: number; } -function getPos(this: ContentEditableElement): number { - if (this.isContentEditable) { - const _range = (this.getRootNode() as Document) +function getPos(elem: ContentEditableElement): number { + if (elem.isContentEditable) { + const _range = (elem.getRootNode() as Document) .getSelection() ?.getRangeAt(0); if (!_range) return 0; const range = _range.cloneRange(); - range.selectNodeContents(this); + range.selectNodeContents(elem); range.setEnd(_range.endContainer, _range.endOffset); return range.toString().length; } else { - return this.selectionStart || 0; + return elem.selectionStart || 0; } } const moveSelection = lock(async function ( - this: DatagridModel, + model: DatagridModel, table: RegularTable, selected_position_map: SelectedPositionMap, active_cell: HTMLElement, @@ -77,8 +88,8 @@ const moveSelection = lock(async function ( ): Promise { const meta = table.getMeta(active_cell); if (!meta || meta.type !== "body") return; - const num_columns = this._column_paths.length; - const num_rows = this._num_rows; + const num_columns = model._column_paths.length; + const num_rows = model._num_rows; const selected_position = selected_position_map.get(table); if (!selected_position) { return; @@ -122,7 +133,7 @@ function isLastCell( } export function keydownListener( - this: DatagridModel, + model: DatagridModel, table: RegularTable, _viewer: PerspectiveViewerElement, selected_position_map: SelectedPositionMap, @@ -134,12 +145,12 @@ export function keydownListener( switch (event.key) { case "Enter": event.preventDefault(); - if (isLastCell(this, table, target)) { + if (isLastCell(model, table, target)) { target.blur(); selected_position_map.delete(table); } else if (event.shiftKey) { - moveSelection.call( - this, + moveSelection( + model, table, selected_position_map, target, @@ -147,8 +158,8 @@ export function keydownListener( -1, ); } else { - moveSelection.call( - this, + moveSelection( + model, table, selected_position_map, target, @@ -158,10 +169,10 @@ export function keydownListener( } break; case "ArrowLeft": - if (getPos.call(target as ContentEditableElement) === 0) { + if (getPos(target as ContentEditableElement) === 0) { event.preventDefault(); - moveSelection.call( - this, + moveSelection( + model, table, selected_position_map, target, @@ -172,23 +183,16 @@ export function keydownListener( break; case "ArrowUp": event.preventDefault(); - moveSelection.call( - this, - table, - selected_position_map, - target, - 0, - -1, - ); + moveSelection(model, table, selected_position_map, target, 0, -1); break; case "ArrowRight": if ( - getPos.call(target as ContentEditableElement) === + getPos(target as ContentEditableElement) === (target.textContent?.length || 0) ) { event.preventDefault(); - moveSelection.call( - this, + moveSelection( + model, table, selected_position_map, target, @@ -199,14 +203,7 @@ export function keydownListener( break; case "ArrowDown": event.preventDefault(); - moveSelection.call( - this, - table, - selected_position_map, - target, - 0, - 1, - ); + moveSelection(model, table, selected_position_map, target, 0, 1); break; default: } diff --git a/packages/viewer-datagrid/src/ts/event_handlers/row_select_click.ts b/packages/viewer-datagrid/src/ts/event_handlers/row_select_click.ts deleted file mode 100644 index 7891a9e8ec..0000000000 --- a/packages/viewer-datagrid/src/ts/event_handlers/row_select_click.ts +++ /dev/null @@ -1,92 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import getCellConfig from "../get_cell_config.js"; -import { - type RegularTable, - type DatagridModel, - type PerspectiveViewerElement, - type HandledMouseEvent, - PerspectiveSelectDetail, -} from "../types.js"; - -type SelectedRowsMap = Map; - -export async function selectionListener( - this: DatagridModel, - regularTable: RegularTable, - viewer: PerspectiveViewerElement, - selected_rows_map: SelectedRowsMap, - event: HandledMouseEvent, -): Promise { - const meta = regularTable.getMeta(event.target as HTMLElement); - if (!viewer.hasAttribute("selectable")) return; - if (event.handled) return; - if (event.shiftKey) return; - if (event.button !== 0) { - return; - } - - event.stopImmediatePropagation(); - - if (!meta) { - return; - } - - if ((meta.type === "body" || meta.type === "row_header") && meta.y >= 0) { - const id = this._ids?.[meta.y - meta.y0]; - const selected = selected_rows_map.get(regularTable); - const key_match = - !!selected && - selected.reduce((agg, x, i) => agg && x === id[i], true); - - const is_deselect = - !!selected && id.length === selected.length && key_match; - - const { row, column_names, config } = await getCellConfig( - this, - meta.y, - meta.type === "body" ? meta.x : 0, - ); - - let detail: PerspectiveSelectDetail; - if (is_deselect) { - selected_rows_map.delete(regularTable); - detail = new PerspectiveSelectDetail( - false, - row, - [], - [], - [{ filter: structuredClone(this._config.filter) }], - ); - } else { - selected_rows_map.set(regularTable, id); - detail = new PerspectiveSelectDetail( - true, - row, - column_names, - [], - [config], - ); - } - - await regularTable.draw({ preserve_width: true }); - event.handled = true; - viewer.dispatchEvent( - new CustomEvent("perspective-select", { - bubbles: true, - composed: true, - detail, - }), - ); - } -} diff --git a/packages/viewer-datagrid/src/ts/event_handlers/select_region.ts b/packages/viewer-datagrid/src/ts/event_handlers/select_region.ts index 40f5276fbe..96240a6881 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/select_region.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/select_region.ts @@ -13,16 +13,24 @@ import { RegularTableElement } from "regular-table"; import type { DatagridPluginElement, + EditMode, PerspectiveViewerElement, SelectionArea, } from "../types.js"; import { ViewWindow } from "@perspective-dev/client"; +import { CellMetadataBody } from "regular-table/dist/esm/types.js"; const MOUSE_SELECTED_AREA_CLASS = "mouse-selected-area"; +export type OnSelectCallback = ( + area: SelectionArea, + isDeselect: boolean, +) => void; + interface AddAreaMouseSelectionOptions { className?: string; selected?: SelectionArea[]; + onSelect?: OnSelectCallback; } export const addAreaMouseSelection = ( @@ -31,6 +39,7 @@ export const addAreaMouseSelection = ( { className = MOUSE_SELECTED_AREA_CLASS, selected = [], + onSelect, }: AddAreaMouseSelectionOptions = {}, ): RegularTableElement => { datagrid.model!._selection_state = { @@ -50,7 +59,7 @@ export const addAreaMouseSelection = ( table.addEventListener( "mouseup", - getMouseupListener(datagrid, table, className), + getMouseupListener(datagrid, table, className, onSelect), ); table.addStyleListener(() => @@ -60,6 +69,10 @@ export const addAreaMouseSelection = ( return table; }; +function isSingleClickMode(mode: EditMode): boolean { + return mode === "SELECT_ROW_TREE"; +} + const getMousedownListener = ( datagrid: DatagridPluginElement, @@ -70,10 +83,9 @@ const getMousedownListener = const mouseEvent = event as MouseEvent; if ( mouseEvent.button === 0 && - (datagrid.model!._edit_mode === "SELECT_REGION" || - datagrid.model!._edit_mode === "SELECT_ROW" || - datagrid.model!._edit_mode === "SELECT_COLUMN") + isSelectionMode(datagrid.model!._edit_mode) ) { + if (isSingleClickMode(datagrid.model!._edit_mode)) return; datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES = {}; const meta = table.getMeta(mouseEvent.target as HTMLElement); if ( @@ -122,11 +134,8 @@ const getMouseoverListener = ) => (event: Event): void => { const mouseEvent = event as MouseEvent; - if ( - datagrid.model!._edit_mode === "SELECT_REGION" || - datagrid.model!._edit_mode === "SELECT_ROW" || - datagrid.model!._edit_mode === "SELECT_COLUMN" - ) { + const mode = datagrid.model!._edit_mode; + if (isSelectionMode(mode) && !isSingleClickMode(mode)) { if ( datagrid.model!._selection_state .CURRENT_MOUSEDOWN_COORDINATES && @@ -183,17 +192,63 @@ const getMouseupListener = datagrid: DatagridPluginElement, table: RegularTableElement, className: string, + onSelect?: OnSelectCallback, ) => (event: Event): void => { const mouseEvent = event as MouseEvent; - if ( - datagrid.model!._edit_mode === "SELECT_REGION" || - datagrid.model!._edit_mode === "SELECT_ROW" || - datagrid.model!._edit_mode === "SELECT_COLUMN" - ) { + const mode = datagrid.model!._edit_mode; + if (isSelectionMode(mode)) { const meta = table.getMeta(mouseEvent.target as HTMLElement); if (!meta) return; + // For single-click modes (SELECT_ROW_TREE), handle toggle + if (isSingleClickMode(mode)) { + if ( + (meta.type === "body" || meta.type === "row_header") && + meta.y !== undefined && + meta.y >= 0 + ) { + const existing = + datagrid.model!._selection_state.selected_areas; + const isSameRow = + existing.length > 0 && existing[0].y0 === meta.y; + + if (isSameRow) { + // Deselect + datagrid.model!._selection_state.selected_areas = []; + datagrid.model!._selection_state.dirty = true; + applyMouseAreaSelections( + datagrid, + table, + className, + [], + ); + onSelect?.(existing[0], true); + } else { + // Select new row + const area: SelectionArea = { + x0: 0, + x1: 0, + y0: meta.y, + y1: meta.y, + }; + datagrid.model!._selection_state.selected_areas = [ + area, + ]; + datagrid.model!._selection_state.dirty = true; + applyMouseAreaSelections(datagrid, table, className); + onSelect?.(area, false); + } + } + + datagrid.model!._selection_state.CURRENT_MOUSEDOWN_COORDINATES = + {}; + datagrid.model!._selection_state.potential_selection = + undefined; + return; + } + + // Drag-based modes (SELECT_ROW, SELECT_COLUMN, SELECT_REGION) if ( (datagrid.model!._selection_state.old_selected_areas?.length ?? 0) > 0 @@ -260,6 +315,18 @@ const getMouseupListener = } }; +function modeIncludesColumns(mode: EditMode): boolean { + return mode === "SELECT_COLUMN" || mode === "SELECT_REGION"; +} + +function modeIncludesRows(mode: EditMode): boolean { + return ( + mode === "SELECT_ROW" || + mode === "SELECT_REGION" || + mode === "SELECT_ROW_TREE" + ); +} + function set_psp_selection( viewer: PerspectiveViewerElement, datagrid: DatagridPluginElement, @@ -267,73 +334,82 @@ function set_psp_selection( ): void { const viewport: ViewWindow = {}; const mode = datagrid.model!._edit_mode; - if ( - x0 !== undefined && - ["SELECT_COLUMN", "SELECT_REGION"].indexOf(mode) > -1 - ) { + if (x0 !== undefined && modeIncludesColumns(mode)) { viewport.start_col = x0; } - if ( - x1 !== undefined && - ["SELECT_COLUMN", "SELECT_REGION"].indexOf(mode) > -1 - ) { + if (x1 !== undefined && modeIncludesColumns(mode)) { viewport.end_col = x1 + 1; } - if ( - y0 !== undefined && - ["SELECT_ROW", "SELECT_REGION"].indexOf(mode) > -1 - ) { + if (y0 !== undefined && modeIncludesRows(mode)) { viewport.start_row = y0; } - if ( - y1 !== undefined && - ["SELECT_ROW", "SELECT_REGION"].indexOf(mode) > -1 - ) { + if (y1 !== undefined && modeIncludesRows(mode)) { viewport.end_row = y1 + 1; } viewer.setSelection(viewport); } +type CellPredicate = (meta: CellMetadataBody, area: SelectionArea) => boolean; + +const SELECTION_PREDICATES: Record = { + SELECT_REGION: (m, a) => + a.x0 <= m.x && m.x <= a.x1 && a.y0 <= m.y && m.y <= a.y1, + SELECT_ROW: (m, a) => a.y0 <= m.y && m.y <= a.y1, + SELECT_ROW_TREE: (m, a) => a.y0 <= m.y && m.y <= a.y1, + SELECT_COLUMN: (m, a) => a.x0 <= m.x && m.x <= a.x1, +}; + +function isSelectionMode(mode: EditMode): boolean { + return ( + mode === "SELECT_REGION" || + mode === "SELECT_ROW" || + mode === "SELECT_COLUMN" || + mode === "SELECT_ROW_TREE" + ); +} + export const applyMouseAreaSelections = ( datagrid: DatagridPluginElement, table: RegularTableElement, className: string, selected?: SelectionArea[], ): void => { - if ( - datagrid.model!._edit_mode === "SELECT_REGION" || - datagrid.model!._edit_mode === "SELECT_ROW" || - datagrid.model!._edit_mode === "SELECT_COLUMN" - ) { + const mode = datagrid.model!._edit_mode; + if (isSelectionMode(mode)) { selected = datagrid.model!._selection_state.selected_areas.slice(0); if (datagrid.model!._selection_state.potential_selection) { selected.push(datagrid.model!._selection_state.potential_selection); } - const tds = table.querySelectorAll("tbody td"); - if (selected.length > 0) { set_psp_selection( - datagrid.parentElement as any, + datagrid.parentElement as PerspectiveViewerElement, datagrid, selected[0], ); - applyMouseAreaSelection(datagrid, table, selected, className); + + // SELECT_ROW_TREE styling is handled entirely by the + // identity-based system in body.ts, which styles both td + // and th uniformly in a single draw pass. + if (!isSingleClickMode(mode)) { + applyMouseAreaSelection(datagrid, table, selected, className); + } } else { - (datagrid.parentElement as any).setSelection(); + (datagrid.parentElement as PerspectiveViewerElement).setSelection(); + const tds = table.querySelectorAll("tbody td"); for (const td of tds) { td.classList.remove(className); } } } else if (datagrid.model!._selection_state.dirty) { datagrid.model!._selection_state.dirty = false; - const tds = table.querySelectorAll("tbody td"); - for (const td of tds) { - td.classList.remove(className); + const cells = table.querySelectorAll("tbody td, tbody th"); + for (const cell of cells) { + cell.classList.remove(className); } } }; @@ -344,96 +420,29 @@ const applyMouseAreaSelection = ( selected: SelectionArea[], className: string, ): void => { - if (datagrid.model!._edit_mode === "SELECT_REGION" && selected.length > 0) { - const tds = table.querySelectorAll("tbody td"); - - for (const td of tds) { - const meta = table.getMeta(td as HTMLElement); - if (!meta || meta.type !== "body") continue; - let rendered = false; - for (const { x0, x1, y0, y1 } of selected) { - if ( - x0 !== undefined && - y0 !== undefined && - x1 !== undefined && - y1 !== undefined - ) { - if ( - x0 <= meta.x && - meta.x <= x1 && - y0 <= meta.y && - meta.y <= y1 - ) { - rendered = true; - datagrid.model!._selection_state.dirty = true; - td.classList.add(className); - } - } - } - - if (!rendered) { - td.classList.remove(className); - } - } - } else if ( - datagrid.model!._edit_mode === "SELECT_ROW" && - selected.length > 0 - ) { - const tds = table.querySelectorAll("tbody td"); - - for (const td of tds) { - const meta = table.getMeta(td as HTMLElement); - if (!meta) continue; - let rendered = false; - for (const { x0, x1, y0, y1 } of selected) { - if ( - x0 !== undefined && - y0 !== undefined && - x1 !== undefined && - y1 !== undefined && - meta?.type === "body" - ) { - if (y0 <= meta.y && meta.y <= y1) { - datagrid.model!._selection_state.dirty = true; - rendered = true; - td.classList.add(className); - } - } - } - - if (!rendered) { - td.classList.remove(className); + const predicate = SELECTION_PREDICATES[datagrid.model!._edit_mode]; + if (!predicate || selected.length === 0) return; + const tds = table.querySelectorAll("tbody td"); + for (const td of tds) { + const meta = table.getMeta(td as HTMLElement); + if (!meta || meta.type !== "body") continue; + let rendered = false; + for (const area of selected) { + if ( + area.x0 !== undefined && + area.y0 !== undefined && + area.x1 !== undefined && + area.y1 !== undefined && + predicate(meta, area) + ) { + rendered = true; + datagrid.model!._selection_state.dirty = true; + td.classList.add(className); } } - } else if ( - datagrid.model!._edit_mode === "SELECT_COLUMN" && - selected.length > 0 - ) { - const tds = table.querySelectorAll("tbody td"); - - for (const td of tds) { - const meta = table.getMeta(td as HTMLElement); - if (!meta) continue; - let rendered = false; - for (const { x0, x1, y0, y1 } of selected) { - if ( - x0 !== undefined && - y0 !== undefined && - x1 !== undefined && - y1 !== undefined && - meta?.type === "body" - ) { - if (x0 <= meta.x && meta.x <= x1) { - datagrid.model!._selection_state.dirty = true; - rendered = true; - td.classList.add(className); - } - } - } - if (!rendered) { - td.classList.remove(className); - } + if (!rendered) { + td.classList.remove(className); } } }; diff --git a/packages/viewer-datagrid/src/ts/event_handlers/sort.ts b/packages/viewer-datagrid/src/ts/event_handlers/sort.ts index 08923b5690..21348e18af 100644 --- a/packages/viewer-datagrid/src/ts/event_handlers/sort.ts +++ b/packages/viewer-datagrid/src/ts/event_handlers/sort.ts @@ -31,14 +31,10 @@ const ROW_COL_SORT_ORDER: SortRotationOrder = { asc: undefined, "desc abs": "asc abs", "asc abs": undefined, - // "col desc": "col asc", - // "col asc": undefined, - // "col desc abs": "col asc abs", - // "col asc abs": undefined, }; export async function sortHandler( - this: DatagridModel, + model: DatagridModel, regularTable: RegularTableElement, viewer: PerspectiveViewerElement, event: MouseEvent, @@ -46,7 +42,7 @@ export async function sortHandler( ): Promise { const meta = regularTable.getMeta(target); if (!meta?.column_header) return; - const column_name = meta.column_header[this._config.split_by.length]; + const column_name = meta.column_header[model._config.split_by.length]; const sort_method = event.ctrlKey || (event as MouseEvent & { metaKey?: boolean }).metaKey || @@ -55,22 +51,22 @@ export async function sortHandler( : override_sort; const abs = event.shiftKey; - const sort = sort_method.call(this, `${column_name}`, abs); + const sort = sort_method(model, `${column_name}`, abs); await viewer.restore({ sort }); } export function append_sort( - this: DatagridModel, + model: DatagridModel, column_name: string, abs: boolean, ): SortTerm[] { const sort: SortTerm[] = []; let found = false; - for (const sort_term of this._config.sort) { + for (const sort_term of model._config.sort) { const [_column_name, _sort_dir] = sort_term; if (_column_name === column_name) { found = true; - const term = create_sort.call(this, column_name, _sort_dir, abs); + const term = create_sort(model, column_name, _sort_dir, abs); if (term) { sort.push(term); } @@ -87,13 +83,13 @@ export function append_sort( } export function override_sort( - this: DatagridModel, + model: DatagridModel, column_name: string, abs: boolean, ): SortTerm[] { - for (const [_column_name, _sort_dir] of this._config.sort) { + for (const [_column_name, _sort_dir] of model._config.sort) { if (_column_name === column_name) { - const sort = create_sort.call(this, column_name, _sort_dir, abs); + const sort = create_sort(model, column_name, _sort_dir, abs); return sort ? [sort] : []; } } @@ -102,12 +98,12 @@ export function override_sort( } export function create_sort( - this: DatagridModel, + model: DatagridModel, column_name: string, sort_dir: SortDir | undefined, _abs: boolean, ): SortTerm | undefined { - const is_col_sortable = this._config.split_by.length > 0; + const is_col_sortable = model._config.split_by.length > 0; const order = is_col_sortable ? ROW_COL_SORT_ORDER : ROW_SORT_ORDER; const inc_sort_dir: SortDir | undefined = sort_dir ? order[sort_dir] diff --git a/packages/viewer-datagrid/src/ts/model/column_overrides.ts b/packages/viewer-datagrid/src/ts/model/column_overrides.ts index 9f238df749..128f2edb08 100644 --- a/packages/viewer-datagrid/src/ts/model/column_overrides.ts +++ b/packages/viewer-datagrid/src/ts/model/column_overrides.ts @@ -46,7 +46,10 @@ export function restore_column_size_overrides( this._cached_column_sizes = old_sizes; } - const overrides: Record = {}; + const regular_table = this.regular_table as RegularTableWithOverrides; + const overrides: Record = { + ...regular_table.saveColumnSizes(), + }; const { group_by } = this.model!._config; const tree_header_offset = group_by?.length > 0 ? group_by.length + 1 : 0; @@ -57,15 +60,21 @@ export function restore_column_size_overrides( | undefined; } else { const index = this.model!._column_paths.indexOf(key); + // Skip keys that don't resolve to a known column — e.g. on the + // first draw after `activate`, `_column_paths` has not yet been + // populated by the data listener, so we leave any existing + // `regular-table` widths untouched rather than clobbering them + // with garbage indices. + if (index === -1) { + continue; + } overrides[index + tree_header_offset] = old_sizes[key] as | number | undefined; } } - (this.regular_table as RegularTableWithOverrides).restoreColumnSizes( - overrides, - ); + regular_table.restoreColumnSizes(overrides); } /** diff --git a/packages/viewer-datagrid/src/ts/model/create.ts b/packages/viewer-datagrid/src/ts/model/create.ts index aa29b70c2b..2b5bde3641 100644 --- a/packages/viewer-datagrid/src/ts/model/create.ts +++ b/packages/viewer-datagrid/src/ts/model/create.ts @@ -31,6 +31,25 @@ import { } from "../types.js"; import { CellMetadata } from "regular-table/dist/esm/types.js"; +function arraysChanged(a: T[], b: T[]): boolean { + if (a.length !== b.length) return true; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return true; + } + return false; +} + +function nestedArraysChanged(a: T[][], b: T[][]): boolean { + if (a.length !== b.length) return true; + for (let i = 0; i < a.length; i++) { + if (a[i].length !== b[i].length) return true; + for (let j = 0; j < a[i].length; j++) { + if (a[i][j] !== b[i][j]) return true; + } + } + return false; +} + function get_rule(regular: HTMLElement, tag: string, def: string): string { const color = window.getComputedStyle(regular).getPropertyValue(tag).trim(); if (color.length > 0) { @@ -75,52 +94,20 @@ export async function createModel( const config = (await view.get_config()) as ViewConfig; if (this?.model?._config) { const old = this.model._config; - let group_by_changed = old.group_by.length !== config.group_by.length; + const group_by_changed = arraysChanged(old.group_by, config.group_by); const type_changed = (old.group_by.length === 0 || config.group_by.length === 0) && group_by_changed; - - if (!group_by_changed) { - for (const lvl in old.group_by) { - group_by_changed ||= config.group_by[lvl] !== old.group_by[lvl]; - } - } - - let split_by_changed = old.split_by.length !== config.split_by.length; - if (!split_by_changed) { - for (const lvl in old.split_by) { - split_by_changed ||= config.split_by[lvl] !== old.split_by[lvl]; - } - } - - let columns_changed = old.columns.length !== config.columns.length; - if (!columns_changed) { - for (const lvl in old.columns) { - columns_changed ||= config.columns[lvl] !== old.columns[lvl]; - } - } - - let filter_changed = old.filter.length !== config.filter.length; - if (!filter_changed) { - for (const lvl in old.filter) { - for (const i in config.filter[lvl]) { - filter_changed ||= - config.filter[lvl][i as unknown as number] !== - old.filter[lvl][i as unknown as number]; - } - } - } - - let sort_changed = old.sort.length !== config.sort.length; - if (!sort_changed) { - for (const lvl in old.sort) { - for (const i in config.sort[lvl]) { - sort_changed ||= - config.sort[lvl][i as unknown as number] !== - old.sort[lvl][i as unknown as number]; - } - } - } + const split_by_changed = arraysChanged(old.split_by, config.split_by); + const columns_changed = arraysChanged(old.columns, config.columns); + const filter_changed = nestedArraysChanged( + old.filter as unknown[][], + config.filter as unknown[][], + ); + const sort_changed = nestedArraysChanged( + old.sort as unknown[][], + config.sort as unknown[][], + ); const group_rollup_mode_changed = old.group_rollup_mode !== config.group_rollup_mode; @@ -188,7 +175,15 @@ export async function createModel( const _is_editable: boolean[] = []; const _column_types: ColumnType[] = []; - const _edit_mode: EditMode = this._edit_mode || "READ_ONLY"; + let _edit_mode: EditMode = this._edit_mode || "READ_ONLY"; + if ( + _edit_mode === "SELECT_ROW_TREE" && + (config.group_by.length === 0 || config.group_rollup_mode === "flat") + ) { + _edit_mode = "READ_ONLY"; + this._edit_mode = _edit_mode; + } + this._edit_button!.dataset.editMode = _edit_mode; const model: DatagridModel = Object.assign(extend, { diff --git a/packages/viewer-datagrid/src/ts/model/toolbar.ts b/packages/viewer-datagrid/src/ts/model/toolbar.ts index cea37e2bf9..6fb1065a35 100644 --- a/packages/viewer-datagrid/src/ts/model/toolbar.ts +++ b/packages/viewer-datagrid/src/ts/model/toolbar.ts @@ -10,7 +10,11 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import type { DatagridPluginElement, EditMode } from "../types.js"; +import type { + DatagridModel, + DatagridPluginElement, + EditMode, +} from "../types.js"; export const EDIT_MODES: readonly EditMode[] = [ "READ_ONLY", @@ -18,23 +22,37 @@ export const EDIT_MODES: readonly EditMode[] = [ "SELECT_ROW", "SELECT_COLUMN", "SELECT_REGION", + "SELECT_ROW_TREE", ] as const; +function isSelectRowTreeAvailable(model?: DatagridModel): boolean { + if (!model) return false; + return ( + model._config.group_by.length > 0 && + model._config.group_rollup_mode !== "flat" + ); +} + export function toggle_edit_mode( this: DatagridPluginElement, mode?: EditMode, ): void { if (typeof mode === "undefined") { - mode = - EDIT_MODES[ - (EDIT_MODES.indexOf(this._edit_mode) + 1) % EDIT_MODES.length - ]; + let idx = EDIT_MODES.indexOf(this._edit_mode); + do { + idx = (idx + 1) % EDIT_MODES.length; + } while ( + EDIT_MODES[idx] === "SELECT_ROW_TREE" && + !isSelectRowTreeAvailable(this.model) + ); + mode = EDIT_MODES[idx]; } (this.parentElement as any)?.setSelection?.(); this._edit_mode = mode; if (this.model) { this.model._edit_mode = mode; + this.model._tree_selection_id = undefined; this.model._selection_state = { selected_areas: [], dirty: true, @@ -44,8 +62,6 @@ export function toggle_edit_mode( if (this._edit_button !== undefined) { this._edit_button.dataset.editMode = mode; } - - this.dataset.editMode = mode; } export function toggle_scroll_lock( diff --git a/packages/viewer-datagrid/src/ts/plugin/activate.ts b/packages/viewer-datagrid/src/ts/plugin/activate.ts index 37a56ce931..b7a6a04b3e 100644 --- a/packages/viewer-datagrid/src/ts/plugin/activate.ts +++ b/packages/viewer-datagrid/src/ts/plugin/activate.ts @@ -12,31 +12,40 @@ import { style_selected_column } from "../style_handlers/column_header.js"; import { - click_listener, - mousedown_listener, + createMousedownListener, + createClickListener, + createDblclickListener, } from "../event_handlers/header_click.js"; -import { focusinListener, focusoutListener } from "../event_handlers/focus.js"; -import { keydownListener, clickListener } from "../event_handlers/click.js"; - -import { selectionListener } from "../event_handlers/row_select_click.js"; -import { deselect_all_listener } from "../event_handlers/deselect_all.js"; +import { + createFocusinListener, + createFocusoutListener, +} from "../event_handlers/focus.js"; +import { + createKeydownListener, + createEditClickListener, +} from "../event_handlers/click.js"; import { createModel } from "../model/create.js"; -import { dispatch_click_listener } from "../event_handlers/dispatch_click.js"; - -import { addAreaMouseSelection } from "../event_handlers/select_region.js"; +import { createDispatchClickListener } from "../event_handlers/dispatch_click.js"; import { - createConsolidatedStyleListener, - installConsolidatedStyleMethods, -} from "../style_handlers/consolidated.js"; + addAreaMouseSelection, + type OnSelectCallback, +} from "../event_handlers/select_region.js"; + +import { createConsolidatedStyleListener } from "../style_handlers/consolidated.js"; + +import getCellConfig from "../get_cell_config.js"; import type { View } from "@perspective-dev/client"; -import type { - DatagridPluginElement, - PerspectiveViewerElement, - SelectedPosition, +import { + type DatagridPluginElement, + type PerspectiveViewerElement, + type SelectedPosition, + type SelectedPositionMap, + type SelectionArea, + PerspectiveSelectDetail, } from "../types.js"; import type { RegularTableElement } from "regular-table"; @@ -76,126 +85,144 @@ export async function activate( return; } + const model = this.model; + const regularTable = this.regular_table; + const onSelect: OnSelectCallback = async ( + area: SelectionArea, + isDeselect: boolean, + ) => { + if (model._edit_mode !== "SELECT_ROW_TREE") return; + + // Store the selected row identity on the model so it persists + // even when the selected row scrolls out of the viewport. + if (isDeselect) { + model._tree_selection_id = undefined; + } else { + const idx = area.y0 - (model._last_window?.start_row ?? 0); + if (idx >= 0 && idx < model._ids.length) { + model._tree_selection_id = model._ids[idx]; + } + } + + const { row, column_names, config } = await getCellConfig( + model, + area.y0, + 0, + ); + + let detail: PerspectiveSelectDetail; + if (isDeselect) { + if ((model._last_insert_configs?.length || 0) > 0) { + detail = new PerspectiveSelectDetail( + false, + row, + [], + model._last_insert_configs ?? [], + [], + ); + } else { + throw new Error("Suprious deselect"); + } + + model._last_insert_configs = undefined; + } else { + detail = new PerspectiveSelectDetail( + true, + row, + column_names, + model._last_insert_configs ?? [], + [config], + ); + model._last_insert_configs = [config]; + } + + await regularTable.draw({ preserve_width: true }); + viewer.dispatchEvent( + new CustomEvent( + "perspective-global-filter", + { + bubbles: true, + composed: true, + detail, + }, + ), + ); + }; + addAreaMouseSelection(this, this.regular_table, { className: "psp-select-region", + onSelect, }); - // Create shared state maps for selection and focus tracking - const selected_rows_map = new WeakMap< - RegularTableElement, - Set - >(); - const selected_position_map = new WeakMap< - RegularTableElement, - SelectedPosition - >(); - - // Install consolidated style methods on model prototype - installConsolidatedStyleMethods(this.model); - - // Single consolidated style listener replaces: - // - table_cell_style_listener - // - group_header_style_listener - // - column_header_style_listener - // - selectionStyleListener - // - editable_style_listener - // - focus_style_listener + // Create shared state map for focus tracking + const selected_position_map: SelectedPositionMap = new WeakMap(); + this.regular_table.addStyleListener( createConsolidatedStyleListener( this, - selected_rows_map as any, - selected_position_map as any, - ).bind(this.model, this.regular_table, viewer), - ); - - // uh .. - this.regular_table.addEventListener( - "click", - click_listener.bind( - this.model, - this.regular_table, - ) as EventListener, - ); - - this.regular_table.addEventListener( - "mousedown", - selectionListener.bind( this.model, this.regular_table, viewer, - selected_rows_map as any, - ) as unknown as EventListener, + selected_position_map, + ), ); this.regular_table.addEventListener( - "psp-deselect-all", - deselect_all_listener.bind( - this.model, - this.regular_table, - viewer, - selected_rows_map as any, - ) as unknown as EventListener, + "click", + createClickListener(this.regular_table), ); // User event click this.regular_table.addEventListener( "click", - dispatch_click_listener.bind( - this.model, - this.regular_table, - viewer, - ) as unknown as EventListener, + createDispatchClickListener(this.model, this.regular_table, viewer), ); // tree collapse, expand, edit button headers this.regular_table.addEventListener( "mousedown", - mousedown_listener.bind( - this.model, - this.regular_table, - viewer, - ) as unknown as EventListener, + createMousedownListener(this.model, this.regular_table, viewer), + ); + + this.regular_table.addEventListener( + "dblclick", + createDblclickListener(this.model, this.regular_table, viewer), ); - // Editing event handlers (style handling is now in consolidated listener) - // TODO relies on this.model._is_editable + // Editing event handlers this.regular_table.addEventListener( "click", - clickListener.bind( - this.model, - this.regular_table, - viewer, - ) as EventListener, + createEditClickListener(this.model, this.regular_table, viewer), ); this.regular_table.addEventListener( "focusin", - focusinListener.bind( + createFocusinListener( this.model, this.regular_table, viewer, - selected_position_map as any, - ) as EventListener, + selected_position_map, + ), ); this.regular_table.addEventListener( "focusout", - focusoutListener.bind( + createFocusoutListener( this.model, this.regular_table, viewer, - selected_position_map as any, - ) as EventListener, + selected_position_map, + ), ); this.regular_table.addEventListener( "keydown", - keydownListener.bind( + createKeydownListener( this.model, this.regular_table, viewer, - selected_position_map as any, - ) as EventListener, + selected_position_map, + ), ); // viewer event listeners @@ -204,7 +231,7 @@ export async function activate( (event: Event) => { const toggleEvent = event as ToggleColumnSettingsEvent; if (this.isConnected) { - style_selected_column.call( + style_selected_column( this.model!, this.regular_table, viewer, diff --git a/packages/viewer-datagrid/src/ts/plugin/save.ts b/packages/viewer-datagrid/src/ts/plugin/save.ts index b709da49e1..87bcf560f3 100644 --- a/packages/viewer-datagrid/src/ts/plugin/save.ts +++ b/packages/viewer-datagrid/src/ts/plugin/save.ts @@ -41,5 +41,6 @@ export function save( return JSON.parse(JSON.stringify(token)); } + return {}; } diff --git a/packages/viewer-datagrid/src/ts/style_handlers/body.ts b/packages/viewer-datagrid/src/ts/style_handlers/body.ts index 7031d74136..79a0fbf54d 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/body.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/body.ts @@ -14,9 +14,7 @@ import { RegularTableElement } from "regular-table"; import { type DatagridModel, - type PerspectiveViewerElement, type ColumnsConfig, - type DatagridPluginElement, get_psp_type, } from "../types.js"; @@ -25,39 +23,31 @@ import { cell_style_string } from "./table_cell/string.js"; import { cell_style_datetime } from "./table_cell/datetime.js"; import { cell_style_boolean } from "./table_cell/boolean.js"; import { cell_style_row_header } from "./table_cell/row_header.js"; -import { - CollectedCell, - LocalSelectedPositionMap, - LocalSelectedRowsMap, -} from "./types.js"; +import { CollectedCell } from "./types.js"; /** * Apply styles to all body cells in a single pass. */ export function applyBodyCellStyles( - this: DatagridModel, + model: DatagridModel, cells: CollectedCell[], plugins: ColumnsConfig, isSettingsOpen: boolean, isSelectable: boolean, isEditable: boolean, regularTable: RegularTableElement, - selectedRowsMap: LocalSelectedRowsMap, - selectedPositionMap: LocalSelectedPositionMap, - viewer: PerspectiveViewerElement, ): void { - const hasSelected = selectedRowsMap.has(regularTable); - const selected = selectedRowsMap.get(regularTable); + const selectedId = isSelectable ? model._tree_selection_id : undefined; regularTable.classList.toggle( "flat-group-rollup-mode", - this._config.group_rollup_mode === "flat", + model._config.group_rollup_mode === "flat", ); for (const { element: td, metadata, isHeader } of cells) { const column_name = - metadata.column_header?.[this._config.split_by.length]; - const type = get_psp_type(this, metadata); + metadata.column_header?.[model._config.split_by.length]; + const type = get_psp_type(model, metadata); const plugin = column_name ? plugins[column_name.toString()] : undefined; @@ -66,13 +56,13 @@ export function applyBodyCellStyles( // Calculate aggregate depth visibility // @ts-ignore metadata._is_hidden_by_aggregate_depth = - this._config.group_rollup_mode === "rollup" && + model._config.group_rollup_mode === "rollup" && ((x?: number) => x === 0 || x === undefined ? false : x - 1 < Math.min( - this._config.group_by.length, + model._config.group_by.length, plugin?.aggregate_depth || 0, ))( (metadata.row_header as unknown[] | undefined)?.filter( @@ -82,19 +72,19 @@ export function applyBodyCellStyles( // Apply type-specific cell styling if (is_numeric) { - cell_style_numeric.call( - this, + cell_style_numeric( + model, plugin as any, td, metadata as any, isSettingsOpen, ); } else if (type === "boolean") { - cell_style_boolean.call(this, plugin, td, metadata as any); + cell_style_boolean(model, plugin, td, metadata as any); } else if (type === "string") { - cell_style_string.call(this, plugin as any, td, metadata as any); + cell_style_string(model, plugin as any, td, metadata as any); } else if (type === "date" || type === "datetime") { - cell_style_datetime.call(this, plugin as any, td, metadata); + cell_style_datetime(model, plugin as any, td, metadata); } else { td.style.backgroundColor = ""; td.style.color = ""; @@ -109,10 +99,10 @@ export function applyBodyCellStyles( td.classList.toggle("psp-null", metadata.value === null); td.classList.toggle("psp-align-right", !isHeader && is_numeric); td.classList.toggle("psp-align-left", isHeader || !is_numeric); - if (this._column_settings_selected_column) { + if (model._column_settings_selected_column) { td.classList.toggle( "psp-menu-open", - column_name === this._column_settings_selected_column, + column_name === model._column_settings_selected_column, ); } else { td.classList.toggle("psp-menu-open", false); @@ -123,9 +113,14 @@ export function applyBodyCellStyles( plugin?.number_fg_mode === "bar" && is_numeric, ); + td.classList.toggle( + "psp-color-mode-label-bar", + plugin?.number_fg_mode === "label-bar" && is_numeric, + ); + // Apply row header styling if (isHeader) { - cell_style_row_header.call(this, regularTable, td, metadata as any); + cell_style_row_header(model, regularTable, td, metadata as any); } // Set data attributes @@ -152,55 +147,53 @@ export function applyBodyCellStyles( delete td.dataset.x; } - // Apply selection styling (if selectable) + // Apply tree selection styling (SELECT_ROW_TREE). + // psp-select-region-inactive is exclusively a tree-selection class, + // so always clean it up. psp-select-region is shared with the + // coordinate-based selection modes, so only touch it when in + // SELECT_ROW_TREE mode (isSelectable). + td.classList.toggle("psp-select-region-inactive", false); if (isSelectable) { - if (!hasSelected) { - td.classList.toggle("psp-row-selected", false); - td.classList.toggle("psp-row-subselected", false); + if (!selectedId) { + td.classList.toggle("psp-select-region", false); } else { - const id = this._ids[(metadata.y ?? 0) - (metadata.y0 ?? 0)]; - const key_match = (selected as unknown[]).reduce( + const id = model._ids[(metadata.y ?? 0) - (metadata.y0 ?? 0)]; + const key_match = selectedId.reduce( (agg, x, i) => agg && x === id[i], true, ); - const selectedArr = selected as unknown[]; + const isExact = id.length === selectedId.length && key_match; + const isSub = id.length !== selectedId.length && key_match; + if (isHeader) { if ( metadata.type === "row_header" && metadata.row_header_x !== undefined && !!id[metadata.row_header_x] ) { - td.classList.toggle("psp-row-selected", false); - td.classList.toggle("psp-row-subselected", false); + td.classList.toggle("psp-select-region", false); } else { + td.classList.toggle("psp-select-region", isExact); td.classList.toggle( - "psp-row-selected", - id.length === selectedArr.length && key_match, - ); - td.classList.toggle( - "psp-row-subselected", - id.length !== selectedArr.length && key_match, + "psp-select-region-inactive", + isSub, ); } } else { - td.classList.toggle( - "psp-row-selected", - id.length === selectedArr.length && key_match, - ); - td.classList.toggle( - "psp-row-subselected", - id.length !== selectedArr.length && key_match, - ); + td.classList.toggle("psp-select-region", isExact); + td.classList.toggle("psp-select-region-inactive", isSub); } } + } else { + td.classList.toggle("psp-select-region", false); } // Apply editable styling (if editable) if (!isHeader && metadata.type === "body") { - if (isEditable && this._is_editable[metadata.x]) { + if (isEditable && model._is_editable[metadata.x]) { const col_name = - metadata.column_header?.[this._config.split_by.length]; + metadata.column_header?.[model._config.split_by.length]; const col_name_str = col_name?.toString(); if ( col_name_str && diff --git a/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts b/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts index 49890b68ff..c1e8a62554 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts @@ -24,7 +24,7 @@ import { CollectedHeaderRow } from "./types.js"; * the column settings panel. */ export function style_selected_column( - this: DatagridModel, + model: DatagridModel, regularTable: RegularTableElement, viewer: PerspectiveViewerElement, selectedColumn: string | undefined, @@ -72,7 +72,7 @@ export function style_selected_column( const open = title.textContent === selectedColumn; title.classList.toggle("psp-menu-open", open); editBtn.classList.toggle("psp-menu-open", open); - if (this._config.columns.length > 1) { + if (model._config.columns.length > 1) { for (const r of regularTable.querySelectorAll("td")) { const meta = regularTable.getMeta(r); if (!meta?.column_header) continue; @@ -92,16 +92,16 @@ export function style_selected_column( * Style a single column header row. */ export function styleColumnHeaderRow( - this: DatagridModel, + model: DatagridModel, headerRow: CollectedHeaderRow, regularTable: RegularTableElement, is_menu_row: boolean, ): void { const header_depth = - this._config.group_by.length - - (this._config.group_rollup_mode === "flat" ? 1 : 0); + model._config.group_by.length - + (model._config.group_rollup_mode === "flat" ? 1 : 0); - const selectedColumn = this._column_settings_selected_column; + const selectedColumn = model._column_settings_selected_column; for (const { element: td, metadata } of headerRow.cells) { if ( !metadata || @@ -111,15 +111,15 @@ export function styleColumnHeaderRow( continue; const column_name = - metadata.column_header?.[this._config.split_by.length]; + metadata.column_header?.[model._config.split_by.length]; - const sort = this._config.sort.find((x) => x[0] === column_name); + const sort = model._config.sort.find((x) => x[0] === column_name); const is_corner = typeof metadata.x === "undefined"; const needs_border = (metadata.type === "corner" && metadata.row_header_x === header_depth) || (!is_corner && - (metadata.x + 1) % this._config.columns.length === 0); + (metadata.x + 1) % model._config.columns.length === 0); td.classList.toggle("psp-header-border", needs_border); td.classList.toggle("psp-header-group", false); @@ -159,7 +159,7 @@ export function styleColumnHeaderRow( !is_menu_row && !!sort && sort[1] === "col desc abs", ); - const type = get_psp_type(this, metadata); + const type = get_psp_type(model, metadata); const is_numeric = type === "integer" || type === "float"; const is_string = type === "string"; const is_date = type === "date"; @@ -171,13 +171,13 @@ export function styleColumnHeaderRow( "psp-menu-enabled", (is_string || is_numeric || is_date || is_datetime) && !is_corner && - metadata.column_header_y === this._config.split_by.length + 1, + metadata.column_header_y === model._config.split_by.length + 1, ); td.classList.toggle( "psp-sort-enabled", (is_string || is_numeric || is_date || is_datetime) && !is_corner && - metadata.column_header_y === this._config.split_by.length, + metadata.column_header_y === model._config.split_by.length, ); td.classList.toggle( "psp-is-width-override", @@ -185,7 +185,7 @@ export function styleColumnHeaderRow( ); // Apply menu-open for selected column - if (this._config.columns.length > 1 && selectedColumn) { + if (model._config.columns.length > 1 && selectedColumn) { const isOpen = metadata.column_header?.[metadata.column_header.length - 2] === selectedColumn; diff --git a/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts b/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts index 1dc10b65a5..61436d4029 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts @@ -17,66 +17,16 @@ import type { PerspectiveViewerElement, ColumnsConfig, DatagridPluginElement, - SelectedPosition, + SelectedPositionMap, } from "../types.js"; +import { isEditableMode } from "../types.js"; import { applyFocusStyle } from "./focus.js"; -import { styleColumnHeaderRow } from "./column_header.js"; import { applyColumnHeaderStyles } from "./editable.js"; import { applyGroupHeaderStyles } from "./group_header.js"; import { applyBodyCellStyles } from "./body.js"; import { CellMetadata } from "regular-table/dist/esm/types.js"; - -interface CollectedCell { - element: HTMLElement; - metadata: CellMetadata; - isHeader: boolean; -} - -interface CollectedHeaderRow { - row: HTMLTableRowElement; - cells: Array<{ - element: HTMLTableCellElement; - metadata: CellMetadata | undefined; - }>; -} - -/** - * Context object passed through consolidated styling - */ -export interface StyleContext { - model: DatagridModel; - regularTable: RegularTableElement; - viewer: PerspectiveViewerElement; - datagrid: DatagridPluginElement; - plugins: ColumnsConfig; - isSettingsOpen: boolean; - isSelectable: boolean; - isEditable: boolean; - selectedRowsMap: Map; - selectedPositionMap: Map; -} - -// Local types for selection maps - match the actual runtime usage -// (activate.ts uses `as any` casts when passing these) -type LocalSelectedRowsMap = WeakMap; -type LocalSelectedPositionMap = WeakMap; - -function isEditableMode( - model: DatagridModel, - viewer: PerspectiveViewerElement, - allowed: boolean = false, -): boolean { - const has_pivots = - model._config.group_by.length === 0 && - model._config.split_by.length === 0; - const selectable = viewer.hasAttribute("selectable"); - const plugin = viewer.children[0] as - | (DatagridPluginElement & { dataset: DOMStringMap }) - | undefined; - const editable = allowed || plugin?.dataset?.editMode === "EDIT"; - return has_pivots && !selectable && editable; -} +import { CollectedCell, CollectedHeaderRow } from "./types.js"; /** * Consolidated style listener that handles all cell styling in a single pass. @@ -86,24 +36,18 @@ function isEditableMode( */ export function createConsolidatedStyleListener( datagrid: DatagridPluginElement, - selectedRowsMap: LocalSelectedRowsMap, - selectedPositionMap: LocalSelectedPositionMap, -): ( - this: DatagridModel, + model: DatagridModel, regularTable: RegularTableElement, viewer: PerspectiveViewerElement, -) => void { - return function consolidatedStyleListener( - this: DatagridModel, - regularTable: RegularTableElement, - viewer: PerspectiveViewerElement, - ): void { + selectedPositionMap: SelectedPositionMap, +): () => void { + return function consolidatedStyleListener(): void { const plugins: ColumnsConfig = (regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {}; const isSettingsOpen = viewer.hasAttribute("settings"); - const isSelectable = viewer.hasAttribute("selectable"); - const isEditable = isEditableMode(this, viewer); - const isEditableAllowed = isEditableMode(this, viewer, true); + const isSelectable = model._edit_mode === "SELECT_ROW_TREE"; + const isEditable = isEditableMode(model, viewer); + const isEditableAllowed = isEditableMode(model, viewer, true); // Toggle edit mode class on datagrid datagrid.classList.toggle("edit-mode-allowed", isEditableAllowed); @@ -117,7 +61,11 @@ export function createConsolidatedStyleListener( cell as HTMLElement, ) as CellMetadata | undefined; - if (metadata) { + if ( + metadata && + (metadata.type === "body" || + metadata.type === "row_header") + ) { const isHeader = cell.tagName === "TH"; bodyCells.push({ element: cell as HTMLElement, @@ -152,67 +100,18 @@ export function createConsolidatedStyleListener( } } - this._applyBodyCellStyles( + applyBodyCellStyles( + model, bodyCells, plugins, isSettingsOpen, isSelectable, isEditable, regularTable, - selectedRowsMap, - selectedPositionMap, - viewer, ); - this._applyGroupHeaderStyles(groupHeaderRows, regularTable); - this._applyColumnHeaderStyles(groupHeaderRows, regularTable, viewer); - this._applyFocusStyle(bodyCells, regularTable, selectedPositionMap); + applyGroupHeaderStyles(model, groupHeaderRows, regularTable); + applyColumnHeaderStyles(model, groupHeaderRows, regularTable, viewer); + applyFocusStyle(model, bodyCells, regularTable, selectedPositionMap); }; } - -declare module "../types.js" { - interface DatagridModel { - _applyBodyCellStyles( - cells: CollectedCell[], - plugins: ColumnsConfig, - isSettingsOpen: boolean, - isSelectable: boolean, - isEditable: boolean, - regularTable: RegularTableElement, - selectedRowsMap: LocalSelectedRowsMap, - selectedPositionMap: LocalSelectedPositionMap, - viewer: PerspectiveViewerElement, - ): void; - _applyGroupHeaderStyles( - headerRows: CollectedHeaderRow[], - regularTable: RegularTableElement, - ): void; - _applyColumnHeaderStyles( - headerRows: CollectedHeaderRow[], - regularTable: RegularTableElement, - viewer: PerspectiveViewerElement, - ): void; - _applyFocusStyle( - cells: CollectedCell[], - regularTable: RegularTableElement, - selectedPositionMap: LocalSelectedPositionMap, - ): void; - _styleColumnHeaderRow( - headerRow: CollectedHeaderRow, - regularTable: RegularTableElement, - is_menu_row: boolean, - ): void; - } -} - -/** - * Install the styling methods on the DatagridModel prototype. - * This should be called once during module initialization. - */ -export function installConsolidatedStyleMethods(modelPrototype: any): void { - modelPrototype._applyBodyCellStyles = applyBodyCellStyles; - modelPrototype._applyGroupHeaderStyles = applyGroupHeaderStyles; - modelPrototype._applyColumnHeaderStyles = applyColumnHeaderStyles; - modelPrototype._applyFocusStyle = applyFocusStyle; - modelPrototype._styleColumnHeaderRow = styleColumnHeaderRow; -} diff --git a/packages/viewer-datagrid/src/ts/style_handlers/editable.ts b/packages/viewer-datagrid/src/ts/style_handlers/editable.ts index 3e75f1e9b1..608267fd7b 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/editable.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/editable.ts @@ -13,6 +13,7 @@ import { RegularTableElement } from "regular-table"; import type { DatagridModel, PerspectiveViewerElement } from "../types.js"; +import { styleColumnHeaderRow } from "./column_header.js"; import { CollectedHeaderRow } from "./types.js"; @@ -20,7 +21,7 @@ import { CollectedHeaderRow } from "./types.js"; * Apply styles to column header rows. */ export function applyColumnHeaderStyles( - this: DatagridModel, + model: DatagridModel, headerRows: CollectedHeaderRow[], regularTable: RegularTableElement, viewer: PerspectiveViewerElement, @@ -28,7 +29,7 @@ export function applyColumnHeaderStyles( if (headerRows.length === 0) return; // Style selected column for settings panel - const selectedColumn = this._column_settings_selected_column; + const selectedColumn = model._column_settings_selected_column; const len = headerRows.length; const settings_open = viewer.hasAttribute("settings"); @@ -76,19 +77,19 @@ export function applyColumnHeaderStyles( } // Style the actual column header rows - const colHeadersIndex = this._config.split_by.length; + const colHeadersIndex = model._config.split_by.length; if (colHeadersIndex < headerRows.length) { const colHeaders = headerRows[colHeadersIndex]; if (colHeaders) { - this._styleColumnHeaderRow(colHeaders, regularTable, false); + styleColumnHeaderRow(model, colHeaders, regularTable, false); } } - const menuHeadersIndex = this._config.split_by.length + 1; + const menuHeadersIndex = model._config.split_by.length + 1; if (menuHeadersIndex < headerRows.length) { const menuHeaders = headerRows[menuHeadersIndex]; if (menuHeaders) { - this._styleColumnHeaderRow(menuHeaders, regularTable, true); + styleColumnHeaderRow(model, menuHeaders, regularTable, true); } } } diff --git a/packages/viewer-datagrid/src/ts/style_handlers/focus.ts b/packages/viewer-datagrid/src/ts/style_handlers/focus.ts index 71cfa3021d..b2e732d0ce 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/focus.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/focus.ts @@ -11,18 +11,18 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { RegularTableElement } from "regular-table"; -import type { DatagridModel, SelectedPosition } from "../types.js"; -import { CollectedCell, LocalSelectedPositionMap } from "./types.js"; +import type { DatagridModel, SelectedPositionMap } from "../types.js"; +import { CollectedCell } from "./types.js"; /** * Apply focus style to the selected cell. * Optimized to use collected cells instead of querySelectorAll. */ export function applyFocusStyle( - this: DatagridModel, + _model: DatagridModel, cells: CollectedCell[], regularTable: RegularTableElement, - selectedPositionMap: LocalSelectedPositionMap, + selectedPositionMap: SelectedPositionMap, ): void { const selected_position = selectedPositionMap.get(regularTable); const host = regularTable.getRootNode() as Document; @@ -60,7 +60,7 @@ export function applyFocusStyle( */ export function focusSelectedCell( regularTable: RegularTableElement, - selectedPositionMap: Map, + selectedPositionMap: SelectedPositionMap, ): boolean { const selected_position = selectedPositionMap.get(regularTable); if (!selected_position) { diff --git a/packages/viewer-datagrid/src/ts/style_handlers/group_header.ts b/packages/viewer-datagrid/src/ts/style_handlers/group_header.ts index 00befe849b..4140469c62 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/group_header.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/group_header.ts @@ -19,11 +19,11 @@ import { CollectedHeaderRow } from "./types.js"; * Apply styles to group header rows. */ export function applyGroupHeaderStyles( - this: DatagridModel, + model: DatagridModel, headerRows: CollectedHeaderRow[], regularTable: RegularTableElement, ): void { - const header_depth = this._config.group_by.length; + const header_depth = model._config.group_by.length; const m: boolean[][] = []; let marked = new Set(); @@ -54,6 +54,7 @@ export function applyGroupHeaderStyles( ); td.classList.toggle("psp-color-mode-bar", false); + td.classList.toggle("psp-color-mode-label-bar", false); td.classList.toggle("psp-header-sort-asc", false); td.classList.toggle("psp-header-sort-desc", false); td.classList.toggle("psp-header-sort-col-asc", false); diff --git a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/boolean.ts b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/boolean.ts index d11d6ac959..caa0d4f5f1 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/boolean.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/boolean.ts @@ -14,7 +14,7 @@ import { CellMetadata } from "regular-table/dist/esm/types.js"; import type { DatagridModel, ColumnConfig, ColorRecord } from "../../types.js"; export function cell_style_boolean( - this: DatagridModel, + model: DatagridModel, _plugin: ColumnConfig | undefined, td: HTMLElement, metadata: CellMetadata, @@ -26,9 +26,9 @@ export function cell_style_boolean( } else { const [hex]: ColorRecord | [string, number, number, number, string] = metadata.user === true - ? this._pos_fg_color + ? model._pos_fg_color : metadata.user === false - ? this._neg_fg_color + ? model._neg_fg_color : ["", 0, 0, 0, ""]; td.style.backgroundColor = ""; td.style.color = hex; diff --git a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/cell_flash.ts b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/cell_flash.ts index 90f23cca3e..3b1095ab9e 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/cell_flash.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/cell_flash.ts @@ -14,14 +14,14 @@ import { CellMetadataBody } from "regular-table/dist/esm/types.js"; import type { DatagridModel, ColorRecord } from "../../types.js"; export function style_cell_flash( - this: DatagridModel, + model: DatagridModel, metadata: CellMetadataBody, td: HTMLElement, [, , , , , pos_s, pos_e]: ColorRecord, [, , , , , neg_s, neg_e]: ColorRecord, is_settings_open: boolean, ): void { - const id = this._ids?.[metadata.dy ?? 0]?.join("|"); + const id = model._ids?.[metadata.dy ?? 0]?.join("|"); const metadata_path = ( is_settings_open ? (metadata.column_header ?? []).slice(0, -1) @@ -29,19 +29,19 @@ export function style_cell_flash( ).join("|"); if ( - this.last_reverse_columns?.has(metadata_path) && - this.last_reverse_ids?.has(id) + model.last_reverse_columns?.has(metadata_path) && + model.last_reverse_ids?.has(id) ) { - const row_idx = this.last_reverse_ids?.get(id); - const col_idx = this.last_reverse_columns.get(metadata_path); - if (!this._is_old_viewport) { + const row_idx = model.last_reverse_ids?.get(id); + const col_idx = model.last_reverse_columns.get(metadata_path); + if (!model._is_old_viewport) { td.style.animation = ""; } else if ( col_idx !== undefined && row_idx !== undefined && - (this.last_meta?.[col_idx]?.[row_idx] as number | undefined) !== + (model.last_meta?.[col_idx]?.[row_idx] as number | undefined) !== undefined && - (this.last_meta![col_idx]![row_idx] as number) > + (model.last_meta![col_idx]![row_idx] as number) > ((metadata.user ?? 0) as number) ) { td.style.setProperty("--pulse--background-color-start", neg_s); @@ -54,9 +54,9 @@ export function style_cell_flash( } else if ( col_idx !== undefined && row_idx !== undefined && - (this.last_meta?.[col_idx]?.[row_idx] as number | undefined) !== + (model.last_meta?.[col_idx]?.[row_idx] as number | undefined) !== undefined && - (this.last_meta![col_idx]![row_idx] as number) < + (model.last_meta![col_idx]![row_idx] as number) < ((metadata.user ?? 0) as number) ) { td.style.setProperty("--pulse--background-color-start", pos_s); diff --git a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/datetime.ts b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/datetime.ts index 8a37381278..0d8a6ce7c8 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/datetime.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/datetime.ts @@ -22,7 +22,7 @@ interface PluginWithColor extends Omit { } export function cell_style_datetime( - this: DatagridModel, + model: DatagridModel, plugin: PluginWithColor, td: HTMLElement, metadata: CellMetadata, @@ -31,7 +31,7 @@ export function cell_style_datetime( if (plugin?.color !== undefined) { return plugin.color; } else { - return this._color; + return model._color; } })(); @@ -51,7 +51,7 @@ export function cell_style_datetime( plugin?.datetime_color_mode === "background" && metadata.user !== null ) { - const source = this._plugin_background as [number, number, number]; + const source = model._plugin_background as [number, number, number]; const foreground = infer_foreground_from_background( rgbaToRgb([r, g, b, 1], source), ); diff --git a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/numeric.ts b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/numeric.ts index 5fb4c67c9d..f61c6b0b14 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/numeric.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/numeric.ts @@ -36,7 +36,7 @@ interface PluginWithColors } export function cell_style_numeric( - this: DatagridModel, + model: DatagridModel, plugin: PluginWithColors | undefined, td: HTMLElement, metadata: CellMetaWithExtras, @@ -49,14 +49,14 @@ export function cell_style_numeric( if (plugin?.pos_bg_color !== undefined) { pos_bg_color = plugin.pos_bg_color; } else { - pos_bg_color = this._pos_bg_color; + pos_bg_color = model._pos_bg_color; } let neg_bg_color: ColorRecord; if (plugin?.neg_bg_color !== undefined) { neg_bg_color = plugin.neg_bg_color; } else { - neg_bg_color = this._neg_bg_color; + neg_bg_color = model._neg_bg_color; } const bg_tuple: ColorRecord = is_positive @@ -65,9 +65,9 @@ export function cell_style_numeric( ? neg_bg_color : [ "", - this._plugin_background[0], - this._plugin_background[1], - this._plugin_background[2], + model._plugin_background[0], + model._plugin_background[1], + model._plugin_background[2], "", "", "", @@ -91,7 +91,7 @@ export function cell_style_numeric( Math.abs((metadata.user ?? 0) / (plugin.bg_gradient ?? 1)), ), ); - const source = this._plugin_background as [number, number, number]; + const source = model._plugin_background as [number, number, number]; const foreground = infer_foreground_from_background( rgbaToRgb([r, g, b, a], source), ); @@ -100,8 +100,8 @@ export function cell_style_numeric( td.style.color = foreground; td.style.backgroundColor = `rgba(${r},${g},${b},${a})`; } else if (plugin?.number_bg_mode === "pulse") { - style_cell_flash.call( - this, + style_cell_flash( + model, metadata as any, td, pos_bg_color, @@ -129,23 +129,23 @@ export function cell_style_numeric( ? plugin.neg_fg_color! : [ "", - this._plugin_background[0], - this._plugin_background[1], - this._plugin_background[2], + model._plugin_background[0], + model._plugin_background[1], + model._plugin_background[2], "", "", "", ]; } else { return is_positive - ? this._pos_fg_color + ? model._pos_fg_color : is_negative - ? this._neg_fg_color + ? model._neg_fg_color : [ "", - this._plugin_background[0], - this._plugin_background[1], - this._plugin_background[2], + model._plugin_background[0], + model._plugin_background[1], + model._plugin_background[2], "", "", "", @@ -160,7 +160,7 @@ export function cell_style_numeric( td.style.color = ""; } else if (plugin?.number_fg_mode === "disabled") { if (plugin?.number_bg_mode === "color") { - const source = this._plugin_background as [number, number, number]; + const source = model._plugin_background as [number, number, number]; const foreground = infer_foreground_from_background( rgbaToRgb([bg_tuple[1], bg_tuple[2], bg_tuple[3], 1], source), ); @@ -173,13 +173,12 @@ export function cell_style_numeric( } else if (plugin?.number_fg_mode === "bar") { td.style.color = ""; td.style.position = "relative"; - if ( - gradhex !== "" && - td.children.length > 0 && - td.children[0].nodeType === Node.ELEMENT_NODE - ) { - (td.children[0] as HTMLElement).style.background = gradhex; - } + td.style.setProperty("--psp-label-bar-color", gradhex); + td.style.setProperty("--psp-label-bar-bg", hex); + } else if (plugin?.number_fg_mode === "label-bar") { + td.style.color = ""; + td.style.setProperty("--psp-label-bar-color", gradhex); + td.style.setProperty("--psp-label-bar-bg", hex); } else if (plugin?.number_fg_mode === "color" || !plugin?.number_fg_mode) { td.style.color = hex; } diff --git a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/row_header.ts b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/row_header.ts index ed60f62dc5..5d15b40a8f 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/row_header.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/row_header.ts @@ -18,7 +18,7 @@ import type { DatagridModel } from "../../types.js"; import { RegularTableElement } from "regular-table"; export function cell_style_row_header( - this: DatagridModel, + model: DatagridModel, regularTable: RegularTableElement, td: HTMLElement, metadata: CellMetadataRowHeader, @@ -28,7 +28,7 @@ export function cell_style_row_header( metadata.value !== null && metadata.value?.toString()?.trim().length > 0; const is_leaf = - (metadata.row_header_x ?? 0) >= this._config.group_by.length; + (metadata.row_header_x ?? 0) >= model._config.group_by.length; const next = regularTable.getMeta({ dx: 0, dy: (metadata.y ?? 0) - (metadata.y0 ?? 0) + 1, diff --git a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/string.ts b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/string.ts index 5885132ce3..7e4fe3ea07 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/table_cell/string.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/table_cell/string.ts @@ -28,17 +28,17 @@ interface PluginWithColor extends Omit { } export function cell_style_string( - this: DatagridModel, + model: DatagridModel, plugin: PluginWithColor | undefined, td: HTMLElement, metadata: CellMetaWithExtras, ): void { - const column_name = metadata.column_header?.[this._config.split_by.length]; + const column_name = metadata.column_header?.[model._config.split_by.length]; const colorRecord: ColorRecord = (() => { if (plugin?.color !== undefined) { return plugin.color; } else { - return this._color; + return model._color; } })(); @@ -60,7 +60,7 @@ export function cell_style_string( plugin?.string_color_mode === "background" && metadata.user !== null ) { - const source = this._plugin_background as [number, number, number]; + const source = model._plugin_background as [number, number, number]; const foreground = infer_foreground_from_background( rgbaToRgb([r, g, b, 1], source), ); @@ -71,16 +71,16 @@ export function cell_style_string( metadata.user !== null && column_name ) { - if (!this._series_color_map.has(column_name)) { - this._series_color_map.set(column_name, new Map()); - this._series_color_seed.set(column_name, 0); + if (!model._series_color_map.has(column_name)) { + model._series_color_map.set(column_name, new Map()); + model._series_color_seed.set(column_name, 0); } - const series_map = this._series_color_map.get(column_name)!; + const series_map = model._series_color_map.get(column_name)!; if (metadata.user && !series_map.has(metadata.user)) { - const seed = this._series_color_seed.get(column_name) ?? 0; + const seed = model._series_color_seed.get(column_name) ?? 0; series_map.set(metadata.user, seed); - this._series_color_seed.set(column_name, seed + 1); + model._series_color_seed.set(column_name, seed + 1); } const color_seed = series_map.get(metadata.user!) ?? 0; @@ -89,7 +89,7 @@ export function cell_style_string( const color2 = chroma(h, s, l, "hsl"); const [r2, g2, b2] = color2.rgb(); const hex2 = color2.hex(); - const source = this._plugin_background as [number, number, number]; + const source = model._plugin_background as [number, number, number]; const foreground = infer_foreground_from_background( rgbaToRgb([r2, g2, b2, 1], source), ); diff --git a/packages/viewer-datagrid/src/ts/style_handlers/types.ts b/packages/viewer-datagrid/src/ts/style_handlers/types.ts index f57cac0d2e..b9ac4c1b2a 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/types.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/types.ts @@ -10,13 +10,11 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { RegularTableElement } from "regular-table"; import { CellMetadata, CellMetadataBody, CellMetadataRowHeader, } from "regular-table/dist/esm/types.js"; -import type { SelectedPosition } from "../types.js"; export interface CollectedCell { element: HTMLElement; @@ -31,11 +29,3 @@ export interface CollectedHeaderRow { metadata: CellMetadata | undefined; }>; } - -// Local types for selection maps - match the actual runtime usage -// (activate.ts uses `as any` casts when passing these) -export type LocalSelectedRowsMap = WeakMap; -export type LocalSelectedPositionMap = WeakMap< - RegularTableElement, - SelectedPosition ->; diff --git a/packages/viewer-datagrid/src/ts/types.ts b/packages/viewer-datagrid/src/ts/types.ts index 6f15e0985b..bfea5d1820 100644 --- a/packages/viewer-datagrid/src/ts/types.ts +++ b/packages/viewer-datagrid/src/ts/types.ts @@ -46,7 +46,8 @@ export type EditMode = | "EDIT" | "SELECT_COLUMN" | "SELECT_ROW" - | "SELECT_REGION"; + | "SELECT_REGION" + | "SELECT_ROW_TREE"; // Color record for styling - tuple returned by make_color_record export type ColorRecord = [ @@ -177,6 +178,8 @@ export interface DatagridModel { _column_types: ColumnType[]; _is_editable: boolean[]; _edit_mode: EditMode; + _tree_selection_id?: unknown[]; + _last_insert_configs?: ViewConfigUpdate[]; _selection_state: SelectionState; _row_header_types: ColumnType[]; _series_color_map: Map>; @@ -221,6 +224,7 @@ export interface PerspectiveViewerElement extends HTMLElement { toggleColumnSettings(columnName?: string): Promise; hasAttribute(name: string): boolean; setSelection(viewport?: ViewWindow): void; + getSelection(): ViewWindow | undefined; dispatchEvent(event: Event): boolean; children: HTMLCollectionOf; } @@ -255,7 +259,7 @@ export interface PerspectiveClickDetail { config: Partial; } -export { PerspectiveSelectDetail } from "@perspective-dev/viewer"; +export { PerspectiveSelectDetail } from "@perspective-dev/viewer/src/ts/extensions.js"; // Mouse event with handled flag export interface HandledMouseEvent extends MouseEvent { @@ -281,9 +285,22 @@ export interface DatagridPluginElement extends HTMLElement { _reset_column_size?: boolean; } -// Map types for selected rows and positions -export type SelectedRowsMap = WeakMap>; +// Map types for selected positions export type SelectedPositionMap = WeakMap< RegularTableElement, SelectedPosition >; + +// Centralized editable mode check - used by style handlers and event handlers +export function isEditableMode( + model: DatagridModel, + viewer: PerspectiveViewerElement, + allowed: boolean = false, +): boolean { + const has_pivots = + model._config.group_by.length === 0 && + model._config.split_by.length === 0; + const plugin = viewer.children[0] as DatagridPluginElement | undefined; + const editable = allowed || plugin?._edit_mode === "EDIT"; + return has_pivots && editable; +} diff --git a/packages/workspace/src/ts/workspace/commands.ts b/packages/workspace/src/ts/workspace/commands.ts index df99063195..6940965485 100644 --- a/packages/workspace/src/ts/workspace/commands.ts +++ b/packages/workspace/src/ts/workspace/commands.ts @@ -17,6 +17,7 @@ import { Signal } from "@lumino/signaling"; import type { HTMLPerspectiveViewerCopyMenuElement, HTMLPerspectiveViewerExportMenuElement, + ViewerConfigUpdate, } from "@perspective-dev/viewer"; import type { PerspectiveWorkspace } from "./workspace"; @@ -141,7 +142,7 @@ export const createCommands = ( args.target_widget_name as string, )!; - const config = await target_widget.save(); + const config = (await target_widget.save()) as ViewerConfigUpdate; const new_widget = await workspace._createWidgetAndNode({ config, slot: undefined, diff --git a/packages/workspace/src/ts/workspace/widget.ts b/packages/workspace/src/ts/workspace/widget.ts index a921b8794b..5ab037d2f3 100644 --- a/packages/workspace/src/ts/workspace/widget.ts +++ b/packages/workspace/src/ts/workspace/widget.ts @@ -67,11 +67,8 @@ export class PerspectiveViewerWidget extends Widget { } async save() { - let config = { - ...(await this.viewer.save()), - }; - - delete config["settings"]; + let config = await this.viewer.save(); + config["settings"] = false; return config; } diff --git a/packages/workspace/src/ts/workspace/workspace.ts b/packages/workspace/src/ts/workspace/workspace.ts index 403d228da6..f2ce3f59c3 100644 --- a/packages/workspace/src/ts/workspace/workspace.ts +++ b/packages/workspace/src/ts/workspace/workspace.ts @@ -16,11 +16,9 @@ import { CommandRegistry } from "@lumino/commands"; import { SplitPanel, Panel, DockPanel } from "@lumino/widgets"; import uniqBy from "lodash/uniqBy"; import { DebouncedFunc, DebouncedFuncLeading, isEqual } from "lodash"; -import { throttle } from "lodash"; -import debounce from "lodash/debounce"; import type { HTMLPerspectiveViewerElement, - ViewerConfigUpdate, + ViewerConfig, } from "@perspective-dev/viewer"; import type * as psp from "@perspective-dev/client"; import type * as psp_viewer from "@perspective-dev/viewer"; @@ -29,6 +27,7 @@ import { PerspectiveDockPanel } from "./dockpanel"; import { WorkspaceMenu } from "./menu"; import { createCommands } from "./commands"; import { PerspectiveViewerWidget } from "./widget"; +import type { Filter } from "@perspective-dev/client"; class AsyncMutex { _lock: Promise | null; @@ -329,7 +328,7 @@ export class PerspectiveWorkspace extends SplitPanel { for (const widget of this.masterPanel.widgets) { const psp_widget = widget as PerspectiveViewerWidget; layout.viewers[psp_widget.viewer.getAttribute("slot")!] = - await psp_widget.save(); + (await psp_widget.save()) as psp_viewer.ViewerConfigUpdate; } const widgets = PerspectiveDockPanel.getWidgets( @@ -342,7 +341,8 @@ export class PerspectiveWorkspace extends SplitPanel { widgets.map(async (widget) => { const psp_widget = widget as PerspectiveViewerWidget; const slot = psp_widget.viewer.getAttribute("slot")!; - layout.viewers[slot] = await psp_widget.save(); + layout.viewers[slot] = + (await psp_widget.save()) as psp_viewer.ViewerConfigUpdate; layout.viewers[slot]!.settings = false; }), ); @@ -515,10 +515,18 @@ export class PerspectiveWorkspace extends SplitPanel { if (master) { widget.viewer.classList.add("workspace-master-widget"); - widget.viewer.toggleAttribute("selectable", true); + await widget.viewer.restore({ + plugin_config: { edit_mode: "SELECT_ROW_TREE" }, + }); + + widget.viewer.addEventListener( + "perspective-global-filter", + this.on_global_filter_callback, + ); + widget.viewer.addEventListener( "perspective-select", - this.onPerspectiveSelect.bind(this), + this.on_select_callback, ); // TODO remove event listener @@ -613,7 +621,7 @@ export class PerspectiveWorkspace extends SplitPanel { this._unmaximize(); } - const config = await widget.save(); + const config = (await widget.save()) as psp_viewer.ViewerConfigUpdate; config.settings = false; config.title = config.title ? `${config.title} (*)` : ""; const duplicate = await this._createWidgetAndNode({ @@ -688,30 +696,26 @@ export class PerspectiveWorkspace extends SplitPanel { candidates: Set, ) { const config = await viewer.save(); - const table = await viewer.getTable(); - const availableColumns = Object.keys(await table.schema()); - const currentFilters = config.filter || []; - const columnAvailable = (filter: psp.Filter) => - filter[0] && availableColumns.includes(filter[0]); - - const clearColumns = new Set(removeFilters.map((f) => f[0])); - const validFilters = insertFilters.filter(columnAvailable); - validFilters.push( - ...currentFilters.filter( - (x: [string, ..._: string[]]) => - !candidates.has(x[0]) && !clearColumns.has(x[0]), - ), - ); - const newFilters = uniqBy(validFilters, (item) => item[0]); - await viewer.restore({ filter: newFilters }); + await viewer.restore({ + filter: config.filter + .filter((x) => !removeFilters.find((y) => y[0] === x[0])) + .concat(insertFilters), + }); } - async onPerspectiveSelect(event: CustomEvent) { + async onPerspectiveSelect( + filterFun: (config: ViewerConfig) => boolean, + event: CustomEvent, + ) { const config = await ( event.target as HTMLPerspectiveViewerElement ).save(); + if (!filterFun(config)) { + return; + } + const candidates = new Set([ ...(config["group_by"] || []), ...(config["split_by"] || []), @@ -735,13 +739,26 @@ export class PerspectiveWorkspace extends SplitPanel { }); } + private on_select_callback = this.onPerspectiveSelect.bind( + this, + (config) => config.plugin !== "Datagrid", + ); + + private on_global_filter_callback = this.onPerspectiveSelect.bind( + this, + // (config) => config.plugin === "Datagrid", + (config) => true, + ); + async makeMaster(widget: PerspectiveViewerWidget) { if (widget.viewer.hasAttribute("settings")) { await widget.toggleConfig(); } widget.viewer.classList.add("workspace-master-widget"); - widget.viewer.toggleAttribute("selectable", true); + await widget.viewer.restore({ + plugin_config: { edit_mode: "SELECT_ROW_TREE" }, + }); if (!this.masterPanel.isAttached) { this.detailPanel.close(); this.setupMasterPanel(DEFAULT_WORKSPACE_SIZE); @@ -750,15 +767,23 @@ export class PerspectiveWorkspace extends SplitPanel { this.masterPanel.addWidget(widget); widget.isHidden && widget.show(); widget.viewer.restyleElement(); + + widget.viewer.addEventListener( + "perspective-global-filter", + this.on_global_filter_callback, + ); + widget.viewer.addEventListener( "perspective-select", - this.onPerspectiveSelect.bind(this), + this.on_select_callback, ); } - makeDetail(widget: PerspectiveViewerWidget) { + async makeDetail(widget: PerspectiveViewerWidget) { widget.viewer.classList.remove("workspace-master-widget"); - widget.viewer.toggleAttribute("selectable", false); + await widget.viewer.restore({ + plugin_config: { edit_mode: "READ_ONLY" }, + }); this.dockpanel.addWidget(widget, { mode: `split-left` }); if (this.masterPanel.widgets.length === 0) { this.detailPanel.close(); @@ -768,9 +793,14 @@ export class PerspectiveWorkspace extends SplitPanel { } widget.viewer.restyleElement(); + widget.viewer.removeEventListener( + "perspective-global-filter", + this.on_global_filter_callback, + ); + widget.viewer.removeEventListener( "perspective-select", - this.onPerspectiveSelect.bind(this), + this.on_select_callback, ); } diff --git a/packages/workspace/test/js/global_filter.spec.js b/packages/workspace/test/js/global_filter.spec.js index b577835245..3ff2701371 100644 --- a/packages/workspace/test/js/global_filter.spec.js +++ b/packages/workspace/test/js/global_filter.spec.js @@ -206,7 +206,7 @@ function tests(context, compare) { ".workspace-master-widget", ); masterViewer.dispatchEvent( - new CustomEvent("perspective-select", { + new CustomEvent("perspective-global-filter", { bubbles: true, composed: true, detail: new PerspectiveSelectDetail( @@ -233,7 +233,7 @@ function tests(context, compare) { ".workspace-master-widget", ); masterViewer.dispatchEvent( - new CustomEvent("perspective-select", { + new CustomEvent("perspective-global-filter", { bubbles: true, composed: true, detail: new PerspectiveSelectDetail( @@ -309,7 +309,7 @@ function tests(context, compare) { ".workspace-master-widget", ); masterViewer.dispatchEvent( - new CustomEvent("perspective-select", { + new CustomEvent("perspective-global-filter", { bubbles: true, composed: true, detail: new PerspectiveSelectDetail( @@ -343,7 +343,7 @@ function tests(context, compare) { ".workspace-master-widget", ); masterViewer.dispatchEvent( - new CustomEvent("perspective-select", { + new CustomEvent("perspective-global-filter", { bubbles: true, composed: true, detail: new PerspectiveSelectDetail( @@ -426,6 +426,7 @@ function tests(context, compare) { await page .locator(".workspace-master-widget my-grid") .locator("tbody tr:nth-child(6) th:last-of-type") + .click(); let cfg = await cfgPromise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e406c24b2f..720c324297 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4359,6 +4359,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} @@ -5691,11 +5692,11 @@ packages: glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} diff --git a/rust/metadata/main.rs b/rust/metadata/main.rs index b8723b6fb8..c2db5c0d2a 100644 --- a/rust/metadata/main.rs +++ b/rust/metadata/main.rs @@ -35,12 +35,13 @@ use perspective_client::{ ColumnWindow, DeleteOptions, JoinOptions, OnUpdateData, OnUpdateOptions, SystemInfo, TableInitOptions, UpdateOptions, ViewWindow, }; -use perspective_viewer::config::ViewerConfigUpdate; +use perspective_viewer::config::{ViewerConfig, ViewerConfigUpdate}; use ts_rs::TS; pub fn generate_type_bindings_viewer() -> Result<(), Box> { let path = std::env::current_dir()?.join("../perspective-viewer/src/ts/ts-rs"); ViewerConfigUpdate::export_all_to(&path)?; + ViewerConfig::::export_all_to(&path)?; OnUpdateData::export_all_to(&path)?; Ok(()) } diff --git a/rust/perspective-python/src/server/virtual_server_sync.rs b/rust/perspective-python/src/server/virtual_server_sync.rs index 817df6171a..664306ec58 100644 --- a/rust/perspective-python/src/server/virtual_server_sync.rs +++ b/rust/perspective-python/src/server/virtual_server_sync.rs @@ -402,6 +402,7 @@ impl PyVirtualDataSlice { PyVirtualDataSlice(Arc::new(Mutex::new(VirtualDataSlice::new(config)))) } + #[allow(clippy::wrong_self_convention)] pub fn from_arrow_ipc(&self, ipc: &[u8]) -> PyResult<()> { self.0 .lock() diff --git a/rust/perspective-server/build.mjs b/rust/perspective-server/build.mjs index c188045544..c7f093f905 100644 --- a/rust/perspective-server/build.mjs +++ b/rust/perspective-server/build.mjs @@ -81,7 +81,7 @@ try { ...make_flags, ]); - fs.cpSync("build/release/web", "dist/wasm", { recursive: true }); + fs.cpSync(`build/${env}/web`, "dist/wasm", { recursive: true }); if (!process.env.PSP_HEAP_INSTRUMENTS) { compress( `./dist/wasm/perspective-server.wasm`, diff --git a/rust/perspective-server/cmake/modules/FindInstallDependency.cmake b/rust/perspective-server/cmake/modules/FindInstallDependency.cmake index 0e89113578..e4921584be 100644 --- a/rust/perspective-server/cmake/modules/FindInstallDependency.cmake +++ b/rust/perspective-server/cmake/modules/FindInstallDependency.cmake @@ -49,6 +49,7 @@ function(psp_build_dep name cmake_file) set(ARROW_WITH_ZSTD ON) set(ARROW_WITH_LZ4 ON) set(ARROW_NO_EXPORT ON) + set(ARROW_CXXFLAGS " -Wno-documentation ") set(ARROW_DEPENDENCY_SOURCE "BUNDLED" CACHE STRING "override arrow's dependency source" FORCE) # if (PSP_ENABLE_WASM) # set(ARROW_CXX_FLAGS_RELEASE " -msimd128 -mbulk-memory -mrelaxed-simd -s MEMORY64=1 " CACHE STRING "override arrow's dependency source" FORCE) diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/base.h b/rust/perspective-server/cpp/perspective/src/include/perspective/base.h index cf5896d1d5..b3a2737dd7 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/base.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/base.h @@ -175,7 +175,7 @@ PERSPECTIVE_EXPORT ESM_EXPORT("psp_heap_size") extern "C" size_t { \ std::stringstream __SS__; \ __SS__ << (X) << "\n"; \ - __SS__ << psp_stack_trace(); \ + __SS__ << perspective::psp_stack_trace(); \ std::cout << __SS__.str() << '\n'; \ perspective::psp_abort(__SS__.str()); \ } diff --git a/rust/perspective-viewer/src/rust/components/number_column_style.rs b/rust/perspective-viewer/src/rust/components/number_column_style.rs index f0440c9408..11b892b393 100644 --- a/rust/perspective-viewer/src/rust/components/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/number_column_style.rs @@ -259,6 +259,14 @@ impl Component for NumberColumnStyle { }, + NumberForegroundMode::LabelBar => html! { + <> +
+ +
+ + + }, }; let bg_controls = match self.bg_mode { diff --git a/rust/perspective-viewer/src/rust/components/viewer.rs b/rust/perspective-viewer/src/rust/components/viewer.rs index 26ac41a57c..043e8521e3 100644 --- a/rust/perspective-viewer/src/rust/components/viewer.rs +++ b/rust/perspective-viewer/src/rust/components/viewer.rs @@ -294,7 +294,6 @@ impl Component for PerspectiveViewer { ToggleSettingsComplete(_, resolve) if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) => { - ctx.props().presentation.set_open_column_settings(None); if let Err(e) = resolve.send(()) { tracing::error!("toggle settings failed {:?}", e); } diff --git a/rust/perspective-viewer/src/rust/config/number_column_style.rs b/rust/perspective-viewer/src/rust/config/number_column_style.rs index a13d65ef2d..73c89bb7e1 100644 --- a/rust/perspective-viewer/src/rust/config/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/config/number_column_style.rs @@ -29,6 +29,9 @@ pub enum NumberForegroundMode { #[serde(rename = "bar")] Bar, + + #[serde(rename = "label-bar")] + LabelBar, } impl FromStr for NumberForegroundMode { @@ -38,6 +41,7 @@ impl FromStr for NumberForegroundMode { match s { "color" => Ok(Self::Color), "bar" => Ok(Self::Bar), + "label-bar" => Ok(Self::LabelBar), x => Err(format!("Unknown NumberForegroundMode::{x}")), } } @@ -53,7 +57,7 @@ impl NumberForegroundMode { } pub fn needs_gradient(&self) -> bool { - *self == Self::Bar + *self == Self::Bar || *self == Self::LabelBar } } diff --git a/rust/perspective-viewer/src/rust/config/viewer_config.rs b/rust/perspective-viewer/src/rust/config/viewer_config.rs index 40795154af..365323e851 100644 --- a/rust/perspective-viewer/src/rust/config/viewer_config.rs +++ b/rust/perspective-viewer/src/rust/config/viewer_config.rs @@ -26,9 +26,9 @@ use crate::presentation::ColumnConfigMap; /// The state of an entire `custom_elements::PerspectiveViewerElement` component /// and its `Plugin`. -#[derive(Debug, Default, Serialize, PartialEq)] +#[derive(Debug, Default, Serialize, PartialEq, TS)] #[serde(deny_unknown_fields)] -pub struct ViewerConfig { +pub struct ViewerConfig { pub version: V, pub columns_config: ColumnConfigMap, pub plugin: String, diff --git a/rust/perspective-viewer/src/rust/custom_elements/debug_plugin.rs b/rust/perspective-viewer/src/rust/custom_elements/debug_plugin.rs index 64e997e73e..2d2fe201ae 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/debug_plugin.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/debug_plugin.rs @@ -97,12 +97,12 @@ impl PerspectiveDebugPluginElement { ApiFuture::default() } - pub fn save(&self) -> ApiFuture<()> { - ApiFuture::default() + pub fn save(&self) -> ApiResult { + Ok(JsValue::null()) } - pub fn restore(&self) -> ApiFuture<()> { - ApiFuture::default() + pub fn restore(&self, _config: Option) -> ApiResult<()> { + Ok(()) } pub fn delete(&self) -> ApiFuture<()> { diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index e3eae3dc8b..b3d68233f6 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -39,6 +39,15 @@ use crate::tasks::*; use crate::utils::*; use crate::*; +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Promise")] + pub type JsViewerConfigPromise; + + #[wasm_bindgen(typescript_type = "ViewerConfigUpdate")] + pub type JsViewerConfigUpdate; +} + #[derive(serde::Deserialize, Default)] struct ResizeOptions { dimensions: Option, @@ -563,7 +572,7 @@ impl PerspectiveViewerElement { /// ```javascript /// await viewer.restore({group_by: ["State"]}); /// ``` - pub fn restore(&self, update: JsValue) -> ApiFuture<()> { + pub fn restore(&self, update: JsViewerConfigUpdate) -> ApiFuture<()> { let this = self.clone(); ApiFuture::new_throttled(async move { let decoded_update = ViewerConfigUpdate::decode(&update)?; @@ -646,9 +655,9 @@ impl PerspectiveViewerElement { /// await viewer.restore(token); /// }); /// ``` - pub fn save(&self) -> ApiFuture { + pub fn save(&self) -> JsViewerConfigPromise { let this = self.clone(); - ApiFuture::new(async move { + let fut = ApiFuture::new(async move { let viewer_config = this .renderer .clone() @@ -656,7 +665,9 @@ impl PerspectiveViewerElement { .await?; viewer_config.encode() - }) + }); + + js_sys::Promise::from(fut).unchecked_into() } /// Download this viewer's internal [`View`] data via a browser download diff --git a/rust/perspective-viewer/src/rust/lib.rs b/rust/perspective-viewer/src/rust/lib.rs index 6eb752e264..1b007d66fe 100644 --- a/rust/perspective-viewer/src/rust/lib.rs +++ b/rust/perspective-viewer/src/rust/lib.rs @@ -73,6 +73,11 @@ import type { ViewConfigUpdate, SystemInfo, } from "@perspective-dev/client"; + +export type * from "../../src/ts/ts-rs/ViewerConfig.d.ts"; +export type * from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts"; +import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts"; +import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts"; "#; /// Register a plugin globally. diff --git a/rust/perspective-viewer/src/rust/renderer.rs b/rust/perspective-viewer/src/rust/renderer.rs index 66393a081f..e7cecf80ba 100644 --- a/rust/perspective-viewer/src/rust/renderer.rs +++ b/rust/perspective-viewer/src/rust/renderer.rs @@ -35,7 +35,7 @@ use futures::future::{join_all, select_all}; use perspective_client::utils::*; use perspective_client::{View, ViewWindow}; use perspective_js::json; -use perspective_js::utils::ApiResult; +use perspective_js::utils::{ApiResult, ResultTApiErrorExt}; use wasm_bindgen::prelude::*; use web_sys::*; use yew::html::ImplicitClone; @@ -246,7 +246,7 @@ impl Renderer { PluginUpdate::Update(plugin) => plugin, }; - let idx = self.find_plugin_idx(name).expect("f"); + let idx = self.find_plugin_idx(name)?; let changed = !matches!( self.0.borrow().plugins_idx, Some(selected_idx) if selected_idx == idx @@ -403,13 +403,18 @@ impl Renderer { if let Some(cb) = self.0.on_render_limits_changed.borrow().as_ref() { cb.emit(limits); } + let viewer_elem = &self.0.borrow().viewer_elem.clone(); - if is_update { + let result = if is_update { let task = plugin.update(view.clone().into(), limits.max_cols, limits.max_rows, false); - activate_plugin(viewer_elem, &plugin, task).await?; + activate_plugin(viewer_elem, &plugin, task).await } else { let task = plugin.draw(view.clone().into(), limits.max_cols, limits.max_rows, false); - activate_plugin(viewer_elem, &plugin, task).await?; + activate_plugin(viewer_elem, &plugin, task).await + }; + + if let Err(error) = result.ignore_view_delete() { + tracing::warn!("{}", error); } remove_inactive_plugin( diff --git a/rust/perspective-viewer/src/rust/tasks/restore_and_render.rs b/rust/perspective-viewer/src/rust/tasks/restore_and_render.rs index 1c76b6d9f5..129b1bbdf1 100644 --- a/rust/perspective-viewer/src/rust/tasks/restore_and_render.rs +++ b/rust/perspective-viewer/src/rust/tasks/restore_and_render.rs @@ -104,11 +104,12 @@ pub trait RestoreAndRender: HasRenderer + HasSession + HasPresentation { // TODO this should be part of the API for `draw()` above, such that // the plugin need not render twice when a theme is provided. - if needs_restyle && presentation.is_visible() { - let view = session.get_view().into_apierror()?; + if needs_restyle + && presentation.is_visible() + && let Some(view) = session.get_view() + { renderer.restyle_all(&view).await?; } - // } Ok(()) }) diff --git a/rust/perspective-viewer/src/svg/datagrid-select-row-tree.svg b/rust/perspective-viewer/src/svg/datagrid-select-row-tree.svg new file mode 100644 index 0000000000..f0057881ca --- /dev/null +++ b/rust/perspective-viewer/src/svg/datagrid-select-row-tree.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/rust/perspective-viewer/src/themes/defaults.css b/rust/perspective-viewer/src/themes/defaults.css index 9bab442ea7..c37d7820eb 100644 --- a/rust/perspective-viewer/src/themes/defaults.css +++ b/rust/perspective-viewer/src/themes/defaults.css @@ -22,6 +22,24 @@ perspective-date-column-style, perspective-datetime-column-style, perspective-number-column-style, perspective-string-column-style { + /* Colors */ + --psp--color: #161616; + + color: var(--psp--color); + background-color: transparent; + + --psp-inactive--color: #ababab; + --psp-inactive--border-color: #dadada; + --psp-active--color: #2670a9; + --psp-error--color: #ff471e; + --psp--background-color: #ffffff; + --psp-icon-overflow-hint--color: rgba(0, 0, 0, 0.2); + --psp-select--background-color: none; + --psp-warning--background: #042121; + --psp-warning--color: #fdfffd; + + /* TODO deprecate me */ + --psp-icon-overflow-hint--color: #fdfffd; /* Colors */ --psp-placeholder--background: #8b868045; diff --git a/rust/perspective-viewer/src/themes/icons.css b/rust/perspective-viewer/src/themes/icons.css index 1ada5fcf33..024145f7e0 100644 --- a/rust/perspective-viewer/src/themes/icons.css +++ b/rust/perspective-viewer/src/themes/icons.css @@ -90,24 +90,6 @@ perspective-string-column-style { --psp-plugin-selector--y-scatter--content: url(../svg/mega-menu-icons-y-scatter.svg); --psp-plugin-selector--datagrid--content: url(../svg/mega-menu-icons-datagrid.svg); - /* Colors */ - color: #161616; - background-color: transparent; - --psp--color: #161616; - --psp-inactive--color: #ababab; - --psp-inactive--border-color: #dadada; - - --psp-active--color: #2670a9; - --psp-error--color: #ff471e; - --psp--background-color: #ffffff; - --psp-icon-overflow-hint--color: rgba(0, 0, 0, 0.2); - --psp-select--background-color: none; - --psp-warning--background: #042121; - --psp-warning--color: #fdfffd; - - /* TODO deprecate me */ - --psp-icon-overflow-hint--color: #fdfffd; - /* Datagrid */ /* `regular-table` icons */ --psp-label--column-style-open-button--content: "style"; @@ -121,4 +103,5 @@ perspective-string-column-style { --psp-toolbar-edit-mode-select-row--content: url("../svg/datagrid-select-row.svg"); --psp-toolbar-edit-mode-select-column--content: url("../svg/datagrid-select-column.svg"); --psp-toolbar-edit-mode-select-region--content: url("../svg/datagrid-select-region.svg"); + --psp-toolbar-edit-mode-select-row-tree--content: url("../svg/datagrid-select-row-tree.svg"); } diff --git a/rust/perspective-viewer/src/ts/extensions.ts b/rust/perspective-viewer/src/ts/extensions.ts index a8a2336fca..0b5efbae65 100644 --- a/rust/perspective-viewer/src/ts/extensions.ts +++ b/rust/perspective-viewer/src/ts/extensions.ts @@ -26,7 +26,6 @@ export class PerspectiveSelectDetail { column_names?: string[]; removeConfigs: ViewConfigUpdate[]; insertConfigs: ViewConfigUpdate[]; - constructor( selected: boolean, row: Record, @@ -49,6 +48,7 @@ export class PerspectiveSelectDetail { return this.insertConfigs.flatMap((x) => x.filter ?? []); } } + import type { ExportDropDownMenuElement, CopyDropDownMenuElement, @@ -190,6 +190,12 @@ export interface PerspectiveViewerElementExt { options?: { signal: AbortSignal }, ): void; + addEventListener( + name: "perspective-global-filter", + cb: (e: CustomEvent) => void, + options?: { signal: AbortSignal }, + ): void; + addEventListener( name: "perspective-toggle-settings", cb: (e: CustomEvent) => void, @@ -228,6 +234,7 @@ export interface PerspectiveViewerElementExt { removeEventListener(name: "perspective-click", cb: any): void; removeEventListener(name: "perspective-select", cb: any): void; + removeEventListener(name: "perspective-global-filter", cb: any): void; removeEventListener(name: "perspective-toggle-settings", cb: any): void; removeEventListener( name: "perspective-toggle-settings-before", diff --git a/rust/perspective-viewer/src/ts/perspective-viewer.ts b/rust/perspective-viewer/src/ts/perspective-viewer.ts index e676572516..0b789f6bc4 100644 --- a/rust/perspective-viewer/src/ts/perspective-viewer.ts +++ b/rust/perspective-viewer/src/ts/perspective-viewer.ts @@ -37,6 +37,7 @@ export { HTMLPerspectiveViewerPluginElement } from "./plugin"; export type * from "./extensions.ts"; export { PerspectiveSelectDetail } from "./extensions.ts"; export type * from "./ts-rs/ViewerConfigUpdate.d.ts"; +export type * from "./ts-rs/ViewerConfig.d.ts"; export type * from "./ts-rs/ColumnConfigValues.d.ts"; export type * from "./ts-rs/Filter.d.ts"; export type * from "./ts-rs/FilterTerm.d.ts"; diff --git a/rust/perspective-viewer/src/ts/plugin.ts b/rust/perspective-viewer/src/ts/plugin.ts index b7a7bee9a8..4473b2b66f 100644 --- a/rust/perspective-viewer/src/ts/plugin.ts +++ b/rust/perspective-viewer/src/ts/plugin.ts @@ -157,13 +157,13 @@ export interface IPerspectiveViewerPlugin { * Like `update()`, but for when the dimensions of the plugin have changed * and the underlying data has not. */ - resize(): Promise; + resize(view: View): Promise; /** * Notify the plugin that the style environment has changed. Useful for * plugins which read CSS styles via `window.getComputedStyle()`. */ - restyle(): Promise; + restyle(view: View): Promise; /** * Save this plugin's state to a JSON-serializable value. While this value @@ -176,17 +176,17 @@ export interface IPerspectiveViewerPlugin { * reload. For example, `@perspective-dev/viewer-d3fc` uses * `plugin_config` to remember the user-repositionable legend coordinates. */ - save(): Promise; + save(): any; /** * Restore this plugin to a state previously returned by `save()`. */ - restore(config: any): Promise; + restore(config: any): void; /** * Free any resources acquired by this plugin and prepare to be deleted. */ - delete(): Promise; + delete(): void; } /** @@ -257,19 +257,19 @@ export class HTMLPerspectiveViewerPluginElement this.innerHTML = ""; } - async resize(): Promise { + async resize(view: View): Promise { // Not Implemented } - async restyle(): Promise { + async restyle(view: View): Promise { // Not Implemented } - async save(): Promise { + save(): any { // Not Implemented } - async restore(): Promise { + restore(): void { // Not Implemented } diff --git a/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts b/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts index 15ae471ef2..b7057df4e5 100644 --- a/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts +++ b/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts @@ -43,10 +43,13 @@ export async function shadowDragOver(page: Page, src: Locator, tgt: Locator) { await page.mouse.move(srcX, srcY); await page.mouse.down(); + // Small initial move to trigger the browser's drag-start threshold. await page.mouse.move(srcX + 5, srcY, { steps: 2 }); + // Move to the target center, generating dragenter + dragover events. await page.mouse.move(tgtX, tgtY, { steps: 10 }); + // Allow Yew to process events and re-render. await page.waitForTimeout(100); } @@ -65,6 +68,13 @@ export async function shadowDragCancel( const srcX = srcBox.x + srcBox.width / 2; const srcY = srcBox.y + srcBox.height / 2; + await page.evaluate(() => { + window["dragend_resolvers"] = Promise.withResolvers(); + document.body.addEventListener("dragend", () => { + window["dragend_resolvers"].resolve(); + }); + }); + await page.mouse.move(srcX, srcY); await page.mouse.down(); await page.mouse.move(srcX + 5, srcY, { steps: 2 }); @@ -78,9 +88,14 @@ export async function shadowDragCancel( ); } + // Drag is a cruel mistress. + await page.waitForTimeout(100); + // Cancel the drag (fires dragend with no preceding drop). await page.keyboard.press("Escape"); - await page.waitForTimeout(100); + await page.evaluate(async () => { + await window["dragend_resolvers"].promise; + }); } export async function localDrag(page: any, source: any, target: any) { diff --git a/rust/perspective-viewer/test/js/helpers.ts b/rust/perspective-viewer/test/js/helpers.ts index 3414c59f30..ac74e4f15d 100644 --- a/rust/perspective-viewer/test/js/helpers.ts +++ b/rust/perspective-viewer/test/js/helpers.ts @@ -45,7 +45,7 @@ export const DEFAULT_CONFIG: ViewerConfigUpdate = { group_by: [], group_rollup_mode: "rollup", plugin: "", - plugin_config: {}, + plugin_config: null, settings: false, sort: [], split_by: [], diff --git a/rust/perspective-viewer/test/js/viewer_api/dom.spec.ts b/rust/perspective-viewer/test/js/viewer_api/dom.spec.ts index 716d5c3fd1..fc972114a9 100644 --- a/rust/perspective-viewer/test/js/viewer_api/dom.spec.ts +++ b/rust/perspective-viewer/test/js/viewer_api/dom.spec.ts @@ -40,7 +40,7 @@ const RESULT = { filter: [], group_by: [], plugin: "Debug", - plugin_config: {}, + plugin_config: null, settings: false, sort: [], split_by: [], diff --git a/rust/perspective-viewer/test/js/viewer_api/events.spec.ts b/rust/perspective-viewer/test/js/viewer_api/events.spec.ts index ad647a037e..0a2dd1c265 100644 --- a/rust/perspective-viewer/test/js/viewer_api/events.spec.ts +++ b/rust/perspective-viewer/test/js/viewer_api/events.spec.ts @@ -70,7 +70,7 @@ test.describe("Events", () => { expressions: {}, filter: [], plugin: "Debug", - plugin_config: {}, + plugin_config: null, group_by: ["State"], group_rollup_mode: "rollup", settings: true, @@ -89,7 +89,6 @@ test.describe("Events", () => { await page.evaluate(async () => { const viewer = document.querySelector("perspective-viewer"); window["acc"] = []; - await viewer!.restore({ settings: true, }); diff --git a/tools/test/results.tar.gz b/tools/test/results.tar.gz index 5db1287e15..7c25701ac6 100644 Binary files a/tools/test/results.tar.gz and b/tools/test/results.tar.gz differ