From cf92a423cfb16c99fa2eebd5d4a4bb4d21e70bdd Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 16 Sep 2025 14:53:59 +0000 Subject: [PATCH] Refactor DeesTable component: modularize data handling and styles - Moved column computation and data retrieval logic to a new data.ts file for better separation of concerns. - Created a styles.ts file to encapsulate all CSS styles related to the DeesTable component. - Updated the DeesTable class to utilize the new data handling functions and styles. - Introduced selection and filtering features, allowing for single and multi-row selection. - Enhanced rendering logic to accommodate selection checkboxes and filtering capabilities. - Re-exported types from types.ts for better type management and clarity. --- ts_web/elements/dees-table/data.ts | 78 +++ ts_web/elements/dees-table/dees-table.demo.ts | 38 ++ ts_web/elements/dees-table/dees-table.ts | 613 ++++-------------- ts_web/elements/dees-table/styles.ts | 362 +++++++++++ ts_web/elements/dees-table/types.ts | 28 + 5 files changed, 640 insertions(+), 479 deletions(-) create mode 100644 ts_web/elements/dees-table/data.ts create mode 100644 ts_web/elements/dees-table/styles.ts create mode 100644 ts_web/elements/dees-table/types.ts diff --git a/ts_web/elements/dees-table/data.ts b/ts_web/elements/dees-table/data.ts new file mode 100644 index 0000000..fa86932 --- /dev/null +++ b/ts_web/elements/dees-table/data.ts @@ -0,0 +1,78 @@ +import type { Column, TDisplayFunction } from './types.js'; + +export function computeColumnsFromDisplayFunction( + displayFunction: TDisplayFunction, + data: T[] +): Column[] { + if (!data || data.length === 0) return []; + const firstTransformedItem = displayFunction(data[0]); + const keys: string[] = Object.keys(firstTransformedItem); + return keys.map((key) => ({ + key, + header: key, + value: (row: T) => displayFunction(row)[key], + })); +} + +export function computeEffectiveColumns( + columns: Column[] | undefined, + augmentFromDisplayFunction: boolean, + displayFunction: TDisplayFunction, + data: T[] +): Column[] { + const base = (columns || []).slice(); + if (!augmentFromDisplayFunction) return base; + const fromDisplay = computeColumnsFromDisplayFunction(displayFunction, data); + const existingKeys = new Set(base.map((c) => String(c.key))); + for (const col of fromDisplay) { + if (!existingKeys.has(String(col.key))) { + base.push(col); + } + } + return base; +} + +export function getCellValue(row: T, col: Column, displayFunction?: TDisplayFunction): any { + return col.value ? col.value(row) : (row as any)[col.key as any]; +} + +export function getViewData( + data: T[], + effectiveColumns: Column[], + sortKey?: string, + sortDir?: 'asc' | 'desc' | null, + filterText?: string +): T[] { + let arr = data.slice(); + const ft = (filterText || '').trim().toLowerCase(); + if (ft) { + arr = arr.filter((row) => { + for (const col of effectiveColumns) { + if (col.hidden) continue; + const val = getCellValue(row, col); + const s = String(val ?? '').toLowerCase(); + if (s.includes(ft)) return true; + } + return false; + }); + } + if (!sortKey || !sortDir) return arr; + const col = effectiveColumns.find((c) => String(c.key) === sortKey); + if (!col) return arr; + const dir = sortDir === 'asc' ? 1 : -1; + arr.sort((a, b) => { + const va = getCellValue(a, col); + const vb = getCellValue(b, col); + if (va == null && vb == null) return 0; + if (va == null) return -1 * dir; + if (vb == null) return 1 * dir; + if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir; + const sa = String(va).toLowerCase(); + const sb = String(vb).toLowerCase(); + if (sa < sb) return -1 * dir; + if (sa > sb) return 1 * dir; + return 0; + }); + return arr; +} + diff --git a/ts_web/elements/dees-table/dees-table.demo.ts b/ts_web/elements/dees-table/dees-table.demo.ts index 882656d..07f053c 100644 --- a/ts_web/elements/dees-table/dees-table.demo.ts +++ b/ts_web/elements/dees-table/dees-table.demo.ts @@ -467,6 +467,44 @@ export const demoFunc = () => html` dataName="users" > + +
{ console.log('Selection changed', e.detail); }} + @search-changed=${(e: CustomEvent) => { + const tbl = document.getElementById('tableFilterSelectDemo') as any; + if (tbl) tbl.setFilterText(e.detail.value); + }} + @search-submit=${(e: CustomEvent) => { + const tbl = document.getElementById('tableFilterSelectDemo') as any; + if (tbl) tbl.setFilterText(e.detail.value); + }} + > +

Filtering + Multi-Selection (New)

+

Use the search bar to filter rows; toggle selection via checkboxes. Click headers to sort.

+ +
+ +
`; diff --git a/ts_web/elements/dees-table/dees-table.ts b/ts_web/elements/dees-table/dees-table.ts index 3d726eb..0fa95d8 100644 --- a/ts_web/elements/dees-table/dees-table.ts +++ b/ts_web/elements/dees-table/dees-table.ts @@ -1,21 +1,21 @@ import * as plugins from '../00plugins.js'; import { demoFunc } from './dees-table.demo.js'; -import { cssGeistFontFamily } from '../00fonts.js'; -import { - customElement, - html, - DeesElement, - property, - type TemplateResult, - cssManager, - css, - directives, -} from '@design.estate/dees-element'; +import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element'; import { DeesContextmenu } from '../dees-contextmenu.js'; - +import * as plugins from '../00plugins.js'; import * as domtools from '@design.estate/dees-domtools'; import { type TIconKey } from '../dees-icon.js'; +import { tableStyles } from './styles.js'; +import type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js'; +import { + computeColumnsFromDisplayFunction as computeColumnsFromDisplayFunctionFn, + computeEffectiveColumns as computeEffectiveColumnsFn, + getCellValue as getCellValueFn, + getViewData as getViewDataFn, +} from './data.js'; + +export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js'; declare global { interface HTMLElementTagNameMap { @@ -23,62 +23,7 @@ declare global { } } -// interfaces -export interface ITableAction { - name: string; - iconName: TIconKey; - /** - * the table behaviour to use for this action - * e.g. upload: allows to upload files to the table - */ - useTableBehaviour?: 'upload' | 'cancelUpload' | 'none'; - /** - * the type of the action - */ - type: ( - | 'inRow' - | 'contextmenu' - | 'doubleClick' - | 'footer' - | 'header' - | 'preview' - | 'keyCombination' - )[]; - /** - * allows to check if the action is relevant for the given item - * @param itemArg - * @returns - */ - actionRelevancyCheckFunc?: (itemArg: T) => boolean; - /** - * the actual action function implementation - * @param itemArg - * @returns - */ - actionFunc: (actionDataArg: ITableActionDataArg) => Promise; -} - -export interface ITableActionDataArg { - item: T; - table: DeesTable; -} - -// schema-first columns API (Phase 1) -export interface Column { - /** key in the raw item or a computed key name */ - key: keyof T | string; - /** header label or template; defaults to key */ - header?: string | TemplateResult; - /** compute the cell value when not reading directly by key */ - value?: (row: T) => any; - /** optional cell renderer */ - renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column }) => TemplateResult | string; - /** reserved for future phases; present to sketch intent */ - sortable?: boolean; - hidden?: boolean; -} - -export type TDisplayFunction = (itemArg: T) => object; +// interfaces moved to ./types.ts and re-exported above // the table implementation @customElement('dees-table') @@ -219,374 +164,29 @@ export class DeesTable extends DeesElement { @property({ attribute: false }) private sortDir: 'asc' | 'desc' | null = null; + // simple client-side filtering (Phase 1) + @property({ type: String }) + public filterText: string = ''; + + // selection (Phase 1) + @property({ type: String }) + public selectionMode: 'none' | 'single' | 'multi' = 'none'; + @property({ attribute: false }) + private selectedIds: Set = new Set(); + private _rowIdMap = new WeakMap(); + private _rowIdCounter = 0; + constructor() { super(); } - public static styles = [ - cssManager.defaultStyles, - css` - :host { - display: block; - width: 100%; - } - - .mainbox { - color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; - font-family: ${cssGeistFontFamily}; - font-weight: 400; - font-size: 14px; - display: block; - width: 100%; - background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; - border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - border-radius: 8px; - overflow: hidden; - cursor: default; - } - - .header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 24px; - min-height: 64px; - border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - .headingContainer { - flex: 1; - } - - .heading { - line-height: 1.5; - } - - .heading1 { - font-size: 18px; - font-weight: 600; - color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; - letter-spacing: -0.025em; - } - - .heading2 { - font-size: 14px; - color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; - margin-top: 2px; - } - - .headingSeparation { - display: none; - } - - .headerActions { - user-select: none; - display: flex; - flex-direction: row; - gap: 8px; - } - - .headerAction { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - font-size: 14px; - font-weight: 500; - color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; - background: transparent; - border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - } - - .headerAction:hover { - color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; - background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; - border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; - } - - .headerAction dees-icon { - width: 14px; - height: 14px; - } - - .searchGrid { - display: grid; - grid-gap: 16px; - grid-template-columns: 1fr 200px; - padding: 16px 24px; - background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(0 0% 3.9%)')}; - border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - transition: all 0.2s ease; - } - - .searchGrid.hidden { - height: 0px; - opacity: 0; - overflow: hidden; - padding: 0px 24px; - border-bottom-width: 0px; - } - - table { - width: 100%; - caption-side: bottom; - font-size: 14px; - border-collapse: separate; - border-spacing: 0; - } - - .noDataSet { - padding: 48px 24px; - text-align: center; - color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; - } - - thead { - background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')}; - border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; - } - - tbody tr { - transition: background-color 0.15s ease; - position: relative; - } - - /* Default horizontal lines (bottom border only) */ - tbody tr { - border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - tbody tr:last-child { - border-bottom: none; - } - - /* Full horizontal lines when enabled */ - :host([show-horizontal-lines]) tbody tr { - border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - :host([show-horizontal-lines]) tbody tr:first-child { - border-top: none; - } - - :host([show-horizontal-lines]) tbody tr:last-child { - border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - tbody tr:hover { - background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')}; - } - - /* Column hover effect for better traceability */ - td { - position: relative; - } - - td::after { - content: ''; - position: absolute; - top: -1000px; - bottom: -1000px; - left: 0; - right: 0; - background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.3)', 'hsl(0 0% 14.9% / 0.3)')}; - opacity: 0; - pointer-events: none; - transition: opacity 0.15s ease; - z-index: -1; - } - - td:hover::after { - opacity: 1; - } - - /* Grid mode - shows both vertical and horizontal lines */ - :host([show-grid]) th { - border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - border-left: none; - border-top: none; - } - - :host([show-grid]) td { - border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - border-left: none; - border-top: none; - } - - :host([show-grid]) th:first-child, - :host([show-grid]) td:first-child { - border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - :host([show-grid]) tbody tr:first-child td { - border-top: none; - } - - tbody tr.selected { - background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; - } - - tbody tr.hasAttachment { - background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 76.2% 36.3% / 0.1)')}; - } - - th { - height: 48px; - padding: 12px 24px; - text-align: left; - font-weight: 500; - color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; - letter-spacing: -0.01em; - } - - :host([show-vertical-lines]) th { - border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - td { - padding: 12px 24px; - vertical-align: middle; - color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; - } - - :host([show-vertical-lines]) td { - border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - th:first-child, - td:first-child { - padding-left: 24px; - } - - th:last-child, - td:last-child { - padding-right: 24px; - } - - :host([show-vertical-lines]) th:last-child, - :host([show-vertical-lines]) td:last-child { - border-right: none; - } - - .innerCellContainer { - position: relative; - min-height: 24px; - line-height: 24px; - } - td input { - position: absolute; - top: 4px; - bottom: 4px; - left: 20px; - right: 20px; - width: calc(100% - 40px); - height: calc(100% - 8px); - padding: 0 12px; - outline: none; - border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - border-radius: 6px; - background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; - color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; - font-family: inherit; - font-size: inherit; - font-weight: inherit; - transition: all 0.15s ease; - box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - } - - td input:focus { - border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; - outline: 2px solid transparent; - outline-offset: 2px; - box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')}; - } - .actionsContainer { - display: flex; - flex-direction: row; - gap: 4px; - } - - .action { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 6px; - color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; - cursor: pointer; - transition: all 0.15s ease; - } - - .action:hover { - background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; - color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; - } - - .action:active { - background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 11.8%)')}; - } - - .action dees-icon { - width: 16px; - height: 16px; - } - - .footer { - display: flex; - align-items: center; - justify-content: space-between; - height: 52px; - padding: 0 24px; - font-size: 14px; - color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; - background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')}; - border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; - } - - .tableStatistics { - font-weight: 500; - } - - .footerActions { - display: flex; - gap: 8px; - } - - .footerActions .footerAction { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - font-weight: 500; - color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; - border-radius: 6px; - cursor: pointer; - user-select: none; - transition: all 0.15s ease; - } - - .footerActions .footerAction:hover { - background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; - color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; - } - - .footerActions .footerAction dees-icon { - width: 14px; - height: 14px; - } - `, - ]; + public static styles = tableStyles; public render(): TemplateResult { const usingColumns = Array.isArray(this.columns) && this.columns.length > 0; const effectiveColumns: Column[] = usingColumns - ? this.computeEffectiveColumns() - : this.computeColumnsFromDisplayFunction(); + ? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data) + : computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data); return html`
@@ -657,6 +257,20 @@ export class DeesTable extends DeesElement { + ${this.selectionMode !== 'none' + ? html` + + ` + : html``} ${effectiveColumns .filter((c) => !c.hidden) .map((col) => { @@ -681,7 +295,7 @@ export class DeesTable extends DeesElement { - ${this.getViewData(effectiveColumns).map((itemArg, rowIndex) => { + ${getViewDataFn(this.data, effectiveColumns, this.sortKey, this.sortDir, this.filterText).map((itemArg, rowIndex) => { const getTr = (elementArg: HTMLElement): HTMLElement => { if (elementArg.tagName === 'TR') { return elementArg; @@ -693,6 +307,13 @@ export class DeesTable extends DeesElement { { this.selectedDataRow = itemArg; + if (this.selectionMode === 'single') { + const id = this.getRowId(itemArg); + this.selectedIds.clear(); + this.selectedIds.add(id); + this.emitSelectionChange(); + this.requestUpdate(); + } }} @dragenter=${async (eventArg: DragEvent) => { eventArg.preventDefault(); @@ -747,10 +368,22 @@ export class DeesTable extends DeesElement { }} class="${itemArg === this.selectedDataRow ? 'selected' : ''}" > + ${this.selectionMode !== 'none' + ? html`` + : html``} ${effectiveColumns .filter((c) => !c.hidden) .map((col, colIndex) => { - const value = this.getCellValue(itemArg, col); + const value = getCellValueFn(itemArg, col, this.displayFunction); const content = col.renderer ? col.renderer(value, itemArg, { rowIndex, colIndex, column: col }) : value; @@ -910,55 +543,7 @@ export class DeesTable extends DeesElement { table.style.tableLayout = 'fixed'; } - private computeColumnsFromDisplayFunction(): Column[] { - if (!this.data || this.data.length === 0) return []; - const firstTransformedItem = this.displayFunction(this.data[0]); - const keys: string[] = Object.keys(firstTransformedItem); - return keys.map((key) => ({ - key, - header: key, - value: (row: T) => this.displayFunction(row)[key], - })); - } - - private computeEffectiveColumns(): Column[] { - const base = (this.columns || []).slice(); - if (!this.augmentFromDisplayFunction) return base; - const fromDisplay = this.computeColumnsFromDisplayFunction(); - const existingKeys = new Set(base.map((c) => String(c.key))); - for (const col of fromDisplay) { - if (!existingKeys.has(String(col.key))) { - base.push(col); - } - } - return base; - } - - private getCellValue(row: T, col: Column): any { - return col.value ? col.value(row) : (row as any)[col.key as any]; - } - - private getViewData(effectiveColumns: Column[]): T[] { - if (!this.sortKey || !this.sortDir) return this.data; - const col = effectiveColumns.find((c) => String(c.key) === this.sortKey); - if (!col) return this.data; - const arr = this.data.slice(); - const dir = this.sortDir === 'asc' ? 1 : -1; - arr.sort((a, b) => { - const va = this.getCellValue(a, col); - const vb = this.getCellValue(b, col); - if (va == null && vb == null) return 0; - if (va == null) return -1 * dir; - if (vb == null) return 1 * dir; - if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir; - const sa = String(va).toLowerCase(); - const sb = String(vb).toLowerCase(); - if (sa < sb) return -1 * dir; - if (sa > sb) return 1 * dir; - return 0; - }); - return arr; - } + // compute helpers moved to ./data.ts private toggleSort(col: Column) { const key = String(col.key); @@ -991,6 +576,76 @@ export class DeesTable extends DeesElement { return html`${this.sortDir === 'asc' ? '▲' : '▼'}`; } + // filtering helpers + public setFilterText(value: string) { + const prev = this.filterText; + this.filterText = value ?? ''; + if (prev !== this.filterText) { + this.dispatchEvent( + new CustomEvent('filterChange', { + detail: { text: this.filterText }, + bubbles: true, + }) + ); + this.requestUpdate(); + } + } + + // selection helpers + private getRowId(row: T): string { + if (this.rowKey) { + if (typeof this.rowKey === 'function') return this.rowKey(row); + return String((row as any)[this.rowKey]); + } + const key = row as any as object; + if (!this._rowIdMap.has(key)) { + this._rowIdMap.set(key, String(++this._rowIdCounter)); + } + return this._rowIdMap.get(key); + } + + private isRowSelected(row: T): boolean { + return this.selectedIds.has(this.getRowId(row)); + } + + private toggleRowSelected(row: T) { + const id = this.getRowId(row); + if (this.selectionMode === 'single') { + this.selectedIds.clear(); + this.selectedIds.add(id); + } else if (this.selectionMode === 'multi') { + if (this.selectedIds.has(id)) this.selectedIds.delete(id); + else this.selectedIds.add(id); + } + this.emitSelectionChange(); + this.requestUpdate(); + } + + private areAllSelected(): boolean { + return this.data.length > 0 && this.selectedIds.size === this.data.length; + } + + private toggleSelectAll() { + if (this.areAllSelected()) { + this.selectedIds.clear(); + } else { + this.selectedIds = new Set(this.data.map((r) => this.getRowId(r))); + } + this.emitSelectionChange(); + this.requestUpdate(); + } + + private emitSelectionChange() { + const selectedIds = Array.from(this.selectedIds); + const selectedRows = this.data.filter((r) => this.selectedIds.has(this.getRowId(r))); + this.dispatchEvent( + new CustomEvent('selectionChange', { + detail: { selectedIds, selectedRows }, + bubbles: true, + }) + ); + } + getActionsForType(typeArg: ITableAction['type'][0]) { const actions: ITableAction[] = []; for (const action of this.dataActions) { diff --git a/ts_web/elements/dees-table/styles.ts b/ts_web/elements/dees-table/styles.ts new file mode 100644 index 0000000..9342d44 --- /dev/null +++ b/ts_web/elements/dees-table/styles.ts @@ -0,0 +1,362 @@ +import { cssManager, css, type CSSResult } from '@design.estate/dees-element'; +import { cssGeistFontFamily } from '../00fonts.js'; + +export const tableStyles: CSSResult[] = [ + cssManager.defaultStyles, + css` + :host { + display: block; + width: 100%; + } + + .mainbox { + color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; + font-family: ${cssGeistFontFamily}; + font-weight: 400; + font-size: 14px; + display: block; + width: 100%; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + border-radius: 8px; + overflow: hidden; + cursor: default; + } + + .header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + min-height: 64px; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + .headingContainer { + flex: 1; + } + + .heading { + line-height: 1.5; + } + + .heading1 { + font-size: 18px; + font-weight: 600; + color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; + letter-spacing: -0.025em; + } + + .heading2 { + font-size: 14px; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; + margin-top: 2px; + } + + .headingSeparation { + display: none; + } + + .headerActions { + user-select: none; + display: flex; + flex-direction: row; + gap: 8px; + } + + .headerAction { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 14px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; + background: transparent; + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + } + + .headerAction:hover { + color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; + background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; + border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; + } + + .headerAction dees-icon { + width: 14px; + height: 14px; + } + + .searchGrid { + display: grid; + grid-gap: 16px; + grid-template-columns: 1fr 200px; + padding: 16px 24px; + background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(0 0% 3.9%)')}; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + transition: all 0.2s ease; + } + + .searchGrid.hidden { + height: 0px; + opacity: 0; + overflow: hidden; + padding: 0px 24px; + border-bottom-width: 0px; + } + + table { + width: 100%; + caption-side: bottom; + font-size: 14px; + border-collapse: separate; + border-spacing: 0; + } + + .noDataSet { + padding: 48px 24px; + text-align: center; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; + } + + thead { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')}; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; + } + + tbody tr { + transition: background-color 0.15s ease; + position: relative; + } + + /* Default horizontal lines (bottom border only) */ + tbody tr { + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + tbody tr:last-child { + border-bottom: none; + } + + /* Full horizontal lines when enabled */ + :host([show-horizontal-lines]) tbody tr { + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + :host([show-horizontal-lines]) tbody tr:first-child { + border-top: none; + } + + :host([show-horizontal-lines]) tbody tr:last-child { + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + tbody tr:hover { + background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')}; + } + + /* Column hover effect for better traceability */ + td { + position: relative; + } + + td::after { + content: ''; + position: absolute; + top: -1000px; + bottom: -1000px; + left: 0; + right: 0; + background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.3)', 'hsl(0 0% 14.9% / 0.3)')}; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + z-index: -1; + } + + td:hover::after { + opacity: 1; + } + + /* Grid mode - shows both vertical and horizontal lines */ + :host([show-grid]) th { + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + border-left: none; + border-top: none; + } + + :host([show-grid]) td { + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + border-left: none; + border-top: none; + } + + :host([show-grid]) th:first-child, + :host([show-grid]) td:first-child { + border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + :host([show-grid]) tbody tr:first-child td { + border-top: none; + } + + tbody tr.selected { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; + } + + tbody tr.hasAttachment { + background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 76.2% 36.3% / 0.1)')}; + } + + th { + height: 48px; + padding: 12px 24px; + text-align: left; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + letter-spacing: -0.01em; + } + + :host([show-vertical-lines]) th { + border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + td { + padding: 12px 24px; + vertical-align: middle; + color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; + } + + :host([show-vertical-lines]) td { + border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + th:first-child, + td:first-child { + padding-left: 24px; + } + + th:last-child, + td:last-child { + padding-right: 24px; + } + + :host([show-vertical-lines]) th:last-child, + :host([show-vertical-lines]) td:last-child { + border-right: none; + } + + .innerCellContainer { + position: relative; + min-height: 24px; + line-height: 24px; + } + td input { + position: absolute; + top: 4px; + bottom: 4px; + left: 20px; + right: 20px; + width: calc(100% - 40px); + height: calc(100% - 8px); + padding: 0 12px; + outline: none; + border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + border-radius: 6px; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + transition: all 0.15s ease; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + } + + td input:focus { + border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; + outline: 2px solid transparent; + outline-offset: 2px; + box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')}; + } + .actionsContainer { + display: flex; + flex-direction: row; + gap: 4px; + } + + .action { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + cursor: pointer; + transition: all 0.15s ease; + } + + .action:hover { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; + } + + .action:active { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 11.8%)')}; + } + + .action dees-icon { + width: 16px; + height: 16px; + } + + .footer { + display: flex; + align-items: center; + justify-content: space-between; + height: 52px; + padding: 0 24px; + font-size: 14px; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')}; + border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; + } + + .tableStatistics { + font-weight: 500; + } + + .footerActions { + display: flex; + gap: 8px; + } + + .footerActions .footerAction { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-weight: 500; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + border-radius: 6px; + cursor: pointer; + user-select: none; + transition: all 0.15s ease; + } + + .footerActions .footerAction:hover { + background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; + color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; + } + + .footerActions .footerAction dees-icon { + width: 14px; + height: 14px; + } + `, +]; + diff --git a/ts_web/elements/dees-table/types.ts b/ts_web/elements/dees-table/types.ts new file mode 100644 index 0000000..4c957fb --- /dev/null +++ b/ts_web/elements/dees-table/types.ts @@ -0,0 +1,28 @@ +import type { TemplateResult } from '@design.estate/dees-element'; +import type { TIconKey } from '../dees-icon.js'; + +export interface ITableActionDataArg { + item: T; + table: any; // avoid circular typing with DeesTable; consumers rely on shape only +} + +export interface ITableAction { + name: string; + iconName: TIconKey; + useTableBehaviour?: 'upload' | 'cancelUpload' | 'none'; + type: ('inRow' | 'contextmenu' | 'doubleClick' | 'footer' | 'header' | 'preview' | 'keyCombination')[]; + actionRelevancyCheckFunc?: (itemArg: T) => boolean; + actionFunc: (actionDataArg: ITableActionDataArg) => Promise; +} + +export interface Column { + key: keyof T | string; + header?: string | TemplateResult; + value?: (row: T) => any; + renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column }) => TemplateResult | string; + sortable?: boolean; + hidden?: boolean; +} + +export type TDisplayFunction = (itemArg: T) => Record; +
+ ${this.selectionMode === 'multi' + ? html` { + e.stopPropagation(); + this.toggleSelectAll(); + }} />` + : html``} +
+ { + e.stopPropagation(); + this.toggleRowSelected(itemArg); + }} + /> +