@design.estate/dees-catalog 3.69.1 → 3.70.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +420 -50
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +84 -0
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.demo.d.ts +1 -0
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.demo.js +69 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +331 -10
- package/dist_ts_web/elements/00group-dataview/dees-table/styles.js +64 -1
- package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.js +2 -4
- package/dist_watch/bundle.js +418 -48
- package/dist_watch/bundle.js.map +3 -3
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-dataview/dees-table/dees-table.demo.ts +66 -0
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +323 -8
- package/ts_web/elements/00group-dataview/dees-table/styles.ts +66 -0
- package/ts_web/elements/00group-overlay/dees-modal/dees-modal.ts +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-catalog",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.70.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
|
6
6
|
"main": "dist_ts_web/index.js",
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/dees-catalog',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.70.1',
|
|
7
7
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
|
8
8
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ITableAction } from './dees-table.js';
|
|
2
2
|
import * as plugins from '../../00plugins.js';
|
|
3
3
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
|
4
|
+
import '@design.estate/dees-wcctools/demotools';
|
|
4
5
|
|
|
5
6
|
interface ITableDemoData {
|
|
6
7
|
date: string;
|
|
@@ -742,6 +743,71 @@ export const demoFunc = () => html`
|
|
|
742
743
|
] as ITableAction[]}
|
|
743
744
|
></dees-table>
|
|
744
745
|
</div>
|
|
746
|
+
|
|
747
|
+
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
|
748
|
+
const tableEl = elementArg.querySelector('#demoLiveFlash') as any;
|
|
749
|
+
if (!tableEl) return;
|
|
750
|
+
// Guard against double-start if runAfterRender fires more than once
|
|
751
|
+
// (e.g. across hot-reload cycles).
|
|
752
|
+
if (tableEl.__liveFlashTimerId) {
|
|
753
|
+
window.clearInterval(tableEl.__liveFlashTimerId);
|
|
754
|
+
}
|
|
755
|
+
const tick = () => {
|
|
756
|
+
if (!Array.isArray(tableEl.data) || tableEl.data.length === 0) return;
|
|
757
|
+
const next = tableEl.data.map((r: any) => ({ ...r }));
|
|
758
|
+
const count = 1 + Math.floor(Math.random() * 3);
|
|
759
|
+
for (let i = 0; i < count; i++) {
|
|
760
|
+
const idx = Math.floor(Math.random() * next.length);
|
|
761
|
+
const delta = +((Math.random() * 2 - 1) * 3).toFixed(2);
|
|
762
|
+
const newPrice = Math.max(1, +(next[idx].price + delta).toFixed(2));
|
|
763
|
+
next[idx] = {
|
|
764
|
+
...next[idx],
|
|
765
|
+
price: newPrice,
|
|
766
|
+
change: delta,
|
|
767
|
+
updatedAt: new Date().toLocaleTimeString(),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
tableEl.data = next;
|
|
771
|
+
};
|
|
772
|
+
tableEl.__liveFlashTimerId = window.setInterval(tick, 1500);
|
|
773
|
+
}}>
|
|
774
|
+
<div class="demo-section">
|
|
775
|
+
<h2 class="demo-title">Live Updates with Flash Highlighting</h2>
|
|
776
|
+
<p class="demo-description">
|
|
777
|
+
Opt-in cell-flash via <code>highlight-updates="flash"</code>. The ticker below mutates
|
|
778
|
+
random rows every 1.5s and reassigns <code>.data</code>. Updated cells briefly flash
|
|
779
|
+
amber and fade out. Requires <code>rowKey</code> (here <code>"symbol"</code>). Honors
|
|
780
|
+
<code>prefers-reduced-motion</code>. Row selection persists across updates — click a
|
|
781
|
+
row, then watch it stay selected as the data churns.
|
|
782
|
+
</p>
|
|
783
|
+
<dees-table
|
|
784
|
+
id="demoLiveFlash"
|
|
785
|
+
.rowKey=${'symbol'}
|
|
786
|
+
highlight-updates="flash"
|
|
787
|
+
.selectionMode=${'multi'}
|
|
788
|
+
heading1="Live Market Feed"
|
|
789
|
+
heading2="Flashing cells indicate updated values"
|
|
790
|
+
.columns=${[
|
|
791
|
+
{ key: 'symbol', header: 'Symbol', sortable: true },
|
|
792
|
+
{ key: 'price', header: 'Price', sortable: true },
|
|
793
|
+
{ key: 'change', header: 'Δ', sortable: true },
|
|
794
|
+
{ key: 'updatedAt', header: 'Updated' },
|
|
795
|
+
]}
|
|
796
|
+
.data=${[
|
|
797
|
+
{ symbol: 'AAPL', price: 182.52, change: 0, updatedAt: '—' },
|
|
798
|
+
{ symbol: 'MSFT', price: 414.18, change: 0, updatedAt: '—' },
|
|
799
|
+
{ symbol: 'GOOG', price: 168.74, change: 0, updatedAt: '—' },
|
|
800
|
+
{ symbol: 'AMZN', price: 186.13, change: 0, updatedAt: '—' },
|
|
801
|
+
{ symbol: 'TSLA', price: 248.50, change: 0, updatedAt: '—' },
|
|
802
|
+
{ symbol: 'NVDA', price: 877.35, change: 0, updatedAt: '—' },
|
|
803
|
+
{ symbol: 'META', price: 492.96, change: 0, updatedAt: '—' },
|
|
804
|
+
{ symbol: 'NFLX', price: 605.88, change: 0, updatedAt: '—' },
|
|
805
|
+
{ symbol: 'AMD', price: 165.24, change: 0, updatedAt: '—' },
|
|
806
|
+
{ symbol: 'INTC', price: 42.15, change: 0, updatedAt: '—' },
|
|
807
|
+
]}
|
|
808
|
+
></dees-table>
|
|
809
|
+
</div>
|
|
810
|
+
</dees-demowrapper>
|
|
745
811
|
</div>
|
|
746
812
|
</div>
|
|
747
813
|
`;
|
|
@@ -214,6 +214,30 @@ export class DeesTable<T> extends DeesElement {
|
|
|
214
214
|
@property({ type: Number, attribute: 'virtual-overscan' })
|
|
215
215
|
accessor virtualOverscan: number = 8;
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Opt-in visual indication of cell-value changes across data updates.
|
|
219
|
+
*
|
|
220
|
+
* - `'none'` (default): no diffing, zero overhead.
|
|
221
|
+
* - `'flash'`: when `data` is reassigned to a new array reference, diff the
|
|
222
|
+
* new rows against the previous snapshot and briefly flash any cells
|
|
223
|
+
* whose resolved value changed. Equality is strict `===`; object-valued
|
|
224
|
+
* cells are compared by reference. The currently-edited cell is never
|
|
225
|
+
* flashed. User-initiated cell edits do not flash.
|
|
226
|
+
*
|
|
227
|
+
* Requires `rowKey` to be set — without it, the feature silently no-ops
|
|
228
|
+
* and renders a visible dev warning banner. Honors `prefers-reduced-motion`
|
|
229
|
+
* (fades are replaced with a static background hint of the same duration).
|
|
230
|
+
*/
|
|
231
|
+
@property({ type: String, attribute: 'highlight-updates' })
|
|
232
|
+
accessor highlightUpdates: 'none' | 'flash' = 'none';
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Duration of the flash animation in milliseconds. Fed into the
|
|
236
|
+
* `--dees-table-flash-duration` CSS variable on the host.
|
|
237
|
+
*/
|
|
238
|
+
@property({ type: Number, attribute: 'highlight-duration' })
|
|
239
|
+
accessor highlightDuration: number = 900;
|
|
240
|
+
|
|
217
241
|
/**
|
|
218
242
|
* When set, the table renders inside a fixed-height scroll container
|
|
219
243
|
* (`max-height: var(--table-max-height, 360px)`) and the header sticks
|
|
@@ -268,6 +292,23 @@ export class DeesTable<T> extends DeesElement {
|
|
|
268
292
|
@state()
|
|
269
293
|
private accessor __floatingActive: boolean = false;
|
|
270
294
|
|
|
295
|
+
// ─── Flash-on-update state (only populated when highlightUpdates === 'flash') ──
|
|
296
|
+
/** rowId → set of colKey strings currently flashing. */
|
|
297
|
+
@state()
|
|
298
|
+
private accessor __flashingCells: Map<string, Set<string>> = new Map();
|
|
299
|
+
|
|
300
|
+
/** rowId → (colKey → last-seen resolved cell value). Populated per diff pass. */
|
|
301
|
+
private __prevSnapshot?: Map<string, Map<string, unknown>>;
|
|
302
|
+
|
|
303
|
+
/** Single shared timer that clears __flashingCells after highlightDuration ms. */
|
|
304
|
+
private __flashClearTimer?: ReturnType<typeof setTimeout>;
|
|
305
|
+
|
|
306
|
+
/** Monotonic counter bumped each flash batch so directives.keyed recreates the cell node and restarts the animation. */
|
|
307
|
+
private __flashTick: number = 0;
|
|
308
|
+
|
|
309
|
+
/** One-shot console.warn gate for missing rowKey in flash mode. */
|
|
310
|
+
private __flashWarnedNoRowKey: boolean = false;
|
|
311
|
+
|
|
271
312
|
// ─── Render memoization ──────────────────────────────────────────────
|
|
272
313
|
// These caches let render() short-circuit when the relevant inputs
|
|
273
314
|
// (by reference) haven't changed. They are NOT @state — mutating them
|
|
@@ -557,6 +598,15 @@ export class DeesTable<T> extends DeesElement {
|
|
|
557
598
|
</div>
|
|
558
599
|
</div>
|
|
559
600
|
<div class="headingSeparation"></div>
|
|
601
|
+
${this.highlightUpdates === 'flash' && !this.rowKey
|
|
602
|
+
? html`<div class="flashConfigWarning" role="alert">
|
|
603
|
+
<dees-icon .icon=${'lucide:triangleAlert'}></dees-icon>
|
|
604
|
+
<span>
|
|
605
|
+
<code>highlight-updates="flash"</code> requires
|
|
606
|
+
<code>rowKey</code> to be set. Flash is disabled.
|
|
607
|
+
</span>
|
|
608
|
+
</div>`
|
|
609
|
+
: html``}
|
|
560
610
|
<div class="searchGrid hidden">
|
|
561
611
|
<dees-input-text
|
|
562
612
|
.label=${'lucene syntax search'}
|
|
@@ -606,9 +656,13 @@ export class DeesTable<T> extends DeesElement {
|
|
|
606
656
|
${useVirtual && topSpacerHeight > 0
|
|
607
657
|
? html`<tr aria-hidden="true" style="height:${topSpacerHeight}px"><td></td></tr>`
|
|
608
658
|
: html``}
|
|
609
|
-
${
|
|
659
|
+
${directives.repeat(
|
|
660
|
+
renderRows,
|
|
661
|
+
(itemArg, sliceIdx) => `${this.getRowId(itemArg)}::${renderStart + sliceIdx}`,
|
|
662
|
+
(itemArg, sliceIdx) => {
|
|
610
663
|
const rowIndex = renderStart + sliceIdx;
|
|
611
664
|
const rowId = this.getRowId(itemArg);
|
|
665
|
+
const flashSet = this.__flashingCells.get(rowId);
|
|
612
666
|
return html`
|
|
613
667
|
<tr
|
|
614
668
|
data-row-idx=${rowIndex}
|
|
@@ -640,6 +694,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
640
694
|
const isEditing =
|
|
641
695
|
this.__editingCell?.rowId === rowId &&
|
|
642
696
|
this.__editingCell?.colKey === editKey;
|
|
697
|
+
const isFlashing = !!flashSet?.has(editKey);
|
|
643
698
|
const cellClasses = [
|
|
644
699
|
isEditable ? 'editable' : '',
|
|
645
700
|
isFocused && !isEditing ? 'focused' : '',
|
|
@@ -647,14 +702,22 @@ export class DeesTable<T> extends DeesElement {
|
|
|
647
702
|
]
|
|
648
703
|
.filter(Boolean)
|
|
649
704
|
.join(' ');
|
|
705
|
+
const innerHtml = html`<div
|
|
706
|
+
class=${isFlashing ? 'innerCellContainer flashing' : 'innerCellContainer'}
|
|
707
|
+
>
|
|
708
|
+
${isEditing ? this.renderCellEditor(itemArg, col) : content}
|
|
709
|
+
</div>`;
|
|
650
710
|
return html`
|
|
651
711
|
<td
|
|
652
712
|
class=${cellClasses}
|
|
653
713
|
data-col-key=${editKey}
|
|
654
714
|
>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
715
|
+
${isFlashing
|
|
716
|
+
? directives.keyed(
|
|
717
|
+
`${rowId}:${editKey}:${this.__flashTick}`,
|
|
718
|
+
innerHtml
|
|
719
|
+
)
|
|
720
|
+
: innerHtml}
|
|
658
721
|
</td>
|
|
659
722
|
`;
|
|
660
723
|
})}
|
|
@@ -685,7 +748,8 @@ export class DeesTable<T> extends DeesElement {
|
|
|
685
748
|
}
|
|
686
749
|
})()}
|
|
687
750
|
</tr>`;
|
|
688
|
-
}
|
|
751
|
+
}
|
|
752
|
+
)}
|
|
689
753
|
${useVirtual && bottomSpacerHeight > 0
|
|
690
754
|
? html`<tr aria-hidden="true" style="height:${bottomSpacerHeight}px"><td></td></tr>`
|
|
691
755
|
: html``}
|
|
@@ -801,7 +865,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
801
865
|
const key = String(col.key);
|
|
802
866
|
if (col.filterable === false) return html`<th></th>`;
|
|
803
867
|
return html`<th>
|
|
804
|
-
<input type="text" placeholder="Filter..." .value=${this.columnFilters[key] || ''}
|
|
868
|
+
<input type="text" placeholder="Filter..." data-col-key=${key} .value=${this.columnFilters[key] || ''}
|
|
805
869
|
@input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
|
|
806
870
|
</th>`;
|
|
807
871
|
})}
|
|
@@ -957,6 +1021,84 @@ export class DeesTable<T> extends DeesElement {
|
|
|
957
1021
|
if (fh) fh.classList.remove('active');
|
|
958
1022
|
}
|
|
959
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* If a filter `<input>` inside the floating-header clone currently has
|
|
1026
|
+
* focus, copy its value, caret, and selection range onto the matching
|
|
1027
|
+
* input in the real header, then focus that real input. This lets the
|
|
1028
|
+
* user keep typing uninterrupted when filter input causes the table to
|
|
1029
|
+
* shrink below the viewport stick line and the floating header has to
|
|
1030
|
+
* unmount.
|
|
1031
|
+
*
|
|
1032
|
+
* Safe to call at any time — it is a no-op unless an input inside the
|
|
1033
|
+
* floating header is focused and has a `data-col-key` attribute that
|
|
1034
|
+
* matches a real-header input.
|
|
1035
|
+
*/
|
|
1036
|
+
private __transferFocusToRealHeader(): void {
|
|
1037
|
+
const fh = this.__floatingHeaderEl;
|
|
1038
|
+
if (!fh) return;
|
|
1039
|
+
const active = this.shadowRoot?.activeElement as HTMLElement | null;
|
|
1040
|
+
if (!active || !fh.contains(active)) return;
|
|
1041
|
+
const colKey = active.getAttribute('data-col-key');
|
|
1042
|
+
if (!colKey) return;
|
|
1043
|
+
const fromInput = active as HTMLInputElement;
|
|
1044
|
+
const real = this.shadowRoot?.querySelector(
|
|
1045
|
+
`.tableScroll > table > thead input[data-col-key="${CSS.escape(colKey)}"]`
|
|
1046
|
+
) as HTMLInputElement | null;
|
|
1047
|
+
if (!real || real === fromInput) return;
|
|
1048
|
+
const selStart = fromInput.selectionStart;
|
|
1049
|
+
const selEnd = fromInput.selectionEnd;
|
|
1050
|
+
const selDir = fromInput.selectionDirection as any;
|
|
1051
|
+
real.focus({ preventScroll: true });
|
|
1052
|
+
try {
|
|
1053
|
+
if (selStart != null && selEnd != null) {
|
|
1054
|
+
real.setSelectionRange(selStart, selEnd, selDir || undefined);
|
|
1055
|
+
}
|
|
1056
|
+
} catch {
|
|
1057
|
+
/* setSelectionRange throws on unsupported input types — ignore */
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Symmetric counterpart to `__transferFocusToRealHeader`. When the
|
|
1063
|
+
* floating header has just activated and a real-header filter input
|
|
1064
|
+
* was focused (and is now scrolled off-screen behind the floating
|
|
1065
|
+
* clone), move focus to the clone's matching input so the user keeps
|
|
1066
|
+
* typing in the visible one.
|
|
1067
|
+
*
|
|
1068
|
+
* Called from `__syncFloatingHeader` inside the post-activation
|
|
1069
|
+
* `updateComplete` callback — by then the clone subtree exists in the
|
|
1070
|
+
* DOM and can receive focus.
|
|
1071
|
+
*/
|
|
1072
|
+
private __transferFocusToFloatingHeader(): void {
|
|
1073
|
+
const fh = this.__floatingHeaderEl;
|
|
1074
|
+
if (!fh || !this.__floatingActive) return;
|
|
1075
|
+
const active = this.shadowRoot?.activeElement as HTMLElement | null;
|
|
1076
|
+
if (!active) return;
|
|
1077
|
+
// Only handle focus that lives in the real header (not already in the clone).
|
|
1078
|
+
const realThead = this.shadowRoot?.querySelector(
|
|
1079
|
+
'.tableScroll > table > thead'
|
|
1080
|
+
) as HTMLElement | null;
|
|
1081
|
+
if (!realThead || !realThead.contains(active)) return;
|
|
1082
|
+
const colKey = active.getAttribute('data-col-key');
|
|
1083
|
+
if (!colKey) return;
|
|
1084
|
+
const fromInput = active as HTMLInputElement;
|
|
1085
|
+
const clone = fh.querySelector(
|
|
1086
|
+
`input[data-col-key="${CSS.escape(colKey)}"]`
|
|
1087
|
+
) as HTMLInputElement | null;
|
|
1088
|
+
if (!clone || clone === fromInput) return;
|
|
1089
|
+
const selStart = fromInput.selectionStart;
|
|
1090
|
+
const selEnd = fromInput.selectionEnd;
|
|
1091
|
+
const selDir = fromInput.selectionDirection as any;
|
|
1092
|
+
clone.focus({ preventScroll: true });
|
|
1093
|
+
try {
|
|
1094
|
+
if (selStart != null && selEnd != null) {
|
|
1095
|
+
clone.setSelectionRange(selStart, selEnd, selDir || undefined);
|
|
1096
|
+
}
|
|
1097
|
+
} catch {
|
|
1098
|
+
/* ignore */
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
960
1102
|
// ─── Virtualization ─────────────────────────────────────────────────
|
|
961
1103
|
|
|
962
1104
|
/**
|
|
@@ -1062,6 +1204,15 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1062
1204
|
const shouldBeActive = tableRect.top < stick.top && distance > 0;
|
|
1063
1205
|
|
|
1064
1206
|
if (shouldBeActive !== this.__floatingActive) {
|
|
1207
|
+
if (!shouldBeActive) {
|
|
1208
|
+
// Before we flag the clone for unmount, hand off any focused
|
|
1209
|
+
// filter input to its counterpart in the real header. This is the
|
|
1210
|
+
// "user is typing in a sticky filter input, filter shrinks the
|
|
1211
|
+
// table so the floating header hides" case — without this
|
|
1212
|
+
// handoff the user's focus (and caret position) would be lost
|
|
1213
|
+
// when the clone unmounts.
|
|
1214
|
+
this.__transferFocusToRealHeader();
|
|
1215
|
+
}
|
|
1065
1216
|
this.__floatingActive = shouldBeActive;
|
|
1066
1217
|
fh.classList.toggle('active', shouldBeActive);
|
|
1067
1218
|
if (!shouldBeActive) {
|
|
@@ -1072,8 +1223,14 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1072
1223
|
}
|
|
1073
1224
|
if (shouldBeActive) {
|
|
1074
1225
|
// Clone subtree doesn't exist yet — wait for the next render to
|
|
1075
|
-
// materialize it, then complete geometry sync.
|
|
1076
|
-
|
|
1226
|
+
// materialize it, then complete geometry sync. Additionally, if a
|
|
1227
|
+
// real-header filter input was focused when we activated, hand
|
|
1228
|
+
// off to the clone once it exists so the user keeps typing in
|
|
1229
|
+
// the visible (floating) input.
|
|
1230
|
+
this.updateComplete.then(() => {
|
|
1231
|
+
this.__syncFloatingHeader();
|
|
1232
|
+
this.__transferFocusToFloatingHeader();
|
|
1233
|
+
});
|
|
1077
1234
|
return;
|
|
1078
1235
|
}
|
|
1079
1236
|
}
|
|
@@ -1127,6 +1284,10 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1127
1284
|
public async disconnectedCallback() {
|
|
1128
1285
|
super.disconnectedCallback();
|
|
1129
1286
|
this.teardownFloatingHeader();
|
|
1287
|
+
if (this.__flashClearTimer) {
|
|
1288
|
+
clearTimeout(this.__flashClearTimer);
|
|
1289
|
+
this.__flashClearTimer = undefined;
|
|
1290
|
+
}
|
|
1130
1291
|
}
|
|
1131
1292
|
|
|
1132
1293
|
public async firstUpdated() {
|
|
@@ -1134,9 +1295,141 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1134
1295
|
// table markup actually exists (it only renders when data.length > 0).
|
|
1135
1296
|
}
|
|
1136
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Runs before each render. Drives two independent concerns:
|
|
1300
|
+
*
|
|
1301
|
+
* 1. **Selection rebind** — when `data` is reassigned to a fresh array
|
|
1302
|
+
* (typical live-data pattern), `selectedDataRow` still points at the
|
|
1303
|
+
* stale row object from the old array. We re-resolve it by rowKey so
|
|
1304
|
+
* consumers of `selectedDataRow` (footer indicator, header/footer
|
|
1305
|
+
* actions, copy fallback) see the live reference. `selectedIds`,
|
|
1306
|
+
* `__focusedCell`, `__editingCell`, `__selectionAnchorId` are all
|
|
1307
|
+
* keyed by string rowId and persist automatically — no change needed.
|
|
1308
|
+
* This runs regardless of `highlightUpdates` — it is a baseline
|
|
1309
|
+
* correctness fix for live data.
|
|
1310
|
+
*
|
|
1311
|
+
* 2. **Flash diff** — when `highlightUpdates === 'flash'`, diff the new
|
|
1312
|
+
* data against `__prevSnapshot` and populate `__flashingCells` with
|
|
1313
|
+
* the (rowId, colKey) pairs whose resolved cell value changed. A
|
|
1314
|
+
* single shared timer clears `__flashingCells` after
|
|
1315
|
+
* `highlightDuration` ms. Skipped if `rowKey` is missing (with a
|
|
1316
|
+
* one-shot console.warn; the render surface also shows a warning
|
|
1317
|
+
* banner).
|
|
1318
|
+
*/
|
|
1319
|
+
public willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
1320
|
+
// --- Phase 1: selection rebind (always runs) ---
|
|
1321
|
+
if (changedProperties.has('data') && this.selectedDataRow && this.rowKey) {
|
|
1322
|
+
const prevId = this.getRowId(this.selectedDataRow);
|
|
1323
|
+
let found: T | undefined;
|
|
1324
|
+
for (const row of this.data) {
|
|
1325
|
+
if (this.getRowId(row) === prevId) {
|
|
1326
|
+
found = row;
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (found) {
|
|
1331
|
+
if (found !== this.selectedDataRow) this.selectedDataRow = found;
|
|
1332
|
+
} else {
|
|
1333
|
+
this.selectedDataRow = undefined as unknown as T;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// --- Phase 2: flash diff ---
|
|
1338
|
+
if (this.highlightUpdates !== 'flash') {
|
|
1339
|
+
// Mode was toggled off (or never on) — drop any lingering state so
|
|
1340
|
+
// re-enabling later starts with a clean slate.
|
|
1341
|
+
if (this.__prevSnapshot || this.__flashingCells.size > 0) {
|
|
1342
|
+
this.__prevSnapshot = undefined;
|
|
1343
|
+
if (this.__flashingCells.size > 0) this.__flashingCells = new Map();
|
|
1344
|
+
if (this.__flashClearTimer) {
|
|
1345
|
+
clearTimeout(this.__flashClearTimer);
|
|
1346
|
+
this.__flashClearTimer = undefined;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (!this.rowKey) {
|
|
1352
|
+
if (!this.__flashWarnedNoRowKey) {
|
|
1353
|
+
this.__flashWarnedNoRowKey = true;
|
|
1354
|
+
console.warn(
|
|
1355
|
+
'[dees-table] highlightUpdates="flash" requires `rowKey` to be set. Flash is disabled. ' +
|
|
1356
|
+
'Set the rowKey property/attribute to a stable identifier on your row data (e.g. `rowKey="id"`).'
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (!changedProperties.has('data')) return;
|
|
1362
|
+
|
|
1363
|
+
const effectiveColumns = this.__getEffectiveColumns();
|
|
1364
|
+
const visibleCols = effectiveColumns.filter((c) => !c.hidden);
|
|
1365
|
+
const nextSnapshot = new Map<string, Map<string, unknown>>();
|
|
1366
|
+
const newlyFlashing = new Map<string, Set<string>>();
|
|
1367
|
+
|
|
1368
|
+
for (const row of this.data) {
|
|
1369
|
+
const rowId = this.getRowId(row);
|
|
1370
|
+
const cellMap = new Map<string, unknown>();
|
|
1371
|
+
for (const col of visibleCols) {
|
|
1372
|
+
cellMap.set(String(col.key), getCellValueFn(row, col, this.displayFunction));
|
|
1373
|
+
}
|
|
1374
|
+
nextSnapshot.set(rowId, cellMap);
|
|
1375
|
+
|
|
1376
|
+
const prevCells = this.__prevSnapshot?.get(rowId);
|
|
1377
|
+
if (!prevCells) continue; // new row — not an "update"
|
|
1378
|
+
for (const [colKey, nextVal] of cellMap) {
|
|
1379
|
+
if (prevCells.get(colKey) !== nextVal) {
|
|
1380
|
+
// Don't flash the cell the user is actively editing.
|
|
1381
|
+
if (
|
|
1382
|
+
this.__editingCell &&
|
|
1383
|
+
this.__editingCell.rowId === rowId &&
|
|
1384
|
+
this.__editingCell.colKey === colKey
|
|
1385
|
+
) continue;
|
|
1386
|
+
let set = newlyFlashing.get(rowId);
|
|
1387
|
+
if (!set) {
|
|
1388
|
+
set = new Set();
|
|
1389
|
+
newlyFlashing.set(rowId, set);
|
|
1390
|
+
}
|
|
1391
|
+
set.add(colKey);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const hadPrev = !!this.__prevSnapshot;
|
|
1397
|
+
this.__prevSnapshot = nextSnapshot;
|
|
1398
|
+
if (!hadPrev) return; // first time seeing data — no flashes
|
|
1399
|
+
|
|
1400
|
+
if (newlyFlashing.size === 0) return;
|
|
1401
|
+
|
|
1402
|
+
// Merge with any in-flight flashes from a rapid second update so a cell
|
|
1403
|
+
// that changes twice before its animation ends gets a single clean
|
|
1404
|
+
// restart (via __flashTick / directives.keyed) instead of stacking.
|
|
1405
|
+
for (const [rowId, cols] of newlyFlashing) {
|
|
1406
|
+
const existing = this.__flashingCells.get(rowId);
|
|
1407
|
+
if (existing) {
|
|
1408
|
+
for (const c of cols) existing.add(c);
|
|
1409
|
+
} else {
|
|
1410
|
+
this.__flashingCells.set(rowId, cols);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
this.__flashTick++;
|
|
1414
|
+
// Reactivity nudge: we've mutated the Map in place, so give Lit a fresh
|
|
1415
|
+
// reference so the @state change fires for render.
|
|
1416
|
+
this.__flashingCells = new Map(this.__flashingCells);
|
|
1417
|
+
if (this.__flashClearTimer) clearTimeout(this.__flashClearTimer);
|
|
1418
|
+
this.__flashClearTimer = setTimeout(() => {
|
|
1419
|
+
this.__flashingCells = new Map();
|
|
1420
|
+
this.__flashClearTimer = undefined;
|
|
1421
|
+
}, Math.max(0, this.highlightDuration));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1137
1424
|
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
|
1138
1425
|
super.updated(changedProperties);
|
|
1139
1426
|
|
|
1427
|
+
// Feed highlightDuration into the CSS variable so JS and CSS stay in
|
|
1428
|
+
// sync via a single source of truth.
|
|
1429
|
+
if (changedProperties.has('highlightDuration')) {
|
|
1430
|
+
this.style.setProperty('--dees-table-flash-duration', `${this.highlightDuration}ms`);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1140
1433
|
// Only re-measure column widths when the data or schema actually changed
|
|
1141
1434
|
// (or on first paint). `determineColumnWidths` is the single biggest
|
|
1142
1435
|
// first-paint cost — it forces multiple layout flushes per row.
|
|
@@ -2090,6 +2383,10 @@ export class DeesTable<T> extends DeesElement {
|
|
|
2090
2383
|
}
|
|
2091
2384
|
if (parsed !== oldValue) {
|
|
2092
2385
|
(item as any)[col.key] = parsed;
|
|
2386
|
+
// Keep the flash-diff snapshot in sync so the next external update
|
|
2387
|
+
// does not see this user edit as an external change (which would
|
|
2388
|
+
// otherwise flash the cell the user just typed into).
|
|
2389
|
+
this.__recordCellInSnapshot(item, col);
|
|
2093
2390
|
this.dispatchEvent(
|
|
2094
2391
|
new CustomEvent('cellEdit', {
|
|
2095
2392
|
detail: { row: item, key, oldValue, newValue: parsed },
|
|
@@ -2103,6 +2400,24 @@ export class DeesTable<T> extends DeesElement {
|
|
|
2103
2400
|
this.requestUpdate();
|
|
2104
2401
|
}
|
|
2105
2402
|
|
|
2403
|
+
/**
|
|
2404
|
+
* Updates the flash diff snapshot for a single cell to match its current
|
|
2405
|
+
* resolved value. Called from `commitCellEdit` so a user-initiated edit
|
|
2406
|
+
* does not register as an external change on the next diff pass.
|
|
2407
|
+
* No-op when flash mode is off or no snapshot exists yet.
|
|
2408
|
+
*/
|
|
2409
|
+
private __recordCellInSnapshot(item: T, col: Column<T>): void {
|
|
2410
|
+
if (this.highlightUpdates !== 'flash' || !this.__prevSnapshot) return;
|
|
2411
|
+
if (!this.rowKey) return;
|
|
2412
|
+
const rowId = this.getRowId(item);
|
|
2413
|
+
let cellMap = this.__prevSnapshot.get(rowId);
|
|
2414
|
+
if (!cellMap) {
|
|
2415
|
+
cellMap = new Map();
|
|
2416
|
+
this.__prevSnapshot.set(rowId, cellMap);
|
|
2417
|
+
}
|
|
2418
|
+
cellMap.set(String(col.key), getCellValueFn(item, col, this.displayFunction));
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2106
2421
|
/** Renders the appropriate dees-input-* component for this column. */
|
|
2107
2422
|
private renderCellEditor(item: T, col: Column<T>): TemplateResult {
|
|
2108
2423
|
const raw = (item as any)[col.key];
|
|
@@ -373,6 +373,72 @@ export const tableStyles: CSSResult[] = [
|
|
|
373
373
|
line-height: 24px;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/* ---- Cell flash highlighting (opt-in via highlight-updates="flash") ----
|
|
377
|
+
Bloomberg/TradingView-style: the text itself briefly takes an accent
|
|
378
|
+
color then fades back to the default. No background tint, no layout
|
|
379
|
+
shift, no weight change. Readable, modern, subtle.
|
|
380
|
+
Consumers can override per instance:
|
|
381
|
+
dees-table#myTable { --dees-table-flash-color: hsl(142 76% 40%); }
|
|
382
|
+
*/
|
|
383
|
+
:host {
|
|
384
|
+
--dees-table-flash-color: ${cssManager.bdTheme(
|
|
385
|
+
'hsl(32 95% 44%)',
|
|
386
|
+
'hsl(45 93% 62%)'
|
|
387
|
+
)};
|
|
388
|
+
--dees-table-flash-easing: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.innerCellContainer.flashing {
|
|
392
|
+
animation: dees-table-cell-flash
|
|
393
|
+
var(--dees-table-flash-duration, 900ms)
|
|
394
|
+
var(--dees-table-flash-easing);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Hold the accent color briefly, then fade back to the theme's default
|
|
398
|
+
text color. Inherits to child text and to SVG icons that use
|
|
399
|
+
currentColor. Cells with explicit color overrides in renderers are
|
|
400
|
+
intentionally unaffected. */
|
|
401
|
+
@keyframes dees-table-cell-flash {
|
|
402
|
+
0%,
|
|
403
|
+
35% { color: var(--dees-table-flash-color); }
|
|
404
|
+
100% { color: var(--dees-color-text-primary); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@media (prefers-reduced-motion: reduce) {
|
|
408
|
+
.innerCellContainer.flashing {
|
|
409
|
+
animation: none;
|
|
410
|
+
color: var(--dees-table-flash-color);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* Dev-time warning banner shown when highlight-updates="flash" but
|
|
415
|
+
rowKey is missing. Consumers should never ship this to production. */
|
|
416
|
+
.flashConfigWarning {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 8px;
|
|
420
|
+
margin: 8px 16px 0;
|
|
421
|
+
padding: 8px 12px;
|
|
422
|
+
border-left: 3px solid ${cssManager.bdTheme('hsl(38 92% 50%)', 'hsl(48 96% 63%)')};
|
|
423
|
+
background: ${cssManager.bdTheme('hsl(48 96% 89% / 0.6)', 'hsl(48 96% 30% / 0.15)')};
|
|
424
|
+
color: ${cssManager.bdTheme('hsl(32 81% 29%)', 'hsl(48 96% 80%)')};
|
|
425
|
+
font-size: 12px;
|
|
426
|
+
line-height: 1.4;
|
|
427
|
+
border-radius: 4px;
|
|
428
|
+
}
|
|
429
|
+
.flashConfigWarning dees-icon {
|
|
430
|
+
width: 14px;
|
|
431
|
+
height: 14px;
|
|
432
|
+
flex: 0 0 auto;
|
|
433
|
+
}
|
|
434
|
+
.flashConfigWarning code {
|
|
435
|
+
padding: 1px 4px;
|
|
436
|
+
border-radius: 3px;
|
|
437
|
+
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.6)', 'hsl(0 0% 0% / 0.3)')};
|
|
438
|
+
font-family: ${cssGeistFontFamily};
|
|
439
|
+
font-size: 11px;
|
|
440
|
+
}
|
|
441
|
+
|
|
376
442
|
/* Editable cell affordances */
|
|
377
443
|
td.editable {
|
|
378
444
|
cursor: text;
|