feat: add per-column filtering and sticky header support to DeesTable component

This commit is contained in:
2025-09-16 15:17:33 +00:00
parent cf92a423cf
commit 6427510c98
5 changed files with 169 additions and 24 deletions

View File

@@ -3,7 +3,6 @@ import { demoFunc } from './dees-table.demo.js';
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';
@@ -167,6 +166,13 @@ export class DeesTable<T> extends DeesElement {
// simple client-side filtering (Phase 1)
@property({ type: String })
public filterText: string = '';
// per-column quick filters
@property({ attribute: false })
public columnFilters: Record<string, string> = {};
@property({ type: Boolean, attribute: 'show-column-filters' })
public showColumnFilters: boolean = false;
@property({ type: Boolean, reflect: true, attribute: 'sticky-header' })
public stickyHeader: boolean = false;
// selection (Phase 1)
@property({ type: String })
@@ -254,6 +260,7 @@ export class DeesTable<T> extends DeesElement {
<style></style>
${this.data.length > 0
? html`
<div class="tableScroll">
<table>
<thead>
<tr>
@@ -261,12 +268,15 @@ export class DeesTable<T> extends DeesElement {
? html`
<th style="width:42px; text-align:center;">
${this.selectionMode === 'multi'
? html`<input type="checkbox"
.checked=${this.areAllSelected()}
@click=${(e: Event) => {
e.stopPropagation();
this.toggleSelectAll();
}} />`
? html`
<dees-input-checkbox
.value=${this.areAllSelected()}
@newValue=${(e: CustomEvent<boolean>) => {
e.stopPropagation();
this.setSelectAll(e.detail === true);
}}
></dees-input-checkbox>
`
: html``}
</th>
`
@@ -293,9 +303,31 @@ export class DeesTable<T> extends DeesElement {
}
})()}
</tr>
${this.showColumnFilters
? html`<tr class="filtersRow">
${this.selectionMode !== 'none'
? html`<th style="width:42px;"></th>`
: html``}
${effectiveColumns
.filter((c) => !c.hidden)
.map((col) => {
const key = String(col.key);
if (col.filterable === false) return html`<th></th>`;
return html`<th>
<input type="text" placeholder="Filter..." .value=${this.columnFilters[key] || ''}
@input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
</th>`;
})}
${(() => {
if (this.dataActions && this.dataActions.length > 0) {
return html` <th></th> `;
}
})()}
</tr>`
: html``}
</thead>
<tbody>
${getViewDataFn(this.data, effectiveColumns, this.sortKey, this.sortDir, this.filterText).map((itemArg, rowIndex) => {
${getViewDataFn(this.data, effectiveColumns, this.sortKey, this.sortDir, this.filterText, this.columnFilters).map((itemArg, rowIndex) => {
const getTr = (elementArg: HTMLElement): HTMLElement => {
if (elementArg.tagName === 'TR') {
return elementArg;
@@ -370,14 +402,13 @@ export class DeesTable<T> extends DeesElement {
>
${this.selectionMode !== 'none'
? html`<td style="width:42px; text-align:center;">
<input
type="checkbox"
.checked=${this.isRowSelected(itemArg)}
@click=${(e: Event) => {
<dees-input-checkbox
.value=${this.isRowSelected(itemArg)}
@newValue=${(e: CustomEvent<boolean>) => {
e.stopPropagation();
this.toggleRowSelected(itemArg);
this.setRowSelected(itemArg, e.detail === true);
}}
/>
></dees-input-checkbox>
</td>`
: html``}
${effectiveColumns
@@ -435,6 +466,7 @@ export class DeesTable<T> extends DeesElement {
})}
</tbody>
</table>
</div>
`
: html` <div class="noDataSet">No data set!</div> `}
<div class="footer">
@@ -583,7 +615,7 @@ export class DeesTable<T> extends DeesElement {
if (prev !== this.filterText) {
this.dispatchEvent(
new CustomEvent('filterChange', {
detail: { text: this.filterText },
detail: { text: this.filterText, columns: { ...this.columnFilters } },
bubbles: true,
})
);
@@ -591,6 +623,17 @@ export class DeesTable<T> extends DeesElement {
}
}
public setColumnFilter(key: string, value: string) {
this.columnFilters = { ...this.columnFilters, [key]: value };
this.dispatchEvent(
new CustomEvent('filterChange', {
detail: { text: this.filterText, columns: { ...this.columnFilters } },
bubbles: true,
})
);
this.requestUpdate();
}
// selection helpers
private getRowId(row: T): string {
if (this.rowKey) {
@@ -621,6 +664,19 @@ export class DeesTable<T> extends DeesElement {
this.requestUpdate();
}
private setRowSelected(row: T, checked: boolean) {
const id = this.getRowId(row);
if (this.selectionMode === 'single') {
this.selectedIds.clear();
if (checked) this.selectedIds.add(id);
} else if (this.selectionMode === 'multi') {
if (checked) this.selectedIds.add(id);
else this.selectedIds.delete(id);
}
this.emitSelectionChange();
this.requestUpdate();
}
private areAllSelected(): boolean {
return this.data.length > 0 && this.selectedIds.size === this.data.length;
}
@@ -635,6 +691,16 @@ export class DeesTable<T> extends DeesElement {
this.requestUpdate();
}
private setSelectAll(checked: boolean) {
if (checked) {
this.selectedIds = new Set(this.data.map((r) => this.getRowId(r)));
} else {
this.selectedIds.clear();
}
this.emitSelectionChange();
this.requestUpdate();
}
private emitSelectionChange() {
const selectedIds = Array.from(this.selectedIds);
const selectedRows = this.data.filter((r) => this.selectedIds.has(this.getRowId(r)));