kempo-ui 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +0 -71
- package/.github/skills/highlight-code/SKILL.md +17 -30
- package/.github/skills/new-component/SKILL.md +190 -0
- package/bin/highlight_code.js +34 -7
- package/dist/components/Table.js +37 -4
- package/dist/components/tableControls/DeleteSelected.js +7 -0
- package/docs/components/table.html +3 -1
- package/docs/components/tableServerSync.html +143 -0
- package/docs/icons/delete_sweep.svg +1 -0
- package/docs/src/components/Table.js +37 -4
- package/docs/src/components/tableControls/DeleteSelected.js +7 -0
- package/icons/delete_sweep.svg +1 -0
- package/package.json +2 -1
- package/src/components/Table.js +137 -70
- package/src/components/tableControls/DeleteSelected.js +27 -0
|
@@ -88,79 +88,8 @@ Do not prefix identifiers with underscores.
|
|
|
88
88
|
- Use clear, descriptive names without prefixes.
|
|
89
89
|
- When true privacy is needed inside classes, prefer native JavaScript private fields (e.g., `#myField`) instead of simulated privacy via underscores.
|
|
90
90
|
|
|
91
|
-
## Components
|
|
92
|
-
|
|
93
|
-
### Base Component Architecture
|
|
94
|
-
|
|
95
|
-
The project provides three base components for different rendering strategies. Choose the appropriate base component and extend it:
|
|
96
|
-
|
|
97
|
-
#### ShadowComponent
|
|
98
|
-
For components that need shadow DOM encapsulation and automatic `/kempo.css` stylesheet injection.
|
|
99
|
-
|
|
100
|
-
```javascript
|
|
101
|
-
import ShadowComponent from './ShadowComponent.js';
|
|
102
|
-
|
|
103
|
-
export default class MyComponent extends ShadowComponent {
|
|
104
|
-
render() {
|
|
105
|
-
return html`<p>Shadow DOM content with scoped styles</p>`;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
#### LightComponent
|
|
111
|
-
For components that render directly to light DOM without shadow DOM encapsulation.
|
|
112
|
-
|
|
113
|
-
```javascript
|
|
114
|
-
import LightComponent from './LightComponent.js';
|
|
115
|
-
|
|
116
|
-
export default class MyComponent extends LightComponent {
|
|
117
|
-
renderLightDom() {
|
|
118
|
-
return html`<p>Light DOM content</p>`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
#### HybridComponent
|
|
124
|
-
For components that need both shadow DOM (with automatic `/kempo.css`) and light DOM rendering.
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
127
|
-
import HybridComponent from './HybridComponent.js';
|
|
128
|
-
|
|
129
|
-
export default class MyComponent extends HybridComponent {
|
|
130
|
-
render() {
|
|
131
|
-
return html`<p>Shadow DOM content</p>`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
renderLightDom() {
|
|
135
|
-
return html`<p>Light DOM content alongside natural children</p>`;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**Important:** Always call `super.updated()` when overriding the `updated()` method in LightComponent or HybridComponent to ensure proper rendering.
|
|
141
|
-
|
|
142
|
-
### Component Architecture and Communication
|
|
143
|
-
|
|
144
|
-
- Use methods to cause actions; do not emit events to trigger logic. Events are for notifying that something already happened.
|
|
145
|
-
- Prefer `el.closest('ktf-test-framework')?.enqueueSuite({...})` over firing an `enqueue` event.
|
|
146
|
-
|
|
147
|
-
- Wrap dependent components inside a parent `ktf-test-framework` element. Children find it via `closest('ktf-test-framework')` and call its methods. The framework can query its subtree to orchestrate children.
|
|
148
|
-
|
|
149
|
-
- Avoid `window` globals and global custom events for coordination. If broadcast is needed, scope events to the framework element; reserve window events for global, non-visual concerns (e.g., settings changes).
|
|
150
|
-
|
|
151
91
|
## Development Workflow
|
|
152
92
|
|
|
153
|
-
### Icons
|
|
154
|
-
- All icons are stored in the `icons/` directory as SVG files
|
|
155
|
-
- Icons should have no `width` or `height` attributes, and use `fill="currentColor"` for theming
|
|
156
|
-
- To download new Material Icons from Google's CDN:
|
|
157
|
-
```bash
|
|
158
|
-
npm run geticon icon_name
|
|
159
|
-
```
|
|
160
|
-
Example: `npm run geticon format_bold`
|
|
161
|
-
- The script automatically formats the icon with proper attributes
|
|
162
|
-
- Browse available icons at [Google Material Symbols](https://fonts.google.com/icons)
|
|
163
|
-
|
|
164
93
|
### Local Development Server
|
|
165
94
|
- **DO NOT** start a development server - one is already running
|
|
166
95
|
- Default port: **8083**
|
|
@@ -11,47 +11,34 @@ Use this skill whenever you need syntax-highlighted HTML to embed in a documenta
|
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Or via npx:
|
|
19
|
-
```bash
|
|
20
|
-
npx kempo-highlightcode <lang> "<code>"
|
|
14
|
+
```powershell
|
|
15
|
+
node bin/highlight_code.js <lang> <code>
|
|
21
16
|
```
|
|
22
17
|
|
|
23
18
|
**Supported languages:** `html`, `css`, `js` / `javascript`, `ts` / `typescript`, `json`, `md` / `markdown`, `sh` / `bash`, `xml`
|
|
24
19
|
|
|
25
20
|
## Output
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Example
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npm run highlightcode -- html "<p>Hello World</p>"
|
|
33
|
-
```
|
|
22
|
+
Prints the inner highlighted content (hljs spans, no `<pre><code>` wrapper) to stdout, with `<br>` tags instead of newlines — a single line suitable for embedding in HTML.
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
<span class="hljs-tag"><<span class="hljs-name">p</span>></span>Hello World<span class="hljs-tag"></<span class="hljs-name">p</span>></span>
|
|
38
|
-
```
|
|
24
|
+
Input is automatically beautified before highlighting, so you can pass minified code.
|
|
39
25
|
|
|
40
|
-
##
|
|
26
|
+
## Workflow for Updating Doc Code Samples
|
|
41
27
|
|
|
42
|
-
|
|
28
|
+
Run the script with minified code as a single argument, capture to `$hl`, then use PowerShell's `.Replace()` to patch the HTML file:
|
|
43
29
|
|
|
44
30
|
```powershell
|
|
45
|
-
$
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"@
|
|
50
|
-
node bin/highlight_code.js js $code
|
|
31
|
+
$hl = node bin/highlight_code.js xml "<k-table id=myExample></k-table><script type=module>doSomething('foo');</script>"
|
|
32
|
+
$html = [System.IO.File]::ReadAllText((Resolve-Path 'docs/components/my-component.html'))
|
|
33
|
+
$old = [regex]::Match($html, '(?s)<pre><code class="hljs xml">.*?</code></pre>').Value
|
|
34
|
+
[System.IO.File]::WriteAllText((Resolve-Path 'docs/components/my-component.html'), $html.Replace($old, "<pre><code class=`"hljs xml`">$hl</code></pre>"))
|
|
51
35
|
```
|
|
52
36
|
|
|
53
|
-
##
|
|
37
|
+
## Important Notes
|
|
54
38
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
- Use **outer double-quotes** for the PowerShell argument string, and **single-quotes** for JS string literals inside. This preserves all quoting correctly through to node.
|
|
40
|
+
- HTML attribute values should be **unquoted** in the argument (`id=foo` not `id="foo"`) — the beautifier adds consistent formatting anyway.
|
|
41
|
+
- Use `[System.IO.File]::ReadAllText` / `WriteAllText` + `.Replace()` (not `-replace`) to avoid regex interpretation of the replacement string.
|
|
42
|
+
- If a page has multiple `<pre><code>` blocks, adjust the regex to match the specific one (e.g. match by a nearby unique string, or loop with an index).
|
|
43
|
+
- The beautifier handles indentation and formatting automatically.
|
|
44
|
+
- Output uses `<br>` instead of `\n`, keeping the block on one line in the HTML source.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: new-component
|
|
3
|
+
description: Creates a new Kempo UI web component — choosing the right base class, writing the source file, registering the custom element, and adding documentation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# New Component
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Use this skill any time you are asked to create a new component or add a new custom element to the project.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Creating a component involves four steps:
|
|
17
|
+
|
|
18
|
+
1. **Choose the base component** — pick the right rendering strategy
|
|
19
|
+
2. **Write the source file** in `src/components/`
|
|
20
|
+
3. **Register the custom element** at the bottom of the source file
|
|
21
|
+
4. **Add documentation** in `docs/components/`
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Step 1: Choose the Base Component
|
|
26
|
+
|
|
27
|
+
Three base classes are available. Pick based on rendering needs:
|
|
28
|
+
|
|
29
|
+
### `ShadowComponent`
|
|
30
|
+
Use when the component needs shadow DOM encapsulation. The base class automatically injects the `/kempo.css` stylesheet into the shadow root.
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { html } from '../lit-all.min.js';
|
|
34
|
+
import ShadowComponent from './ShadowComponent.js';
|
|
35
|
+
|
|
36
|
+
export default class MyComponent extends ShadowComponent {
|
|
37
|
+
render() {
|
|
38
|
+
return html`<p>Shadow DOM content with scoped styles</p>`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `LightComponent`
|
|
44
|
+
Use when the component renders to the light DOM (no encapsulation, inherits page styles).
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { html } from '../lit-all.min.js';
|
|
48
|
+
import LightComponent from './LightComponent.js';
|
|
49
|
+
|
|
50
|
+
export default class MyComponent extends LightComponent {
|
|
51
|
+
renderLightDom() {
|
|
52
|
+
return html`<p>Light DOM content</p>`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `HybridComponent`
|
|
58
|
+
Use when the component needs both a shadow DOM portion and a light DOM portion (e.g. slotted children that also need managed light DOM output).
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import { html } from '../lit-all.min.js';
|
|
62
|
+
import HybridComponent from './HybridComponent.js';
|
|
63
|
+
|
|
64
|
+
export default class MyComponent extends HybridComponent {
|
|
65
|
+
render() {
|
|
66
|
+
return html`<p>Shadow DOM content</p>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
renderLightDom() {
|
|
70
|
+
return html`<p>Light DOM content alongside natural children</p>`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Important:** Always call `super.updated()` when overriding the `updated()` method in `LightComponent` or `HybridComponent`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Step 2: Write the Source File
|
|
80
|
+
|
|
81
|
+
Create `src/components/MyComponent.js`. Follow these conventions:
|
|
82
|
+
|
|
83
|
+
- Use multi-line comments to separate logical sections: `Lifecycle Callbacks`, `Event Handlers`, `Public Methods`, `Utility Functions`, `Rendering`, `Styles`.
|
|
84
|
+
- Declare reactive properties with `static properties = { ... }` and initialize defaults in the constructor.
|
|
85
|
+
- Use `static styles = css\`...\`` for component-scoped CSS (shadow DOM only).
|
|
86
|
+
- Use arrow functions for class methods to avoid `.bind(this)`.
|
|
87
|
+
- Do not prefix private fields with underscores — use native JS private fields (`#field`) when true privacy is needed.
|
|
88
|
+
|
|
89
|
+
Example skeleton:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
import { html, css } from '../lit-all.min.js';
|
|
93
|
+
import ShadowComponent from './ShadowComponent.js';
|
|
94
|
+
|
|
95
|
+
export default class MyComponent extends ShadowComponent {
|
|
96
|
+
static properties = {
|
|
97
|
+
value: { type: String, reflect: true }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
Lifecycle Calblacks
|
|
102
|
+
*/
|
|
103
|
+
constructor(){
|
|
104
|
+
super();
|
|
105
|
+
this.value = 'default value';
|
|
106
|
+
}
|
|
107
|
+
connectedCallback(){
|
|
108
|
+
super.connectedCallback();
|
|
109
|
+
// Do things
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
Event Handlers
|
|
114
|
+
*/
|
|
115
|
+
handleClick = () => {
|
|
116
|
+
this.dispatchEvent(new CustomEvent('my-event', { bubbles: true, composed: true }));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
Rendering
|
|
121
|
+
*/
|
|
122
|
+
render() {
|
|
123
|
+
return html`
|
|
124
|
+
<button @click=${this.handleClick}>${this.value}</button>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
/*
|
|
128
|
+
Styles
|
|
129
|
+
*/
|
|
130
|
+
static styles = css`
|
|
131
|
+
:host {
|
|
132
|
+
display: block;
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
Static Methods
|
|
138
|
+
*/
|
|
139
|
+
static staticMethod(){
|
|
140
|
+
// Do things
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
customElements.define('k-my-component', MyComponent);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Custom element naming
|
|
148
|
+
- All elements use the `k-` prefix (e.g. `k-spinner`, `k-color-picker`).
|
|
149
|
+
- Use kebab-case for multi-word names.
|
|
150
|
+
- The `customElements.define(...)` call goes at the **bottom** of the file, after the class.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Step 3: Register in the Nav (if it should appear in docs)
|
|
155
|
+
|
|
156
|
+
Add a link to the component in `docs/nav.inc.html` inside the `<menu>` under the appropriate section, in alphabetical order:
|
|
157
|
+
|
|
158
|
+
```html
|
|
159
|
+
<a href="./components/my-component.html">My Component</a>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Step 4: Add Documentation
|
|
165
|
+
|
|
166
|
+
Create `docs/components/my-component.html`. Use an existing docs page (e.g. `docs/components/spinner.html`) as a reference for structure and conventions.
|
|
167
|
+
|
|
168
|
+
Key requirements:
|
|
169
|
+
- Consistent `<head>` with title, stylesheets, and `window.litDisableBundleWarning = true`.
|
|
170
|
+
- `<k-import src="../nav-1.inc.html"></k-import>` at the top of `<body>`.
|
|
171
|
+
- A Table of Contents accordion.
|
|
172
|
+
- Sections for: examples/usage, and a JavaScript Reference covering constructor, attributes/properties, CSS variables, events, and public methods (as applicable).
|
|
173
|
+
- Use the `highlight-code` skill for all code examples that need syntax highlighting.
|
|
174
|
+
- At the bottom of the file, load the component and its dependencies as `<script type="module">` tags:
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<script type="module" src="../src/components/MyComponent.js"></script>
|
|
178
|
+
<script type="module" src="../src/components/Import.js"></script>
|
|
179
|
+
<script type="module" src="../src/components/Accordion.js"></script>
|
|
180
|
+
<script type="module" src="../src/components/Card.js"></script>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Component Architecture and Communication
|
|
186
|
+
|
|
187
|
+
- Use **methods** to trigger actions. Events notify that something already happened; they should not be used to trigger logic.
|
|
188
|
+
- Prefer `el.closest('k-parent')?.doSomething()` over dispatching an event and listening for it.
|
|
189
|
+
- Child components should locate their parent via `closest('k-parent-element')` and call its methods directly.
|
|
190
|
+
- Avoid `window` globals and global custom events for coordination. Scope events to the relevant element; reserve `window` events for global, non-visual concerns (e.g. settings changes).
|
package/bin/highlight_code.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { highlight } from '../tools/highlight.js';
|
|
3
|
+
import beautify from 'js-beautify';
|
|
3
4
|
|
|
4
5
|
const LANG_ALIASES = {
|
|
5
6
|
js: 'javascript',
|
|
@@ -16,11 +17,17 @@ const LANG_ALIASES = {
|
|
|
16
17
|
markdown: 'markdown'
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
const BEAUTIFY_OPTIONS = {
|
|
21
|
+
indent_size: 2,
|
|
22
|
+
wrap_line_length: 0,
|
|
23
|
+
preserve_newlines: false,
|
|
24
|
+
max_preserve_newlines: 0
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
const args = process.argv.slice(2);
|
|
20
28
|
|
|
21
|
-
if(args.length <
|
|
22
|
-
console.error('Usage:
|
|
23
|
-
console.error('Example: kempo-highlightcode html "<p>Hello World</p>"');
|
|
29
|
+
if(args.length < 1){
|
|
30
|
+
console.error('Usage: node bin/highlight_code.js <lang> <code>');
|
|
24
31
|
process.exit(1);
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -33,7 +40,27 @@ if(!lang){
|
|
|
33
40
|
process.exit(1);
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const beautifyCode = (code) => {
|
|
44
|
+
const fns = { javascript: beautify.js, css: beautify.css, html: beautify.html, xml: beautify.html };
|
|
45
|
+
const fn = fns[lang] || beautify.js;
|
|
46
|
+
return fn(code, BEAUTIFY_OPTIONS);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const run = (code) => {
|
|
50
|
+
const formatted = beautifyCode(code);
|
|
51
|
+
const full = highlight(formatted, lang);
|
|
52
|
+
process.stdout.write(
|
|
53
|
+
full
|
|
54
|
+
.replace(/^<pre><code class="hljs [^"]*">/, '')
|
|
55
|
+
.replace(/<\/code><\/pre>\n?$/, '')
|
|
56
|
+
.replace(/\n/g, '<br>') + '\n'
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if(rest.length > 0){
|
|
61
|
+
run(rest.join(' '));
|
|
62
|
+
} else {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
process.stdin.on('data', chunk => chunks.push(chunk));
|
|
65
|
+
process.stdin.on('end', () => run(chunks.join('')));
|
|
66
|
+
}
|
package/dist/components/Table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{toTitleCase}from"../utils/string.js";import{boolExists}from"../utils/propConverters.js";const selected=Symbol("selected"),hidden=Symbol("hidden"),index=Symbol("index"),editing=Symbol("editing");export default class Table extends ShadowComponent{static properties={enablePages:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-pages"},pageSize:{type:Number,reflect:!0,attribute:"page-size"},currentPage:{type:Number,reflect:!0,attribute:"current-page"},pageSizeOptions:{type:Array,attribute:"page-size-options"},enableSelection:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-selection"},enableSorting:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-sorting"},caseSensitiveFilters:{type:Boolean,reflect:!0,converter:boolExists,attribute:"case-sensitive-filters"},fields:{type:Array},records:{type:Array},filters:{type:Array},sort:{type:Array},columnSizes:{type:Object},fetchPending:{type:Boolean}};constructor(e={}){super(),void 0===this.pageSize&&(this.pageSize=50),void 0===this.currentPage&&(this.currentPage=1),void 0===this.pageSizeOptions&&(this.pageSizeOptions=[10,25,50,100,500]),void 0===this.records&&(this.records=e.records||[]),void 0===this.fields&&(this.fields=e.fields||[]),void 0===this.filters&&(this.filters=e.filters||[]),void 0===this.sort&&(this.sort=[]),void 0===this.columnSizes&&(this.columnSizes={}),void 0===this.fetchPending&&(this.fetchPending=!1)}handleSelectAllChange=e=>{e.target.checked?this.selectAllOnPage():this.deselectAllOnPage()};handleFieldClick=e=>{const t=this.sort.find(t=>t.name===e),s=!t||!t.asc;this.sortBy(e,s)};handleRecordSelectionChange=(e,t)=>{e[selected]=!!t.target.checked,this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))};firstUpdated(){this.setData({records:this.records,fields:this.fields,filters:this.filters})}childrenUpdated(){this.requestUpdate()}updated(e){if(super.updated(e),this.enableSelection){const e=this.shadowRoot.getElementById("select-all");e&&(e.checked=this.allOnPageSelected())}}renderColgroupTemplate(){const e=[];return this.enableSelection&&e.push(html`<col style="width: 40px" />`),this.hasBeforeControls()&&e.push(html`<col style="width: ${this.columnSizes.beforeControls}px" />`),this.fields.forEach(({size:t,hidden:s})=>{s||e.push(t?html`<col style="width: ${t}px" />`:html`<col />`)}),this.hasAfterControls()&&e.push(html`<col style="width: ${this.columnSizes.afterControls}px" />`),e}getColumnCount(){let e=0;return this.enableSelection&&e++,this.hasBeforeControls()&&e++,this.fields.forEach(({hidden:t})=>{t||e++}),this.hasAfterControls()&&e++,e}renderFieldsTemplate(){const e=[];return this.enableSelection&&e.push(html`
|
|
1
|
+
import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{toTitleCase}from"../utils/string.js";import{boolExists}from"../utils/propConverters.js";const selected=Symbol("selected"),hidden=Symbol("hidden"),index=Symbol("index"),editing=Symbol("editing");export default class Table extends ShadowComponent{static properties={enablePages:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-pages"},pageSize:{type:Number,reflect:!0,attribute:"page-size"},currentPage:{type:Number,reflect:!0,attribute:"current-page"},pageSizeOptions:{type:Array,attribute:"page-size-options"},enableSelection:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-selection"},enableSorting:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-sorting"},caseSensitiveFilters:{type:Boolean,reflect:!0,converter:boolExists,attribute:"case-sensitive-filters"},requestEdit:{type:Boolean,reflect:!0,converter:boolExists,attribute:"request-edit"},requestDelete:{type:Boolean,reflect:!0,converter:boolExists,attribute:"request-delete"},fields:{type:Array},records:{type:Array},filters:{type:Array},sort:{type:Array},columnSizes:{type:Object},fetchPending:{type:Boolean}};constructor(e={}){super(),void 0===this.pageSize&&(this.pageSize=50),void 0===this.currentPage&&(this.currentPage=1),void 0===this.pageSizeOptions&&(this.pageSizeOptions=[10,25,50,100,500]),void 0===this.records&&(this.records=e.records||[]),void 0===this.fields&&(this.fields=e.fields||[]),void 0===this.filters&&(this.filters=e.filters||[]),void 0===this.sort&&(this.sort=[]),void 0===this.columnSizes&&(this.columnSizes={}),void 0===this.fetchPending&&(this.fetchPending=!1),void 0===this.requestEdit&&(this.requestEdit=!1),void 0===this.requestDelete&&(this.requestDelete=!1)}handleSelectAllChange=e=>{e.target.checked?this.selectAllOnPage():this.deselectAllOnPage()};handleFieldClick=e=>{const t=this.sort.find(t=>t.name===e),s=!t||!t.asc;this.sortBy(e,s)};handleRecordSelectionChange=(e,t)=>{e[selected]=!!t.target.checked,this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))};firstUpdated(){this.setData({records:this.records,fields:this.fields,filters:this.filters})}childrenUpdated(){this.requestUpdate()}updated(e){if(super.updated(e),this.enableSelection){const e=this.shadowRoot.getElementById("select-all");e&&(e.checked=this.allOnPageSelected())}}renderColgroupTemplate(){const e=[];return this.enableSelection&&e.push(html`<col style="width: 40px" />`),this.hasBeforeControls()&&e.push(html`<col style="width: ${this.columnSizes.beforeControls}px" />`),this.fields.forEach(({size:t,hidden:s})=>{s||e.push(t?html`<col style="width: ${t}px" />`:html`<col />`)}),this.hasAfterControls()&&e.push(html`<col style="width: ${this.columnSizes.afterControls}px" />`),e}getColumnCount(){let e=0;return this.enableSelection&&e++,this.hasBeforeControls()&&e++,this.fields.forEach(({hidden:t})=>{t||e++}),this.hasAfterControls()&&e++,e}renderFieldsTemplate(){const e=[];return this.enableSelection&&e.push(html`
|
|
2
2
|
<th class="controls field-select">
|
|
3
3
|
<input type="checkbox" id="select-all" @change=${this.handleSelectAllChange} />
|
|
4
4
|
</th>
|
|
@@ -34,13 +34,13 @@ import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowCompon
|
|
|
34
34
|
</select>
|
|
35
35
|
`;default:return html`<input type="text" .value=${s} />`}}renderDisplayCell(e,t,s,i,r){return i?i(e,this):r?r(s):s}renderBeforeControlsTemplate(){const e=[];return this.querySelectorAll('[slot="before"]').forEach(t=>{const s=t.tagName.toLowerCase(),i=document.createElement(s);Array.from(t.attributes).forEach(e=>{"slot"!==e.name&&i.setAttribute(e.name,e.value)}),t.innerHTML&&(i.innerHTML=t.innerHTML),e.push(i)}),html`
|
|
36
36
|
<td class="cell controls controls-before">
|
|
37
|
-
|
|
37
|
+
<div>${e}</div>
|
|
38
38
|
</td>
|
|
39
39
|
`}renderAfterControlsTemplate(){const e=[];return this.querySelectorAll('[slot="after"]').forEach(t=>{const s=t.tagName.toLowerCase(),i=document.createElement(s);Array.from(t.attributes).forEach(e=>{"slot"!==e.name&&i.setAttribute(e.name,e.value)}),t.innerHTML&&(i.innerHTML=t.innerHTML),e.push(i)}),html`
|
|
40
40
|
<td class="cell controls controls-after">
|
|
41
|
-
|
|
41
|
+
<div>${e}</div>
|
|
42
42
|
</td>
|
|
43
|
-
`}hasBeforeControls(){return!!this.querySelector('[slot="before"]')}hasAfterControls(){return!!this.querySelector('[slot="after"]')}hasTopControls(){return!!this.querySelector('[slot="top"]')}hasBottomControls(){return!!this.querySelector(":scope > :not([slot])")}editRecord(e){e[editing]=!0;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.add("editing"),t.setAttribute("editing","true"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";if(t.innerHTML="",i.calculator){const s=document.createElement("input");s.disabled=!0,s.value=i.calculator(e,this),t.appendChild(s)}else if(i.editor)t.appendChild(i.editor(r));else{const e=i.type||typeof r,s=Table.editors[e]||Table.editors.string;t.appendChild(s(r))}}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!0},bubbles:!0}))}saveEditedRecord(e){const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i&&!i.calculator){const i=t.querySelector("input, select");i&&(e[s]=i.value)}}),e[editing]=!1,t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))}cancelEditedRecord(e){e[editing]=!1;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))}recordIsEditing(e){return e[editing]}getCurrentPage(){return this.currentPage}getTotalPages(){return Math.ceil(this.getDisplayedRecords().length/this.pageSize)}setPage(e){e<1||e>this.getTotalPages()||(this.currentPage=e,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageChange",{bubbles:!0})))}firstPage(){1!==this.currentPage&&this.setPage(1)}nextPage(){this.currentPage<this.getTotalPages()&&this.setPage(this.currentPage+1)}prevPage(){this.currentPage>1&&this.setPage(this.currentPage-1)}lastPage(){this.currentPage!==this.getTotalPages()&&this.setPage(this.getTotalPages())}setPageSize(e){this.pageSize=e,this.currentPage=1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageSizeChange",{bubbles:!0}))}getPageSize(){return this.pageSize}getPageSizeOptions(){return this.pageSizeOptions}getFieldLabel(e){const t=this.fields.find(t=>t.name===e);return t?t.label:toTitleCase(e)}setPageSizeOptions(e){this.pageSizeOptions=e,this.requestUpdate()}setData({records:e=!1,fields:t=!1,pageSize:s=!1,pageSizeOptions:i=!1,currentPage:r=!1,enableSelection:o}={}){let n=!1,l=this.getTotalPages(),a=this.currentPage;e&&(this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),n=!0),s&&(this.pageSize=s,n=!0),i&&(this.pageSizeOptions=i),r&&(this.currentPage=r,n=!0),void 0!==o&&(this.enableSelection=o,n=!0),n&&this.requestUpdate();const d=this.getTotalPages();d!==l&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),a>d&&this.setPage(d)}setRecords(e,t){let s=this.getTotalPages(),i=this.currentPage;this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordsSet",{detail:{records:e},bubbles:!0}));const r=this.getTotalPages();r!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),i>r&&this.setPage(r)}setupFetchRecords(e,t){const s=this.records.length,i=this.getTotalPages();if(s<e){this.records.length=e,this.records.fill(null,s),this.requestUpdate();const t=this.getTotalPages();t!==i&&setTimeout(()=>{this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:t},bubbles:!0}))},0)}this.addEventListener("fetchRecords",async e=>{if(this.fetchPending)return;this.fetchPending=!0;const{start:s,count:i}=e.detail,r=await t(s,i);r.forEach((e,t)=>{e[index]=s+t,void 0===e[selected]&&(e[selected]=!1),void 0===e[hidden]&&(e[hidden]=!1),void 0===e[editing]&&(e[editing]=!1)}),this.records.splice(s,r.length,...r),this.fetchPending=!1,this.requestUpdate()})}addRecord(e){e[selected]=!1,e[hidden]=!1,e[index]=this.records.length,this.records.push(e),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordAdded",{detail:{record:e},bubbles:!0}))}updateRecord(e,t){let s=!1,i=this.records.find(t=>t===e);if(i||void 0===e[index]||(i=this.records[e[index]]),Object.keys(t).forEach(e=>{i.hasOwnProperty(e)&&(i[e]=t[e],s=!0)}),s){const e=(this.currentPage-1)*this.pageSize,t=e+this.pageSize;(!this.enablePages||i[index]>=e&&i[index]<t)&&this.requestUpdate()}}deleteRecord(e){let t=this.records.find(t=>t===e),s=this.getTotalPages();if(t||void 0===e[index]||(t=this.records[e[index]]),t){const e=this.records.indexOf(t);this.records.splice(e,1),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0})),this.dispatchEvent(new CustomEvent("recordDeleted",{detail:{index:e},bubbles:!0}));const i=this.getTotalPages();this.currentPage>i&&this.setPage(i),i!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:i},bubbles:!0}))}}deleteSelected(){let e=this.getTotalPages();this.getSelectedRecords().forEach(e=>{let t=this.records.find(t=>t===e);if(t||void 0===e[index]||(t=this.records[e[index]]),t){const e=this.records.indexOf(t);this.records.splice(e,1)}}),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate();const t=this.getTotalPages();this.currentPage>t&&this.setPage(t),t!==e&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:t},bubbles:!0})),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))}getSelectedRecords(){return this.records.filter(e=>e[selected])}selectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!0;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}deselectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!1;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}allOnPageSelected(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)if(!this.records[s][selected])return!1;return!0}sortBy(e,t=!0){this.sort=this.sort.filter(t=>t.name!==e),this.sort.push({name:e,asc:t}),this.requestUpdate()}hideRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!0,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})))}showRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})))}showAllRecords(){this.records.forEach(e=>{e[hidden]=!1}),this.filters.length&&(this.filters=[],this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0}))),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})),this.dispatchEvent(new CustomEvent("allRecordsShown",{bubbles:!0}))}addFilter(e,t,s){this.filters.push({field:e,condition:t,value:s}),this.dispatchEvent(new CustomEvent("filterAdded",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),this.requestUpdate()}removeFilter(e,t,s,i=!0){const r=this.filters.findIndex(i=>i.field===e&&i.condition===t&&i.value===s);-1!==r&&(this.records.forEach(i=>{this.testFilter(i,e,t,s)||(i[hidden]=!1)}),this.filters.splice(r,1),this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),i&&this.requestUpdate())}testFilter(e,t,s,i){let r=e[t],o=i;switch(this.caseSensitiveFilters||"string"!=typeof r||"string"!=typeof i||(r=r.toLowerCase(),o=i.toLowerCase()),s){case"equals":return r===o;case"not-equals":return r!==o;case"contains":return r.includes(o);case"not-contains":return!r.includes(o);case"greater-than":return r>o;case"less-than":return r<o;case"greater-than-or-equal":return r>=o;case"less-than-or-equal":return r<=o;default:return!0}}removeAllFilters(){this.filters.length&&([...this.filters].forEach(({field:e,condition:t,value:s})=>{this.removeFilter(e,t,s,!1)}),this.requestUpdate())}search(e){const t=e.trim().toLowerCase();let s=!1;this.records.forEach(e=>{if(e[hidden])return;let i=!1;this.fields.forEach(({name:s})=>{(e[s]?.toString().toLowerCase()||"").includes(t)&&(i=!0)}),e[hidden]!==!i&&(e[hidden]=!i,s=!0)}),s&&(this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})),this.requestUpdate()),this.dispatchEvent(new CustomEvent("search",{detail:{term:e},bubbles:!0}))}getDisplayedRecords(){this.filters.forEach(({field:e,condition:t,value:s})=>{this.records.forEach(i=>{null!==i&&(this.testFilter(i,e,t,s)||(i[hidden]=!0))})});let e=this.records.filter(e=>null===e||!e[hidden]);return this.sort.forEach(({name:t,asc:s})=>{e.sort((e,i)=>null===e||null===i?0:e[t]<i[t]?s?-1:1:e[t]>i[t]?s?1:-1:0)}),e}getHiddenRecords(){return this.records.filter(e=>e[hidden])}calculateColumnSizes(){const e=Array.from(this.querySelectorAll('[slot="before"]')),t=Array.from(this.querySelectorAll('[slot="after"]')),s={beforeControls:e.reduce((e,t)=>e+(t.maxWidth||40),0),afterControls:t.reduce((e,t)=>e+(t.maxWidth||40),0)},i=[...e,...t].some(e=>void 0===e.maxWidth);return JSON.stringify(this.columnSizes)!==JSON.stringify(s)&&(this.columnSizes=s),i&&setTimeout(()=>this.calculateColumnSizes(),0),this.columnSizes}setFieldHiddenState(e,t){const s=this.fields.find(t=>t.name===e);s&&(s.hidden=t,this.calculateColumnSizes(),this.requestUpdate(),this.dispatchEvent(new CustomEvent("fieldVisibilityChanged",{detail:{field:s},bubbles:!0})),this.dispatchEvent(new CustomEvent(t?"fieldHidden":"fieldShown",{detail:{field:s},bubbles:!0})))}hideField(e){this.setFieldHiddenState(e,!0)}showField(e){this.setFieldHiddenState(e,!1)}reorderFields(e){const t=[];e.forEach(e=>{const s=this.fields.find(t=>t.name===e);s&&t.push(s)}),this.fields=t,this.requestUpdate()}render(){return this.records&&this.fields?(this.calculateColumnSizes(),this.hasTopControls()?this.setAttribute("top-controls","true"):this.removeAttribute("top-controls"),this.hasBottomControls()?this.setAttribute("bottom-controls","true"):this.removeAttribute("bottom-controls"),html`
|
|
43
|
+
`}hasBeforeControls(){return!!this.querySelector('[slot="before"]')}hasAfterControls(){return!!this.querySelector('[slot="after"]')}hasTopControls(){return!!this.querySelector('[slot="top"]')}hasBottomControls(){return!!this.querySelector(":scope > :not([slot])")}editRecord(e){e[editing]=!0;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.add("editing"),t.setAttribute("editing","true"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";if(t.innerHTML="",i.calculator){const s=document.createElement("input");s.disabled=!0,s.value=i.calculator(e,this),t.appendChild(s)}else if(i.editor)t.appendChild(i.editor(r));else{const e=i.type||typeof r,s=Table.editors[e]||Table.editors.string;t.appendChild(s(r))}}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!0},bubbles:!0}))}saveEditedRecord(e){const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`),s={};t&&t.querySelectorAll(".cell[data-field]").forEach(e=>{const t=e.dataset.field,i=this.fields.find(e=>e.name===t);if(i&&!i.calculator){const i=e.querySelector("input, select");i&&(s[t]=i.value)}});const i=()=>{Object.assign(e,s),e[editing]=!1,t&&(t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))};if(Object.keys(s).some(t=>String(e[t]??"")!==s[t]))if(this.requestEdit){t?.classList.add("pending");const r=()=>{t?.classList.remove("pending"),i()},o=()=>{t?.classList.remove("pending")};this.dispatchEvent(new CustomEvent("requestSave",{detail:{record:e,newData:s,approve:r,reject:o},bubbles:!0}))}else i();else this.cancelEditedRecord(e)}cancelEditedRecord(e){e[editing]=!1;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))}recordIsEditing(e){return e[editing]}getCurrentPage(){return this.currentPage}getTotalPages(){return Math.ceil(this.getDisplayedRecords().length/this.pageSize)}setPage(e){e<1||e>this.getTotalPages()||(this.currentPage=e,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageChange",{bubbles:!0})))}firstPage(){1!==this.currentPage&&this.setPage(1)}nextPage(){this.currentPage<this.getTotalPages()&&this.setPage(this.currentPage+1)}prevPage(){this.currentPage>1&&this.setPage(this.currentPage-1)}lastPage(){this.currentPage!==this.getTotalPages()&&this.setPage(this.getTotalPages())}setPageSize(e){this.pageSize=e,this.currentPage=1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageSizeChange",{bubbles:!0}))}getPageSize(){return this.pageSize}getPageSizeOptions(){return this.pageSizeOptions}getFieldLabel(e){const t=this.fields.find(t=>t.name===e);return t?t.label:toTitleCase(e)}setPageSizeOptions(e){this.pageSizeOptions=e,this.requestUpdate()}setData({records:e=!1,fields:t=!1,pageSize:s=!1,pageSizeOptions:i=!1,currentPage:r=!1,enableSelection:o}={}){let n=!1,l=this.getTotalPages(),a=this.currentPage;e&&(this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),n=!0),s&&(this.pageSize=s,n=!0),i&&(this.pageSizeOptions=i),r&&(this.currentPage=r,n=!0),void 0!==o&&(this.enableSelection=o,n=!0),n&&this.requestUpdate();const d=this.getTotalPages();d!==l&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),a>d&&this.setPage(d)}setRecords(e,t){let s=this.getTotalPages(),i=this.currentPage;this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordsSet",{detail:{records:e},bubbles:!0}));const r=this.getTotalPages();r!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),i>r&&this.setPage(r)}setupFetchRecords(e,t){const s=this.records.length,i=this.getTotalPages();if(s<e){this.records.length=e,this.records.fill(null,s),this.requestUpdate();const t=this.getTotalPages();t!==i&&setTimeout(()=>{this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:t},bubbles:!0}))},0)}this.addEventListener("fetchRecords",async e=>{if(this.fetchPending)return;this.fetchPending=!0;const{start:s,count:i}=e.detail,r=await t(s,i);r.forEach((e,t)=>{e[index]=s+t,void 0===e[selected]&&(e[selected]=!1),void 0===e[hidden]&&(e[hidden]=!1),void 0===e[editing]&&(e[editing]=!1)}),this.records.splice(s,r.length,...r),this.fetchPending=!1,this.requestUpdate()})}addRecord(e){e[selected]=!1,e[hidden]=!1,e[index]=this.records.length,this.records.push(e),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordAdded",{detail:{record:e},bubbles:!0}))}updateRecord(e,t){let s=!1,i=this.records.find(t=>t===e);if(i||void 0===e[index]||(i=this.records[e[index]]),Object.keys(t).forEach(e=>{i.hasOwnProperty(e)&&(i[e]=t[e],s=!0)}),s){const e=(this.currentPage-1)*this.pageSize,t=e+this.pageSize;(!this.enablePages||i[index]>=e&&i[index]<t)&&this.requestUpdate()}}deleteRecord(e){let t=this.records.find(t=>t===e);const s=this.getTotalPages();if(t||void 0===e[index]||(t=this.records[e[index]]),!t)return;const i=()=>{const e=this.records.indexOf(t);this.records.splice(e,1),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0})),this.dispatchEvent(new CustomEvent("recordDeleted",{detail:{index:e},bubbles:!0}));const i=this.getTotalPages();this.currentPage>i&&this.setPage(i),i!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:i},bubbles:!0}))};if(this.requestDelete){const e=this.shadowRoot.querySelector(`.record[data-index="${t[index]}"]`);e?.classList.add("pending");const s=()=>{e?.classList.remove("pending"),i()},r=()=>{e?.classList.remove("pending")};this.dispatchEvent(new CustomEvent("requestDelete",{detail:{records:[t],approve:s,reject:r},bubbles:!0}))}else i()}deleteSelected(){const e=this.getTotalPages(),t=this.getSelectedRecords().map(e=>this.records.find(t=>t===e)??(void 0!==e[index]?this.records[e[index]]:null)).filter(Boolean);if(!t.length)return;const s=()=>{t.forEach(e=>{const t=this.records.indexOf(e);-1!==t&&this.records.splice(t,1)}),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate();const s=this.getTotalPages();this.currentPage>s&&this.setPage(s),s!==e&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:s},bubbles:!0})),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))};if(this.requestDelete){const e=t.map(e=>this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`)).filter(Boolean);e.forEach(e=>e.classList.add("pending"));const i=()=>{e.forEach(e=>e.classList.remove("pending")),s()},r=()=>{e.forEach(e=>e.classList.remove("pending"))};this.dispatchEvent(new CustomEvent("requestDelete",{detail:{records:t,approve:i,reject:r},bubbles:!0}))}else s()}getSelectedRecords(){return this.records.filter(e=>e[selected])}selectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!0;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}deselectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!1;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}allOnPageSelected(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)if(!this.records[s][selected])return!1;return!0}sortBy(e,t=!0){this.sort=this.sort.filter(t=>t.name!==e),this.sort.push({name:e,asc:t}),this.requestUpdate()}hideRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!0,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})))}showRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})))}showAllRecords(){this.records.forEach(e=>{e[hidden]=!1}),this.filters.length&&(this.filters=[],this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0}))),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})),this.dispatchEvent(new CustomEvent("allRecordsShown",{bubbles:!0}))}addFilter(e,t,s){this.filters.push({field:e,condition:t,value:s}),this.dispatchEvent(new CustomEvent("filterAdded",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),this.requestUpdate()}removeFilter(e,t,s,i=!0){const r=this.filters.findIndex(i=>i.field===e&&i.condition===t&&i.value===s);-1!==r&&(this.records.forEach(i=>{this.testFilter(i,e,t,s)||(i[hidden]=!1)}),this.filters.splice(r,1),this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),i&&this.requestUpdate())}testFilter(e,t,s,i){let r=e[t],o=i;switch(this.caseSensitiveFilters||"string"!=typeof r||"string"!=typeof i||(r=r.toLowerCase(),o=i.toLowerCase()),s){case"equals":return r===o;case"not-equals":return r!==o;case"contains":return r.includes(o);case"not-contains":return!r.includes(o);case"greater-than":return r>o;case"less-than":return r<o;case"greater-than-or-equal":return r>=o;case"less-than-or-equal":return r<=o;default:return!0}}removeAllFilters(){this.filters.length&&([...this.filters].forEach(({field:e,condition:t,value:s})=>{this.removeFilter(e,t,s,!1)}),this.requestUpdate())}search(e){const t=e.trim().toLowerCase();let s=!1;this.records.forEach(e=>{if(e[hidden])return;let i=!1;this.fields.forEach(({name:s})=>{(e[s]?.toString().toLowerCase()||"").includes(t)&&(i=!0)}),e[hidden]!==!i&&(e[hidden]=!i,s=!0)}),s&&(this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})),this.requestUpdate()),this.dispatchEvent(new CustomEvent("search",{detail:{term:e},bubbles:!0}))}getDisplayedRecords(){this.filters.forEach(({field:e,condition:t,value:s})=>{this.records.forEach(i=>{null!==i&&(this.testFilter(i,e,t,s)||(i[hidden]=!0))})});let e=this.records.filter(e=>null===e||!e[hidden]);return this.sort.forEach(({name:t,asc:s})=>{e.sort((e,i)=>null===e||null===i?0:e[t]<i[t]?s?-1:1:e[t]>i[t]?s?1:-1:0)}),e}getHiddenRecords(){return this.records.filter(e=>e[hidden])}calculateColumnSizes(){const e=Array.from(this.querySelectorAll('[slot="before"]')),t=Array.from(this.querySelectorAll('[slot="after"]')),s={beforeControls:e.reduce((e,t)=>e+(t.maxWidth||40),0),afterControls:t.reduce((e,t)=>e+(t.maxWidth||40),0)},i=[...e,...t].some(e=>void 0===e.maxWidth);return JSON.stringify(this.columnSizes)!==JSON.stringify(s)&&(this.columnSizes=s),i&&setTimeout(()=>this.calculateColumnSizes(),0),this.columnSizes}setFieldHiddenState(e,t){const s=this.fields.find(t=>t.name===e);s&&(s.hidden=t,this.calculateColumnSizes(),this.requestUpdate(),this.dispatchEvent(new CustomEvent("fieldVisibilityChanged",{detail:{field:s},bubbles:!0})),this.dispatchEvent(new CustomEvent(t?"fieldHidden":"fieldShown",{detail:{field:s},bubbles:!0})))}hideField(e){this.setFieldHiddenState(e,!0)}showField(e){this.setFieldHiddenState(e,!1)}reorderFields(e){const t=[];e.forEach(e=>{const s=this.fields.find(t=>t.name===e);s&&t.push(s)}),this.fields=t,this.requestUpdate()}render(){return this.records&&this.fields?(this.calculateColumnSizes(),this.hasTopControls()?this.setAttribute("top-controls","true"):this.removeAttribute("top-controls"),this.hasBottomControls()?this.setAttribute("bottom-controls","true"):this.removeAttribute("bottom-controls"),html`
|
|
44
44
|
<div id="wrapper">
|
|
45
45
|
<div id="top"><slot name="top"></slot></div>
|
|
46
46
|
<div id="table-container">
|
|
@@ -122,10 +122,43 @@ import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowCompon
|
|
|
122
122
|
tbody tr:last-child td {
|
|
123
123
|
border-bottom: none;
|
|
124
124
|
}
|
|
125
|
+
tr.editing td.cell[data-field] {
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
tr.editing td.cell[data-field] input,
|
|
129
|
+
tr.editing td.cell[data-field] select {
|
|
130
|
+
width: 100%;
|
|
131
|
+
height: 100%;
|
|
132
|
+
box-sizing: border-box;
|
|
133
|
+
}
|
|
134
|
+
tr.pending {
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
}
|
|
137
|
+
tr.pending td {
|
|
138
|
+
position: relative;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
}
|
|
141
|
+
tr.pending td::before {
|
|
142
|
+
content: '';
|
|
143
|
+
position: absolute;
|
|
144
|
+
inset: 0;
|
|
145
|
+
background: linear-gradient(90deg, transparent, rgba(128, 128, 128, 0.15), transparent);
|
|
146
|
+
transform: translateX(-100%);
|
|
147
|
+
animation: row-pending 1.2s ease-in-out infinite;
|
|
148
|
+
}
|
|
149
|
+
@keyframes row-pending {
|
|
150
|
+
0% { transform: translateX(-100%); }
|
|
151
|
+
100% { transform: translateX(100%); }
|
|
152
|
+
}
|
|
125
153
|
th.controls,
|
|
126
154
|
td.controls {
|
|
127
155
|
padding: 0;
|
|
128
156
|
}
|
|
157
|
+
td.controls-after > div,
|
|
158
|
+
td.controls-before > div {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
}
|
|
129
162
|
.field-select,
|
|
130
163
|
.selection {
|
|
131
164
|
width: 40px;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import TableControl from"./TableControl.js";import{html}from"../../lit-all.min.js";import"../Icon.js";export default class DeleteSelected extends TableControl{deleteSelected=()=>{this.table.deleteSelected()};render(){return html`
|
|
2
|
+
<button class="no-btn icon-btn" title="Delete Selected" @click="${this.deleteSelected}">
|
|
3
|
+
<slot>
|
|
4
|
+
<k-icon name="delete_sweep"></k-icon>
|
|
5
|
+
</slot>
|
|
6
|
+
</button>
|
|
7
|
+
`}}customElements.define("k-tc-delete-selected",DeleteSelected);
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
<a href="./tableSorting.html">Sorting</a><br />
|
|
35
35
|
<a href="./tableFieldSortHide.html">Field Sorting and Hiding</a><br />
|
|
36
36
|
<a href="./tableFetchRecords.html">Fetching Records</a><br />
|
|
37
|
+
<a href="./tableServerSync.html">Server Sync</a><br />
|
|
37
38
|
<h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
|
|
38
39
|
<a href="#constructor">Constructor</a><br />
|
|
39
40
|
<a href="#requirements">Requirements</a><br />
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
<p>In HTML create a <code><k-table></code>, and then in JavaScript call the <code>setData</code> method passing in an options object containing your data.</p>
|
|
51
52
|
|
|
52
53
|
<k-card label="HTML">
|
|
53
|
-
<pre><code class="hljs xml"><span class="hljs-tag"><<span class="hljs-name">k-table</span> <span class="hljs-attr">id</span>=<span class="hljs-string">
|
|
54
|
+
<pre><code class="hljs xml"><span class="hljs-tag"><<span class="hljs-name">k-table</span> <span class="hljs-attr">id</span>=<span class="hljs-string">basicUsageExample</span>></span><span class="hljs-tag"></<span class="hljs-name">k-table</span>></span><br><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">module</span>></span><span class="language-javascript"><br> <span class="hljs-keyword">await</span> <span class="hljs-variable language_">window</span>.<span class="hljs-property">customElements</span>.<span class="hljs-title function_">whenDefined</span>(<span class="hljs-string">'k-table'</span>);<br> <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'basicUsageExample'</span>).<span class="hljs-title function_">setData</span>({<br> <span class="hljs-attr">records</span>: [{<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'Dustin'</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">'(111) 111-1111'</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">'dustin@mailserver.com'</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">'Male'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'Kayla'</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">'(222) 222-2222'</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">'kayla@mailserver.com'</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">'Female'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'Alexander'</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">'(333) 333-33333'</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">'alex@mailserver.com'</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">'Male'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'Amelia'</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">'(444) 444-44444'</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">'amelia@mailserver.com'</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">'Female'</span><br> }]<br> });<br></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></code></pre>
|
|
54
55
|
</k-card>
|
|
55
56
|
<k-card label="Results">
|
|
56
57
|
<k-table id="basicUsageExample"></k-table>
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
<li><a href="./tableSorting.html">Sorting</a></li>
|
|
102
103
|
<li><a href="./tableFieldSortHide.html">Field Sorting and Hiding</a></li>
|
|
103
104
|
<li><a href="./tableFetchRecords.html">Fetching Records</a></li>
|
|
105
|
+
<li><a href="./tableServerSync.html">Server Sync</a></li>
|
|
104
106
|
</ul>
|
|
105
107
|
|
|
106
108
|
<br />
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Server Sync - Table - Kempo Docs - A Web Components Solution</title>
|
|
7
|
+
<link rel="icon" type="image/png" sizes="48x48" href="../media/icon48.png">
|
|
8
|
+
<link rel="manifest" href="../manifest.json" />
|
|
9
|
+
<link rel="stylesheet" href="../kempo-vars.css" />
|
|
10
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kempo-css@1.3.11/dist/kempo.min.css" />
|
|
11
|
+
<link rel="stylesheet" href="../kempo-hljs.css" />
|
|
12
|
+
<link rel="stylesheet" href="../styles.css" />
|
|
13
|
+
<script>window.litDisableBundleWarning = true;</script>
|
|
14
|
+
<script>
|
|
15
|
+
window.contacts = [];
|
|
16
|
+
function generateContact(){
|
|
17
|
+
const maleNames = [
|
|
18
|
+
"Liam", "Noah", "Oliver", "Elijah", "William", "James", "Benjamin",
|
|
19
|
+
"Lucas", "Henry", "Alexander", "Mason", "Michael", "Ethan", "Daniel",
|
|
20
|
+
"Jacob", "Logan", "Jackson", "Levi", "Sebastian", "Mateo", "Jack",
|
|
21
|
+
"Owen", "Theodore", "Aiden", "Samuel", "Joseph", "John", "David",
|
|
22
|
+
"Wyatt", "Matthew"
|
|
23
|
+
];
|
|
24
|
+
const femaleNames = [
|
|
25
|
+
"Olivia", "Emma", "Ava", "Sophia", "Isabella", "Mia", "Amelia",
|
|
26
|
+
"Harper", "Evelyn", "Abigail", "Emily", "Ella", "Elizabeth",
|
|
27
|
+
"Camila", "Luna", "Sofia", "Avery", "Mila", "Aria", "Scarlett",
|
|
28
|
+
"Penelope", "Layla", "Chloe", "Victoria", "Madison", "Eleanor",
|
|
29
|
+
"Grace", "Nora", "Riley", "Zoey"
|
|
30
|
+
];
|
|
31
|
+
const lastNames = [
|
|
32
|
+
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
|
|
33
|
+
"Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez",
|
|
34
|
+
"Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
|
|
35
|
+
"Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark",
|
|
36
|
+
"Ramirez", "Lewis", "Robinson"
|
|
37
|
+
];
|
|
38
|
+
const gender = Math.random() > 0.5 ? 'm' : 'f';
|
|
39
|
+
const firstName = gender === 'm' ? maleNames[Math.floor(Math.random() * maleNames.length)] : femaleNames[Math.floor(Math.random() * femaleNames.length)];
|
|
40
|
+
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
|
41
|
+
const name = `${firstName} ${lastName}`;
|
|
42
|
+
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@mailserver.com`;
|
|
43
|
+
const birthday = `${Math.floor(Math.random() * 100) + 1920}-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`;
|
|
44
|
+
const phoneNumber = `(${Math.floor(Math.random() * 900) + 100}) ${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 9000) + 1000}`;
|
|
45
|
+
return { name, email, birthday, phoneNumber, gender };
|
|
46
|
+
}
|
|
47
|
+
for(let i = 0; i < 10; i++){
|
|
48
|
+
contacts.push(generateContact());
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<k-import src="../nav-1.inc.html"></k-import>
|
|
54
|
+
<h1 class="ta-center">Table - Server Sync</h1>
|
|
55
|
+
<main>
|
|
56
|
+
<a href="./table.html"><h5>Back to Table Component Documentation</h5></a>
|
|
57
|
+
|
|
58
|
+
<p>
|
|
59
|
+
The table supports an optional server-sync workflow for saving edits and deleting records. When the
|
|
60
|
+
<code>request-edit</code> attribute is present, calling <code>saveEditedRecord()</code> fires a
|
|
61
|
+
<code>requestSave</code> event instead of applying the changes immediately. When the <code>request-delete</code>
|
|
62
|
+
attribute is present, calling <code>deleteRecord()</code> or <code>deleteSelected()</code> fires a
|
|
63
|
+
<code>requestDelete</code> event instead of deleting immediately.
|
|
64
|
+
</p>
|
|
65
|
+
<p>
|
|
66
|
+
Both events expose an <code>approve()</code> function on <code>event.detail</code>. Call it once the server
|
|
67
|
+
operation succeeds to commit the change. If the operation fails, simply do not call <code>approve()</code> —
|
|
68
|
+
the record stays in its current state so the user can retry. <code>requestSave</code> also provides
|
|
69
|
+
<code>event.detail.record</code> (the original record) and <code>event.detail.newData</code> (the values
|
|
70
|
+
read from the editor inputs). <code>requestDelete</code> provides <code>event.detail.records</code>, an
|
|
71
|
+
array of the records targeted for deletion.
|
|
72
|
+
</p>
|
|
73
|
+
<p>
|
|
74
|
+
The example below mocks a server call that takes 1–3 seconds and randomly fails 50% of the time.
|
|
75
|
+
Toasts are used to notify the user of success or failure.
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
<pre><code class="hljs xml"><span class="hljs-tag"><<span class="hljs-name">k-table</span> <span class="hljs-attr">id</span>=<span class="hljs-string">serverSyncExample</span> <span class="hljs-attr">enable-selection</span> <span class="hljs-attr">request-edit</span> <span class="hljs-attr">request-delete</span>></span><span class="hljs-tag"><<span class="hljs-name">k-tc-edit</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">after</span>></span><span class="hljs-tag"></<span class="hljs-name">k-tc-edit</span>></span><span class="hljs-tag"><<span class="hljs-name">k-tc-delete-record</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">after</span>></span><span class="hljs-tag"></<span class="hljs-name">k-tc-delete-record</span>></span><span class="hljs-tag"><<span class="hljs-name">k-tc-delete-selected</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">top</span>></span><span class="hljs-tag"></<span class="hljs-name">k-tc-delete-selected</span>></span><span class="hljs-tag"></<span class="hljs-name">k-table</span>></span><br><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">module</span>></span><span class="language-javascript"><br> <span class="hljs-keyword">import</span> <span class="hljs-title class_">Toast</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'/src/components/Toast.js'</span>;<br> <span class="hljs-keyword">await</span> <span class="hljs-variable language_">window</span>.<span class="hljs-property">customElements</span>.<span class="hljs-title function_">whenDefined</span>(<span class="hljs-string">'k-table'</span>);<br> <span class="hljs-keyword">const</span> table = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'serverSyncExample'</span>);<br> <span class="hljs-keyword">const</span> <span class="hljs-title function_">mockServer</span> = (<span class="hljs-params"></span>) => <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =></span> {<br> <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =></span> {<br> <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">random</span>() < <span class="hljs-number">0.8</span> ? <span class="hljs-title function_">resolve</span>() : <span class="hljs-title function_">reject</span>();<br> }, <span class="hljs-number">1000</span> + <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">random</span>() * <span class="hljs-number">2000</span>);<br> });<br> table.<span class="hljs-title function_">setData</span>({<br> <span class="hljs-attr">records</span>: contacts,<br> <span class="hljs-attr">fields</span>: [{<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'name'</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">'Name'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'email'</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">'Email'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'birthday'</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">'Birthday'</span>,<br> <span class="hljs-attr">type</span>: <span class="hljs-string">'date'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'phoneNumber'</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">'Phone Number'</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">'gender'</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">'Gender'</span>,<br> <span class="hljs-attr">formatter</span>: <span class="hljs-function"><span class="hljs-params">v</span> =></span> v === <span class="hljs-string">'m'</span> ? <span class="hljs-string">'Male'</span> : <span class="hljs-string">'Female'</span><br> }]<br> });<br> table.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'requestSave'</span>, <span class="hljs-title function_">async</span> ({<br> <span class="hljs-attr">detail</span>: {<br> approve,<br> reject<br> }<br> }) => {<br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-keyword">await</span> <span class="hljs-title function_">mockServer</span>();<br> <span class="hljs-title function_">approve</span>();<br> <span class="hljs-title class_">Toast</span>.<span class="hljs-title function_">success</span>(<span class="hljs-string">'Record saved.'</span>, {<br> <span class="hljs-attr">timeout</span>: <span class="hljs-number">3000</span><br> });<br> } <span class="hljs-keyword">catch</span> {<br> <span class="hljs-title function_">reject</span>();<br> <span class="hljs-title class_">Toast</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">'Save failed. The record remains open for editing.'</span>, {<br> <span class="hljs-attr">timeout</span>: <span class="hljs-number">5000</span><br> });<br> }<br> });<br> table.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'requestDelete'</span>, <span class="hljs-title function_">async</span> ({<br> <span class="hljs-attr">detail</span>: {<br> records,<br> approve,<br> reject<br> }<br> }) => {<br> <span class="hljs-keyword">const</span> count = records.<span class="hljs-property">length</span>;<br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-keyword">await</span> <span class="hljs-title function_">mockServer</span>();<br> <span class="hljs-title function_">approve</span>();<br> <span class="hljs-title class_">Toast</span>.<span class="hljs-title function_">success</span>(count === <span class="hljs-number">1</span> ? <span class="hljs-string">'1 record deleted.'</span> : count + <span class="hljs-string">' records deleted.'</span>, {<br> <span class="hljs-attr">timeout</span>: <span class="hljs-number">3000</span><br> });<br> } <span class="hljs-keyword">catch</span> {<br> <span class="hljs-title function_">reject</span>();<br> <span class="hljs-title class_">Toast</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">'Delete failed. Please try again.'</span>, {<br> <span class="hljs-attr">timeout</span>: <span class="hljs-number">5000</span><br> });<br> }<br> });<br></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></code></pre>
|
|
79
|
+
<k-table
|
|
80
|
+
id="serverSyncExample"
|
|
81
|
+
enable-selection
|
|
82
|
+
request-edit
|
|
83
|
+
request-delete
|
|
84
|
+
>
|
|
85
|
+
<k-tc-edit slot="after"></k-tc-edit>
|
|
86
|
+
<k-tc-delete-record slot="after"></k-tc-delete-record>
|
|
87
|
+
<k-tc-delete-selected slot="top"></k-tc-delete-selected>
|
|
88
|
+
</k-table>
|
|
89
|
+
<script type="module">
|
|
90
|
+
import Toast from '../src/components/Toast.js';
|
|
91
|
+
await window.customElements.whenDefined('k-table');
|
|
92
|
+
const table = document.getElementById('serverSyncExample');
|
|
93
|
+
|
|
94
|
+
const mockServer = () => new Promise((resolve, reject) => {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
Math.random() < 0.8 ? resolve() : reject();
|
|
97
|
+
}, 1000 + Math.random() * 2000);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
table.setData({
|
|
101
|
+
records: contacts,
|
|
102
|
+
fields: [
|
|
103
|
+
{ name: 'name', label: 'Name' },
|
|
104
|
+
{ name: 'email', label: 'Email' },
|
|
105
|
+
{ name: 'birthday', label: 'Birthday', type: 'date' },
|
|
106
|
+
{ name: 'phoneNumber', label: 'Phone Number' },
|
|
107
|
+
{ name: 'gender', label: 'Gender', formatter: v => v === 'm' ? 'Male' : 'Female' }
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
table.addEventListener('requestSave', async ({ detail: { approve, reject } }) => {
|
|
112
|
+
try {
|
|
113
|
+
await mockServer();
|
|
114
|
+
approve();
|
|
115
|
+
Toast.success('Record saved.', { timeout: 3000 });
|
|
116
|
+
} catch {
|
|
117
|
+
reject();
|
|
118
|
+
Toast.error('Save failed. The record remains open for editing.', { timeout: 5000 });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
table.addEventListener('requestDelete', async ({ detail: { records, approve, reject } }) => {
|
|
123
|
+
const count = records.length;
|
|
124
|
+
try {
|
|
125
|
+
await mockServer();
|
|
126
|
+
approve();
|
|
127
|
+
Toast.success(count === 1 ? '1 record deleted.' : count + ' records deleted.', { timeout: 3000 });
|
|
128
|
+
} catch {
|
|
129
|
+
reject();
|
|
130
|
+
Toast.error('Delete failed. Please try again.', { timeout: 5000 });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
</main>
|
|
135
|
+
<div style="height: 33.333vh"></div>
|
|
136
|
+
<script type="module" src="../src/components/Import.js"></script>
|
|
137
|
+
<script type="module" src="../src/components/Table.js"></script>
|
|
138
|
+
<script type="module" src="../src/components/tableControls/Edit.js"></script>
|
|
139
|
+
<script type="module" src="../src/components/tableControls/DeleteRecord.js"></script>
|
|
140
|
+
<script type="module" src="../src/components/tableControls/DeleteSelected.js"></script>
|
|
141
|
+
<script type="module" src="../src/components/Toast.js"></script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M600-240v-80h160v80H600Zm0-320v-80h280v80H600Zm0 160v-80h240v80H600ZM120-640H80v-80h160v-60h160v60h160v80h-40v360q0 33-23.5 56.5T440-200H200q-33 0-56.5-23.5T120-280v-360Zm80 0v360h240v-360H200Zm0 0v360-360Z"/></svg>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{toTitleCase}from"../utils/string.js";import{boolExists}from"../utils/propConverters.js";const selected=Symbol("selected"),hidden=Symbol("hidden"),index=Symbol("index"),editing=Symbol("editing");export default class Table extends ShadowComponent{static properties={enablePages:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-pages"},pageSize:{type:Number,reflect:!0,attribute:"page-size"},currentPage:{type:Number,reflect:!0,attribute:"current-page"},pageSizeOptions:{type:Array,attribute:"page-size-options"},enableSelection:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-selection"},enableSorting:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-sorting"},caseSensitiveFilters:{type:Boolean,reflect:!0,converter:boolExists,attribute:"case-sensitive-filters"},fields:{type:Array},records:{type:Array},filters:{type:Array},sort:{type:Array},columnSizes:{type:Object},fetchPending:{type:Boolean}};constructor(e={}){super(),void 0===this.pageSize&&(this.pageSize=50),void 0===this.currentPage&&(this.currentPage=1),void 0===this.pageSizeOptions&&(this.pageSizeOptions=[10,25,50,100,500]),void 0===this.records&&(this.records=e.records||[]),void 0===this.fields&&(this.fields=e.fields||[]),void 0===this.filters&&(this.filters=e.filters||[]),void 0===this.sort&&(this.sort=[]),void 0===this.columnSizes&&(this.columnSizes={}),void 0===this.fetchPending&&(this.fetchPending=!1)}handleSelectAllChange=e=>{e.target.checked?this.selectAllOnPage():this.deselectAllOnPage()};handleFieldClick=e=>{const t=this.sort.find(t=>t.name===e),s=!t||!t.asc;this.sortBy(e,s)};handleRecordSelectionChange=(e,t)=>{e[selected]=!!t.target.checked,this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))};firstUpdated(){this.setData({records:this.records,fields:this.fields,filters:this.filters})}childrenUpdated(){this.requestUpdate()}updated(e){if(super.updated(e),this.enableSelection){const e=this.shadowRoot.getElementById("select-all");e&&(e.checked=this.allOnPageSelected())}}renderColgroupTemplate(){const e=[];return this.enableSelection&&e.push(html`<col style="width: 40px" />`),this.hasBeforeControls()&&e.push(html`<col style="width: ${this.columnSizes.beforeControls}px" />`),this.fields.forEach(({size:t,hidden:s})=>{s||e.push(t?html`<col style="width: ${t}px" />`:html`<col />`)}),this.hasAfterControls()&&e.push(html`<col style="width: ${this.columnSizes.afterControls}px" />`),e}getColumnCount(){let e=0;return this.enableSelection&&e++,this.hasBeforeControls()&&e++,this.fields.forEach(({hidden:t})=>{t||e++}),this.hasAfterControls()&&e++,e}renderFieldsTemplate(){const e=[];return this.enableSelection&&e.push(html`
|
|
1
|
+
import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{toTitleCase}from"../utils/string.js";import{boolExists}from"../utils/propConverters.js";const selected=Symbol("selected"),hidden=Symbol("hidden"),index=Symbol("index"),editing=Symbol("editing");export default class Table extends ShadowComponent{static properties={enablePages:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-pages"},pageSize:{type:Number,reflect:!0,attribute:"page-size"},currentPage:{type:Number,reflect:!0,attribute:"current-page"},pageSizeOptions:{type:Array,attribute:"page-size-options"},enableSelection:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-selection"},enableSorting:{type:Boolean,reflect:!0,converter:boolExists,attribute:"enable-sorting"},caseSensitiveFilters:{type:Boolean,reflect:!0,converter:boolExists,attribute:"case-sensitive-filters"},requestEdit:{type:Boolean,reflect:!0,converter:boolExists,attribute:"request-edit"},requestDelete:{type:Boolean,reflect:!0,converter:boolExists,attribute:"request-delete"},fields:{type:Array},records:{type:Array},filters:{type:Array},sort:{type:Array},columnSizes:{type:Object},fetchPending:{type:Boolean}};constructor(e={}){super(),void 0===this.pageSize&&(this.pageSize=50),void 0===this.currentPage&&(this.currentPage=1),void 0===this.pageSizeOptions&&(this.pageSizeOptions=[10,25,50,100,500]),void 0===this.records&&(this.records=e.records||[]),void 0===this.fields&&(this.fields=e.fields||[]),void 0===this.filters&&(this.filters=e.filters||[]),void 0===this.sort&&(this.sort=[]),void 0===this.columnSizes&&(this.columnSizes={}),void 0===this.fetchPending&&(this.fetchPending=!1),void 0===this.requestEdit&&(this.requestEdit=!1),void 0===this.requestDelete&&(this.requestDelete=!1)}handleSelectAllChange=e=>{e.target.checked?this.selectAllOnPage():this.deselectAllOnPage()};handleFieldClick=e=>{const t=this.sort.find(t=>t.name===e),s=!t||!t.asc;this.sortBy(e,s)};handleRecordSelectionChange=(e,t)=>{e[selected]=!!t.target.checked,this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))};firstUpdated(){this.setData({records:this.records,fields:this.fields,filters:this.filters})}childrenUpdated(){this.requestUpdate()}updated(e){if(super.updated(e),this.enableSelection){const e=this.shadowRoot.getElementById("select-all");e&&(e.checked=this.allOnPageSelected())}}renderColgroupTemplate(){const e=[];return this.enableSelection&&e.push(html`<col style="width: 40px" />`),this.hasBeforeControls()&&e.push(html`<col style="width: ${this.columnSizes.beforeControls}px" />`),this.fields.forEach(({size:t,hidden:s})=>{s||e.push(t?html`<col style="width: ${t}px" />`:html`<col />`)}),this.hasAfterControls()&&e.push(html`<col style="width: ${this.columnSizes.afterControls}px" />`),e}getColumnCount(){let e=0;return this.enableSelection&&e++,this.hasBeforeControls()&&e++,this.fields.forEach(({hidden:t})=>{t||e++}),this.hasAfterControls()&&e++,e}renderFieldsTemplate(){const e=[];return this.enableSelection&&e.push(html`
|
|
2
2
|
<th class="controls field-select">
|
|
3
3
|
<input type="checkbox" id="select-all" @change=${this.handleSelectAllChange} />
|
|
4
4
|
</th>
|
|
@@ -34,13 +34,13 @@ import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowCompon
|
|
|
34
34
|
</select>
|
|
35
35
|
`;default:return html`<input type="text" .value=${s} />`}}renderDisplayCell(e,t,s,i,r){return i?i(e,this):r?r(s):s}renderBeforeControlsTemplate(){const e=[];return this.querySelectorAll('[slot="before"]').forEach(t=>{const s=t.tagName.toLowerCase(),i=document.createElement(s);Array.from(t.attributes).forEach(e=>{"slot"!==e.name&&i.setAttribute(e.name,e.value)}),t.innerHTML&&(i.innerHTML=t.innerHTML),e.push(i)}),html`
|
|
36
36
|
<td class="cell controls controls-before">
|
|
37
|
-
|
|
37
|
+
<div>${e}</div>
|
|
38
38
|
</td>
|
|
39
39
|
`}renderAfterControlsTemplate(){const e=[];return this.querySelectorAll('[slot="after"]').forEach(t=>{const s=t.tagName.toLowerCase(),i=document.createElement(s);Array.from(t.attributes).forEach(e=>{"slot"!==e.name&&i.setAttribute(e.name,e.value)}),t.innerHTML&&(i.innerHTML=t.innerHTML),e.push(i)}),html`
|
|
40
40
|
<td class="cell controls controls-after">
|
|
41
|
-
|
|
41
|
+
<div>${e}</div>
|
|
42
42
|
</td>
|
|
43
|
-
`}hasBeforeControls(){return!!this.querySelector('[slot="before"]')}hasAfterControls(){return!!this.querySelector('[slot="after"]')}hasTopControls(){return!!this.querySelector('[slot="top"]')}hasBottomControls(){return!!this.querySelector(":scope > :not([slot])")}editRecord(e){e[editing]=!0;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.add("editing"),t.setAttribute("editing","true"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";if(t.innerHTML="",i.calculator){const s=document.createElement("input");s.disabled=!0,s.value=i.calculator(e,this),t.appendChild(s)}else if(i.editor)t.appendChild(i.editor(r));else{const e=i.type||typeof r,s=Table.editors[e]||Table.editors.string;t.appendChild(s(r))}}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!0},bubbles:!0}))}saveEditedRecord(e){const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i&&!i.calculator){const i=t.querySelector("input, select");i&&(e[s]=i.value)}}),e[editing]=!1,t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))}cancelEditedRecord(e){e[editing]=!1;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))}recordIsEditing(e){return e[editing]}getCurrentPage(){return this.currentPage}getTotalPages(){return Math.ceil(this.getDisplayedRecords().length/this.pageSize)}setPage(e){e<1||e>this.getTotalPages()||(this.currentPage=e,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageChange",{bubbles:!0})))}firstPage(){1!==this.currentPage&&this.setPage(1)}nextPage(){this.currentPage<this.getTotalPages()&&this.setPage(this.currentPage+1)}prevPage(){this.currentPage>1&&this.setPage(this.currentPage-1)}lastPage(){this.currentPage!==this.getTotalPages()&&this.setPage(this.getTotalPages())}setPageSize(e){this.pageSize=e,this.currentPage=1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageSizeChange",{bubbles:!0}))}getPageSize(){return this.pageSize}getPageSizeOptions(){return this.pageSizeOptions}getFieldLabel(e){const t=this.fields.find(t=>t.name===e);return t?t.label:toTitleCase(e)}setPageSizeOptions(e){this.pageSizeOptions=e,this.requestUpdate()}setData({records:e=!1,fields:t=!1,pageSize:s=!1,pageSizeOptions:i=!1,currentPage:r=!1,enableSelection:o}={}){let n=!1,l=this.getTotalPages(),a=this.currentPage;e&&(this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),n=!0),s&&(this.pageSize=s,n=!0),i&&(this.pageSizeOptions=i),r&&(this.currentPage=r,n=!0),void 0!==o&&(this.enableSelection=o,n=!0),n&&this.requestUpdate();const d=this.getTotalPages();d!==l&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),a>d&&this.setPage(d)}setRecords(e,t){let s=this.getTotalPages(),i=this.currentPage;this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordsSet",{detail:{records:e},bubbles:!0}));const r=this.getTotalPages();r!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),i>r&&this.setPage(r)}setupFetchRecords(e,t){const s=this.records.length,i=this.getTotalPages();if(s<e){this.records.length=e,this.records.fill(null,s),this.requestUpdate();const t=this.getTotalPages();t!==i&&setTimeout(()=>{this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:t},bubbles:!0}))},0)}this.addEventListener("fetchRecords",async e=>{if(this.fetchPending)return;this.fetchPending=!0;const{start:s,count:i}=e.detail,r=await t(s,i);r.forEach((e,t)=>{e[index]=s+t,void 0===e[selected]&&(e[selected]=!1),void 0===e[hidden]&&(e[hidden]=!1),void 0===e[editing]&&(e[editing]=!1)}),this.records.splice(s,r.length,...r),this.fetchPending=!1,this.requestUpdate()})}addRecord(e){e[selected]=!1,e[hidden]=!1,e[index]=this.records.length,this.records.push(e),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordAdded",{detail:{record:e},bubbles:!0}))}updateRecord(e,t){let s=!1,i=this.records.find(t=>t===e);if(i||void 0===e[index]||(i=this.records[e[index]]),Object.keys(t).forEach(e=>{i.hasOwnProperty(e)&&(i[e]=t[e],s=!0)}),s){const e=(this.currentPage-1)*this.pageSize,t=e+this.pageSize;(!this.enablePages||i[index]>=e&&i[index]<t)&&this.requestUpdate()}}deleteRecord(e){let t=this.records.find(t=>t===e),s=this.getTotalPages();if(t||void 0===e[index]||(t=this.records[e[index]]),t){const e=this.records.indexOf(t);this.records.splice(e,1),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0})),this.dispatchEvent(new CustomEvent("recordDeleted",{detail:{index:e},bubbles:!0}));const i=this.getTotalPages();this.currentPage>i&&this.setPage(i),i!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:i},bubbles:!0}))}}deleteSelected(){let e=this.getTotalPages();this.getSelectedRecords().forEach(e=>{let t=this.records.find(t=>t===e);if(t||void 0===e[index]||(t=this.records[e[index]]),t){const e=this.records.indexOf(t);this.records.splice(e,1)}}),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate();const t=this.getTotalPages();this.currentPage>t&&this.setPage(t),t!==e&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:t},bubbles:!0})),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))}getSelectedRecords(){return this.records.filter(e=>e[selected])}selectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!0;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}deselectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!1;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}allOnPageSelected(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)if(!this.records[s][selected])return!1;return!0}sortBy(e,t=!0){this.sort=this.sort.filter(t=>t.name!==e),this.sort.push({name:e,asc:t}),this.requestUpdate()}hideRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!0,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})))}showRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})))}showAllRecords(){this.records.forEach(e=>{e[hidden]=!1}),this.filters.length&&(this.filters=[],this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0}))),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})),this.dispatchEvent(new CustomEvent("allRecordsShown",{bubbles:!0}))}addFilter(e,t,s){this.filters.push({field:e,condition:t,value:s}),this.dispatchEvent(new CustomEvent("filterAdded",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),this.requestUpdate()}removeFilter(e,t,s,i=!0){const r=this.filters.findIndex(i=>i.field===e&&i.condition===t&&i.value===s);-1!==r&&(this.records.forEach(i=>{this.testFilter(i,e,t,s)||(i[hidden]=!1)}),this.filters.splice(r,1),this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),i&&this.requestUpdate())}testFilter(e,t,s,i){let r=e[t],o=i;switch(this.caseSensitiveFilters||"string"!=typeof r||"string"!=typeof i||(r=r.toLowerCase(),o=i.toLowerCase()),s){case"equals":return r===o;case"not-equals":return r!==o;case"contains":return r.includes(o);case"not-contains":return!r.includes(o);case"greater-than":return r>o;case"less-than":return r<o;case"greater-than-or-equal":return r>=o;case"less-than-or-equal":return r<=o;default:return!0}}removeAllFilters(){this.filters.length&&([...this.filters].forEach(({field:e,condition:t,value:s})=>{this.removeFilter(e,t,s,!1)}),this.requestUpdate())}search(e){const t=e.trim().toLowerCase();let s=!1;this.records.forEach(e=>{if(e[hidden])return;let i=!1;this.fields.forEach(({name:s})=>{(e[s]?.toString().toLowerCase()||"").includes(t)&&(i=!0)}),e[hidden]!==!i&&(e[hidden]=!i,s=!0)}),s&&(this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})),this.requestUpdate()),this.dispatchEvent(new CustomEvent("search",{detail:{term:e},bubbles:!0}))}getDisplayedRecords(){this.filters.forEach(({field:e,condition:t,value:s})=>{this.records.forEach(i=>{null!==i&&(this.testFilter(i,e,t,s)||(i[hidden]=!0))})});let e=this.records.filter(e=>null===e||!e[hidden]);return this.sort.forEach(({name:t,asc:s})=>{e.sort((e,i)=>null===e||null===i?0:e[t]<i[t]?s?-1:1:e[t]>i[t]?s?1:-1:0)}),e}getHiddenRecords(){return this.records.filter(e=>e[hidden])}calculateColumnSizes(){const e=Array.from(this.querySelectorAll('[slot="before"]')),t=Array.from(this.querySelectorAll('[slot="after"]')),s={beforeControls:e.reduce((e,t)=>e+(t.maxWidth||40),0),afterControls:t.reduce((e,t)=>e+(t.maxWidth||40),0)},i=[...e,...t].some(e=>void 0===e.maxWidth);return JSON.stringify(this.columnSizes)!==JSON.stringify(s)&&(this.columnSizes=s),i&&setTimeout(()=>this.calculateColumnSizes(),0),this.columnSizes}setFieldHiddenState(e,t){const s=this.fields.find(t=>t.name===e);s&&(s.hidden=t,this.calculateColumnSizes(),this.requestUpdate(),this.dispatchEvent(new CustomEvent("fieldVisibilityChanged",{detail:{field:s},bubbles:!0})),this.dispatchEvent(new CustomEvent(t?"fieldHidden":"fieldShown",{detail:{field:s},bubbles:!0})))}hideField(e){this.setFieldHiddenState(e,!0)}showField(e){this.setFieldHiddenState(e,!1)}reorderFields(e){const t=[];e.forEach(e=>{const s=this.fields.find(t=>t.name===e);s&&t.push(s)}),this.fields=t,this.requestUpdate()}render(){return this.records&&this.fields?(this.calculateColumnSizes(),this.hasTopControls()?this.setAttribute("top-controls","true"):this.removeAttribute("top-controls"),this.hasBottomControls()?this.setAttribute("bottom-controls","true"):this.removeAttribute("bottom-controls"),html`
|
|
43
|
+
`}hasBeforeControls(){return!!this.querySelector('[slot="before"]')}hasAfterControls(){return!!this.querySelector('[slot="after"]')}hasTopControls(){return!!this.querySelector('[slot="top"]')}hasBottomControls(){return!!this.querySelector(":scope > :not([slot])")}editRecord(e){e[editing]=!0;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.add("editing"),t.setAttribute("editing","true"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";if(t.innerHTML="",i.calculator){const s=document.createElement("input");s.disabled=!0,s.value=i.calculator(e,this),t.appendChild(s)}else if(i.editor)t.appendChild(i.editor(r));else{const e=i.type||typeof r,s=Table.editors[e]||Table.editors.string;t.appendChild(s(r))}}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!0},bubbles:!0}))}saveEditedRecord(e){const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`),s={};t&&t.querySelectorAll(".cell[data-field]").forEach(e=>{const t=e.dataset.field,i=this.fields.find(e=>e.name===t);if(i&&!i.calculator){const i=e.querySelector("input, select");i&&(s[t]=i.value)}});const i=()=>{Object.assign(e,s),e[editing]=!1,t&&(t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))};if(Object.keys(s).some(t=>String(e[t]??"")!==s[t]))if(this.requestEdit){t?.classList.add("pending");const r=()=>{t?.classList.remove("pending"),i()},o=()=>{t?.classList.remove("pending")};this.dispatchEvent(new CustomEvent("requestSave",{detail:{record:e,newData:s,approve:r,reject:o},bubbles:!0}))}else i();else this.cancelEditedRecord(e)}cancelEditedRecord(e){e[editing]=!1;const t=this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`);t&&(t.classList.remove("editing"),t.removeAttribute("editing"),t.querySelectorAll(".cell[data-field]").forEach(t=>{const s=t.dataset.field,i=this.fields.find(e=>e.name===s);if(i){const r=e[s]||"";i.calculator?t.textContent=i.calculator(e,this):i.formatter?t.innerHTML=i.formatter(r):t.textContent=r}})),this.dispatchEvent(new CustomEvent("editingChange",{detail:{record:e,editing:!1},bubbles:!0}))}recordIsEditing(e){return e[editing]}getCurrentPage(){return this.currentPage}getTotalPages(){return Math.ceil(this.getDisplayedRecords().length/this.pageSize)}setPage(e){e<1||e>this.getTotalPages()||(this.currentPage=e,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageChange",{bubbles:!0})))}firstPage(){1!==this.currentPage&&this.setPage(1)}nextPage(){this.currentPage<this.getTotalPages()&&this.setPage(this.currentPage+1)}prevPage(){this.currentPage>1&&this.setPage(this.currentPage-1)}lastPage(){this.currentPage!==this.getTotalPages()&&this.setPage(this.getTotalPages())}setPageSize(e){this.pageSize=e,this.currentPage=1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("pageSizeChange",{bubbles:!0}))}getPageSize(){return this.pageSize}getPageSizeOptions(){return this.pageSizeOptions}getFieldLabel(e){const t=this.fields.find(t=>t.name===e);return t?t.label:toTitleCase(e)}setPageSizeOptions(e){this.pageSizeOptions=e,this.requestUpdate()}setData({records:e=!1,fields:t=!1,pageSize:s=!1,pageSizeOptions:i=!1,currentPage:r=!1,enableSelection:o}={}){let n=!1,l=this.getTotalPages(),a=this.currentPage;e&&(this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),n=!0),s&&(this.pageSize=s,n=!0),i&&(this.pageSizeOptions=i),r&&(this.currentPage=r,n=!0),void 0!==o&&(this.enableSelection=o,n=!0),n&&this.requestUpdate();const d=this.getTotalPages();d!==l&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),a>d&&this.setPage(d)}setRecords(e,t){let s=this.getTotalPages(),i=this.currentPage;this.records=e.map(e=>({...e})),this.records.forEach((e,t)=>{e[index]=t,e[selected]=!1,e[hidden]=!1,e[editing]=!1}),this.fields=t||Table.extractFieldsFromRecords(this.records),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordsSet",{detail:{records:e},bubbles:!0}));const r=this.getTotalPages();r!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:this.getTotalPages()},bubbles:!0})),i>r&&this.setPage(r)}setupFetchRecords(e,t){const s=this.records.length,i=this.getTotalPages();if(s<e){this.records.length=e,this.records.fill(null,s),this.requestUpdate();const t=this.getTotalPages();t!==i&&setTimeout(()=>{this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:t},bubbles:!0}))},0)}this.addEventListener("fetchRecords",async e=>{if(this.fetchPending)return;this.fetchPending=!0;const{start:s,count:i}=e.detail,r=await t(s,i);r.forEach((e,t)=>{e[index]=s+t,void 0===e[selected]&&(e[selected]=!1),void 0===e[hidden]&&(e[hidden]=!1),void 0===e[editing]&&(e[editing]=!1)}),this.records.splice(s,r.length,...r),this.fetchPending=!1,this.requestUpdate()})}addRecord(e){e[selected]=!1,e[hidden]=!1,e[index]=this.records.length,this.records.push(e),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordAdded",{detail:{record:e},bubbles:!0}))}updateRecord(e,t){let s=!1,i=this.records.find(t=>t===e);if(i||void 0===e[index]||(i=this.records[e[index]]),Object.keys(t).forEach(e=>{i.hasOwnProperty(e)&&(i[e]=t[e],s=!0)}),s){const e=(this.currentPage-1)*this.pageSize,t=e+this.pageSize;(!this.enablePages||i[index]>=e&&i[index]<t)&&this.requestUpdate()}}deleteRecord(e){let t=this.records.find(t=>t===e);const s=this.getTotalPages();if(t||void 0===e[index]||(t=this.records[e[index]]),!t)return;const i=()=>{const e=this.records.indexOf(t);this.records.splice(e,1),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate(),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0})),this.dispatchEvent(new CustomEvent("recordDeleted",{detail:{index:e},bubbles:!0}));const i=this.getTotalPages();this.currentPage>i&&this.setPage(i),i!==s&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:i},bubbles:!0}))};if(this.requestDelete){const e=this.shadowRoot.querySelector(`.record[data-index="${t[index]}"]`);e?.classList.add("pending");const s=()=>{e?.classList.remove("pending"),i()},r=()=>{e?.classList.remove("pending")};this.dispatchEvent(new CustomEvent("requestDelete",{detail:{records:[t],approve:s,reject:r},bubbles:!0}))}else i()}deleteSelected(){const e=this.getTotalPages(),t=this.getSelectedRecords().map(e=>this.records.find(t=>t===e)??(void 0!==e[index]?this.records[e[index]]:null)).filter(Boolean);if(!t.length)return;const s=()=>{t.forEach(e=>{const t=this.records.indexOf(e);-1!==t&&this.records.splice(t,1)}),this.records.forEach((e,t)=>{e[index]=t}),this.requestUpdate();const s=this.getTotalPages();this.currentPage>s&&this.setPage(s),s!==e&&this.dispatchEvent(new CustomEvent("pageCountChanged",{detail:{totalPages:s},bubbles:!0})),this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))};if(this.requestDelete){const e=t.map(e=>this.shadowRoot.querySelector(`.record[data-index="${e[index]}"]`)).filter(Boolean);e.forEach(e=>e.classList.add("pending"));const i=()=>{e.forEach(e=>e.classList.remove("pending")),s()},r=()=>{e.forEach(e=>e.classList.remove("pending"))};this.dispatchEvent(new CustomEvent("requestDelete",{detail:{records:t,approve:i,reject:r},bubbles:!0}))}else s()}getSelectedRecords(){return this.records.filter(e=>e[selected])}selectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!0;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}deselectAllOnPage(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)this.records[s][selected]=!1;this.requestUpdate(),setTimeout(()=>{this.dispatchEvent(new CustomEvent("selectionChange",{bubbles:!0}))},0)}allOnPageSelected(){const e=(this.currentPage-1)*this.pageSize,t=Math.min(e+this.pageSize,this.records.length);for(let s=e;s<t;s++)if(!this.records[s][selected])return!1;return!0}sortBy(e,t=!0){this.sort=this.sort.filter(t=>t.name!==e),this.sort.push({name:e,asc:t}),this.requestUpdate()}hideRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!0,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})))}showRecord(e){let t=this.records.find(t=>t===e);t||void 0===e[index]||(t=this.records[e[index]]),t&&(t[hidden]=!1,this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})))}showAllRecords(){this.records.forEach(e=>{e[hidden]=!1}),this.filters.length&&(this.filters=[],this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0}))),this.requestUpdate(),this.dispatchEvent(new CustomEvent("recordShown",{bubbles:!0})),this.dispatchEvent(new CustomEvent("allRecordsShown",{bubbles:!0}))}addFilter(e,t,s){this.filters.push({field:e,condition:t,value:s}),this.dispatchEvent(new CustomEvent("filterAdded",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),this.requestUpdate()}removeFilter(e,t,s,i=!0){const r=this.filters.findIndex(i=>i.field===e&&i.condition===t&&i.value===s);-1!==r&&(this.records.forEach(i=>{this.testFilter(i,e,t,s)||(i[hidden]=!1)}),this.filters.splice(r,1),this.dispatchEvent(new CustomEvent("filterRemoved",{bubbles:!0})),this.dispatchEvent(new CustomEvent("filterChange",{bubbles:!0})),i&&this.requestUpdate())}testFilter(e,t,s,i){let r=e[t],o=i;switch(this.caseSensitiveFilters||"string"!=typeof r||"string"!=typeof i||(r=r.toLowerCase(),o=i.toLowerCase()),s){case"equals":return r===o;case"not-equals":return r!==o;case"contains":return r.includes(o);case"not-contains":return!r.includes(o);case"greater-than":return r>o;case"less-than":return r<o;case"greater-than-or-equal":return r>=o;case"less-than-or-equal":return r<=o;default:return!0}}removeAllFilters(){this.filters.length&&([...this.filters].forEach(({field:e,condition:t,value:s})=>{this.removeFilter(e,t,s,!1)}),this.requestUpdate())}search(e){const t=e.trim().toLowerCase();let s=!1;this.records.forEach(e=>{if(e[hidden])return;let i=!1;this.fields.forEach(({name:s})=>{(e[s]?.toString().toLowerCase()||"").includes(t)&&(i=!0)}),e[hidden]!==!i&&(e[hidden]=!i,s=!0)}),s&&(this.dispatchEvent(new CustomEvent("recordHidden",{bubbles:!0})),this.requestUpdate()),this.dispatchEvent(new CustomEvent("search",{detail:{term:e},bubbles:!0}))}getDisplayedRecords(){this.filters.forEach(({field:e,condition:t,value:s})=>{this.records.forEach(i=>{null!==i&&(this.testFilter(i,e,t,s)||(i[hidden]=!0))})});let e=this.records.filter(e=>null===e||!e[hidden]);return this.sort.forEach(({name:t,asc:s})=>{e.sort((e,i)=>null===e||null===i?0:e[t]<i[t]?s?-1:1:e[t]>i[t]?s?1:-1:0)}),e}getHiddenRecords(){return this.records.filter(e=>e[hidden])}calculateColumnSizes(){const e=Array.from(this.querySelectorAll('[slot="before"]')),t=Array.from(this.querySelectorAll('[slot="after"]')),s={beforeControls:e.reduce((e,t)=>e+(t.maxWidth||40),0),afterControls:t.reduce((e,t)=>e+(t.maxWidth||40),0)},i=[...e,...t].some(e=>void 0===e.maxWidth);return JSON.stringify(this.columnSizes)!==JSON.stringify(s)&&(this.columnSizes=s),i&&setTimeout(()=>this.calculateColumnSizes(),0),this.columnSizes}setFieldHiddenState(e,t){const s=this.fields.find(t=>t.name===e);s&&(s.hidden=t,this.calculateColumnSizes(),this.requestUpdate(),this.dispatchEvent(new CustomEvent("fieldVisibilityChanged",{detail:{field:s},bubbles:!0})),this.dispatchEvent(new CustomEvent(t?"fieldHidden":"fieldShown",{detail:{field:s},bubbles:!0})))}hideField(e){this.setFieldHiddenState(e,!0)}showField(e){this.setFieldHiddenState(e,!1)}reorderFields(e){const t=[];e.forEach(e=>{const s=this.fields.find(t=>t.name===e);s&&t.push(s)}),this.fields=t,this.requestUpdate()}render(){return this.records&&this.fields?(this.calculateColumnSizes(),this.hasTopControls()?this.setAttribute("top-controls","true"):this.removeAttribute("top-controls"),this.hasBottomControls()?this.setAttribute("bottom-controls","true"):this.removeAttribute("bottom-controls"),html`
|
|
44
44
|
<div id="wrapper">
|
|
45
45
|
<div id="top"><slot name="top"></slot></div>
|
|
46
46
|
<div id="table-container">
|
|
@@ -122,10 +122,43 @@ import{html,css,unsafeStatic,literal}from"../lit-all.min.js";import ShadowCompon
|
|
|
122
122
|
tbody tr:last-child td {
|
|
123
123
|
border-bottom: none;
|
|
124
124
|
}
|
|
125
|
+
tr.editing td.cell[data-field] {
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
tr.editing td.cell[data-field] input,
|
|
129
|
+
tr.editing td.cell[data-field] select {
|
|
130
|
+
width: 100%;
|
|
131
|
+
height: 100%;
|
|
132
|
+
box-sizing: border-box;
|
|
133
|
+
}
|
|
134
|
+
tr.pending {
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
}
|
|
137
|
+
tr.pending td {
|
|
138
|
+
position: relative;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
}
|
|
141
|
+
tr.pending td::before {
|
|
142
|
+
content: '';
|
|
143
|
+
position: absolute;
|
|
144
|
+
inset: 0;
|
|
145
|
+
background: linear-gradient(90deg, transparent, rgba(128, 128, 128, 0.15), transparent);
|
|
146
|
+
transform: translateX(-100%);
|
|
147
|
+
animation: row-pending 1.2s ease-in-out infinite;
|
|
148
|
+
}
|
|
149
|
+
@keyframes row-pending {
|
|
150
|
+
0% { transform: translateX(-100%); }
|
|
151
|
+
100% { transform: translateX(100%); }
|
|
152
|
+
}
|
|
125
153
|
th.controls,
|
|
126
154
|
td.controls {
|
|
127
155
|
padding: 0;
|
|
128
156
|
}
|
|
157
|
+
td.controls-after > div,
|
|
158
|
+
td.controls-before > div {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
}
|
|
129
162
|
.field-select,
|
|
130
163
|
.selection {
|
|
131
164
|
width: 40px;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import TableControl from"./TableControl.js";import{html}from"../../lit-all.min.js";import"../Icon.js";export default class DeleteSelected extends TableControl{deleteSelected=()=>{this.table.deleteSelected()};render(){return html`
|
|
2
|
+
<button class="no-btn icon-btn" title="Delete Selected" @click="${this.deleteSelected}">
|
|
3
|
+
<slot>
|
|
4
|
+
<k-icon name="delete_sweep"></k-icon>
|
|
5
|
+
</slot>
|
|
6
|
+
</button>
|
|
7
|
+
`}}customElements.define("k-tc-delete-selected",DeleteSelected);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M600-240v-80h160v80H600Zm0-320v-80h280v80H600Zm0 160v-80h240v80H600ZM120-640H80v-80h160v-60h160v60h160v80h-40v360q0 33-23.5 56.5T440-200H200q-33 0-56.5-23.5T120-280v-360Zm80 0v360h240v-360H200Zm0 0v360-360Z"/></svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.80",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A Lit based web-component library",
|
|
6
6
|
"main": "index.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"homepage": "https://github.com/dustinpoissant/kempo-ui#readme",
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"highlight.js": "^11.11.1",
|
|
39
|
+
"js-beautify": "^1.15.4",
|
|
39
40
|
"kempo-server": "^1.8.2",
|
|
40
41
|
"kempo-testing-framework": "^1.4.0",
|
|
41
42
|
"terser": "^5.43.1"
|
package/src/components/Table.js
CHANGED
|
@@ -17,6 +17,8 @@ export default class Table extends ShadowComponent {
|
|
|
17
17
|
enableSelection: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-selection' },
|
|
18
18
|
enableSorting: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-sorting' },
|
|
19
19
|
caseSensitiveFilters: { type: Boolean, reflect: true, converter: boolExists, attribute: 'case-sensitive-filters' },
|
|
20
|
+
requestEdit: { type: Boolean, reflect: true, converter: boolExists, attribute: 'request-edit' },
|
|
21
|
+
requestDelete: { type: Boolean, reflect: true, converter: boolExists, attribute: 'request-delete' },
|
|
20
22
|
fields: { type: Array },
|
|
21
23
|
records: { type: Array },
|
|
22
24
|
filters: { type: Array },
|
|
@@ -38,6 +40,8 @@ export default class Table extends ShadowComponent {
|
|
|
38
40
|
if(this.sort === undefined) this.sort = [];
|
|
39
41
|
if(this.columnSizes === undefined) this.columnSizes = {};
|
|
40
42
|
if(this.fetchPending === undefined) this.fetchPending = false;
|
|
43
|
+
if(this.requestEdit === undefined) this.requestEdit = false;
|
|
44
|
+
if(this.requestDelete === undefined) this.requestDelete = false;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/*
|
|
@@ -276,7 +280,7 @@ export default class Table extends ShadowComponent {
|
|
|
276
280
|
});
|
|
277
281
|
return html`
|
|
278
282
|
<td class="cell controls controls-before">
|
|
279
|
-
|
|
283
|
+
<div>${controls}</div>
|
|
280
284
|
</td>
|
|
281
285
|
`;
|
|
282
286
|
}
|
|
@@ -294,7 +298,7 @@ export default class Table extends ShadowComponent {
|
|
|
294
298
|
});
|
|
295
299
|
return html`
|
|
296
300
|
<td class="cell controls controls-after">
|
|
297
|
-
|
|
301
|
+
<div>${controls}</div>
|
|
298
302
|
</td>
|
|
299
303
|
`;
|
|
300
304
|
}
|
|
@@ -358,39 +362,59 @@ export default class Table extends ShadowComponent {
|
|
|
358
362
|
|
|
359
363
|
saveEditedRecord(record) {
|
|
360
364
|
const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
|
|
365
|
+
const newData = {};
|
|
361
366
|
if(recordEl){
|
|
362
367
|
recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
|
|
363
368
|
const field = $cell.dataset.field;
|
|
364
369
|
const fieldDef = this.fields.find(f => f.name === field);
|
|
365
370
|
if(fieldDef && !fieldDef.calculator){
|
|
366
371
|
const $input = $cell.querySelector('input, select');
|
|
367
|
-
if($input)
|
|
368
|
-
record[field] = $input.value;
|
|
369
|
-
}
|
|
372
|
+
if($input) newData[field] = $input.value;
|
|
370
373
|
}
|
|
371
374
|
});
|
|
375
|
+
}
|
|
376
|
+
const commit = () => {
|
|
377
|
+
Object.assign(record, newData);
|
|
372
378
|
record[editing] = false;
|
|
373
|
-
recordEl
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
if(recordEl){
|
|
380
|
+
recordEl.classList.remove('editing');
|
|
381
|
+
recordEl.removeAttribute('editing');
|
|
382
|
+
recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
|
|
383
|
+
const field = $cell.dataset.field;
|
|
384
|
+
const fieldDef = this.fields.find(f => f.name === field);
|
|
385
|
+
if(fieldDef){
|
|
386
|
+
const value = record[field] || '';
|
|
387
|
+
if(fieldDef.calculator){
|
|
388
|
+
$cell.textContent = fieldDef.calculator(record, this);
|
|
389
|
+
} else if(fieldDef.formatter){
|
|
390
|
+
$cell.innerHTML = fieldDef.formatter(value);
|
|
391
|
+
} else {
|
|
392
|
+
$cell.textContent = value;
|
|
393
|
+
}
|
|
386
394
|
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
this.dispatchEvent(new CustomEvent('editingChange', {
|
|
398
|
+
detail: { record, editing: false },
|
|
399
|
+
bubbles: true
|
|
400
|
+
}));
|
|
401
|
+
};
|
|
402
|
+
const hasChanges = Object.keys(newData).some(key => String(record[key] ?? '') !== newData[key]);
|
|
403
|
+
if(!hasChanges){
|
|
404
|
+
this.cancelEditedRecord(record);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if(this.requestEdit){
|
|
408
|
+
recordEl?.classList.add('pending');
|
|
409
|
+
const wrappedCommit = () => { recordEl?.classList.remove('pending'); commit(); };
|
|
410
|
+
const reject = () => { recordEl?.classList.remove('pending'); };
|
|
411
|
+
this.dispatchEvent(new CustomEvent('requestSave', {
|
|
412
|
+
detail: { record, newData, approve: wrappedCommit, reject },
|
|
413
|
+
bubbles: true
|
|
414
|
+
}));
|
|
415
|
+
} else {
|
|
416
|
+
commit();
|
|
389
417
|
}
|
|
390
|
-
this.dispatchEvent(new CustomEvent('editingChange', {
|
|
391
|
-
detail: { record, editing: false },
|
|
392
|
-
bubbles: true
|
|
393
|
-
}));
|
|
394
418
|
}
|
|
395
419
|
|
|
396
420
|
cancelEditedRecord(record) {
|
|
@@ -649,69 +673,79 @@ export default class Table extends ShadowComponent {
|
|
|
649
673
|
|
|
650
674
|
deleteRecord(record) {
|
|
651
675
|
let originalRecord = this.records.find(r => r === record);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (originalRecord) {
|
|
676
|
+
const totalPagesBefore = this.getTotalPages();
|
|
677
|
+
if(!originalRecord && record[index] !== undefined) originalRecord = this.records[record[index]];
|
|
678
|
+
if(!originalRecord) return;
|
|
679
|
+
const commit = () => {
|
|
659
680
|
const recordIndex = this.records.indexOf(originalRecord);
|
|
660
681
|
this.records.splice(recordIndex, 1);
|
|
661
|
-
this.records.forEach((rec, idx) => {
|
|
662
|
-
rec[index] = idx;
|
|
663
|
-
});
|
|
682
|
+
this.records.forEach((rec, idx) => { rec[index] = idx; });
|
|
664
683
|
this.requestUpdate();
|
|
665
684
|
this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
|
|
666
|
-
this.dispatchEvent(new CustomEvent('recordDeleted', {
|
|
685
|
+
this.dispatchEvent(new CustomEvent('recordDeleted', {
|
|
667
686
|
detail: { index: recordIndex },
|
|
668
|
-
bubbles: true
|
|
687
|
+
bubbles: true
|
|
669
688
|
}));
|
|
670
|
-
|
|
671
689
|
const totalPages = this.getTotalPages();
|
|
672
|
-
if
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (totalPages !== totalPagesBefore) {
|
|
676
|
-
this.dispatchEvent(new CustomEvent('pageCountChanged', {
|
|
690
|
+
if(this.currentPage > totalPages) this.setPage(totalPages);
|
|
691
|
+
if(totalPages !== totalPagesBefore){
|
|
692
|
+
this.dispatchEvent(new CustomEvent('pageCountChanged', {
|
|
677
693
|
detail: { totalPages },
|
|
678
|
-
bubbles: true
|
|
694
|
+
bubbles: true
|
|
679
695
|
}));
|
|
680
696
|
}
|
|
697
|
+
};
|
|
698
|
+
if(this.requestDelete){
|
|
699
|
+
const recordEl = this.shadowRoot.querySelector(`.record[data-index="${originalRecord[index]}"]`);
|
|
700
|
+
recordEl?.classList.add('pending');
|
|
701
|
+
const wrappedCommit = () => { recordEl?.classList.remove('pending'); commit(); };
|
|
702
|
+
const reject = () => { recordEl?.classList.remove('pending'); };
|
|
703
|
+
this.dispatchEvent(new CustomEvent('requestDelete', {
|
|
704
|
+
detail: { records: [originalRecord], approve: wrappedCommit, reject },
|
|
705
|
+
bubbles: true
|
|
706
|
+
}));
|
|
707
|
+
} else {
|
|
708
|
+
commit();
|
|
681
709
|
}
|
|
682
710
|
}
|
|
683
711
|
|
|
684
712
|
deleteSelected() {
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
713
|
+
const totalPagesBefore = this.getTotalPages();
|
|
714
|
+
const originalRecords = this.getSelectedRecords()
|
|
715
|
+
.map(record => this.records.find(r => r === record) ?? (record[index] !== undefined ? this.records[record[index]] : null))
|
|
716
|
+
.filter(Boolean);
|
|
717
|
+
if(!originalRecords.length) return;
|
|
718
|
+
const commit = () => {
|
|
719
|
+
originalRecords.forEach(record => {
|
|
720
|
+
const i = this.records.indexOf(record);
|
|
721
|
+
if(i !== -1) this.records.splice(i, 1);
|
|
722
|
+
});
|
|
723
|
+
this.records.forEach((rec, idx) => { rec[index] = idx; });
|
|
724
|
+
this.requestUpdate();
|
|
725
|
+
const totalPages = this.getTotalPages();
|
|
726
|
+
if(this.currentPage > totalPages) this.setPage(totalPages);
|
|
727
|
+
if(totalPages !== totalPagesBefore){
|
|
728
|
+
this.dispatchEvent(new CustomEvent('pageCountChanged', {
|
|
729
|
+
detail: { totalPages },
|
|
730
|
+
bubbles: true
|
|
731
|
+
}));
|
|
696
732
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
this.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
this.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
this.dispatchEvent(new CustomEvent('pageCountChanged', {
|
|
710
|
-
detail: { totalPages },
|
|
711
|
-
bubbles: true
|
|
733
|
+
this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
|
|
734
|
+
};
|
|
735
|
+
if(this.requestDelete){
|
|
736
|
+
const rowEls = originalRecords
|
|
737
|
+
.map(r => this.shadowRoot.querySelector(`.record[data-index="${r[index]}"]`))
|
|
738
|
+
.filter(Boolean);
|
|
739
|
+
rowEls.forEach(el => el.classList.add('pending'));
|
|
740
|
+
const wrappedCommit = () => { rowEls.forEach(el => el.classList.remove('pending')); commit(); };
|
|
741
|
+
const reject = () => { rowEls.forEach(el => el.classList.remove('pending')); };
|
|
742
|
+
this.dispatchEvent(new CustomEvent('requestDelete', {
|
|
743
|
+
detail: { records: originalRecords, approve: wrappedCommit, reject },
|
|
744
|
+
bubbles: true
|
|
712
745
|
}));
|
|
746
|
+
} else {
|
|
747
|
+
commit();
|
|
713
748
|
}
|
|
714
|
-
this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
|
|
715
749
|
}
|
|
716
750
|
|
|
717
751
|
getSelectedRecords() {
|
|
@@ -1065,10 +1099,43 @@ export default class Table extends ShadowComponent {
|
|
|
1065
1099
|
tbody tr:last-child td {
|
|
1066
1100
|
border-bottom: none;
|
|
1067
1101
|
}
|
|
1102
|
+
tr.editing td.cell[data-field] {
|
|
1103
|
+
padding: 0;
|
|
1104
|
+
}
|
|
1105
|
+
tr.editing td.cell[data-field] input,
|
|
1106
|
+
tr.editing td.cell[data-field] select {
|
|
1107
|
+
width: 100%;
|
|
1108
|
+
height: 100%;
|
|
1109
|
+
box-sizing: border-box;
|
|
1110
|
+
}
|
|
1111
|
+
tr.pending {
|
|
1112
|
+
pointer-events: none;
|
|
1113
|
+
}
|
|
1114
|
+
tr.pending td {
|
|
1115
|
+
position: relative;
|
|
1116
|
+
overflow: hidden;
|
|
1117
|
+
}
|
|
1118
|
+
tr.pending td::before {
|
|
1119
|
+
content: '';
|
|
1120
|
+
position: absolute;
|
|
1121
|
+
inset: 0;
|
|
1122
|
+
background: linear-gradient(90deg, transparent, rgba(128, 128, 128, 0.15), transparent);
|
|
1123
|
+
transform: translateX(-100%);
|
|
1124
|
+
animation: row-pending 1.2s ease-in-out infinite;
|
|
1125
|
+
}
|
|
1126
|
+
@keyframes row-pending {
|
|
1127
|
+
0% { transform: translateX(-100%); }
|
|
1128
|
+
100% { transform: translateX(100%); }
|
|
1129
|
+
}
|
|
1068
1130
|
th.controls,
|
|
1069
1131
|
td.controls {
|
|
1070
1132
|
padding: 0;
|
|
1071
1133
|
}
|
|
1134
|
+
td.controls-after > div,
|
|
1135
|
+
td.controls-before > div {
|
|
1136
|
+
display: flex;
|
|
1137
|
+
align-items: center;
|
|
1138
|
+
}
|
|
1072
1139
|
.field-select,
|
|
1073
1140
|
.selection {
|
|
1074
1141
|
width: 40px;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import TableControl from './TableControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
export default class DeleteSelected extends TableControl {
|
|
6
|
+
/*
|
|
7
|
+
Public Methods
|
|
8
|
+
*/
|
|
9
|
+
deleteSelected = () => {
|
|
10
|
+
this.table.deleteSelected();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Rendering
|
|
15
|
+
*/
|
|
16
|
+
render() {
|
|
17
|
+
return html`
|
|
18
|
+
<button class="no-btn icon-btn" title="Delete Selected" @click="${this.deleteSelected}">
|
|
19
|
+
<slot>
|
|
20
|
+
<k-icon name="delete_sweep"></k-icon>
|
|
21
|
+
</slot>
|
|
22
|
+
</button>
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
customElements.define('k-tc-delete-selected', DeleteSelected);
|