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.
@@ -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
- ```bash
15
- npm run highlightcode -- <lang> "<code>"
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
- The script returns the **inner highlighted content only** — the hljs span markup without any `<pre><code>` wrapper. Paste it directly inside an existing `<pre><code class="hljs <lang>">...</code></pre>` block.
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
- Returns:
36
- ```
37
- <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello World<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
38
- ```
24
+ Input is automatically beautified before highlighting, so you can pass minified code.
39
25
 
40
- ## Multi-line Code
26
+ ## Workflow for Updating Doc Code Samples
41
27
 
42
- For multi-line code, use a here-string in PowerShell:
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
- $code = @"
46
- class Foo extends Bar {
47
- render(){ return html`<p>hello</p>`; }
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
- ## Workflow for Updating Doc Code Samples
37
+ ## Important Notes
54
38
 
55
- 1. Run `npm run highlightcode -- <lang> "<new code>"` to get the highlighted HTML.
56
- 2. Replace the inner content of the matching `<pre><code class="hljs ...">` block in the doc file with the output.
57
- 3. Also update the live demo `<script type="module">` block with the raw (unhighlighted) code.
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).
@@ -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 < 2){
22
- console.error('Usage: kempo-highlightcode <lang> <code>');
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 code = rest.join(' ');
37
- const full = highlight(code, lang);
38
- // Strip the <pre><code> wrapper — return just the inner highlighted content
39
- process.stdout.write(full.replace(/^<pre><code class="hljs [^"]*">/, '').replace(/<\/code><\/pre>\n?$/, '') + '\n');
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
+ }
@@ -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
- ${e}
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
- ${e}
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>&lt;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">&lt;<span class="hljs-name">k-table</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"basicUsageExample"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-table</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span>&gt;</span><span class="javascript"><br /> <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.customElements.whenDefined(<span class="hljs-string">'k-table'</span>);<br /> <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'basicUsageExample'</span>).setData({<br /> <span class="hljs-attr">records</span>: [<br /> {<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 /> {<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 /> {<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 /> {<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 /> });<br /></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></code></pre>
54
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-table</span> <span class="hljs-attr">id</span>=<span class="hljs-string">basicUsageExample</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-table</span>&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">module</span>&gt;</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">&#x27;k-table&#x27;</span>);<br> <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">&#x27;basicUsageExample&#x27;</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">&#x27;Dustin&#x27;</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">&#x27;(111) 111-1111&#x27;</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">&#x27;dustin@mailserver.com&#x27;</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">&#x27;Male&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;Kayla&#x27;</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">&#x27;(222) 222-2222&#x27;</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">&#x27;kayla@mailserver.com&#x27;</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">&#x27;Female&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;Alexander&#x27;</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">&#x27;(333) 333-33333&#x27;</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">&#x27;alex@mailserver.com&#x27;</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">&#x27;Male&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;Amelia&#x27;</span>,<br> <span class="hljs-attr">phoneNumber</span>: <span class="hljs-string">&#x27;(444) 444-44444&#x27;</span>,<br> <span class="hljs-attr">emailAddress</span>: <span class="hljs-string">&#x27;amelia@mailserver.com&#x27;</span>,<br> <span class="hljs-attr">gender</span>: <span class="hljs-string">&#x27;Female&#x27;</span><br> }]<br> });<br></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</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">&lt;<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>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">k-tc-edit</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">after</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-tc-edit</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">k-tc-delete-record</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">after</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-tc-delete-record</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">k-tc-delete-selected</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">top</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-tc-delete-selected</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-table</span>&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">module</span>&gt;</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">&#x27;/src/components/Toast.js&#x27;</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">&#x27;k-table&#x27;</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">&#x27;serverSyncExample&#x27;</span>);<br> <span class="hljs-keyword">const</span> <span class="hljs-title function_">mockServer</span> = (<span class="hljs-params"></span>) =&gt; <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {<br> <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {<br> <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">random</span>() &lt; <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">&#x27;name&#x27;</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">&#x27;Name&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;email&#x27;</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">&#x27;Email&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;birthday&#x27;</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">&#x27;Birthday&#x27;</span>,<br> <span class="hljs-attr">type</span>: <span class="hljs-string">&#x27;date&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;phoneNumber&#x27;</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">&#x27;Phone Number&#x27;</span><br> }, {<br> <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;gender&#x27;</span>,<br> <span class="hljs-attr">label</span>: <span class="hljs-string">&#x27;Gender&#x27;</span>,<br> <span class="hljs-attr">formatter</span>: <span class="hljs-function"><span class="hljs-params">v</span> =&gt;</span> v === <span class="hljs-string">&#x27;m&#x27;</span> ? <span class="hljs-string">&#x27;Male&#x27;</span> : <span class="hljs-string">&#x27;Female&#x27;</span><br> }]<br> });<br> table.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;requestSave&#x27;</span>, <span class="hljs-title function_">async</span> ({<br> <span class="hljs-attr">detail</span>: {<br> approve,<br> reject<br> }<br> }) =&gt; {<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">&#x27;Record saved.&#x27;</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">&#x27;Save failed. The record remains open for editing.&#x27;</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">&#x27;requestDelete&#x27;</span>, <span class="hljs-title function_">async</span> ({<br> <span class="hljs-attr">detail</span>: {<br> records,<br> approve,<br> reject<br> }<br> }) =&gt; {<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">&#x27;1 record deleted.&#x27;</span> : count + <span class="hljs-string">&#x27; records deleted.&#x27;</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">&#x27;Delete failed. Please try again.&#x27;</span>, {<br> <span class="hljs-attr">timeout</span>: <span class="hljs-number">5000</span><br> });<br> }<br> });<br></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</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
- ${e}
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
- ${e}
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.78",
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"
@@ -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
- ${controls}
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
- ${controls}
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.classList.remove('editing');
374
- recordEl.removeAttribute('editing');
375
- recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
376
- const field = $cell.dataset.field;
377
- const fieldDef = this.fields.find(f => f.name === field);
378
- if(fieldDef){
379
- const value = record[field] || '';
380
- if(fieldDef.calculator){
381
- $cell.textContent = fieldDef.calculator(record, this);
382
- } else if(fieldDef.formatter){
383
- $cell.innerHTML = fieldDef.formatter(value);
384
- } else {
385
- $cell.textContent = value;
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
- let totalPagesBefore = this.getTotalPages();
653
-
654
- if (!originalRecord && record[index] !== undefined) {
655
- originalRecord = this.records[record[index]];
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 (this.currentPage > totalPages) {
673
- this.setPage(totalPages);
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
- let totalPagesBefore = this.getTotalPages();
686
- const selectedRecords = this.getSelectedRecords();
687
-
688
- selectedRecords.forEach(record => {
689
- let originalRecord = this.records.find(r => r === record);
690
- if (!originalRecord && record[index] !== undefined) {
691
- originalRecord = this.records[record[index]];
692
- }
693
- if (originalRecord) {
694
- const recordIndex = this.records.indexOf(originalRecord);
695
- this.records.splice(recordIndex, 1);
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.records.forEach((rec, idx) => {
700
- rec[index] = idx;
701
- });
702
- this.requestUpdate();
703
-
704
- const totalPages = this.getTotalPages();
705
- if (this.currentPage > totalPages) {
706
- this.setPage(totalPages);
707
- }
708
- if (totalPages !== totalPagesBefore) {
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);