feat: implement DeesTable component with schema-first columns API, data actions, and customizable styles
- Added DeesTable class extending DeesElement - Introduced properties for headings, data, actions, and columns - Implemented rendering logic for table headers, rows, and cells - Added support for sorting, searching, and context menus - Included customizable styles for table layout and appearance - Integrated editable fields and drag-and-drop file handling - Enhanced accessibility with ARIA attributes for sorting
This commit is contained in:
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
67
.serena/project.yml
Normal file
67
.serena/project.yml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||||
|
# * For C, use cpp
|
||||||
|
# * For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: typescript
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "dees-catalog"
|
10
package.json
10
package.json
@@ -24,7 +24,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
||||||
"@push.rocks/smarti18n": "^1.0.4",
|
"@push.rocks/smarti18n": "^1.0.4",
|
||||||
"@push.rocks/smartpromise": "^4.2.0",
|
"@push.rocks/smartpromise": "^4.2.0",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.1.0",
|
||||||
"@tiptap/core": "^2.23.0",
|
"@tiptap/core": "^2.23.0",
|
||||||
"@tiptap/extension-link": "^2.23.0",
|
"@tiptap/extension-link": "^2.23.0",
|
||||||
"@tiptap/extension-text-align": "^2.23.0",
|
"@tiptap/extension-text-align": "^2.23.0",
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
"@tiptap/starter-kit": "^2.23.0",
|
"@tiptap/starter-kit": "^2.23.0",
|
||||||
"@tsclass/tsclass": "^9.2.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"@webcontainer/api": "1.2.0",
|
"@webcontainer/api": "1.2.0",
|
||||||
"apexcharts": "^5.3.4",
|
"apexcharts": "^5.3.5",
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"ibantools": "^4.5.1",
|
"ibantools": "^4.5.1",
|
||||||
"lucide": "^0.542.0",
|
"lucide": "^0.544.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.53.0",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0"
|
"xterm-addon-fit": "^0.8.0"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.8",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tstest": "^2.3.6",
|
"@git.zone/tstest": "^2.3.8",
|
||||||
"@git.zone/tswatch": "^2.2.1",
|
"@git.zone/tswatch": "^2.2.1",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/tapbundle": "^6.0.3",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
|
583
pnpm-lock.yaml
generated
583
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- puppeteer
|
@@ -20,7 +20,7 @@ import { DeesInputMultitoggle } from './dees-input-multitoggle.js';
|
|||||||
import { DeesInputPhone } from './dees-input-phone.js';
|
import { DeesInputPhone } from './dees-input-phone.js';
|
||||||
import { DeesInputTypelist } from './dees-input-typelist.js';
|
import { DeesInputTypelist } from './dees-input-typelist.js';
|
||||||
import { DeesFormSubmit } from './dees-form-submit.js';
|
import { DeesFormSubmit } from './dees-form-submit.js';
|
||||||
import { DeesTable } from './dees-table.js';
|
import { DeesTable } from './dees-table/dees-table.js';
|
||||||
import { demoFunc } from './dees-form.demo.js';
|
import { demoFunc } from './dees-form.demo.js';
|
||||||
|
|
||||||
// Unified set for form input types
|
// Unified set for form input types
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { type ITableAction } from './dees-table.js';
|
import { type ITableAction } from './dees-table.js';
|
||||||
import * as plugins from './00plugins.js';
|
import * as plugins from '../00plugins.js';
|
||||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
interface ITableDemoData {
|
interface ITableDemoData {
|
||||||
@@ -427,6 +427,46 @@ export const demoFunc = () => html`
|
|||||||
dataName="items"
|
dataName="items"
|
||||||
></dees-table>
|
></dees-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Schema-First Columns (New)</h2>
|
||||||
|
<p class="demo-description">Defines columns explicitly and renders via schema. No displayFunction needed.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Users (Schema-First)"
|
||||||
|
heading2="Columns define rendering and order"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'name', header: 'Name', sortable: true },
|
||||||
|
{ key: 'email', header: 'Email', renderer: (v: string) => html`<dees-badge>${v}</dees-badge>` },
|
||||||
|
{ key: 'joinedAt', header: 'Joined', renderer: (v: string) => new Date(v).toLocaleDateString() },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ name: 'Alice', email: 'alice@example.com', joinedAt: '2022-08-01' },
|
||||||
|
{ name: 'Bob', email: 'bob@example.com', joinedAt: '2021-12-11' },
|
||||||
|
{ name: 'Carol', email: 'carol@example.com', joinedAt: '2023-03-22' },
|
||||||
|
]}
|
||||||
|
dataName="users"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Partial Schema + Augment (New)</h2>
|
||||||
|
<p class="demo-description">Provides only the important columns; the rest are merged in from displayFunction.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Users (Partial + Augment)"
|
||||||
|
heading2="Missing columns are derived"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'name', header: 'Name', sortable: true },
|
||||||
|
]}
|
||||||
|
.displayFunction=${(u: any) => ({ name: u.name, email: u.email, role: u.role })}
|
||||||
|
.augmentFromDisplayFunction=${true}
|
||||||
|
.data=${[
|
||||||
|
{ name: 'Erin', email: 'erin@example.com', role: 'Admin' },
|
||||||
|
{ name: 'Finn', email: 'finn@example.com', role: 'User' },
|
||||||
|
{ name: 'Gina', email: 'gina@example.com', role: 'User' },
|
||||||
|
]}
|
||||||
|
dataName="users"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
@@ -1,6 +1,6 @@
|
|||||||
import * as plugins from './00plugins.js';
|
import * as plugins from '../00plugins.js';
|
||||||
import { demoFunc } from './dees-table.demo.js';
|
import { demoFunc } from './dees-table.demo.js';
|
||||||
import { cssGeistFontFamily } from './00fonts.js';
|
import { cssGeistFontFamily } from '../00fonts.js';
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
directives,
|
directives,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
import { DeesContextmenu } from '../dees-contextmenu.js';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { type TIconKey } from './dees-icon.js';
|
import { type TIconKey } from '../dees-icon.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -63,6 +63,21 @@ export interface ITableActionDataArg<T> {
|
|||||||
table: DeesTable<T>;
|
table: DeesTable<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// schema-first columns API (Phase 1)
|
||||||
|
export interface Column<T = any> {
|
||||||
|
/** 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<T> }) => TemplateResult | string;
|
||||||
|
/** reserved for future phases; present to sketch intent */
|
||||||
|
sortable?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type TDisplayFunction<T = any> = (itemArg: T) => object;
|
export type TDisplayFunction<T = any> = (itemArg: T) => object;
|
||||||
|
|
||||||
// the table implementation
|
// the table implementation
|
||||||
@@ -134,6 +149,24 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
})
|
})
|
||||||
public dataActions: ITableAction<T>[] = [];
|
public dataActions: ITableAction<T>[] = [];
|
||||||
|
|
||||||
|
// schema-first columns API
|
||||||
|
@property({ attribute: false })
|
||||||
|
public columns: Column<T>[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stable row identity for selection and updates. If provided as a function,
|
||||||
|
* it is only usable as a property (not via attribute).
|
||||||
|
*/
|
||||||
|
@property({ attribute: false })
|
||||||
|
public rowKey?: keyof T | ((row: T) => string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true and columns are provided, merge any missing columns discovered
|
||||||
|
* via displayFunction into the effective schema.
|
||||||
|
*/
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public augmentFromDisplayFunction: boolean = false;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
attribute: false,
|
attribute: false,
|
||||||
})
|
})
|
||||||
@@ -180,6 +213,12 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
|
|
||||||
public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||||
|
|
||||||
|
// simple client-side sorting (Phase 1)
|
||||||
|
@property({ attribute: false })
|
||||||
|
private sortKey?: string;
|
||||||
|
@property({ attribute: false })
|
||||||
|
private sortDir: 'asc' | 'desc' | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -544,6 +583,11 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
|
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
|
||||||
|
const effectiveColumns: Column<T>[] = usingColumns
|
||||||
|
? this.computeEffectiveColumns()
|
||||||
|
: this.computeColumnsFromDisplayFunction();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
<!-- the heading part -->
|
<!-- the heading part -->
|
||||||
@@ -609,32 +653,35 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
<!-- the actual table -->
|
<!-- the actual table -->
|
||||||
<style></style>
|
<style></style>
|
||||||
${this.data.length > 0
|
${this.data.length > 0
|
||||||
? (() => {
|
? html`
|
||||||
// Only pick up the keys from the first transformed data object
|
<table>
|
||||||
// as all data objects are assumed to have the same structure
|
<thead>
|
||||||
const firstTransformedItem = this.displayFunction(this.data[0]);
|
<tr>
|
||||||
const headings: string[] = Object.keys(firstTransformedItem);
|
${effectiveColumns
|
||||||
return html`
|
.filter((c) => !c.hidden)
|
||||||
<table>
|
.map((col) => {
|
||||||
<thead>
|
const isSortable = !!col.sortable;
|
||||||
<tr>
|
const ariaSort = this.getAriaSort(col);
|
||||||
${headings.map(
|
return html`
|
||||||
(headingArg) => html`
|
<th
|
||||||
<th>${headingArg}</th>
|
role="columnheader"
|
||||||
`
|
aria-sort=${ariaSort}
|
||||||
)}
|
style="${isSortable ? 'cursor: pointer;' : ''}"
|
||||||
${(() => {
|
@click=${() => (isSortable ? this.toggleSort(col) : null)}
|
||||||
if (this.dataActions && this.dataActions.length > 0) {
|
>
|
||||||
return html`
|
${col.header ?? (col.key as any)}
|
||||||
<th>Actions</th>
|
${this.renderSortIndicator(col)}
|
||||||
`;
|
</th>`;
|
||||||
}
|
})}
|
||||||
})()}
|
${(() => {
|
||||||
</tr>
|
if (this.dataActions && this.dataActions.length > 0) {
|
||||||
</thead>
|
return html` <th>Actions</th> `;
|
||||||
<tbody>
|
}
|
||||||
${this.data.map((itemArg) => {
|
})()}
|
||||||
const transformedItem = this.displayFunction(itemArg);
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${this.getViewData(effectiveColumns).map((itemArg, rowIndex) => {
|
||||||
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
||||||
if (elementArg.tagName === 'TR') {
|
if (elementArg.tagName === 'TR') {
|
||||||
return elementArg;
|
return elementArg;
|
||||||
@@ -651,8 +698,6 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
eventArg.preventDefault();
|
eventArg.preventDefault();
|
||||||
eventArg.stopPropagation();
|
eventArg.stopPropagation();
|
||||||
const realTarget = getTr(eventArg.target as HTMLElement);
|
const realTarget = getTr(eventArg.target as HTMLElement);
|
||||||
console.log('dragenter');
|
|
||||||
console.log(realTarget);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
realTarget.classList.add('hasAttachment');
|
realTarget.classList.add('hasAttachment');
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -702,29 +747,31 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
}}
|
}}
|
||||||
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
||||||
>
|
>
|
||||||
${headings.map(
|
${effectiveColumns
|
||||||
(headingArg) => html`
|
.filter((c) => !c.hidden)
|
||||||
<td
|
.map((col, colIndex) => {
|
||||||
@dblclick=${(e: Event) => {
|
const value = this.getCellValue(itemArg, col);
|
||||||
if (this.editableFields.includes(headingArg)) {
|
const content = col.renderer
|
||||||
this.handleCellEditing(e, itemArg, headingArg);
|
? col.renderer(value, itemArg, { rowIndex, colIndex, column: col })
|
||||||
} else {
|
: value;
|
||||||
const wantedAction = this.dataActions.find((actionArg) =>
|
const editKey = String(col.key);
|
||||||
|
return html`
|
||||||
|
<td
|
||||||
|
@dblclick=${(e: Event) => {
|
||||||
|
const dblAction = this.dataActions.find((actionArg) =>
|
||||||
actionArg.type.includes('doubleClick')
|
actionArg.type.includes('doubleClick')
|
||||||
);
|
);
|
||||||
if (wantedAction) {
|
if (this.editableFields.includes(editKey)) {
|
||||||
wantedAction.actionFunc({
|
this.handleCellEditing(e, itemArg, editKey);
|
||||||
item: itemArg,
|
} else if (dblAction) {
|
||||||
table: this,
|
dblAction.actionFunc({ item: itemArg, table: this });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<div class="innerCellContainer">${content}</div>
|
||||||
<div class="innerCellContainer">${transformedItem[headingArg]}</div>
|
</td>
|
||||||
</td>
|
`;
|
||||||
`
|
})}
|
||||||
)}
|
|
||||||
${(() => {
|
${(() => {
|
||||||
if (this.dataActions && this.dataActions.length > 0) {
|
if (this.dataActions && this.dataActions.length > 0) {
|
||||||
return html`
|
return html`
|
||||||
@@ -732,36 +779,30 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
<div class="actionsContainer">
|
<div class="actionsContainer">
|
||||||
${this.getActionsForType('inRow').map(
|
${this.getActionsForType('inRow').map(
|
||||||
(actionArg) => html`
|
(actionArg) => html`
|
||||||
<div
|
<div
|
||||||
class="action"
|
class="action"
|
||||||
@click=${() =>
|
@click=${() =>
|
||||||
actionArg.actionFunc({
|
actionArg.actionFunc({
|
||||||
item: itemArg,
|
item: itemArg,
|
||||||
table: this,
|
table: this,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${actionArg.iconName
|
${actionArg.iconName
|
||||||
? html`
|
? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
|
||||||
<dees-icon
|
: actionArg.name}
|
||||||
.icon=${actionArg.iconName}
|
</div>
|
||||||
></dees-icon>
|
`
|
||||||
`
|
)}
|
||||||
: actionArg.name}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</tr>
|
</tr>`;
|
||||||
`;
|
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`;
|
`
|
||||||
})()
|
|
||||||
: html` <div class="noDataSet">No data set!</div> `}
|
: html` <div class="noDataSet">No data set!</div> `}
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="tableStatistics">
|
<div class="tableStatistics">
|
||||||
@@ -869,6 +910,87 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
table.style.tableLayout = 'fixed';
|
table.style.tableLayout = 'fixed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private computeColumnsFromDisplayFunction(): Column<T>[] {
|
||||||
|
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<T>[] {
|
||||||
|
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<T>): any {
|
||||||
|
return col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getViewData(effectiveColumns: Column<T>[]): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleSort(col: Column<T>) {
|
||||||
|
const key = String(col.key);
|
||||||
|
if (this.sortKey !== key) {
|
||||||
|
this.sortKey = key;
|
||||||
|
this.sortDir = 'asc';
|
||||||
|
} else {
|
||||||
|
if (this.sortDir === 'asc') this.sortDir = 'desc';
|
||||||
|
else if (this.sortDir === 'desc') {
|
||||||
|
this.sortDir = null;
|
||||||
|
this.sortKey = undefined;
|
||||||
|
} else this.sortDir = 'asc';
|
||||||
|
}
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('sortChange', {
|
||||||
|
detail: { key: this.sortKey, dir: this.sortDir },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAriaSort(col: Column<T>): 'none' | 'ascending' | 'descending' {
|
||||||
|
if (String(col.key) !== this.sortKey || !this.sortDir) return 'none';
|
||||||
|
return this.sortDir === 'asc' ? 'ascending' : 'descending';
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSortIndicator(col: Column<T>) {
|
||||||
|
if (String(col.key) !== this.sortKey || !this.sortDir) return html``;
|
||||||
|
return html`<span style="margin-left:6px; opacity:0.7;">${this.sortDir === 'asc' ? '▲' : '▼'}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
getActionsForType(typeArg: ITableAction['type'][0]) {
|
getActionsForType(typeArg: ITableAction['type'][0]) {
|
||||||
const actions: ITableAction[] = [];
|
const actions: ITableAction[] = [];
|
||||||
for (const action of this.dataActions) {
|
for (const action of this.dataActions) {
|
||||||
@@ -884,7 +1006,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
const originalColor = target.style.color;
|
const originalColor = target.style.color;
|
||||||
target.style.color = 'transparent';
|
target.style.color = 'transparent';
|
||||||
const transformedItem = this.displayFunction(itemArg);
|
const transformedItem = this.displayFunction(itemArg);
|
||||||
const initialValue = (transformedItem[key] as unknown as string) || '';
|
const initialValue = ((transformedItem as any)[key] ?? (itemArg as any)[key] ?? '') as string;
|
||||||
// Create an input element
|
// Create an input element
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
@@ -57,7 +57,7 @@ export * from './dees-speechbubble.js';
|
|||||||
export * from './dees-spinner.js';
|
export * from './dees-spinner.js';
|
||||||
export * from './dees-statsgrid.js';
|
export * from './dees-statsgrid.js';
|
||||||
export * from './dees-stepper.js';
|
export * from './dees-stepper.js';
|
||||||
export * from './dees-table.js';
|
export * from './dees-table/dees-table.js';
|
||||||
export * from './dees-terminal.js';
|
export * from './dees-terminal.js';
|
||||||
export * from './dees-toast.js';
|
export * from './dees-toast.js';
|
||||||
export * from './dees-updater.js';
|
export * from './dees-updater.js';
|
||||||
|
Reference in New Issue
Block a user