Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
a212536cfa | |||
18297d54c4 | |||
f790ca38d0 | |||
ce2b42ecd5 | |||
09e299bc2e | |||
bbc7dfe29a | |||
49b9e833e8 | |||
f739bb608e | |||
286a6f9088 | |||
e32b9589a5 | |||
6427510c98 | |||
cf92a423cf | |||
3f3677ebaa | |||
edc15a727c | |||
960085145d | |||
7fdb4f19a8 | |||
e21fb79731 | |||
05f669a7bd | |||
8137d79e18 | |||
3b474b7dcc | |||
e449b413d1 | |||
8918dc94bd |
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
6
.serena/memories/done_checklist.md
Normal file
6
.serena/memories/done_checklist.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Before finishing a task:
|
||||||
|
- Run `pnpm run build` to ensure TypeScript compile + bundling succeed.
|
||||||
|
- Verify `dist_ts_web/` and `dist_bundle/bundle.js` updated.
|
||||||
|
- Optionally run `pnpm run test` and inspect failures.
|
||||||
|
- Avoid changing public APIs unless required; keep changes scoped.
|
||||||
|
- Update readme or inline docs only if user-facing behavior changes.
|
11
.serena/memories/project_overview.md
Normal file
11
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Project: @design.estate/dees-catalog
|
||||||
|
Purpose: A component library of dynamic Web Components (TypeScript) for building modern web apps.
|
||||||
|
Tech stack: TypeScript (ES2022, NodeNext), decorators, custom elements via @design.estate/dees-element (Lit-style), bundling with esbuild via @git.zone/tsbundle, TypeScript building via @git.zone/tsbuild (tsfolders), tests with @git.zone/tstest, various UI libs (tiptap, apexcharts, monaco-editor runtime via CDN), DOM helpers via @design.estate/dees-domtools.
|
||||||
|
Structure:
|
||||||
|
- ts_web/: source of web components and pages
|
||||||
|
- dist_ts_web/: transpiled TS output
|
||||||
|
- dist_bundle/: production bundle (bundle.js + map)
|
||||||
|
- test/: tests
|
||||||
|
- html/: static demo assets
|
||||||
|
Key configs: tsconfig.json sets ES2022, NodeNext module/resolution, decorators enabled, skipLibCheck enabled to avoid third-party d.ts issues.
|
||||||
|
Entrypoints: ts_web/index.ts for bundling; custom elements annotated with @customElement.
|
5
.serena/memories/style_and_conventions.md
Normal file
5
.serena/memories/style_and_conventions.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Language: TypeScript, ES2022 target, NodeNext module + resolution.
|
||||||
|
Patterns: Web Components with @customElement decorators; class-based components extending DeesElement; styles via css/cssManager; template render via html tagged literal.
|
||||||
|
Typing: Prefer explicit types where practical; tolerate `any` for external browser-injected libs (e.g., monaco) to keep build healthy.
|
||||||
|
Config: skipLibCheck enabled to avoid third-party d.ts breakages; exclude built declaration outputs.
|
||||||
|
Formatting/Linting: Not explicitly configured; follow existing style (2-space indents, single quotes often, semicolons present).
|
6
.serena/memories/suggested_commands.md
Normal file
6
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Build: pnpm run build
|
||||||
|
Watch: pnpm run watch
|
||||||
|
Test: pnpm run test
|
||||||
|
Docs: pnpm run buildDocs
|
||||||
|
Inspect bundle size: ls -lh dist_bundle/bundle.js
|
||||||
|
Open demo (if applicable): serve static `html/` with any web server
|
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"
|
38
changelog.md
38
changelog.md
@@ -1,5 +1,43 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-09-16 - 1.11.6 - fix(dees-table)
|
||||||
|
Improve Lucene range comparisons, pin monaco-editor to 0.52.2, and add local dev metadata
|
||||||
|
|
||||||
|
- Fix lucene inRange behavior to correctly compare homogeneous types (strings, numbers, dates) and fall back to string comparison when needed (ts_web/elements/dees-table/lucene.ts).
|
||||||
|
- Pin monaco-editor to 0.52.2 in package.json to avoid a breaking upgrade regression observed with ^0.53.0.
|
||||||
|
- Add local development/tooling metadata and conveniences: .claude/settings.local.json (tool permissions) and .serena/ memory files (done_checklist, project_overview, style_and_conventions, suggested_commands).
|
||||||
|
- Minor housekeeping: update project dev docs / memories to capture build/test/checklist guidance.
|
||||||
|
|
||||||
|
## 2025-09-16 - 1.11.5 - fix(ci)
|
||||||
|
Add local Claude agent settings for CI tooling
|
||||||
|
|
||||||
|
- Add .claude/settings.local.json to configure local Claude agent permissions
|
||||||
|
- Allow Bash commands matching pnpm run:* and the mcp__zen__chat permission for development tooling
|
||||||
|
|
||||||
|
## 2025-09-10 - 1.11.4 - fix(readme)
|
||||||
|
Rewrite and expand README with Quick Start, feature highlights, demos and usage examples; add local Claude settings file
|
||||||
|
|
||||||
|
- Completely rewritten and reorganized README: added Quick Start, component highlights, usage examples, demos, development workflow, troubleshooting and links.
|
||||||
|
- Added .claude/settings.local.json with local Claude permission configuration.
|
||||||
|
|
||||||
|
## 2025-09-08 - 1.11.3 - fix(dees-input-list)
|
||||||
|
Prevent list animations from affecting scroll bounds and fix content-visibility issues in dees-input-list; add local developer settings
|
||||||
|
|
||||||
|
- dees-input-list: add overflow:hidden to list items to prevent animations from altering scroll bounds and causing visual/scroll glitches
|
||||||
|
- dees-input-list: force content-visibility/contain to visible/none to avoid unexpected scrolling/layout issues when items animate
|
||||||
|
- Add .claude/settings.local.json with local developer permissions (allows running pnpm scripts via Claude-local tooling)
|
||||||
|
|
||||||
|
## 2025-09-07 - 1.11.2 - fix(DeesFormSubmit)
|
||||||
|
Make form submit robust by locating nearest dees-form via closest(); add local CLAUDE settings
|
||||||
|
|
||||||
|
- Fix: DeesFormSubmit.submit now walks up the DOM with closest('dees-form') to find and call gatherAndDispatch on the parent form. This fixes cases where the submit button is slotted or not a direct child of the form.
|
||||||
|
- Chore: Add .claude/settings.local.json to permit running pnpm scripts in the local CLAUDE environment (allows Bash(pnpm run:*)).
|
||||||
|
|
||||||
|
## 2025-09-06 - 1.11.1 - fix(dees-input-text)
|
||||||
|
Normalize Lucide icon names for password toggle
|
||||||
|
|
||||||
|
- Updated password visibility toggle icons in dees-input-text from 'lucide:eye'/'lucide:eye-off' to 'lucide:Eye'/'lucide:EyeOff' to match Lucide exports and avoid missing icon rendering.
|
||||||
|
|
||||||
## 2025-09-05 - 1.11.0 - feat(dees-icon)
|
## 2025-09-05 - 1.11.0 - feat(dees-icon)
|
||||||
Add full icon list and improve dees-icon demo with copy-all functionality and UI tweaks
|
Add full icon list and improve dees-icon demo with copy-all functionality and UI tweaks
|
||||||
|
|
||||||
|
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "1.11.0",
|
"version": "1.11.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -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.52.2",
|
||||||
"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",
|
||||||
|
568
pnpm-lock.yaml
generated
568
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
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '1.11.0',
|
version: '1.11.6',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
@@ -57,9 +57,10 @@ export class DeesFormSubmit extends DeesElement {
|
|||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentElement: DeesForm = this.parentElement as DeesForm;
|
// Walk up the DOM tree to find the nearest dees-form element
|
||||||
if (parentElement && parentElement.gatherAndDispatch) {
|
const parentFormElement = this.closest('dees-form') as DeesForm;
|
||||||
parentElement.gatherAndDispatch();
|
if (parentFormElement && parentFormElement.gatherAndDispatch) {
|
||||||
|
parentFormElement.gatherAndDispatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -28,6 +28,9 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
|||||||
})
|
})
|
||||||
public value: boolean = false;
|
public value: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public indeterminate: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -166,6 +169,14 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
|
: this.indeterminate
|
||||||
|
? html`
|
||||||
|
<span class="checkmark">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 12H19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
`
|
||||||
: html``}
|
: html``}
|
||||||
</div>
|
</div>
|
||||||
<div class="label-container">
|
<div class="label-container">
|
||||||
|
275
ts_web/elements/dees-input-list.demo.ts
Normal file
275
ts_web/elements/dees-input-list.demo.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import { html, css } from '@design.estate/dees-element';
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
import './dees-panel.js';
|
||||||
|
import './dees-form.js';
|
||||||
|
import './dees-input-text.js';
|
||||||
|
import './dees-form-submit.js';
|
||||||
|
|
||||||
|
export const demoFunc = () => html`
|
||||||
|
<dees-demowrapper>
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.demo-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-panel {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-panel:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-preview {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #374151;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.output-preview {
|
||||||
|
background: #2c2c2c;
|
||||||
|
color: #e4e4e7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-note {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #eff6ff;
|
||||||
|
border-left: 3px solid #3b82f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.feature-note {
|
||||||
|
background: #1e3a5f;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<dees-panel .title=${'1. Basic List Input'} .subtitle=${'Simple list management with add, edit, and delete'}>
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Shopping List'}
|
||||||
|
.placeholder=${'Add item to your list...'}
|
||||||
|
.value=${['Milk', 'Bread', 'Eggs', 'Cheese']}
|
||||||
|
.description=${'Double-click to edit items, or use the edit button'}
|
||||||
|
></dees-input-list>
|
||||||
|
<div class="feature-note">
|
||||||
|
💡 Double-click any item to quickly edit it inline
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'2. Sortable List'} .subtitle=${'Drag and drop to reorder items'}>
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Task Priority'}
|
||||||
|
.placeholder=${'Add a task...'}
|
||||||
|
.sortable=${true}
|
||||||
|
.value=${[
|
||||||
|
'Review pull requests',
|
||||||
|
'Fix critical bug',
|
||||||
|
'Update documentation',
|
||||||
|
'Deploy to production',
|
||||||
|
'Team standup meeting'
|
||||||
|
]}
|
||||||
|
.description=${'Drag items using the handle to reorder them'}
|
||||||
|
></dees-input-list>
|
||||||
|
<div class="feature-note">
|
||||||
|
🔄 Drag the grip handle to reorder tasks by priority
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'3. Validation & Constraints'} .subtitle=${'Lists with minimum/maximum items and duplicate prevention'}>
|
||||||
|
<div class="grid-layout">
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Team Members (Min 2, Max 5)'}
|
||||||
|
.placeholder=${'Add team member...'}
|
||||||
|
.minItems=${2}
|
||||||
|
.maxItems=${5}
|
||||||
|
.value=${['Alice', 'Bob']}
|
||||||
|
.required=${true}
|
||||||
|
.description=${'Add 2-5 team members'}
|
||||||
|
></dees-input-list>
|
||||||
|
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Unique Tags (No Duplicates)'}
|
||||||
|
.placeholder=${'Add unique tag...'}
|
||||||
|
.allowDuplicates=${false}
|
||||||
|
.value=${['frontend', 'backend', 'database']}
|
||||||
|
.description=${'Duplicate items are not allowed'}
|
||||||
|
></dees-input-list>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'4. Delete Confirmation'} .subtitle=${'Require confirmation before deleting items'}>
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Important Documents'}
|
||||||
|
.placeholder=${'Add document name...'}
|
||||||
|
.confirmDelete=${true}
|
||||||
|
.value=${[
|
||||||
|
'Contract_2024.pdf',
|
||||||
|
'Financial_Report_Q3.xlsx',
|
||||||
|
'Project_Proposal.docx',
|
||||||
|
'Meeting_Notes.txt'
|
||||||
|
]}
|
||||||
|
.description=${'Deletion requires confirmation for safety'}
|
||||||
|
></dees-input-list>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only list display'}>
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'System Defaults'}
|
||||||
|
.value=${['Default Setting 1', 'Default Setting 2', 'Default Setting 3']}
|
||||||
|
.disabled=${true}
|
||||||
|
.description=${'These items cannot be modified'}
|
||||||
|
></dees-input-list>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'6. Form Integration'} .subtitle=${'List input working within a form context'}>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Recipe Name'}
|
||||||
|
.placeholder=${'My Amazing Recipe'}
|
||||||
|
.required=${true}
|
||||||
|
.key=${'name'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<div class="grid-layout">
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Ingredients'}
|
||||||
|
.placeholder=${'Add ingredient...'}
|
||||||
|
.required=${true}
|
||||||
|
.minItems=${3}
|
||||||
|
.key=${'ingredients'}
|
||||||
|
.sortable=${true}
|
||||||
|
.value=${[
|
||||||
|
'2 cups flour',
|
||||||
|
'1 cup sugar',
|
||||||
|
'3 eggs'
|
||||||
|
]}
|
||||||
|
.description=${'Add at least 3 ingredients'}
|
||||||
|
></dees-input-list>
|
||||||
|
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Instructions'}
|
||||||
|
.placeholder=${'Add instruction step...'}
|
||||||
|
.required=${true}
|
||||||
|
.minItems=${2}
|
||||||
|
.key=${'instructions'}
|
||||||
|
.sortable=${true}
|
||||||
|
.value=${[
|
||||||
|
'Preheat oven to 350°F',
|
||||||
|
'Mix dry ingredients'
|
||||||
|
]}
|
||||||
|
.description=${'Add cooking instructions in order'}
|
||||||
|
></dees-input-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Notes'}
|
||||||
|
.inputType=${'textarea'}
|
||||||
|
.placeholder=${'Any special notes or tips...'}
|
||||||
|
.key=${'notes'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<dees-form-submit .text=${'Save Recipe'}></dees-form-submit>
|
||||||
|
</dees-form>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Build your own feature list and see the data'}>
|
||||||
|
<dees-input-list
|
||||||
|
id="interactive-list"
|
||||||
|
.label=${'Product Features'}
|
||||||
|
.placeholder=${'Add a feature...'}
|
||||||
|
.sortable=${true}
|
||||||
|
.confirmDelete=${false}
|
||||||
|
.allowDuplicates=${false}
|
||||||
|
.maxItems=${10}
|
||||||
|
@change=${(e: CustomEvent) => {
|
||||||
|
const preview = document.querySelector('#list-json');
|
||||||
|
if (preview) {
|
||||||
|
const data = {
|
||||||
|
items: e.detail.value,
|
||||||
|
count: e.detail.value.length,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
preview.textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-input-list>
|
||||||
|
|
||||||
|
<div class="output-preview" id="list-json">
|
||||||
|
{
|
||||||
|
"items": [],
|
||||||
|
"count": 0,
|
||||||
|
"timestamp": "${new Date().toISOString()}"
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-note">
|
||||||
|
✨ Add, edit, remove, and reorder items to see the JSON output update in real-time
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'8. Advanced Configuration'} .subtitle=${'Combine all features for complex use cases'}>
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Project Milestones'}
|
||||||
|
.placeholder=${'Add milestone...'}
|
||||||
|
.value=${[
|
||||||
|
'Project Kickoff - Week 1',
|
||||||
|
'Requirements Gathering - Week 2-3',
|
||||||
|
'Design Phase - Week 4-6',
|
||||||
|
'Development Sprint 1 - Week 7-9',
|
||||||
|
'Testing & QA - Week 10-11',
|
||||||
|
'Deployment - Week 12'
|
||||||
|
]}
|
||||||
|
.sortable=${true}
|
||||||
|
.confirmDelete=${true}
|
||||||
|
.allowDuplicates=${false}
|
||||||
|
.minItems=${3}
|
||||||
|
.maxItems=${12}
|
||||||
|
.required=${true}
|
||||||
|
.description=${'Manage project milestones (3-12 items, sortable, no duplicates)'}
|
||||||
|
></dees-input-list>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'9. Empty State'} .subtitle=${'How the component looks with no items'}>
|
||||||
|
<dees-input-list
|
||||||
|
.label=${'Your Ideas'}
|
||||||
|
.placeholder=${'Share your ideas...'}
|
||||||
|
.value=${[]}
|
||||||
|
.description=${'Start adding items to build your list'}
|
||||||
|
></dees-input-list>
|
||||||
|
</dees-panel>
|
||||||
|
</div>
|
||||||
|
</dees-demowrapper>
|
||||||
|
`;
|
622
ts_web/elements/dees-input-list.ts
Normal file
622
ts_web/elements/dees-input-list.ts
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import { DeesInputBase } from './dees-input-base.js';
|
||||||
|
import './dees-icon.js';
|
||||||
|
import './dees-button.js';
|
||||||
|
import { demoFunc } from './dees-input-list.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-input-list': DeesInputList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-input-list')
|
||||||
|
export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||||
|
// STATIC
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@property({ type: Array })
|
||||||
|
public value: string[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public placeholder: string = 'Add new item...';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxItems: number = 0; // 0 means unlimited
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public minItems: number = 0;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public allowDuplicates: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public sortable: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public confirmDelete: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public validationText: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private inputValue: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private editingIndex: number = -1;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private editingValue: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private draggedIndex: number = -1;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private dragOverIndex: number = -1;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
...DeesInputBase.baseStyles,
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
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: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container:hover:not(.disabled) {
|
||||||
|
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container:focus-within {
|
||||||
|
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||||
|
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-items {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden; /* Prevent animation from affecting scroll bounds */
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:hover:not(.disabled) {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item.dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 10.8%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item.drag-over {
|
||||||
|
background: ${cssManager.bdTheme('hsl(210 40% 93.1%)', 'hsl(215 20.2% 13.8%)')};
|
||||||
|
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: move;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:hover {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle dees-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-text {
|
||||||
|
flex: 1;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-edit-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button: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%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.save {
|
||||||
|
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.save:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.cancel {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 50.6%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.cancel:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 50.6% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.delete {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 50.6%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.delete:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 50.6% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button dees-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')};
|
||||||
|
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||||
|
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 32px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.list-items::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-items::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-items::-webkit-scrollbar-thumb {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 24.9%)')};
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-items::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 34.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for adding/removing items */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
animation: slideIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any inherited contain/content-visibility that might cause scrolling issues */
|
||||||
|
.list-items, .list-item {
|
||||||
|
content-visibility: visible !important;
|
||||||
|
contain: none !important;
|
||||||
|
contain-intrinsic-size: auto !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="input-wrapper">
|
||||||
|
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
|
||||||
|
|
||||||
|
<div class="list-container ${this.disabled ? 'disabled' : ''}">
|
||||||
|
<div class="list-items">
|
||||||
|
${this.value.length > 0 ? this.value.map((item, index) => html`
|
||||||
|
<div
|
||||||
|
class="list-item ${this.draggedIndex === index ? 'dragging' : ''} ${this.dragOverIndex === index ? 'drag-over' : ''}"
|
||||||
|
draggable="${this.sortable && !this.disabled}"
|
||||||
|
@dragstart=${(e: DragEvent) => this.handleDragStart(e, index)}
|
||||||
|
@dragend=${this.handleDragEnd}
|
||||||
|
@dragover=${(e: DragEvent) => this.handleDragOver(e, index)}
|
||||||
|
@dragleave=${this.handleDragLeave}
|
||||||
|
@drop=${(e: DragEvent) => this.handleDrop(e, index)}
|
||||||
|
>
|
||||||
|
${this.sortable && !this.disabled ? html`
|
||||||
|
<div class="drag-handle">
|
||||||
|
<dees-icon .icon=${'lucide:gripVertical'}></dees-icon>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="item-content">
|
||||||
|
${this.editingIndex === index ? html`
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="item-edit-input"
|
||||||
|
.value=${this.editingValue}
|
||||||
|
@input=${(e: InputEvent) => this.editingValue = (e.target as HTMLInputElement).value}
|
||||||
|
@keydown=${(e: KeyboardEvent) => this.handleEditKeyDown(e, index)}
|
||||||
|
@blur=${() => this.saveEdit(index)}
|
||||||
|
/>
|
||||||
|
` : html`
|
||||||
|
<div class="item-text" @dblclick=${() => !this.disabled && this.startEdit(index)}>
|
||||||
|
${item}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-actions">
|
||||||
|
${this.editingIndex === index ? html`
|
||||||
|
<button class="action-button save" @click=${() => this.saveEdit(index)}>
|
||||||
|
<dees-icon .icon=${'lucide:check'}></dees-icon>
|
||||||
|
</button>
|
||||||
|
<button class="action-button cancel" @click=${() => this.cancelEdit()}>
|
||||||
|
<dees-icon .icon=${'lucide:x'}></dees-icon>
|
||||||
|
</button>
|
||||||
|
` : html`
|
||||||
|
${!this.disabled ? html`
|
||||||
|
<button class="action-button" @click=${() => this.startEdit(index)}>
|
||||||
|
<dees-icon .icon=${'lucide:pencil'}></dees-icon>
|
||||||
|
</button>
|
||||||
|
<button class="action-button delete" @click=${() => this.removeItem(index)}>
|
||||||
|
<dees-icon .icon=${'lucide:trash2'}></dees-icon>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`) : html`
|
||||||
|
<div class="empty-state">
|
||||||
|
No items added yet
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${!this.disabled && (!this.maxItems || this.value.length < this.maxItems) ? html`
|
||||||
|
<div class="add-item-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="add-input"
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.value=${this.inputValue}
|
||||||
|
@input=${this.handleInput}
|
||||||
|
@keydown=${this.handleAddKeyDown}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
/>
|
||||||
|
<dees-button
|
||||||
|
class="add-button"
|
||||||
|
@click=${this.addItem}
|
||||||
|
?disabled=${!this.inputValue.trim()}
|
||||||
|
>
|
||||||
|
<dees-icon .icon=${'lucide:plus'}></dees-icon> Add
|
||||||
|
</dees-button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.validationText ? html`
|
||||||
|
<div class="validation-message">${this.validationText}</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${this.description ? html`
|
||||||
|
<div class="description">${this.description}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInput(e: InputEvent) {
|
||||||
|
this.inputValue = (e.target as HTMLInputElement).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAddKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' && this.inputValue.trim()) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.addItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEditKeyDown(e: KeyboardEvent, index: number) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveEdit(index);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.cancelEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addItem() {
|
||||||
|
const trimmedValue = this.inputValue.trim();
|
||||||
|
if (!trimmedValue) return;
|
||||||
|
|
||||||
|
if (!this.allowDuplicates && this.value.includes(trimmedValue)) {
|
||||||
|
this.validationText = 'This item already exists in the list';
|
||||||
|
setTimeout(() => this.validationText = '', 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.maxItems && this.value.length >= this.maxItems) {
|
||||||
|
this.validationText = `Maximum ${this.maxItems} items allowed`;
|
||||||
|
setTimeout(() => this.validationText = '', 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = [...this.value, trimmedValue];
|
||||||
|
this.inputValue = '';
|
||||||
|
this.validationText = '';
|
||||||
|
|
||||||
|
// Clear the input
|
||||||
|
const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startEdit(index: number) {
|
||||||
|
this.editingIndex = index;
|
||||||
|
this.editingValue = this.value[index];
|
||||||
|
|
||||||
|
// Focus the input after render
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
const input = this.shadowRoot?.querySelector('.item-edit-input') as HTMLInputElement;
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveEdit(index: number) {
|
||||||
|
const trimmedValue = this.editingValue.trim();
|
||||||
|
|
||||||
|
if (!trimmedValue) {
|
||||||
|
this.cancelEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.allowDuplicates && trimmedValue !== this.value[index] && this.value.includes(trimmedValue)) {
|
||||||
|
this.validationText = 'This item already exists in the list';
|
||||||
|
setTimeout(() => this.validationText = '', 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = [...this.value];
|
||||||
|
newValue[index] = trimmedValue;
|
||||||
|
this.value = newValue;
|
||||||
|
|
||||||
|
this.editingIndex = -1;
|
||||||
|
this.editingValue = '';
|
||||||
|
this.validationText = '';
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelEdit() {
|
||||||
|
this.editingIndex = -1;
|
||||||
|
this.editingValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeItem(index: number) {
|
||||||
|
if (this.confirmDelete) {
|
||||||
|
const confirmed = await this.showConfirmDialog(`Delete "${this.value[index]}"?`);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = this.value.filter((_, i) => i !== index);
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showConfirmDialog(message: string): Promise<boolean> {
|
||||||
|
// For now, use native confirm. In production, this should use a proper modal
|
||||||
|
return confirm(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and drop handlers
|
||||||
|
private handleDragStart(e: DragEvent, index: number) {
|
||||||
|
if (!this.sortable || this.disabled) return;
|
||||||
|
|
||||||
|
this.draggedIndex = index;
|
||||||
|
e.dataTransfer!.effectAllowed = 'move';
|
||||||
|
e.dataTransfer!.setData('text/plain', index.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDragEnd() {
|
||||||
|
this.draggedIndex = -1;
|
||||||
|
this.dragOverIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDragOver(e: DragEvent, index: number) {
|
||||||
|
if (!this.sortable || this.disabled) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer!.dropEffect = 'move';
|
||||||
|
this.dragOverIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDragLeave() {
|
||||||
|
this.dragOverIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDrop(e: DragEvent, dropIndex: number) {
|
||||||
|
if (!this.sortable || this.disabled) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const draggedIndex = parseInt(e.dataTransfer!.getData('text/plain'));
|
||||||
|
|
||||||
|
if (draggedIndex !== dropIndex) {
|
||||||
|
const newValue = [...this.value];
|
||||||
|
const [draggedItem] = newValue.splice(draggedIndex, 1);
|
||||||
|
newValue.splice(dropIndex, 0, draggedItem);
|
||||||
|
this.value = newValue;
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.draggedIndex = -1;
|
||||||
|
this.dragOverIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitChange() {
|
||||||
|
this.dispatchEvent(new CustomEvent('change', {
|
||||||
|
detail: { value: this.value },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValue(): string[] {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setValue(value: string[]): void {
|
||||||
|
this.value = value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(): Promise<boolean> {
|
||||||
|
if (this.required && (!this.value || this.value.length === 0)) {
|
||||||
|
this.validationText = 'At least one item is required';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.minItems && this.value.length < this.minItems) {
|
||||||
|
this.validationText = `At least ${this.minItems} items required`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validationText = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -231,7 +231,7 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
${this.isPasswordBool
|
${this.isPasswordBool
|
||||||
? html`
|
? html`
|
||||||
<div class="showPassword" @click=${this.togglePasswordView}>
|
<div class="showPassword" @click=${this.togglePasswordView}>
|
||||||
<dees-icon .icon=${this.showPasswordBool ? 'lucide:eye' : 'lucide:eye-off'}></dees-icon>
|
<dees-icon .icon=${this.showPasswordBool ? 'lucide:Eye' : 'lucide:EyeOff'}></dees-icon>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
|
@@ -1,432 +0,0 @@
|
|||||||
import { type ITableAction } from './dees-table.js';
|
|
||||||
import * as plugins from './00plugins.js';
|
|
||||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
interface ITableDemoData {
|
|
||||||
date: string;
|
|
||||||
amount: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const demoFunc = () => html`
|
|
||||||
<style>
|
|
||||||
${css`
|
|
||||||
.demoWrapper {
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 32px;
|
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 5%)')};
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.demo-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.demo-section {
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
.demo-title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
|
||||||
}
|
|
||||||
.demo-description {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.theme-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<div class="demoWrapper">
|
|
||||||
<dees-button class="theme-toggle" @click=${() => {
|
|
||||||
document.body.classList.toggle('bright');
|
|
||||||
document.body.classList.toggle('dark');
|
|
||||||
}}>Toggle Theme</dees-button>
|
|
||||||
|
|
||||||
<div class="demo-container">
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Basic Table with Actions</h2>
|
|
||||||
<p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="Current Account Statement"
|
|
||||||
heading2="Bunq - Payment Account 2 - April 2021"
|
|
||||||
.editableFields="${['description']}"
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
date: '2021-04-01',
|
|
||||||
amount: '2464.65 €',
|
|
||||||
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2021-04-02',
|
|
||||||
amount: '165.65 €',
|
|
||||||
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2021-04-03',
|
|
||||||
amount: '2999,00 €',
|
|
||||||
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2021-04-01',
|
|
||||||
amount: '2464.65 €',
|
|
||||||
description: 'Office-Supplies - STAPLES BREMEN',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: '2021-04-01',
|
|
||||||
amount: '2464.65 €',
|
|
||||||
description: 'Office-Supplies - STAPLES BREMEN',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataName="transactions"
|
|
||||||
.dataActions="${[
|
|
||||||
{
|
|
||||||
name: 'upload',
|
|
||||||
iconName: 'bell',
|
|
||||||
useTableBehaviour: 'upload',
|
|
||||||
type: ['inRow'],
|
|
||||||
actionFunc: async (optionsArg) => {
|
|
||||||
alert(optionsArg.item.amount);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'visibility',
|
|
||||||
iconName: 'copy',
|
|
||||||
type: ['inRow'],
|
|
||||||
useTableBehaviour: 'preview',
|
|
||||||
actionFunc: async (itemArg: any) => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create new',
|
|
||||||
iconName: 'instagram',
|
|
||||||
type: ['header'],
|
|
||||||
useTableBehaviour: 'preview',
|
|
||||||
actionFunc: async (itemArg: any) => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'to gallery',
|
|
||||||
iconName: 'message',
|
|
||||||
type: ['footer'],
|
|
||||||
useTableBehaviour: 'preview',
|
|
||||||
actionFunc: async (itemArg: any) => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'copy',
|
|
||||||
iconName: 'copySolid',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
action: async () => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edit (from demo)',
|
|
||||||
iconName: 'penToSquare',
|
|
||||||
type: ['contextmenu'],
|
|
||||||
action: async () => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paste',
|
|
||||||
iconName: 'pasteSolid',
|
|
||||||
type: ['contextmenu'],
|
|
||||||
action: async () => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'preview',
|
|
||||||
type: ['doubleClick', 'contextmenu'],
|
|
||||||
iconName: 'eye',
|
|
||||||
actionFunc: async (itemArg) => {
|
|
||||||
alert(itemArg.item.amount);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
] as ITableAction[]}"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Table with Vertical Lines</h2>
|
|
||||||
<p class="demo-description">Enhanced column separation for better data tracking.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="Product Inventory"
|
|
||||||
heading2="Current stock levels across warehouses"
|
|
||||||
.showVerticalLines=${true}
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
product: 'MacBook Pro 16"',
|
|
||||||
warehouse_a: '45',
|
|
||||||
warehouse_b: '32',
|
|
||||||
warehouse_c: '28',
|
|
||||||
total: '105',
|
|
||||||
status: '✓ In Stock'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
product: 'iPhone 15 Pro',
|
|
||||||
warehouse_a: '120',
|
|
||||||
warehouse_b: '89',
|
|
||||||
warehouse_c: '156',
|
|
||||||
total: '365',
|
|
||||||
status: '✓ In Stock'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
product: 'AirPods Pro',
|
|
||||||
warehouse_a: '0',
|
|
||||||
warehouse_b: '12',
|
|
||||||
warehouse_c: '5',
|
|
||||||
total: '17',
|
|
||||||
status: '⚠ Low Stock'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
product: 'iPad Air',
|
|
||||||
warehouse_a: '23',
|
|
||||||
warehouse_b: '45',
|
|
||||||
warehouse_c: '67',
|
|
||||||
total: '135',
|
|
||||||
status: '✓ In Stock'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
dataName="products"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Table with Full Grid</h2>
|
|
||||||
<p class="demo-description">Complete grid lines for maximum readability and structure.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="Server Monitoring Dashboard"
|
|
||||||
heading2="Real-time metrics across regions"
|
|
||||||
.showGrid=${true}
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
server: 'API-1',
|
|
||||||
region: 'US-East',
|
|
||||||
cpu: '45%',
|
|
||||||
memory: '62%',
|
|
||||||
disk: '78%',
|
|
||||||
latency: '12ms',
|
|
||||||
uptime: '99.9%',
|
|
||||||
status: '🟢 Healthy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
server: 'API-2',
|
|
||||||
region: 'EU-West',
|
|
||||||
cpu: '38%',
|
|
||||||
memory: '55%',
|
|
||||||
disk: '45%',
|
|
||||||
latency: '25ms',
|
|
||||||
uptime: '99.8%',
|
|
||||||
status: '🟢 Healthy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
server: 'DB-Master',
|
|
||||||
region: 'US-East',
|
|
||||||
cpu: '72%',
|
|
||||||
memory: '81%',
|
|
||||||
disk: '92%',
|
|
||||||
latency: '8ms',
|
|
||||||
uptime: '100%',
|
|
||||||
status: '🟡 Warning'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
server: 'DB-Replica',
|
|
||||||
region: 'EU-West',
|
|
||||||
cpu: '23%',
|
|
||||||
memory: '34%',
|
|
||||||
disk: '45%',
|
|
||||||
latency: '15ms',
|
|
||||||
uptime: '99.7%',
|
|
||||||
status: '🟢 Healthy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
server: 'Cache-1',
|
|
||||||
region: 'AP-South',
|
|
||||||
cpu: '89%',
|
|
||||||
memory: '92%',
|
|
||||||
disk: '12%',
|
|
||||||
latency: '120ms',
|
|
||||||
uptime: '98.5%',
|
|
||||||
status: '🔴 Critical'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
dataName="servers"
|
|
||||||
.dataActions="${[
|
|
||||||
{
|
|
||||||
name: 'SSH Connect',
|
|
||||||
iconName: 'lucide:terminal',
|
|
||||||
type: ['inRow'],
|
|
||||||
actionFunc: async (optionsArg) => {
|
|
||||||
console.log('Connecting to:', optionsArg.item.server);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'View Logs',
|
|
||||||
iconName: 'lucide:file-text',
|
|
||||||
type: ['inRow', 'contextmenu'],
|
|
||||||
actionFunc: async (optionsArg) => {
|
|
||||||
console.log('Viewing logs for:', optionsArg.item.server);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Restart Server',
|
|
||||||
iconName: 'lucide:refresh-cw',
|
|
||||||
type: ['contextmenu'],
|
|
||||||
actionFunc: async (optionsArg) => {
|
|
||||||
console.log('Restarting:', optionsArg.item.server);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
] as ITableAction[]}"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Table with Horizontal Lines Only</h2>
|
|
||||||
<p class="demo-description">Emphasis on row separation without column dividers.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="Sales Performance"
|
|
||||||
heading2="Top performers this quarter"
|
|
||||||
.showHorizontalLines=${true}
|
|
||||||
.showVerticalLines=${false}
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
salesperson: 'Emily Johnson',
|
|
||||||
region: 'North America',
|
|
||||||
deals_closed: '42',
|
|
||||||
revenue: '$1.2M',
|
|
||||||
quota_achievement: '128%',
|
|
||||||
rating: '⭐⭐⭐⭐⭐'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
salesperson: 'Michael Chen',
|
|
||||||
region: 'Asia Pacific',
|
|
||||||
deals_closed: '38',
|
|
||||||
revenue: '$980K',
|
|
||||||
quota_achievement: '115%',
|
|
||||||
rating: '⭐⭐⭐⭐⭐'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
salesperson: 'Sarah Williams',
|
|
||||||
region: 'Europe',
|
|
||||||
deals_closed: '35',
|
|
||||||
revenue: '$875K',
|
|
||||||
quota_achievement: '108%',
|
|
||||||
rating: '⭐⭐⭐⭐'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
salesperson: 'David Garcia',
|
|
||||||
region: 'Latin America',
|
|
||||||
deals_closed: '31',
|
|
||||||
revenue: '$750K',
|
|
||||||
quota_achievement: '95%',
|
|
||||||
rating: '⭐⭐⭐⭐'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
dataName="sales reps"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Simple Table (No Grid)</h2>
|
|
||||||
<p class="demo-description">Clean, minimal design without grid lines. Set showGrid to false to disable the default grid.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="Team Members"
|
|
||||||
heading2="Engineering Department"
|
|
||||||
.showGrid=${false}
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
name: 'Alice Johnson',
|
|
||||||
role: 'Lead Engineer',
|
|
||||||
email: 'alice@company.com',
|
|
||||||
location: 'San Francisco',
|
|
||||||
joined: '2020-03-15'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Bob Smith',
|
|
||||||
role: 'Senior Developer',
|
|
||||||
email: 'bob@company.com',
|
|
||||||
location: 'New York',
|
|
||||||
joined: '2019-07-22'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Charlie Davis',
|
|
||||||
role: 'DevOps Engineer',
|
|
||||||
email: 'charlie@company.com',
|
|
||||||
location: 'London',
|
|
||||||
joined: '2021-01-10'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Diana Martinez',
|
|
||||||
role: 'Frontend Developer',
|
|
||||||
email: 'diana@company.com',
|
|
||||||
location: 'Barcelona',
|
|
||||||
joined: '2022-05-18'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
dataName="team members"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Table with Custom Display Function</h2>
|
|
||||||
<p class="demo-description">Transform data for display using custom formatting.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="Sales Report"
|
|
||||||
heading2="Q4 2023 Performance"
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
product: 'Enterprise License',
|
|
||||||
units: 45,
|
|
||||||
revenue: 225000,
|
|
||||||
growth: 0.23,
|
|
||||||
forecast: 280000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
product: 'Professional License',
|
|
||||||
units: 128,
|
|
||||||
revenue: 128000,
|
|
||||||
growth: 0.15,
|
|
||||||
forecast: 147000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
product: 'Starter License',
|
|
||||||
units: 342,
|
|
||||||
revenue: 68400,
|
|
||||||
growth: 0.42,
|
|
||||||
forecast: 97000
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
.displayFunction=${(item) => ({
|
|
||||||
Product: item.product,
|
|
||||||
'Units Sold': item.units.toLocaleString(),
|
|
||||||
Revenue: '$' + item.revenue.toLocaleString(),
|
|
||||||
Growth: (item.growth * 100).toFixed(1) + '%',
|
|
||||||
'Q1 2024 Forecast': '$' + item.forecast.toLocaleString()
|
|
||||||
})}
|
|
||||||
dataName="products"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="demo-section">
|
|
||||||
<h2 class="demo-title">Empty Table State</h2>
|
|
||||||
<p class="demo-description">How the table looks when no data is available.</p>
|
|
||||||
<dees-table
|
|
||||||
heading1="No Data Available"
|
|
||||||
heading2="This table is currently empty"
|
|
||||||
.data=${[]}
|
|
||||||
dataName="items"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@@ -1,920 +0,0 @@
|
|||||||
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 { DeesContextmenu } from './dees-contextmenu.js';
|
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
|
||||||
import { type TIconKey } from './dees-icon.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'dees-table': DeesTable<any>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// interfaces
|
|
||||||
export interface ITableAction<T = any> {
|
|
||||||
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<T>) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITableActionDataArg<T> {
|
|
||||||
item: T;
|
|
||||||
table: DeesTable<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TDisplayFunction<T = any> = (itemArg: T) => object;
|
|
||||||
|
|
||||||
// the table implementation
|
|
||||||
@customElement('dees-table')
|
|
||||||
export class DeesTable<T> extends DeesElement {
|
|
||||||
public static demo = demoFunc;
|
|
||||||
|
|
||||||
// INSTANCE
|
|
||||||
@property({
|
|
||||||
type: String,
|
|
||||||
})
|
|
||||||
public heading1: string = 'heading 1';
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: String,
|
|
||||||
})
|
|
||||||
public heading2: string = 'heading 2';
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Array,
|
|
||||||
})
|
|
||||||
public data: T[] = [];
|
|
||||||
|
|
||||||
// dees-form compatibility -----------------------------------------
|
|
||||||
@property({
|
|
||||||
type: String,
|
|
||||||
})
|
|
||||||
public key: string;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: String,
|
|
||||||
})
|
|
||||||
public label: string;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Boolean,
|
|
||||||
})
|
|
||||||
public disabled: boolean = false;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Boolean,
|
|
||||||
})
|
|
||||||
public required: boolean = false;
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
return this.data;
|
|
||||||
}
|
|
||||||
set value(_valueArg) {}
|
|
||||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
|
|
||||||
// end dees-form compatibility -----------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What does a row of data represent?
|
|
||||||
*/
|
|
||||||
@property({
|
|
||||||
type: String,
|
|
||||||
reflect: true,
|
|
||||||
})
|
|
||||||
public dataName: string;
|
|
||||||
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Boolean,
|
|
||||||
})
|
|
||||||
searchable: boolean = true;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Array,
|
|
||||||
})
|
|
||||||
public dataActions: ITableAction<T>[] = [];
|
|
||||||
|
|
||||||
@property({
|
|
||||||
attribute: false,
|
|
||||||
})
|
|
||||||
public displayFunction: TDisplayFunction = (itemArg: T) => itemArg as any;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
attribute: false,
|
|
||||||
})
|
|
||||||
public reverseDisplayFunction: (itemArg: any) => T = (itemArg: any) => itemArg as T;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Object,
|
|
||||||
})
|
|
||||||
public selectedDataRow: T;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Array,
|
|
||||||
})
|
|
||||||
public editableFields: string[] = [];
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
attribute: 'show-vertical-lines'
|
|
||||||
})
|
|
||||||
public showVerticalLines: boolean = false;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
attribute: 'show-horizontal-lines'
|
|
||||||
})
|
|
||||||
public showHorizontalLines: boolean = false;
|
|
||||||
|
|
||||||
@property({
|
|
||||||
type: Boolean,
|
|
||||||
reflect: true,
|
|
||||||
attribute: 'show-grid'
|
|
||||||
})
|
|
||||||
public showGrid: boolean = true;
|
|
||||||
|
|
||||||
public files: File[] = [];
|
|
||||||
public fileWeakMap = new WeakMap();
|
|
||||||
|
|
||||||
public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
|
||||||
|
|
||||||
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 render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="mainbox">
|
|
||||||
<!-- the heading part -->
|
|
||||||
<div class="header">
|
|
||||||
<div class="headingContainer">
|
|
||||||
<div class="heading heading1">${this.label || this.heading1}</div>
|
|
||||||
<div class="heading heading2">${this.heading2}</div>
|
|
||||||
</div>
|
|
||||||
<div class="headerActions">
|
|
||||||
${directives.resolveExec(async () => {
|
|
||||||
const resultArray: TemplateResult[] = [];
|
|
||||||
for (const action of this.dataActions) {
|
|
||||||
if (!action.type.includes('header')) continue;
|
|
||||||
resultArray.push(
|
|
||||||
html`<div
|
|
||||||
class="headerAction"
|
|
||||||
@click=${() => {
|
|
||||||
action.actionFunc({
|
|
||||||
item: this.selectedDataRow,
|
|
||||||
table: this,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${action.iconName
|
|
||||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
|
||||||
${action.name}`
|
|
||||||
: action.name}
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return resultArray;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="headingSeparation"></div>
|
|
||||||
<div class="searchGrid hidden">
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'lucene syntax search'}
|
|
||||||
.description=${`
|
|
||||||
You can use the lucene syntax to search for data, e.g.:
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
name: "john" AND age: 18
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
`}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-multitoggle
|
|
||||||
.label=${'search mode'}
|
|
||||||
.options=${['table', 'data', 'server']}
|
|
||||||
.selectedOption=${'table'}
|
|
||||||
.description=${`
|
|
||||||
There are three basic modes:
|
|
||||||
|
|
||||||
* table: only searches data already in the table
|
|
||||||
* data: searches original data, ignoring table transforms
|
|
||||||
* server: searches data on the server
|
|
||||||
|
|
||||||
`}
|
|
||||||
></dees-input-multitoggle>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- the actual table -->
|
|
||||||
<style></style>
|
|
||||||
${this.data.length > 0
|
|
||||||
? (() => {
|
|
||||||
// Only pick up the keys from the first transformed data object
|
|
||||||
// as all data objects are assumed to have the same structure
|
|
||||||
const firstTransformedItem = this.displayFunction(this.data[0]);
|
|
||||||
const headings: string[] = Object.keys(firstTransformedItem);
|
|
||||||
return html`
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
${headings.map(
|
|
||||||
(headingArg) => html`
|
|
||||||
<th>${headingArg}</th>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${(() => {
|
|
||||||
if (this.dataActions && this.dataActions.length > 0) {
|
|
||||||
return html`
|
|
||||||
<th>Actions</th>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${this.data.map((itemArg) => {
|
|
||||||
const transformedItem = this.displayFunction(itemArg);
|
|
||||||
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
|
||||||
if (elementArg.tagName === 'TR') {
|
|
||||||
return elementArg;
|
|
||||||
} else {
|
|
||||||
return getTr(elementArg.parentElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return html`
|
|
||||||
<tr
|
|
||||||
@click=${() => {
|
|
||||||
this.selectedDataRow = itemArg;
|
|
||||||
}}
|
|
||||||
@dragenter=${async (eventArg: DragEvent) => {
|
|
||||||
eventArg.preventDefault();
|
|
||||||
eventArg.stopPropagation();
|
|
||||||
const realTarget = getTr(eventArg.target as HTMLElement);
|
|
||||||
console.log('dragenter');
|
|
||||||
console.log(realTarget);
|
|
||||||
setTimeout(() => {
|
|
||||||
realTarget.classList.add('hasAttachment');
|
|
||||||
}, 0);
|
|
||||||
}}
|
|
||||||
@dragleave=${async (eventArg: DragEvent) => {
|
|
||||||
eventArg.preventDefault();
|
|
||||||
eventArg.stopPropagation();
|
|
||||||
const realTarget = getTr(eventArg.target as HTMLElement);
|
|
||||||
realTarget.classList.remove('hasAttachment');
|
|
||||||
}}
|
|
||||||
@dragover=${async (eventArg: DragEvent) => {
|
|
||||||
eventArg.preventDefault();
|
|
||||||
}}
|
|
||||||
@drop=${async (eventArg: DragEvent) => {
|
|
||||||
eventArg.preventDefault();
|
|
||||||
const newFiles = [];
|
|
||||||
for (const file of Array.from(eventArg.dataTransfer.files)) {
|
|
||||||
this.files.push(file);
|
|
||||||
newFiles.push(file);
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
const result: File[] = this.fileWeakMap.get(itemArg as object);
|
|
||||||
if (!result) {
|
|
||||||
this.fileWeakMap.set(itemArg as object, newFiles);
|
|
||||||
} else {
|
|
||||||
result.push(...newFiles);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
@contextmenu=${async (eventArg: MouseEvent) => {
|
|
||||||
DeesContextmenu.openContextMenuWithOptions(
|
|
||||||
eventArg,
|
|
||||||
this.getActionsForType('contextmenu').map((action) => {
|
|
||||||
const menuItem: plugins.tsclass.website.IMenuItem = {
|
|
||||||
name: action.name,
|
|
||||||
iconName: action.iconName as any,
|
|
||||||
action: async () => {
|
|
||||||
await action.actionFunc({
|
|
||||||
item: itemArg,
|
|
||||||
table: this,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return menuItem;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
|
||||||
>
|
|
||||||
${headings.map(
|
|
||||||
(headingArg) => html`
|
|
||||||
<td
|
|
||||||
@dblclick=${(e: Event) => {
|
|
||||||
if (this.editableFields.includes(headingArg)) {
|
|
||||||
this.handleCellEditing(e, itemArg, headingArg);
|
|
||||||
} else {
|
|
||||||
const wantedAction = this.dataActions.find((actionArg) =>
|
|
||||||
actionArg.type.includes('doubleClick')
|
|
||||||
);
|
|
||||||
if (wantedAction) {
|
|
||||||
wantedAction.actionFunc({
|
|
||||||
item: itemArg,
|
|
||||||
table: this,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="innerCellContainer">${transformedItem[headingArg]}</div>
|
|
||||||
</td>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${(() => {
|
|
||||||
if (this.dataActions && this.dataActions.length > 0) {
|
|
||||||
return html`
|
|
||||||
<td>
|
|
||||||
<div class="actionsContainer">
|
|
||||||
${this.getActionsForType('inRow').map(
|
|
||||||
(actionArg) => html`
|
|
||||||
<div
|
|
||||||
class="action"
|
|
||||||
@click=${() =>
|
|
||||||
actionArg.actionFunc({
|
|
||||||
item: itemArg,
|
|
||||||
table: this,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${actionArg.iconName
|
|
||||||
? html`
|
|
||||||
<dees-icon
|
|
||||||
.icon=${actionArg.iconName}
|
|
||||||
></dees-icon>
|
|
||||||
`
|
|
||||||
: actionArg.name}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
})()
|
|
||||||
: html` <div class="noDataSet">No data set!</div> `}
|
|
||||||
<div class="footer">
|
|
||||||
<div class="tableStatistics">
|
|
||||||
${this.data.length} ${this.dataName || 'data rows'} (total) |
|
|
||||||
${this.selectedDataRow ? '# ' + `${this.data.indexOf(this.selectedDataRow) + 1}` : `No`}
|
|
||||||
selected
|
|
||||||
</div>
|
|
||||||
<div class="footerActions">
|
|
||||||
${directives.resolveExec(async () => {
|
|
||||||
const resultArray: TemplateResult[] = [];
|
|
||||||
for (const action of this.dataActions) {
|
|
||||||
if (!action.type.includes('footer')) continue;
|
|
||||||
resultArray.push(
|
|
||||||
html`<div
|
|
||||||
class="footerAction"
|
|
||||||
@click=${() => {
|
|
||||||
action.actionFunc({
|
|
||||||
item: this.selectedDataRow,
|
|
||||||
table: this,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${action.iconName
|
|
||||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
|
||||||
${action.name}`
|
|
||||||
: action.name}
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return resultArray;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async firstUpdated() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
this.determineColumnWidths();
|
|
||||||
if (this.searchable) {
|
|
||||||
const existing = this.dataActions.find((actionArg) => actionArg.type.includes('header') && actionArg.name === 'Search');
|
|
||||||
if (!existing) {
|
|
||||||
this.dataActions.unshift({
|
|
||||||
name: 'Search',
|
|
||||||
iconName: 'magnifyingGlass',
|
|
||||||
type: ['header'],
|
|
||||||
actionFunc: async () => {
|
|
||||||
console.log('open search');
|
|
||||||
const searchGrid = this.shadowRoot.querySelector('.searchGrid');
|
|
||||||
searchGrid.classList.toggle('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(this.dataActions);
|
|
||||||
this.requestUpdate();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async determineColumnWidths() {
|
|
||||||
const domtools = await this.domtoolsPromise;
|
|
||||||
await domtools.convenience.smartdelay.delayFor(0);
|
|
||||||
// Get the table element
|
|
||||||
const table = this.shadowRoot.querySelector('table');
|
|
||||||
if (!table) return;
|
|
||||||
|
|
||||||
// Get the first row's cells to measure the widths
|
|
||||||
const cells = table.rows[0].cells;
|
|
||||||
|
|
||||||
const handleColumnByIndex = async (i: number, waitForRenderArg: boolean = false) => {
|
|
||||||
const done = plugins.smartpromise.defer();
|
|
||||||
const cell = cells[i];
|
|
||||||
|
|
||||||
// Get computed width
|
|
||||||
const width = window.getComputedStyle(cell).width;
|
|
||||||
if (cell.textContent.includes('Actions')) {
|
|
||||||
const neededWidth =
|
|
||||||
this.dataActions.filter((actionArg) => actionArg.type.includes('inRow')).length * 36;
|
|
||||||
cell.style.width = `${Math.max(neededWidth, 68)}px`;
|
|
||||||
} else {
|
|
||||||
cell.style.width = width;
|
|
||||||
}
|
|
||||||
if (waitForRenderArg) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
done.resolve();
|
|
||||||
});
|
|
||||||
await done.promise;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cells[cells.length - 1].textContent.includes('Actions')) {
|
|
||||||
await handleColumnByIndex(cells.length - 1, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < cells.length; i++) {
|
|
||||||
if (cells[i].textContent.includes('Actions')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await handleColumnByIndex(i);
|
|
||||||
}
|
|
||||||
table.style.tableLayout = 'fixed';
|
|
||||||
}
|
|
||||||
|
|
||||||
getActionsForType(typeArg: ITableAction['type'][0]) {
|
|
||||||
const actions: ITableAction[] = [];
|
|
||||||
for (const action of this.dataActions) {
|
|
||||||
if (!action.type.includes(typeArg)) continue;
|
|
||||||
actions.push(action);
|
|
||||||
}
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleCellEditing(event: Event, itemArg: T, key: string) {
|
|
||||||
await this.domtoolsPromise;
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
const originalColor = target.style.color;
|
|
||||||
target.style.color = 'transparent';
|
|
||||||
const transformedItem = this.displayFunction(itemArg);
|
|
||||||
const initialValue = (transformedItem[key] as unknown as string) || '';
|
|
||||||
// Create an input element
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'text';
|
|
||||||
input.value = initialValue;
|
|
||||||
|
|
||||||
const blurInput = async (blurArg = true, saveArg = false) => {
|
|
||||||
if (blurArg) {
|
|
||||||
input.blur();
|
|
||||||
}
|
|
||||||
if (saveArg) {
|
|
||||||
itemArg[key] = input.value as any; // Convert string to T (you might need better type casting depending on your data structure)
|
|
||||||
this.changeSubject.next(this);
|
|
||||||
}
|
|
||||||
input.remove();
|
|
||||||
target.style.color = originalColor;
|
|
||||||
this.requestUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the input loses focus or the Enter key is pressed, update the data
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
blurInput(false, false);
|
|
||||||
});
|
|
||||||
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
blurInput(true, true); // This will trigger the blur event handler above
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace the cell's content with the input
|
|
||||||
target.appendChild(input);
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
}
|
|
115
ts_web/elements/dees-table/data.ts
Normal file
115
ts_web/elements/dees-table/data.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import type { Column, TDisplayFunction } from './types.js';
|
||||||
|
|
||||||
|
export function computeColumnsFromDisplayFunction<T>(
|
||||||
|
displayFunction: TDisplayFunction<T>,
|
||||||
|
data: T[]
|
||||||
|
): Column<T>[] {
|
||||||
|
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<T>(
|
||||||
|
columns: Column<T>[] | undefined,
|
||||||
|
augmentFromDisplayFunction: boolean,
|
||||||
|
displayFunction: TDisplayFunction<T>,
|
||||||
|
data: T[]
|
||||||
|
): Column<T>[] {
|
||||||
|
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<T>(row: T, col: Column<T>, displayFunction?: TDisplayFunction<T>): any {
|
||||||
|
return col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getViewData<T>(
|
||||||
|
data: T[],
|
||||||
|
effectiveColumns: Column<T>[],
|
||||||
|
sortKey?: string,
|
||||||
|
sortDir?: 'asc' | 'desc' | null,
|
||||||
|
filterText?: string,
|
||||||
|
columnFilters?: Record<string, string>,
|
||||||
|
filterMode: 'table' | 'data' = 'table',
|
||||||
|
lucenePredicate?: (row: T) => boolean
|
||||||
|
): T[] {
|
||||||
|
let arr = data.slice();
|
||||||
|
const ft = (filterText || '').trim().toLowerCase();
|
||||||
|
const cf = columnFilters || {};
|
||||||
|
const cfKeys = Object.keys(cf).filter((k) => (cf[k] ?? '').trim().length > 0);
|
||||||
|
if (ft || cfKeys.length > 0) {
|
||||||
|
arr = arr.filter((row) => {
|
||||||
|
// column filters (AND across columns)
|
||||||
|
for (const k of cfKeys) {
|
||||||
|
if (filterMode === 'data') {
|
||||||
|
// raw object check for that key
|
||||||
|
const val = (row as any)[k];
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
const needle = String(cf[k]).toLowerCase();
|
||||||
|
if (!s.includes(needle)) return false;
|
||||||
|
} else {
|
||||||
|
const col = effectiveColumns.find((c) => String(c.key) === k);
|
||||||
|
if (!col || col.hidden || col.filterable === false) continue;
|
||||||
|
const val = getCellValue(row, col);
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
const needle = String(cf[k]).toLowerCase();
|
||||||
|
if (!s.includes(needle)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// global filter (OR across visible columns) or lucene predicate
|
||||||
|
if (ft) {
|
||||||
|
if (lucenePredicate) {
|
||||||
|
if (!lucenePredicate(row)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let any = false;
|
||||||
|
if (filterMode === 'data') {
|
||||||
|
for (const val of Object.values(row as any)) {
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
if (s.includes(ft)) { any = true; break; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const col of effectiveColumns) {
|
||||||
|
if (col.hidden) continue;
|
||||||
|
const val = getCellValue(row, col);
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
if (s.includes(ft)) { any = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
678
ts_web/elements/dees-table/dees-table.demo.ts
Normal file
678
ts_web/elements/dees-table/dees-table.demo.ts
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
import { type ITableAction } from './dees-table.js';
|
||||||
|
import * as plugins from '../00plugins.js';
|
||||||
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
interface ITableDemoData {
|
||||||
|
date: string;
|
||||||
|
amount: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const demoFunc = () => html`
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.demoWrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 32px;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 5%)')};
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.demo-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.demo-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.demo-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||||
|
}
|
||||||
|
.demo-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<div class="demoWrapper">
|
||||||
|
<dees-button class="theme-toggle" @click=${() => {
|
||||||
|
document.body.classList.toggle('bright');
|
||||||
|
document.body.classList.toggle('dark');
|
||||||
|
}}>Toggle Theme</dees-button>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Basic Table with Actions</h2>
|
||||||
|
<p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Current Account Statement"
|
||||||
|
heading2="Bunq - Payment Account 2 - April 2021"
|
||||||
|
.editableFields="${['description']}"
|
||||||
|
.data=${[
|
||||||
|
{
|
||||||
|
date: '2021-04-01',
|
||||||
|
amount: '2464.65 €',
|
||||||
|
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-04-02',
|
||||||
|
amount: '165.65 €',
|
||||||
|
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-04-03',
|
||||||
|
amount: '2999,00 €',
|
||||||
|
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-04-01',
|
||||||
|
amount: '2464.65 €',
|
||||||
|
description: 'Office-Supplies - STAPLES BREMEN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2021-04-01',
|
||||||
|
amount: '2464.65 €',
|
||||||
|
description: 'Office-Supplies - STAPLES BREMEN',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
dataName="transactions"
|
||||||
|
.dataActions="${[
|
||||||
|
{
|
||||||
|
name: 'upload',
|
||||||
|
iconName: 'bell',
|
||||||
|
useTableBehaviour: 'upload',
|
||||||
|
type: ['inRow'],
|
||||||
|
actionFunc: async (optionsArg) => {
|
||||||
|
alert(optionsArg.item.amount);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'visibility',
|
||||||
|
iconName: 'copy',
|
||||||
|
type: ['inRow'],
|
||||||
|
useTableBehaviour: 'preview',
|
||||||
|
actionFunc: async (itemArg: any) => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create new',
|
||||||
|
iconName: 'instagram',
|
||||||
|
type: ['header'],
|
||||||
|
useTableBehaviour: 'preview',
|
||||||
|
actionFunc: async (itemArg: any) => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'to gallery',
|
||||||
|
iconName: 'message',
|
||||||
|
type: ['footer'],
|
||||||
|
useTableBehaviour: 'preview',
|
||||||
|
actionFunc: async (itemArg: any) => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'copy',
|
||||||
|
iconName: 'copySolid',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
action: async () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit (from demo)',
|
||||||
|
iconName: 'penToSquare',
|
||||||
|
type: ['contextmenu'],
|
||||||
|
action: async () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'paste',
|
||||||
|
iconName: 'pasteSolid',
|
||||||
|
type: ['contextmenu'],
|
||||||
|
action: async () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preview',
|
||||||
|
type: ['doubleClick', 'contextmenu'],
|
||||||
|
iconName: 'eye',
|
||||||
|
actionFunc: async (itemArg) => {
|
||||||
|
alert(itemArg.item.amount);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
] as ITableAction[]}"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Table with Vertical Lines</h2>
|
||||||
|
<p class="demo-description">Enhanced column separation for better data tracking.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Product Inventory"
|
||||||
|
heading2="Current stock levels across warehouses"
|
||||||
|
.showVerticalLines=${true}
|
||||||
|
.data=${[
|
||||||
|
{
|
||||||
|
product: 'MacBook Pro 16"',
|
||||||
|
warehouse_a: '45',
|
||||||
|
warehouse_b: '32',
|
||||||
|
warehouse_c: '28',
|
||||||
|
total: '105',
|
||||||
|
status: '✓ In Stock'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'iPhone 15 Pro',
|
||||||
|
warehouse_a: '120',
|
||||||
|
warehouse_b: '89',
|
||||||
|
warehouse_c: '156',
|
||||||
|
total: '365',
|
||||||
|
status: '✓ In Stock'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'AirPods Pro',
|
||||||
|
warehouse_a: '0',
|
||||||
|
warehouse_b: '12',
|
||||||
|
warehouse_c: '5',
|
||||||
|
total: '17',
|
||||||
|
status: '⚠ Low Stock'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'iPad Air',
|
||||||
|
warehouse_a: '23',
|
||||||
|
warehouse_b: '45',
|
||||||
|
warehouse_c: '67',
|
||||||
|
total: '135',
|
||||||
|
status: '✓ In Stock'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
dataName="products"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Table with Full Grid</h2>
|
||||||
|
<p class="demo-description">Complete grid lines for maximum readability and structure.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Server Monitoring Dashboard"
|
||||||
|
heading2="Real-time metrics across regions"
|
||||||
|
.showGrid=${true}
|
||||||
|
.data=${[
|
||||||
|
{
|
||||||
|
server: 'API-1',
|
||||||
|
region: 'US-East',
|
||||||
|
cpu: '45%',
|
||||||
|
memory: '62%',
|
||||||
|
disk: '78%',
|
||||||
|
latency: '12ms',
|
||||||
|
uptime: '99.9%',
|
||||||
|
status: '🟢 Healthy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: 'API-2',
|
||||||
|
region: 'EU-West',
|
||||||
|
cpu: '38%',
|
||||||
|
memory: '55%',
|
||||||
|
disk: '45%',
|
||||||
|
latency: '25ms',
|
||||||
|
uptime: '99.8%',
|
||||||
|
status: '🟢 Healthy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: 'DB-Master',
|
||||||
|
region: 'US-East',
|
||||||
|
cpu: '72%',
|
||||||
|
memory: '81%',
|
||||||
|
disk: '92%',
|
||||||
|
latency: '8ms',
|
||||||
|
uptime: '100%',
|
||||||
|
status: '🟡 Warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: 'DB-Replica',
|
||||||
|
region: 'EU-West',
|
||||||
|
cpu: '23%',
|
||||||
|
memory: '34%',
|
||||||
|
disk: '45%',
|
||||||
|
latency: '15ms',
|
||||||
|
uptime: '99.7%',
|
||||||
|
status: '🟢 Healthy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: 'Cache-1',
|
||||||
|
region: 'AP-South',
|
||||||
|
cpu: '89%',
|
||||||
|
memory: '92%',
|
||||||
|
disk: '12%',
|
||||||
|
latency: '120ms',
|
||||||
|
uptime: '98.5%',
|
||||||
|
status: '🔴 Critical'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
dataName="servers"
|
||||||
|
.dataActions="${[
|
||||||
|
{
|
||||||
|
name: 'SSH Connect',
|
||||||
|
iconName: 'lucide:terminal',
|
||||||
|
type: ['inRow'],
|
||||||
|
actionFunc: async (optionsArg) => {
|
||||||
|
console.log('Connecting to:', optionsArg.item.server);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View Logs',
|
||||||
|
iconName: 'lucide:file-text',
|
||||||
|
type: ['inRow', 'contextmenu'],
|
||||||
|
actionFunc: async (optionsArg) => {
|
||||||
|
console.log('Viewing logs for:', optionsArg.item.server);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Restart Server',
|
||||||
|
iconName: 'lucide:refresh-cw',
|
||||||
|
type: ['contextmenu'],
|
||||||
|
actionFunc: async (optionsArg) => {
|
||||||
|
console.log('Restarting:', optionsArg.item.server);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
] as ITableAction[]}"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Table with Horizontal Lines Only</h2>
|
||||||
|
<p class="demo-description">Emphasis on row separation without column dividers.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Sales Performance"
|
||||||
|
heading2="Top performers this quarter"
|
||||||
|
.showHorizontalLines=${true}
|
||||||
|
.showVerticalLines=${false}
|
||||||
|
.data=${[
|
||||||
|
{
|
||||||
|
salesperson: 'Emily Johnson',
|
||||||
|
region: 'North America',
|
||||||
|
deals_closed: '42',
|
||||||
|
revenue: '$1.2M',
|
||||||
|
quota_achievement: '128%',
|
||||||
|
rating: '⭐⭐⭐⭐⭐'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
salesperson: 'Michael Chen',
|
||||||
|
region: 'Asia Pacific',
|
||||||
|
deals_closed: '38',
|
||||||
|
revenue: '$980K',
|
||||||
|
quota_achievement: '115%',
|
||||||
|
rating: '⭐⭐⭐⭐⭐'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
salesperson: 'Sarah Williams',
|
||||||
|
region: 'Europe',
|
||||||
|
deals_closed: '35',
|
||||||
|
revenue: '$875K',
|
||||||
|
quota_achievement: '108%',
|
||||||
|
rating: '⭐⭐⭐⭐'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
salesperson: 'David Garcia',
|
||||||
|
region: 'Latin America',
|
||||||
|
deals_closed: '31',
|
||||||
|
revenue: '$750K',
|
||||||
|
quota_achievement: '95%',
|
||||||
|
rating: '⭐⭐⭐⭐'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
dataName="sales reps"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Simple Table (No Grid)</h2>
|
||||||
|
<p class="demo-description">Clean, minimal design without grid lines. Set showGrid to false to disable the default grid.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Team Members"
|
||||||
|
heading2="Engineering Department"
|
||||||
|
.showGrid=${false}
|
||||||
|
.data=${[
|
||||||
|
{
|
||||||
|
name: 'Alice Johnson',
|
||||||
|
role: 'Lead Engineer',
|
||||||
|
email: 'alice@company.com',
|
||||||
|
location: 'San Francisco',
|
||||||
|
joined: '2020-03-15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Bob Smith',
|
||||||
|
role: 'Senior Developer',
|
||||||
|
email: 'bob@company.com',
|
||||||
|
location: 'New York',
|
||||||
|
joined: '2019-07-22'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Charlie Davis',
|
||||||
|
role: 'DevOps Engineer',
|
||||||
|
email: 'charlie@company.com',
|
||||||
|
location: 'London',
|
||||||
|
joined: '2021-01-10'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Diana Martinez',
|
||||||
|
role: 'Frontend Developer',
|
||||||
|
email: 'diana@company.com',
|
||||||
|
location: 'Barcelona',
|
||||||
|
joined: '2022-05-18'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
dataName="team members"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Table with Custom Display Function</h2>
|
||||||
|
<p class="demo-description">Transform data for display using custom formatting.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="Sales Report"
|
||||||
|
heading2="Q4 2023 Performance"
|
||||||
|
.data=${[
|
||||||
|
{
|
||||||
|
product: 'Enterprise License',
|
||||||
|
units: 45,
|
||||||
|
revenue: 225000,
|
||||||
|
growth: 0.23,
|
||||||
|
forecast: 280000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'Professional License',
|
||||||
|
units: 128,
|
||||||
|
revenue: 128000,
|
||||||
|
growth: 0.15,
|
||||||
|
forecast: 147000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: 'Starter License',
|
||||||
|
units: 342,
|
||||||
|
revenue: 68400,
|
||||||
|
growth: 0.42,
|
||||||
|
forecast: 97000
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
.displayFunction=${(item) => ({
|
||||||
|
Product: item.product,
|
||||||
|
'Units Sold': item.units.toLocaleString(),
|
||||||
|
Revenue: '$' + item.revenue.toLocaleString(),
|
||||||
|
Growth: (item.growth * 100).toFixed(1) + '%',
|
||||||
|
'Q1 2024 Forecast': '$' + item.forecast.toLocaleString()
|
||||||
|
})}
|
||||||
|
dataName="products"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Empty Table State</h2>
|
||||||
|
<p class="demo-description">How the table looks when no data is available.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="No Data Available"
|
||||||
|
heading2="This table is currently empty"
|
||||||
|
.data=${[]}
|
||||||
|
dataName="items"
|
||||||
|
></dees-table>
|
||||||
|
</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 class="demo-section"
|
||||||
|
@selectionChange=${(e: CustomEvent) => { 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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 class="demo-title">Filtering + Multi-Selection (New)</h2>
|
||||||
|
<p class="demo-description">Use the search bar to filter rows; toggle selection via checkboxes. Click headers to sort.</p>
|
||||||
|
<dees-searchbar></dees-searchbar>
|
||||||
|
<div style="height: 12px"></div>
|
||||||
|
<dees-table
|
||||||
|
id="tableFilterSelectDemo"
|
||||||
|
heading1="Inventory (Filter + Select)"
|
||||||
|
heading2="Try typing to filter and selecting multiple rows"
|
||||||
|
.selectionMode=${'multi'}
|
||||||
|
.rowKey=${'sku'}
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'sku', header: 'SKU', sortable: true },
|
||||||
|
{ key: 'name', header: 'Name', sortable: true },
|
||||||
|
{ key: 'stock', header: 'Stock', sortable: true },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ sku: 'A-100', name: 'USB-C Cable', stock: 120 },
|
||||||
|
{ sku: 'A-101', name: 'Wireless Mouse', stock: 55 },
|
||||||
|
{ sku: 'A-102', name: 'Laptop Stand', stock: 18 },
|
||||||
|
{ sku: 'B-200', name: 'Keyboard (ISO)', stock: 89 },
|
||||||
|
{ sku: 'B-201', name: 'HDMI Adapter', stock: 0 },
|
||||||
|
{ sku: 'C-300', name: 'Webcam 1080p', stock: 42 },
|
||||||
|
]}
|
||||||
|
dataName="items"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Column Filters + Sticky Header (New)</h2>
|
||||||
|
<p class="demo-description">Per-column quick filters and sticky header with internal scroll. Try filtering the Name column. Uses --table-max-height var.</p>
|
||||||
|
<style>
|
||||||
|
dees-table[sticky-header] { --table-max-height: 220px; }
|
||||||
|
</style>
|
||||||
|
<dees-table
|
||||||
|
heading1="Employees"
|
||||||
|
heading2="Quick filter per column + sticky header"
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
.stickyHeader=${true}
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'name', header: 'Name', sortable: true },
|
||||||
|
{ key: 'email', header: 'Email', sortable: true },
|
||||||
|
{ key: 'department', header: 'Department', sortable: true },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ name: 'Alice Johnson', email: 'alice@corp.com', department: 'Engineering' },
|
||||||
|
{ name: 'Bob Smith', email: 'bob@corp.com', department: 'Sales' },
|
||||||
|
{ name: 'Charlie Davis', email: 'charlie@corp.com', department: 'HR' },
|
||||||
|
{ name: 'Diana Martinez', email: 'diana@corp.com', department: 'Engineering' },
|
||||||
|
{ name: 'Ethan Brown', email: 'ethan@corp.com', department: 'Finance' },
|
||||||
|
{ name: 'Fiona Clark', email: 'fiona@corp.com', department: 'Sales' },
|
||||||
|
{ name: 'Grace Lee', email: 'grace@corp.com', department: 'Engineering' },
|
||||||
|
{ name: 'Henry Wilson', email: 'henry@corp.com', department: 'Marketing' },
|
||||||
|
{ name: 'Irene Walker', email: 'irene@corp.com', department: 'Finance' },
|
||||||
|
{ name: 'Jack Turner', email: 'jack@corp.com', department: 'Support' },
|
||||||
|
]}
|
||||||
|
dataName="employees"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section"
|
||||||
|
@searchRequest=${async (e: CustomEvent) => {
|
||||||
|
const { query } = e.detail || { query: '' };
|
||||||
|
const table = document.getElementById('serverSearchDemo') as any;
|
||||||
|
const baseData = [
|
||||||
|
{ id: 1, name: 'Alice', city: 'Berlin', title: 'Engineer' },
|
||||||
|
{ id: 2, name: 'Bob', city: 'Paris', title: 'Designer' },
|
||||||
|
{ id: 3, name: 'Charlie', city: 'London', title: 'Manager' },
|
||||||
|
{ id: 4, name: 'Diana', city: 'Madrid', title: 'Engineer' },
|
||||||
|
{ id: 5, name: 'Ethan', city: 'Rome', title: 'Support' },
|
||||||
|
];
|
||||||
|
// Simulate async request
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
const q = String(query || '').toLowerCase();
|
||||||
|
const filtered = q
|
||||||
|
? baseData.filter((r) => Object.values(r).some((v) => String(v).toLowerCase().includes(q)))
|
||||||
|
: baseData;
|
||||||
|
table.data = filtered;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 class="demo-title">Server Search (New)</h2>
|
||||||
|
<p class="demo-description">Select Server mode, type a query, and watch the table fetch simulated results.</p>
|
||||||
|
<dees-table
|
||||||
|
id="serverSearchDemo"
|
||||||
|
heading1="People (Server Search)"
|
||||||
|
heading2="Click Search, choose Server mode, and type"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'name', header: 'Name' },
|
||||||
|
{ key: 'city', header: 'City' },
|
||||||
|
{ key: 'title', header: 'Title' },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ id: 1, name: 'Alice', city: 'Berlin', title: 'Engineer' },
|
||||||
|
{ id: 2, name: 'Bob', city: 'Paris', title: 'Designer' },
|
||||||
|
{ id: 3, name: 'Charlie', city: 'London', title: 'Manager' },
|
||||||
|
{ id: 4, name: 'Diana', city: 'Madrid', title: 'Engineer' },
|
||||||
|
{ id: 5, name: 'Ethan', city: 'Rome', title: 'Support' },
|
||||||
|
]}
|
||||||
|
dataName="people"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Wide Properties + Many Actions</h2>
|
||||||
|
<p class="demo-description">A table with many columns and rich actions to stress test layout and sticky Actions.</p>
|
||||||
|
<dees-table
|
||||||
|
heading1="People Directory"
|
||||||
|
heading2="Many properties and actions"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'id', header: 'ID', sortable: true },
|
||||||
|
{ key: 'name', header: 'Name', sortable: true },
|
||||||
|
{ key: 'role', header: 'Role', sortable: true },
|
||||||
|
{ key: 'department', header: 'Department', sortable: true },
|
||||||
|
{ key: 'email', header: 'Email' },
|
||||||
|
{ key: 'phone', header: 'Phone' },
|
||||||
|
{ key: 'location', header: 'Location', sortable: true },
|
||||||
|
{ key: 'status', header: 'Status', sortable: true },
|
||||||
|
{ key: 'createdAt', header: 'Created', sortable: true },
|
||||||
|
{ key: 'updatedAt', header: 'Updated', sortable: true },
|
||||||
|
{ key: 'lastLogin', header: 'Last Login', sortable: true },
|
||||||
|
{ key: 'projects', header: 'Projects' },
|
||||||
|
{ key: 'tags', header: 'Tags' },
|
||||||
|
{ key: 'notes', header: 'Notes' },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ id: 1, name: 'Alice Johnson', role: 'Engineer', department: 'R&D', email: 'alice@corp.com', phone: '+1 202 555 0111', location: 'Berlin', status: 'Active', createdAt: '2023-01-12', updatedAt: '2024-05-03', lastLogin: '2024-10-01', projects: 5, tags: 'typescript, ui', notes: 'Mentor' },
|
||||||
|
{ id: 2, name: 'Bob Smith', role: 'Designer', department: 'Design', email: 'bob@corp.com', phone: '+1 202 555 0112', location: 'Paris', status: 'Active', createdAt: '2022-11-05', updatedAt: '2024-04-10', lastLogin: '2024-09-28', projects: 8, tags: 'figma, brand', notes: 'Part-time' },
|
||||||
|
{ id: 3, name: 'Charlie Davis', role: 'Manager', department: 'Ops', email: 'charlie@corp.com', phone: '+1 202 555 0113', location: 'London', status: 'On Leave', createdAt: '2021-04-21', updatedAt: '2024-02-15', lastLogin: '2024-08-12', projects: 3, tags: 'sre, leadership', notes: '' },
|
||||||
|
{ id: 4, name: 'Diana Martinez', role: 'Engineer', department: 'Platform', email: 'diana@corp.com', phone: '+1 202 555 0114', location: 'Madrid', status: 'Active', createdAt: '2020-06-30', updatedAt: '2024-06-25', lastLogin: '2024-10-02', projects: 6, tags: 'node, api', notes: 'On-call' },
|
||||||
|
{ id: 5, name: 'Ethan Brown', role: 'Support', department: 'CS', email: 'ethan@corp.com', phone: '+1 202 555 0115', location: 'Rome', status: 'Inactive', createdAt: '2019-09-18', updatedAt: '2024-03-09', lastLogin: '2024-06-19', projects: 2, tags: 'zendesk', notes: 'Rehire' },
|
||||||
|
{ id: 6, name: 'Fiona Clark', role: 'QA', department: 'QA', email: 'fiona@corp.com', phone: '+1 202 555 0116', location: 'Vienna', status: 'Active', createdAt: '2022-03-14', updatedAt: '2024-03-01', lastLogin: '2024-09-07', projects: 7, tags: 'playwright', notes: '' },
|
||||||
|
]}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'View', iconName: 'lucide:eye', type: ['inRow', 'contextmenu'], actionFunc: async ({ item }) => { console.log('view', item); } },
|
||||||
|
{ name: 'Edit', iconName: 'lucide:edit', type: ['inRow', 'contextmenu'], actionFunc: async ({ item }) => { console.log('edit', item); } },
|
||||||
|
{ name: 'Delete', iconName: 'lucide:trash', type: ['inRow', 'contextmenu'], actionFunc: async ({ item }) => { console.log('delete', item); } },
|
||||||
|
{ name: 'Message', iconName: 'lucide:message-square', type: ['inRow'], actionFunc: async ({ item }) => { console.log('message', item); } },
|
||||||
|
{ name: 'History', iconName: 'lucide:clock', type: ['inRow'], actionFunc: async ({ item }) => { console.log('history', item); } },
|
||||||
|
{ name: 'Add New', iconName: 'lucide:plus', type: ['header'], actionFunc: async ({ table }) => { console.log('add'); } },
|
||||||
|
{ name: 'Export CSV', iconName: 'lucide:download', type: ['header'], actionFunc: async ({ table }) => { console.log('export'); } },
|
||||||
|
{ name: 'Bulk Delete', iconName: 'lucide:trash-2', type: ['footer'], actionFunc: async ({ table }) => { console.log('bulk delete'); } },
|
||||||
|
] as ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Scrollable Small Height</h2>
|
||||||
|
<p class="demo-description">Same as above, but with many items and a small fixed height to force vertical scrolling inside the table. Actions remain visible on the right; horizontal scroll appears if needed.</p>
|
||||||
|
<style>
|
||||||
|
#scrollSmallHeight { --table-max-height: 240px; }
|
||||||
|
</style>
|
||||||
|
<dees-table
|
||||||
|
id="scrollSmallHeight"
|
||||||
|
.stickyHeader=${true}
|
||||||
|
heading1="People Directory (Scrollable)"
|
||||||
|
heading2="Forced scrolling with many items"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'id', header: 'ID', sortable: true },
|
||||||
|
{ key: 'name', header: 'Name', sortable: true },
|
||||||
|
{ key: 'role', header: 'Role', sortable: true },
|
||||||
|
{ key: 'department', header: 'Department', sortable: true },
|
||||||
|
{ key: 'email', header: 'Email' },
|
||||||
|
{ key: 'phone', header: 'Phone' },
|
||||||
|
{ key: 'location', header: 'Location', sortable: true },
|
||||||
|
{ key: 'status', header: 'Status', sortable: true },
|
||||||
|
{ key: 'createdAt', header: 'Created', sortable: true },
|
||||||
|
{ key: 'updatedAt', header: 'Updated', sortable: true },
|
||||||
|
{ key: 'lastLogin', header: 'Last Login', sortable: true },
|
||||||
|
{ key: 'projects', header: 'Projects' },
|
||||||
|
{ key: 'tags', header: 'Tags' },
|
||||||
|
{ key: 'notes', header: 'Notes' },
|
||||||
|
]}
|
||||||
|
.data=${Array.from({ length: 100 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: `User ${i + 1}`,
|
||||||
|
role: ['Engineer','Designer','Manager','QA','Support'][i % 5],
|
||||||
|
department: ['R&D','Design','Ops','QA','CS'][i % 5],
|
||||||
|
email: `user${i+1}@corp.com`,
|
||||||
|
phone: `+1 202 555 ${String(1000 + i).slice(-4)}`,
|
||||||
|
location: ['Berlin','Paris','London','Madrid','Rome'][i % 5],
|
||||||
|
status: ['Active','Inactive','On Leave'][i % 3],
|
||||||
|
createdAt: `2023-${String((i%12)+1).padStart(2,'0')}-${String((i%28)+1).padStart(2,'0')}`,
|
||||||
|
updatedAt: `2024-${String(((i+3)%12)+1).padStart(2,'0')}-${String(((i+7)%28)+1).padStart(2,'0')}`,
|
||||||
|
lastLogin: `2024-${String(((i+6)%12)+1).padStart(2,'0')}-${String(((i+10)%28)+1).padStart(2,'0')}`,
|
||||||
|
projects: (i % 12),
|
||||||
|
tags: i % 2 ? 'typescript' : 'design',
|
||||||
|
notes: i % 3 ? '' : 'Note',
|
||||||
|
}))}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'View', iconName: 'lucide:eye', type: ['inRow'], actionFunc: async ({ item }) => {} },
|
||||||
|
{ name: 'Edit', iconName: 'lucide:edit', type: ['inRow'], actionFunc: async ({ item }) => {} },
|
||||||
|
{ name: 'Delete', iconName: 'lucide:trash', type: ['inRow'], actionFunc: async ({ item }) => {} },
|
||||||
|
] as ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
841
ts_web/elements/dees-table/dees-table.ts
Normal file
841
ts_web/elements/dees-table/dees-table.ts
Normal file
@@ -0,0 +1,841 @@
|
|||||||
|
import * as plugins from '../00plugins.js';
|
||||||
|
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 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';
|
||||||
|
import { compileLucenePredicate } from './lucene.js';
|
||||||
|
|
||||||
|
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-table': DeesTable<any>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// interfaces moved to ./types.ts and re-exported above
|
||||||
|
|
||||||
|
// the table implementation
|
||||||
|
@customElement('dees-table')
|
||||||
|
export class DeesTable<T> extends DeesElement {
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
public heading1: string = 'heading 1';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
public heading2: string = 'heading 2';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Array,
|
||||||
|
})
|
||||||
|
public data: T[] = [];
|
||||||
|
|
||||||
|
// dees-form compatibility -----------------------------------------
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
public key: string;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
public label: string;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
public disabled: boolean = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
public required: boolean = false;
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
set value(_valueArg) {}
|
||||||
|
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
|
||||||
|
// end dees-form compatibility -----------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What does a row of data represent?
|
||||||
|
*/
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
public dataName: string;
|
||||||
|
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
searchable: boolean = true;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Array,
|
||||||
|
})
|
||||||
|
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({
|
||||||
|
attribute: false,
|
||||||
|
})
|
||||||
|
public displayFunction: TDisplayFunction = (itemArg: T) => itemArg as any;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
attribute: false,
|
||||||
|
})
|
||||||
|
public reverseDisplayFunction: (itemArg: any) => T = (itemArg: any) => itemArg as T;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Object,
|
||||||
|
})
|
||||||
|
public selectedDataRow: T;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Array,
|
||||||
|
})
|
||||||
|
public editableFields: string[] = [];
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'show-vertical-lines'
|
||||||
|
})
|
||||||
|
public showVerticalLines: boolean = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'show-horizontal-lines'
|
||||||
|
})
|
||||||
|
public showHorizontalLines: boolean = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
attribute: 'show-grid'
|
||||||
|
})
|
||||||
|
public showGrid: boolean = true;
|
||||||
|
|
||||||
|
public files: File[] = [];
|
||||||
|
public fileWeakMap = new WeakMap();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// search row state
|
||||||
|
@property({ type: String })
|
||||||
|
public searchMode: 'table' | 'data' | 'server' = 'table';
|
||||||
|
private __searchTextSub?: { unsubscribe?: () => void };
|
||||||
|
private __searchModeSub?: { unsubscribe?: () => void };
|
||||||
|
|
||||||
|
// selection (Phase 1)
|
||||||
|
@property({ type: String })
|
||||||
|
public selectionMode: 'none' | 'single' | 'multi' = 'none';
|
||||||
|
@property({ attribute: false })
|
||||||
|
private selectedIds: Set<string> = new Set();
|
||||||
|
private _rowIdMap = new WeakMap<object, string>();
|
||||||
|
private _rowIdCounter = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = tableStyles;
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
|
||||||
|
const effectiveColumns: Column<T>[] = usingColumns
|
||||||
|
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
||||||
|
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
||||||
|
|
||||||
|
const lucenePred = compileLucenePredicate<T>(
|
||||||
|
this.filterText,
|
||||||
|
this.searchMode === 'data' ? 'data' : 'table',
|
||||||
|
effectiveColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewData = getViewDataFn(
|
||||||
|
this.data,
|
||||||
|
effectiveColumns,
|
||||||
|
this.sortKey,
|
||||||
|
this.sortDir,
|
||||||
|
this.filterText,
|
||||||
|
this.columnFilters,
|
||||||
|
this.searchMode === 'data' ? 'data' : 'table',
|
||||||
|
lucenePred || undefined
|
||||||
|
);
|
||||||
|
(this as any)._lastViewData = viewData;
|
||||||
|
return html`
|
||||||
|
<div class="mainbox">
|
||||||
|
<!-- the heading part -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="headingContainer">
|
||||||
|
<div class="heading heading1">${this.label || this.heading1}</div>
|
||||||
|
<div class="heading heading2">${this.heading2}</div>
|
||||||
|
</div>
|
||||||
|
<div class="headerActions">
|
||||||
|
${directives.resolveExec(async () => {
|
||||||
|
const resultArray: TemplateResult[] = [];
|
||||||
|
for (const action of this.dataActions) {
|
||||||
|
if (!action.type.includes('header')) continue;
|
||||||
|
resultArray.push(
|
||||||
|
html`<div
|
||||||
|
class="headerAction"
|
||||||
|
@click=${() => {
|
||||||
|
action.actionFunc({
|
||||||
|
item: this.selectedDataRow,
|
||||||
|
table: this,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${action.iconName
|
||||||
|
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||||
|
${action.name}`
|
||||||
|
: action.name}
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resultArray;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="headingSeparation"></div>
|
||||||
|
<div class="searchGrid hidden">
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'lucene syntax search'}
|
||||||
|
.description=${`
|
||||||
|
You can use the lucene syntax to search for data, e.g.:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
name: "john" AND age: 18
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
`}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-multitoggle
|
||||||
|
.label=${'search mode'}
|
||||||
|
.options=${['table', 'data', 'server']}
|
||||||
|
.selectedOption=${'table'}
|
||||||
|
.description=${`
|
||||||
|
There are three basic modes:
|
||||||
|
|
||||||
|
* table: only searches data already in the table
|
||||||
|
* data: searches original data, ignoring table transforms
|
||||||
|
* server: searches data on the server
|
||||||
|
|
||||||
|
`}
|
||||||
|
></dees-input-multitoggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- the actual table -->
|
||||||
|
<style></style>
|
||||||
|
${this.data.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="tableScroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
${this.selectionMode !== 'none'
|
||||||
|
? html`
|
||||||
|
<th style="width:42px; text-align:center;">
|
||||||
|
${this.selectionMode === 'multi'
|
||||||
|
? html`
|
||||||
|
<dees-input-checkbox
|
||||||
|
.value=${this.areAllVisibleSelected()}
|
||||||
|
.indeterminate=${this.isVisibleSelectionIndeterminate()}
|
||||||
|
@newValue=${(e: CustomEvent<boolean>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setSelectVisible(e.detail === true);
|
||||||
|
}}
|
||||||
|
></dees-input-checkbox>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
</th>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
${effectiveColumns
|
||||||
|
.filter((c) => !c.hidden)
|
||||||
|
.map((col) => {
|
||||||
|
const isSortable = !!col.sortable;
|
||||||
|
const ariaSort = this.getAriaSort(col);
|
||||||
|
return html`
|
||||||
|
<th
|
||||||
|
role="columnheader"
|
||||||
|
aria-sort=${ariaSort}
|
||||||
|
style="${isSortable ? 'cursor: pointer;' : ''}"
|
||||||
|
@click=${() => (isSortable ? this.toggleSort(col) : null)}
|
||||||
|
>
|
||||||
|
${col.header ?? (col.key as any)}
|
||||||
|
${this.renderSortIndicator(col)}
|
||||||
|
</th>`;
|
||||||
|
})}
|
||||||
|
${(() => {
|
||||||
|
if (this.dataActions && this.dataActions.length > 0) {
|
||||||
|
return html` <th class="actionsCol">Actions</th> `;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</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>
|
||||||
|
${viewData.map((itemArg, rowIndex) => {
|
||||||
|
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
||||||
|
if (elementArg.tagName === 'TR') {
|
||||||
|
return elementArg;
|
||||||
|
} else {
|
||||||
|
return getTr(elementArg.parentElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<tr
|
||||||
|
@click=${() => {
|
||||||
|
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();
|
||||||
|
eventArg.stopPropagation();
|
||||||
|
const realTarget = getTr(eventArg.target as HTMLElement);
|
||||||
|
setTimeout(() => {
|
||||||
|
realTarget.classList.add('hasAttachment');
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
@dragleave=${async (eventArg: DragEvent) => {
|
||||||
|
eventArg.preventDefault();
|
||||||
|
eventArg.stopPropagation();
|
||||||
|
const realTarget = getTr(eventArg.target as HTMLElement);
|
||||||
|
realTarget.classList.remove('hasAttachment');
|
||||||
|
}}
|
||||||
|
@dragover=${async (eventArg: DragEvent) => {
|
||||||
|
eventArg.preventDefault();
|
||||||
|
}}
|
||||||
|
@drop=${async (eventArg: DragEvent) => {
|
||||||
|
eventArg.preventDefault();
|
||||||
|
const newFiles = [];
|
||||||
|
for (const file of Array.from(eventArg.dataTransfer.files)) {
|
||||||
|
this.files.push(file);
|
||||||
|
newFiles.push(file);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
const result: File[] = this.fileWeakMap.get(itemArg as object);
|
||||||
|
if (!result) {
|
||||||
|
this.fileWeakMap.set(itemArg as object, newFiles);
|
||||||
|
} else {
|
||||||
|
result.push(...newFiles);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
@contextmenu=${async (eventArg: MouseEvent) => {
|
||||||
|
DeesContextmenu.openContextMenuWithOptions(
|
||||||
|
eventArg,
|
||||||
|
this.getActionsForType('contextmenu').map((action) => {
|
||||||
|
const menuItem: plugins.tsclass.website.IMenuItem = {
|
||||||
|
name: action.name,
|
||||||
|
iconName: action.iconName as any,
|
||||||
|
action: async () => {
|
||||||
|
await action.actionFunc({
|
||||||
|
item: itemArg,
|
||||||
|
table: this,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return menuItem;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
||||||
|
>
|
||||||
|
${this.selectionMode !== 'none'
|
||||||
|
? html`<td style="width:42px; text-align:center;">
|
||||||
|
<dees-input-checkbox
|
||||||
|
.value=${this.isRowSelected(itemArg)}
|
||||||
|
@newValue=${(e: CustomEvent<boolean>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setRowSelected(itemArg, e.detail === true);
|
||||||
|
}}
|
||||||
|
></dees-input-checkbox>
|
||||||
|
</td>`
|
||||||
|
: html``}
|
||||||
|
${effectiveColumns
|
||||||
|
.filter((c) => !c.hidden)
|
||||||
|
.map((col, colIndex) => {
|
||||||
|
const value = getCellValueFn(itemArg, col, this.displayFunction);
|
||||||
|
const content = col.renderer
|
||||||
|
? col.renderer(value, itemArg, { rowIndex, colIndex, column: col })
|
||||||
|
: value;
|
||||||
|
const editKey = String(col.key);
|
||||||
|
return html`
|
||||||
|
<td
|
||||||
|
@dblclick=${(e: Event) => {
|
||||||
|
const dblAction = this.dataActions.find((actionArg) =>
|
||||||
|
actionArg.type.includes('doubleClick')
|
||||||
|
);
|
||||||
|
if (this.editableFields.includes(editKey)) {
|
||||||
|
this.handleCellEditing(e, itemArg, editKey);
|
||||||
|
} else if (dblAction) {
|
||||||
|
dblAction.actionFunc({ item: itemArg, table: this });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="innerCellContainer">${content}</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
${(() => {
|
||||||
|
if (this.dataActions && this.dataActions.length > 0) {
|
||||||
|
return html`
|
||||||
|
<td class="actionsCol">
|
||||||
|
<div class="actionsContainer">
|
||||||
|
${this.getActionsForType('inRow').map(
|
||||||
|
(actionArg) => html`
|
||||||
|
<div
|
||||||
|
class="action"
|
||||||
|
@click=${() =>
|
||||||
|
actionArg.actionFunc({
|
||||||
|
item: itemArg,
|
||||||
|
table: this,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${actionArg.iconName
|
||||||
|
? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
|
||||||
|
: actionArg.name}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</tr>`;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html` <div class="noDataSet">No data set!</div> `}
|
||||||
|
<div class="footer">
|
||||||
|
<div class="tableStatistics">
|
||||||
|
${this.data.length} ${this.dataName || 'data rows'} (total) |
|
||||||
|
${this.selectedDataRow ? '# ' + `${this.data.indexOf(this.selectedDataRow) + 1}` : `No`}
|
||||||
|
selected
|
||||||
|
</div>
|
||||||
|
<div class="footerActions">
|
||||||
|
${directives.resolveExec(async () => {
|
||||||
|
const resultArray: TemplateResult[] = [];
|
||||||
|
for (const action of this.dataActions) {
|
||||||
|
if (!action.type.includes('footer')) continue;
|
||||||
|
resultArray.push(
|
||||||
|
html`<div
|
||||||
|
class="footerAction"
|
||||||
|
@click=${() => {
|
||||||
|
action.actionFunc({
|
||||||
|
item: this.selectedDataRow,
|
||||||
|
table: this,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${action.iconName
|
||||||
|
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||||
|
${action.name}`
|
||||||
|
: action.name}
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resultArray;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async firstUpdated() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
this.determineColumnWidths();
|
||||||
|
if (this.searchable) {
|
||||||
|
const existing = this.dataActions.find((actionArg) => actionArg.type.includes('header') && actionArg.name === 'Search');
|
||||||
|
if (!existing) {
|
||||||
|
this.dataActions.unshift({
|
||||||
|
name: 'Search',
|
||||||
|
iconName: 'magnifyingGlass',
|
||||||
|
type: ['header'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
console.log('open search');
|
||||||
|
const searchGrid = this.shadowRoot.querySelector('.searchGrid');
|
||||||
|
searchGrid.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(this.dataActions);
|
||||||
|
this.requestUpdate();
|
||||||
|
};
|
||||||
|
// wire search inputs
|
||||||
|
this.wireSearchInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private __debounceTimer?: any;
|
||||||
|
private debounceRun(fn: () => void, ms = 200) {
|
||||||
|
if (this.__debounceTimer) clearTimeout(this.__debounceTimer);
|
||||||
|
this.__debounceTimer = setTimeout(fn, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private wireSearchInputs() {
|
||||||
|
const searchTextEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-text');
|
||||||
|
const searchModeEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-multitoggle');
|
||||||
|
if (searchTextEl && !this.__searchTextSub) {
|
||||||
|
this.__searchTextSub = searchTextEl.changeSubject.subscribe((el: any) => {
|
||||||
|
const val: string = el?.value ?? '';
|
||||||
|
this.debounceRun(() => {
|
||||||
|
if (this.searchMode === 'server') {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('searchRequest', {
|
||||||
|
detail: { query: val, mode: 'server' },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.setFilterText(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (searchModeEl && !this.__searchModeSub) {
|
||||||
|
this.__searchModeSub = searchModeEl.changeSubject.subscribe((el: any) => {
|
||||||
|
const mode: string = el?.selectedOption || el?.value || 'table';
|
||||||
|
if (mode === 'table' || mode === 'data' || mode === 'server') {
|
||||||
|
this.searchMode = mode as any;
|
||||||
|
// When switching modes, re-apply current text input
|
||||||
|
const val: string = searchTextEl?.value ?? '';
|
||||||
|
this.debounceRun(() => {
|
||||||
|
if (this.searchMode === 'server') {
|
||||||
|
this.dispatchEvent(new CustomEvent('searchRequest', { detail: { query: val, mode: 'server' }, bubbles: true }));
|
||||||
|
} else {
|
||||||
|
this.setFilterText(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async determineColumnWidths() {
|
||||||
|
const domtools = await this.domtoolsPromise;
|
||||||
|
await domtools.convenience.smartdelay.delayFor(0);
|
||||||
|
// Get the table element
|
||||||
|
const table = this.shadowRoot.querySelector('table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
// Get the first row's cells to measure the widths
|
||||||
|
const cells = table.rows[0].cells;
|
||||||
|
|
||||||
|
const handleColumnByIndex = async (i: number, waitForRenderArg: boolean = false) => {
|
||||||
|
const done = plugins.smartpromise.defer();
|
||||||
|
const cell = cells[i];
|
||||||
|
|
||||||
|
// Get computed width
|
||||||
|
const width = window.getComputedStyle(cell).width;
|
||||||
|
if (cell.textContent.includes('Actions')) {
|
||||||
|
const neededWidth =
|
||||||
|
this.dataActions.filter((actionArg) => actionArg.type.includes('inRow')).length * 36;
|
||||||
|
cell.style.width = `${Math.max(neededWidth, 68)}px`;
|
||||||
|
} else {
|
||||||
|
cell.style.width = width;
|
||||||
|
}
|
||||||
|
if (waitForRenderArg) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
await done.promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cells[cells.length - 1].textContent.includes('Actions')) {
|
||||||
|
await handleColumnByIndex(cells.length - 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < cells.length; i++) {
|
||||||
|
if (cells[i].textContent.includes('Actions')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await handleColumnByIndex(i);
|
||||||
|
}
|
||||||
|
table.style.tableLayout = 'fixed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute helpers moved to ./data.ts
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, columns: { ...this.columnFilters } },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 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 areAllVisibleSelected(): boolean {
|
||||||
|
const view: T[] = (this as any)._lastViewData || [];
|
||||||
|
if (view.length === 0) return false;
|
||||||
|
for (const r of view) {
|
||||||
|
if (!this.selectedIds.has(this.getRowId(r))) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isVisibleSelectionIndeterminate(): boolean {
|
||||||
|
const view: T[] = (this as any)._lastViewData || [];
|
||||||
|
if (view.length === 0) return false;
|
||||||
|
let count = 0;
|
||||||
|
for (const r of view) {
|
||||||
|
if (this.selectedIds.has(this.getRowId(r))) count++;
|
||||||
|
}
|
||||||
|
return count > 0 && count < view.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSelectVisible(checked: boolean) {
|
||||||
|
const view: T[] = (this as any)._lastViewData || [];
|
||||||
|
if (checked) {
|
||||||
|
for (const r of view) this.selectedIds.add(this.getRowId(r));
|
||||||
|
} else {
|
||||||
|
for (const r of view) this.selectedIds.delete(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) {
|
||||||
|
if (!action.type.includes(typeArg)) continue;
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCellEditing(event: Event, itemArg: T, key: string) {
|
||||||
|
await this.domtoolsPromise;
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const originalColor = target.style.color;
|
||||||
|
target.style.color = 'transparent';
|
||||||
|
const transformedItem = this.displayFunction(itemArg);
|
||||||
|
const initialValue = ((transformedItem as any)[key] ?? (itemArg as any)[key] ?? '') as string;
|
||||||
|
// Create an input element
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.value = initialValue;
|
||||||
|
|
||||||
|
const blurInput = async (blurArg = true, saveArg = false) => {
|
||||||
|
if (blurArg) {
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
if (saveArg) {
|
||||||
|
itemArg[key] = input.value as any; // Convert string to T (you might need better type casting depending on your data structure)
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
}
|
||||||
|
input.remove();
|
||||||
|
target.style.color = originalColor;
|
||||||
|
this.requestUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the input loses focus or the Enter key is pressed, update the data
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
blurInput(false, false);
|
||||||
|
});
|
||||||
|
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
blurInput(true, true); // This will trigger the blur event handler above
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace the cell's content with the input
|
||||||
|
target.appendChild(input);
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
170
ts_web/elements/dees-table/lucene.ts
Normal file
170
ts_web/elements/dees-table/lucene.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import type { Column } from './types.js';
|
||||||
|
|
||||||
|
type FilterMode = 'table' | 'data';
|
||||||
|
|
||||||
|
export type RowPredicate<T> = (row: T) => boolean;
|
||||||
|
|
||||||
|
interface Term {
|
||||||
|
field?: string; // if undefined, match across all fields
|
||||||
|
value?: string; // lowercased string
|
||||||
|
negate?: boolean;
|
||||||
|
range?: { lower: string; upper: string; inclusive: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Clause {
|
||||||
|
terms: Term[]; // AND across terms
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LuceneQuery {
|
||||||
|
clauses: Clause[]; // OR across clauses
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripQuotes(s: string): string {
|
||||||
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitByOr(input: string): string[] {
|
||||||
|
return input.split(/\s+OR\s+/i).map((s) => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitByAnd(input: string): string[] {
|
||||||
|
return input.split(/\s+AND\s+/i).map((s) => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTerm(raw: string): Term | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
let negate = false;
|
||||||
|
// handle NOT prefix or leading '-'
|
||||||
|
const notMatch = raw.match(/^\s*(NOT\s+|-)\s*(.*)$/i);
|
||||||
|
if (notMatch) {
|
||||||
|
negate = true;
|
||||||
|
raw = notMatch[2];
|
||||||
|
}
|
||||||
|
// range: field:[lower TO upper]
|
||||||
|
const rangeMatch = raw.match(/^([^:\s]+)\s*:\s*\[(.*?)\s+TO\s+(.*?)\]$/i);
|
||||||
|
if (rangeMatch) {
|
||||||
|
return {
|
||||||
|
field: rangeMatch[1],
|
||||||
|
negate,
|
||||||
|
range: { lower: stripQuotes(rangeMatch[2]).toLowerCase(), upper: stripQuotes(rangeMatch[3]).toLowerCase(), inclusive: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// field:value (value may be quoted)
|
||||||
|
const m = raw.match(/^([^:\s]+)\s*:\s*("[^"]*"|'[^']*'|[^"'\s]+)$/);
|
||||||
|
if (m) {
|
||||||
|
return { field: m[1], value: stripQuotes(m[2]).toLowerCase(), negate };
|
||||||
|
}
|
||||||
|
// plain term
|
||||||
|
if (raw.length > 0) {
|
||||||
|
return { value: stripQuotes(raw).toLowerCase(), negate };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLucene(input: string): LuceneQuery | null {
|
||||||
|
if (!input) return null;
|
||||||
|
const clauses = splitByOr(input).map((clauseStr) => {
|
||||||
|
const terms = splitByAnd(clauseStr)
|
||||||
|
.map(parseTerm)
|
||||||
|
.filter((t): t is Term => !!t && !!t.value);
|
||||||
|
return { terms } as Clause;
|
||||||
|
}).filter((c) => c.terms.length > 0);
|
||||||
|
if (clauses.length === 0) return null;
|
||||||
|
return { clauses };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileLucenePredicate<T>(
|
||||||
|
input: string,
|
||||||
|
mode: FilterMode,
|
||||||
|
columns: Column<T>[]
|
||||||
|
): RowPredicate<T> | null {
|
||||||
|
const ast = parseLucene(input);
|
||||||
|
if (!ast) return null;
|
||||||
|
const colMap = new Map<string, Column<T>>(
|
||||||
|
columns.map((c) => [String(c.key), c])
|
||||||
|
);
|
||||||
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
||||||
|
const coerce = (s: any) => {
|
||||||
|
const str = String(s ?? '').toLowerCase();
|
||||||
|
const num = Number(str);
|
||||||
|
const date = Date.parse(str);
|
||||||
|
if (!Number.isNaN(num) && str.trim() !== '') return { t: 'n' as const, v: num };
|
||||||
|
if (!Number.isNaN(date)) return { t: 'd' as const, v: date };
|
||||||
|
return { t: 's' as const, v: str };
|
||||||
|
};
|
||||||
|
const inRange = (val: any, lower: string, upper: string) => {
|
||||||
|
const a = coerce(val);
|
||||||
|
const lo = coerce(lower);
|
||||||
|
const up = coerce(upper);
|
||||||
|
// All strings: lexical compare
|
||||||
|
if (a.t === 's' && lo.t === 's' && up.t === 's') {
|
||||||
|
const av = a.v as string;
|
||||||
|
return cmp(av, lo.v as string) >= 0 && cmp(av, up.v as string) <= 0;
|
||||||
|
}
|
||||||
|
// All numbers
|
||||||
|
if (a.t === 'n' && lo.t === 'n' && up.t === 'n') {
|
||||||
|
const av = a.v as number;
|
||||||
|
return av >= (lo.v as number) && av <= (up.v as number);
|
||||||
|
}
|
||||||
|
// All dates (as numbers)
|
||||||
|
if (a.t === 'd' && lo.t === 'd' && up.t === 'd') {
|
||||||
|
const av = a.v as number;
|
||||||
|
return av >= (lo.v as number) && av <= (up.v as number);
|
||||||
|
}
|
||||||
|
// Fallback: compare string forms
|
||||||
|
const as = String(val ?? '').toLowerCase();
|
||||||
|
return cmp(as, lower) >= 0 && cmp(as, upper) <= 0;
|
||||||
|
};
|
||||||
|
return (row: T) => {
|
||||||
|
for (const clause of ast.clauses) {
|
||||||
|
let clauseOk = true;
|
||||||
|
for (const term of clause.terms) {
|
||||||
|
let ok = false;
|
||||||
|
if (term.range && term.field) {
|
||||||
|
// range compare on field
|
||||||
|
if (mode === 'data') {
|
||||||
|
ok = inRange((row as any)[term.field], term.range.lower, term.range.upper);
|
||||||
|
} else {
|
||||||
|
const col = colMap.get(term.field);
|
||||||
|
if (!col || col.hidden) { ok = false; } else {
|
||||||
|
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
ok = inRange(val, term.range.lower, term.range.upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (term.field && term.value != null) {
|
||||||
|
if (mode === 'data') {
|
||||||
|
const s = String((row as any)[term.field] ?? '').toLowerCase();
|
||||||
|
ok = s.includes(term.value);
|
||||||
|
} else {
|
||||||
|
const col = colMap.get(term.field);
|
||||||
|
if (!col || col.hidden === true) { ok = false; }
|
||||||
|
else {
|
||||||
|
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
ok = s.includes(term.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (term.value != null) {
|
||||||
|
// search across all visible/raw fields
|
||||||
|
if (mode === 'data') {
|
||||||
|
ok = Object.values(row as any).some((v) => String(v ?? '').toLowerCase().includes(term.value!));
|
||||||
|
} else {
|
||||||
|
ok = columns.some((col) => {
|
||||||
|
if (col.hidden) return false;
|
||||||
|
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||||
|
const s = String(val ?? '').toLowerCase();
|
||||||
|
return s.includes(term.value!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (term.negate) ok = !ok;
|
||||||
|
if (!ok) { clauseOk = false; break; }
|
||||||
|
}
|
||||||
|
if (clauseOk) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
429
ts_web/elements/dees-table/styles.ts
Normal file
429
ts_web/elements/dees-table/styles.ts
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
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 max-content;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.searchGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* let search mode size to content (no forced width) */
|
||||||
|
|
||||||
|
.searchGrid.hidden {
|
||||||
|
height: 0px;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0px 24px;
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableScroll {
|
||||||
|
/* enable horizontal scroll only when content exceeds width */
|
||||||
|
overflow-x: auto;
|
||||||
|
/* prevent vertical scroll inside the table container */
|
||||||
|
overflow-y: hidden;
|
||||||
|
/* avoid reserving extra space for classic scrollbars where possible */
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
|
}
|
||||||
|
/* Hide horizontal scrollbar entirely when not using sticky header */
|
||||||
|
:host(:not([sticky-header])) .tableScroll {
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
scrollbar-width: none; /* Firefox (hides both axes) */
|
||||||
|
}
|
||||||
|
:host(:not([sticky-header])) .tableScroll::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
/* In sticky-header mode, hide only the horizontal scrollbar in WebKit/Blink */
|
||||||
|
:host([sticky-header]) .tableScroll::-webkit-scrollbar:horizontal {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
:host([sticky-header]) .tableScroll {
|
||||||
|
max-height: var(--table-max-height, 360px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
/* allow table to grow wider than container so actions column can stick */
|
||||||
|
width: max-content;
|
||||||
|
min-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%)')};
|
||||||
|
}
|
||||||
|
:host([sticky-header]) thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 0;
|
||||||
|
bottom: 0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky Actions column (right pinned) */
|
||||||
|
thead th.actionsCol,
|
||||||
|
tbody td.actionsCol {
|
||||||
|
position: sticky;
|
||||||
|
right: 0;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||||
|
}
|
||||||
|
thead th.actionsCol { z-index: 3; }
|
||||||
|
tbody td.actionsCol {
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: -1px 0 0 0 ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
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)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* filter row */
|
||||||
|
thead tr.filtersRow th {
|
||||||
|
padding: 8px 12px 12px 12px;
|
||||||
|
}
|
||||||
|
thead tr.filtersRow th input[type='text'] {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
29
ts_web/elements/dees-table/types.ts
Normal file
29
ts_web/elements/dees-table/types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { TemplateResult } from '@design.estate/dees-element';
|
||||||
|
import type { TIconKey } from '../dees-icon.js';
|
||||||
|
|
||||||
|
export interface ITableActionDataArg<T> {
|
||||||
|
item: T;
|
||||||
|
table: any; // avoid circular typing with DeesTable; consumers rely on shape only
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITableAction<T = any> {
|
||||||
|
name: string;
|
||||||
|
iconName: TIconKey;
|
||||||
|
useTableBehaviour?: 'upload' | 'cancelUpload' | 'none';
|
||||||
|
type: ('inRow' | 'contextmenu' | 'doubleClick' | 'footer' | 'header' | 'preview' | 'keyCombination')[];
|
||||||
|
actionRelevancyCheckFunc?: (itemArg: T) => boolean;
|
||||||
|
actionFunc: (actionDataArg: ITableActionDataArg<T>) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Column<T = any> {
|
||||||
|
key: keyof T | string;
|
||||||
|
header?: string | TemplateResult;
|
||||||
|
value?: (row: T) => any;
|
||||||
|
renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column<T> }) => TemplateResult | string;
|
||||||
|
sortable?: boolean;
|
||||||
|
/** whether this column participates in per-column quick filtering (default: true) */
|
||||||
|
filterable?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TDisplayFunction<T = any> = (itemArg: T) => Record<string, any>;
|
@@ -32,6 +32,7 @@ export * from './dees-input-datepicker.js';
|
|||||||
export * from './dees-input-dropdown.js';
|
export * from './dees-input-dropdown.js';
|
||||||
export * from './dees-input-fileupload.js';
|
export * from './dees-input-fileupload.js';
|
||||||
export * from './dees-input-iban.js';
|
export * from './dees-input-iban.js';
|
||||||
|
export * from './dees-input-list.js';
|
||||||
export * from './profilepicture/dees-input-profilepicture.js';
|
export * from './profilepicture/dees-input-profilepicture.js';
|
||||||
export * from './dees-input-typelist.js';
|
export * from './dees-input-typelist.js';
|
||||||
export * from './dees-input-phone.js';
|
export * from './dees-input-phone.js';
|
||||||
@@ -56,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