@shotleybuilder/svelte-table-kit 0.6.0 → 0.12.0
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/README.md +13 -1
- package/dist/TableKit.svelte +76 -9
- package/dist/TableKit.svelte.d.ts +8 -8
- package/dist/components/CellContextMenu.svelte +208 -0
- package/dist/components/CellContextMenu.svelte.d.ts +41 -0
- package/dist/components/ColumnMenu.svelte +3 -3
- package/dist/components/FilterBar.svelte +76 -0
- package/dist/components/FilterBar.svelte.d.ts +3 -0
- package/dist/components/FilterCondition.svelte +639 -42
- package/dist/components/FilterCondition.svelte.d.ts +7 -0
- package/dist/components/GroupBar.svelte +5 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -2
- package/dist/stores/persistence.d.ts +10 -1
- package/dist/stores/persistence.js +14 -1
- package/dist/types.d.ts +20 -0
- package/dist/utils/filters.d.ts +13 -2
- package/dist/utils/filters.js +75 -0
- package/dist/utils/fuzzy.d.ts +47 -0
- package/dist/utils/fuzzy.js +142 -0
- package/package.json +76 -76
package/README.md
CHANGED
|
@@ -26,11 +26,16 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
|
|
|
26
26
|
- 📋 **Column context menu** - Quick access to sort, filter, group, and hide actions
|
|
27
27
|
|
|
28
28
|
**Advanced Filtering:**
|
|
29
|
-
-
|
|
29
|
+
- 14 filter operators: equals, contains, starts with, greater than, is_before, is_after, etc.
|
|
30
30
|
- AND/OR logic between conditions
|
|
31
31
|
- Collapsible FilterBar UI (space-efficient)
|
|
32
32
|
- Active filter count badge
|
|
33
33
|
- Real-time filtering as you type
|
|
34
|
+
- **Column order modes** - Cycle between Default, Table Order, and A-Z sorting in field picker (v0.7.0+)
|
|
35
|
+
- **Fuzzy search** - Type to quickly find columns in large tables with highlighted matches (v0.8.0+)
|
|
36
|
+
- **Value suggestions** - Autocomplete dropdown shows existing column values as you type (v0.10.0+)
|
|
37
|
+
- **Numeric range hints** - Numeric columns display min/max range in the value input (v0.10.0+)
|
|
38
|
+
- **Data type awareness** - Operators and value inputs adapt based on column data type (v0.11.0+)
|
|
34
39
|
|
|
35
40
|
**Sorting Options:**
|
|
36
41
|
- **Column header mode** (default) - Click headers to sort with ↑↓↕ indicators
|
|
@@ -55,6 +60,13 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
|
|
|
55
60
|
- Actions conditionally shown based on feature flags
|
|
56
61
|
- Seamlessly integrates with existing controls
|
|
57
62
|
|
|
63
|
+
**Cell Context Menu (v0.9.0+):**
|
|
64
|
+
- Right-click any cell to open context menu
|
|
65
|
+
- **Filter by value** - Instantly filter to show rows matching cell value
|
|
66
|
+
- **Exclude value** - Filter to hide rows with this value
|
|
67
|
+
- **Greater/Less than** - Numeric columns get comparison options
|
|
68
|
+
- Auto-expands FilterBar when filter is created
|
|
69
|
+
|
|
58
70
|
**Developer Experience:**
|
|
59
71
|
- 🎨 Headless design - style it your way
|
|
60
72
|
- 📦 Built on TanStack Table v8 (battle-tested, powerful)
|
package/dist/TableKit.svelte
CHANGED
|
@@ -25,6 +25,7 @@ import FilterBar from "./components/FilterBar.svelte";
|
|
|
25
25
|
import GroupBar from "./components/GroupBar.svelte";
|
|
26
26
|
import SortBar from "./components/SortBar.svelte";
|
|
27
27
|
import ColumnMenu from "./components/ColumnMenu.svelte";
|
|
28
|
+
import CellContextMenu from "./components/CellContextMenu.svelte";
|
|
28
29
|
export let data = [];
|
|
29
30
|
export let columns = [];
|
|
30
31
|
export let config = void 0;
|
|
@@ -46,6 +47,9 @@ export let features = {
|
|
|
46
47
|
export let onRowClick = void 0;
|
|
47
48
|
export let onRowSelect = void 0;
|
|
48
49
|
export let onStateChange = void 0;
|
|
50
|
+
function getColumnId(col) {
|
|
51
|
+
return col.accessorKey || col.id || "";
|
|
52
|
+
}
|
|
49
53
|
let sorting = writable([]);
|
|
50
54
|
let columnVisibility = writable({});
|
|
51
55
|
let columnSizing = writable({});
|
|
@@ -59,6 +63,15 @@ let configInitialized = false;
|
|
|
59
63
|
let grouping = writable([]);
|
|
60
64
|
let expanded = writable(true);
|
|
61
65
|
let groupBarExpanded = false;
|
|
66
|
+
let cellContextMenu = {
|
|
67
|
+
show: false,
|
|
68
|
+
x: 0,
|
|
69
|
+
y: 0,
|
|
70
|
+
value: null,
|
|
71
|
+
columnId: "",
|
|
72
|
+
columnHeader: "",
|
|
73
|
+
isNumeric: false
|
|
74
|
+
};
|
|
62
75
|
$: {
|
|
63
76
|
const configChanged = config?.id && config.id !== previousConfigId;
|
|
64
77
|
const hasConfig = config !== void 0 && config !== null;
|
|
@@ -69,7 +82,7 @@ $: {
|
|
|
69
82
|
if (config.defaultVisibleColumns && columns.length > 0) {
|
|
70
83
|
const visibilityMap = {};
|
|
71
84
|
columns.forEach((col) => {
|
|
72
|
-
const colId = col
|
|
85
|
+
const colId = getColumnId(col);
|
|
73
86
|
if (config && config.defaultVisibleColumns) {
|
|
74
87
|
visibilityMap[colId] = config.defaultVisibleColumns.includes(colId);
|
|
75
88
|
}
|
|
@@ -283,7 +296,38 @@ function handleDrop(targetColumnId) {
|
|
|
283
296
|
draggedColumnId = null;
|
|
284
297
|
}
|
|
285
298
|
$: if ($columnOrder.length === 0 && columns.length > 0) {
|
|
286
|
-
columnOrder.set(columns.map((col) => col
|
|
299
|
+
columnOrder.set(columns.map((col) => getColumnId(col)));
|
|
300
|
+
}
|
|
301
|
+
function showCellContextMenu(event, cell) {
|
|
302
|
+
if (features.filtering === false) return;
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
const value = cell.getValue();
|
|
305
|
+
const columnId = cell.column.id;
|
|
306
|
+
const columnHeader = String(cell.column.columnDef.header || columnId);
|
|
307
|
+
const isNumeric = typeof value === "number" || !isNaN(parseFloat(value));
|
|
308
|
+
cellContextMenu = {
|
|
309
|
+
show: true,
|
|
310
|
+
x: event.clientX,
|
|
311
|
+
y: event.clientY,
|
|
312
|
+
value,
|
|
313
|
+
columnId,
|
|
314
|
+
columnHeader,
|
|
315
|
+
isNumeric
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function closeCellContextMenu() {
|
|
319
|
+
cellContextMenu = { ...cellContextMenu, show: false };
|
|
320
|
+
}
|
|
321
|
+
function addFilterFromCell(columnId, operator, value) {
|
|
322
|
+
const newCondition = {
|
|
323
|
+
id: generateFilterId(),
|
|
324
|
+
field: columnId,
|
|
325
|
+
operator,
|
|
326
|
+
value: String(value ?? "")
|
|
327
|
+
};
|
|
328
|
+
filterConditions.update((conditions) => [...conditions, newCondition]);
|
|
329
|
+
filterBarExpanded = true;
|
|
330
|
+
closeCellContextMenu();
|
|
287
331
|
}
|
|
288
332
|
$: hasActiveFilters = $filterConditions.length > 0;
|
|
289
333
|
$: totalTableWidth = $table.getVisibleLeafColumns().reduce((sum, col) => sum + col.getSize(), 0);
|
|
@@ -292,8 +336,8 @@ $: if (onStateChange) {
|
|
|
292
336
|
columnVisibility: $columnVisibility,
|
|
293
337
|
columnOrder: $columnOrder,
|
|
294
338
|
columnSizing: $columnSizing,
|
|
295
|
-
columnFilters: $
|
|
296
|
-
sorting: $sorting,
|
|
339
|
+
columnFilters: $filterConditions,
|
|
340
|
+
sorting: $sorting.map((s) => ({ columnId: s.id, direction: s.desc ? "desc" : "asc" })),
|
|
297
341
|
pagination: $table.getState().pagination
|
|
298
342
|
});
|
|
299
343
|
}
|
|
@@ -313,6 +357,9 @@ $: if (onStateChange) {
|
|
|
313
357
|
<div class="table-kit-filters">
|
|
314
358
|
<FilterBar
|
|
315
359
|
{columns}
|
|
360
|
+
{data}
|
|
361
|
+
columnOrder={$columnOrder}
|
|
362
|
+
{storageKey}
|
|
316
363
|
conditions={$filterConditions}
|
|
317
364
|
onConditionsChange={(newConditions) => filterConditions.set(newConditions)}
|
|
318
365
|
logic={$filterLogic}
|
|
@@ -595,8 +642,10 @@ $: if (onStateChange) {
|
|
|
595
642
|
>
|
|
596
643
|
{#if !header.isPlaceholder}
|
|
597
644
|
<div class="th-wrapper">
|
|
645
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
598
646
|
<div
|
|
599
647
|
class="th-content"
|
|
648
|
+
role={features.columnReordering !== false ? 'button' : undefined}
|
|
600
649
|
style="padding: {verticalPadding}rem {horizontalPadding}rem; cursor: {features.columnReordering !==
|
|
601
650
|
false
|
|
602
651
|
? 'grab'
|
|
@@ -616,10 +665,7 @@ $: if (onStateChange) {
|
|
|
616
665
|
/>
|
|
617
666
|
</span>
|
|
618
667
|
<span class="sort-icon">
|
|
619
|
-
{
|
|
620
|
-
asc: '↑',
|
|
621
|
-
desc: '↓'
|
|
622
|
-
}[header.column.getIsSorted()] ?? '↕'}
|
|
668
|
+
{header.column.getIsSorted() === 'asc' ? '↑' : header.column.getIsSorted() === 'desc' ? '↓' : '↕'}
|
|
623
669
|
</span>
|
|
624
670
|
</button>
|
|
625
671
|
{:else}
|
|
@@ -766,7 +812,11 @@ $: if (onStateChange) {
|
|
|
766
812
|
<!-- Placeholder cell - empty -->
|
|
767
813
|
{:else}
|
|
768
814
|
<!-- Normal cell -->
|
|
769
|
-
|
|
815
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
816
|
+
<div
|
|
817
|
+
class="cell-content"
|
|
818
|
+
on:contextmenu={(e) => showCellContextMenu(e, cell)}
|
|
819
|
+
>
|
|
770
820
|
<slot name="cell" {cell} column={cell.column.id}>
|
|
771
821
|
<svelte:component
|
|
772
822
|
this={flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
@@ -841,6 +891,23 @@ $: if (onStateChange) {
|
|
|
841
891
|
{/if}
|
|
842
892
|
</div>
|
|
843
893
|
|
|
894
|
+
<!-- Cell Context Menu -->
|
|
895
|
+
{#if cellContextMenu.show}
|
|
896
|
+
<CellContextMenu
|
|
897
|
+
x={cellContextMenu.x}
|
|
898
|
+
y={cellContextMenu.y}
|
|
899
|
+
value={cellContextMenu.value}
|
|
900
|
+
columnId={cellContextMenu.columnId}
|
|
901
|
+
columnHeader={cellContextMenu.columnHeader}
|
|
902
|
+
isNumeric={cellContextMenu.isNumeric}
|
|
903
|
+
on:filterEquals={(e) => addFilterFromCell(e.detail.columnId, 'equals', e.detail.value)}
|
|
904
|
+
on:filterNotEquals={(e) => addFilterFromCell(e.detail.columnId, 'not_equals', e.detail.value)}
|
|
905
|
+
on:filterGreaterThan={(e) => addFilterFromCell(e.detail.columnId, 'greater_than', e.detail.value)}
|
|
906
|
+
on:filterLessThan={(e) => addFilterFromCell(e.detail.columnId, 'less_than', e.detail.value)}
|
|
907
|
+
on:close={closeCellContextMenu}
|
|
908
|
+
/>
|
|
909
|
+
{/if}
|
|
910
|
+
|
|
844
911
|
<style>
|
|
845
912
|
/* Container */
|
|
846
913
|
.table-kit-container {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { SvelteComponent } from "svelte";
|
|
2
2
|
import { type ColumnDef } from '@tanstack/svelte-table';
|
|
3
3
|
import type { TableKitProps } from './types';
|
|
4
|
-
declare class __sveltets_Render<T
|
|
4
|
+
declare class __sveltets_Render<T extends Record<string, unknown>> {
|
|
5
5
|
props(): {
|
|
6
6
|
data?: T[] | undefined;
|
|
7
7
|
columns?: ColumnDef<T>[] | undefined;
|
|
8
8
|
config?: TableKitProps<T_1>["config"];
|
|
9
|
-
storageKey?: TableKitProps<T_1>["storageKey"]
|
|
9
|
+
storageKey?: NonNullable<TableKitProps<T_1>["storageKey"]>;
|
|
10
10
|
persistState?: TableKitProps<T_1>["persistState"];
|
|
11
11
|
align?: TableKitProps<T_1>["align"];
|
|
12
12
|
rowHeight?: TableKitProps<T_1>["rowHeight"];
|
|
13
13
|
columnSpacing?: TableKitProps<T_1>["columnSpacing"];
|
|
14
|
-
features?: TableKitProps<T_1>["features"]
|
|
14
|
+
features?: NonNullable<TableKitProps<T_1>["features"]>;
|
|
15
15
|
onRowClick?: ((row: T) => void) | undefined;
|
|
16
|
-
onRowSelect?: ((rows: T[]) => void) | undefined;
|
|
16
|
+
/** @deprecated Row selection not yet implemented - reserved for future use */ onRowSelect?: ((rows: T[]) => void) | undefined;
|
|
17
17
|
onStateChange?: TableKitProps<T_1>["onStateChange"];
|
|
18
18
|
};
|
|
19
19
|
events(): {} & {
|
|
@@ -28,9 +28,9 @@ declare class __sveltets_Render<T> {
|
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
export type TableKitProps_<T
|
|
32
|
-
export type TableKitEvents<T
|
|
33
|
-
export type TableKitSlots<T
|
|
34
|
-
export default class TableKit<T
|
|
31
|
+
export type TableKitProps_<T extends Record<string, unknown>> = ReturnType<__sveltets_Render<T>['props']>;
|
|
32
|
+
export type TableKitEvents<T extends Record<string, unknown>> = ReturnType<__sveltets_Render<T>['events']>;
|
|
33
|
+
export type TableKitSlots<T extends Record<string, unknown>> = ReturnType<__sveltets_Render<T>['slots']>;
|
|
34
|
+
export default class TableKit<T extends Record<string, unknown>> extends SvelteComponent<TableKitProps_<T>, TableKitEvents<T>, TableKitSlots<T>> {
|
|
35
35
|
}
|
|
36
36
|
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<script>import { onMount, createEventDispatcher } from "svelte";
|
|
2
|
+
export let x;
|
|
3
|
+
export let y;
|
|
4
|
+
export let value;
|
|
5
|
+
export let columnId;
|
|
6
|
+
export let columnHeader;
|
|
7
|
+
export let isNumeric = false;
|
|
8
|
+
const dispatch = createEventDispatcher();
|
|
9
|
+
let menuRef;
|
|
10
|
+
$: displayValue = (() => {
|
|
11
|
+
const str = String(value ?? "");
|
|
12
|
+
if (str.length > 30) {
|
|
13
|
+
return str.substring(0, 27) + "...";
|
|
14
|
+
}
|
|
15
|
+
return str;
|
|
16
|
+
})();
|
|
17
|
+
$: adjustedPosition = (() => {
|
|
18
|
+
let adjustedX = x;
|
|
19
|
+
let adjustedY = y;
|
|
20
|
+
if (typeof window !== "undefined") {
|
|
21
|
+
const menuWidth = 220;
|
|
22
|
+
const menuHeight = isNumeric ? 180 : 100;
|
|
23
|
+
if (x + menuWidth > window.innerWidth) {
|
|
24
|
+
adjustedX = window.innerWidth - menuWidth - 10;
|
|
25
|
+
}
|
|
26
|
+
if (y + menuHeight > window.innerHeight) {
|
|
27
|
+
adjustedY = window.innerHeight - menuHeight - 10;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { x: adjustedX, y: adjustedY };
|
|
31
|
+
})();
|
|
32
|
+
function handleFilterEquals() {
|
|
33
|
+
dispatch("filterEquals", { columnId, value });
|
|
34
|
+
dispatch("close");
|
|
35
|
+
}
|
|
36
|
+
function handleFilterNotEquals() {
|
|
37
|
+
dispatch("filterNotEquals", { columnId, value });
|
|
38
|
+
dispatch("close");
|
|
39
|
+
}
|
|
40
|
+
function handleFilterGreaterThan() {
|
|
41
|
+
dispatch("filterGreaterThan", { columnId, value });
|
|
42
|
+
dispatch("close");
|
|
43
|
+
}
|
|
44
|
+
function handleFilterLessThan() {
|
|
45
|
+
dispatch("filterLessThan", { columnId, value });
|
|
46
|
+
dispatch("close");
|
|
47
|
+
}
|
|
48
|
+
function handleKeydown(event) {
|
|
49
|
+
if (event.key === "Escape") {
|
|
50
|
+
dispatch("close");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function handleClickOutside(event) {
|
|
54
|
+
if (menuRef && !menuRef.contains(event.target)) {
|
|
55
|
+
dispatch("close");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
onMount(() => {
|
|
59
|
+
document.addEventListener("click", handleClickOutside);
|
|
60
|
+
document.addEventListener("keydown", handleKeydown);
|
|
61
|
+
menuRef?.focus();
|
|
62
|
+
return () => {
|
|
63
|
+
document.removeEventListener("click", handleClickOutside);
|
|
64
|
+
document.removeEventListener("keydown", handleKeydown);
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
bind:this={menuRef}
|
|
71
|
+
class="cell-context-menu"
|
|
72
|
+
style="top: {adjustedPosition.y}px; left: {adjustedPosition.x}px;"
|
|
73
|
+
role="menu"
|
|
74
|
+
tabindex="-1"
|
|
75
|
+
>
|
|
76
|
+
<div class="menu-header">
|
|
77
|
+
<span class="column-name">{columnHeader}</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<button class="menu-item" on:click={handleFilterEquals} role="menuitem">
|
|
81
|
+
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
82
|
+
<path
|
|
83
|
+
stroke-linecap="round"
|
|
84
|
+
stroke-linejoin="round"
|
|
85
|
+
stroke-width="2"
|
|
86
|
+
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
87
|
+
/>
|
|
88
|
+
</svg>
|
|
89
|
+
<span>Filter by "<strong>{displayValue}</strong>"</span>
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
<button class="menu-item" on:click={handleFilterNotEquals} role="menuitem">
|
|
93
|
+
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
94
|
+
<path
|
|
95
|
+
stroke-linecap="round"
|
|
96
|
+
stroke-linejoin="round"
|
|
97
|
+
stroke-width="2"
|
|
98
|
+
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
|
99
|
+
/>
|
|
100
|
+
</svg>
|
|
101
|
+
<span>Exclude "<strong>{displayValue}</strong>"</span>
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
{#if isNumeric}
|
|
105
|
+
<div class="menu-divider"></div>
|
|
106
|
+
|
|
107
|
+
<button class="menu-item" on:click={handleFilterGreaterThan} role="menuitem">
|
|
108
|
+
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
109
|
+
<path
|
|
110
|
+
stroke-linecap="round"
|
|
111
|
+
stroke-linejoin="round"
|
|
112
|
+
stroke-width="2"
|
|
113
|
+
d="M9 5l7 7-7 7"
|
|
114
|
+
/>
|
|
115
|
+
</svg>
|
|
116
|
+
<span>Greater than <strong>{displayValue}</strong></span>
|
|
117
|
+
</button>
|
|
118
|
+
|
|
119
|
+
<button class="menu-item" on:click={handleFilterLessThan} role="menuitem">
|
|
120
|
+
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
121
|
+
<path
|
|
122
|
+
stroke-linecap="round"
|
|
123
|
+
stroke-linejoin="round"
|
|
124
|
+
stroke-width="2"
|
|
125
|
+
d="M15 19l-7-7 7-7"
|
|
126
|
+
/>
|
|
127
|
+
</svg>
|
|
128
|
+
<span>Less than <strong>{displayValue}</strong></span>
|
|
129
|
+
</button>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<style>
|
|
134
|
+
.cell-context-menu {
|
|
135
|
+
position: fixed;
|
|
136
|
+
z-index: 1000;
|
|
137
|
+
min-width: 200px;
|
|
138
|
+
background: white;
|
|
139
|
+
border: 1px solid #e5e7eb;
|
|
140
|
+
border-radius: 0.5rem;
|
|
141
|
+
box-shadow:
|
|
142
|
+
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
143
|
+
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
144
|
+
padding: 0.25rem 0;
|
|
145
|
+
outline: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.menu-header {
|
|
149
|
+
padding: 0.5rem 0.75rem;
|
|
150
|
+
border-bottom: 1px solid #e5e7eb;
|
|
151
|
+
margin-bottom: 0.25rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.column-name {
|
|
155
|
+
font-size: 0.75rem;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
color: #6b7280;
|
|
158
|
+
text-transform: uppercase;
|
|
159
|
+
letter-spacing: 0.025em;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.menu-item {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
width: 100%;
|
|
167
|
+
padding: 0.5rem 0.75rem;
|
|
168
|
+
font-size: 0.875rem;
|
|
169
|
+
text-align: left;
|
|
170
|
+
background: none;
|
|
171
|
+
border: none;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition: background-color 0.1s;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.menu-item:hover {
|
|
177
|
+
background: #f3f4f6;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.menu-item:focus {
|
|
181
|
+
outline: none;
|
|
182
|
+
background: #e5e7eb;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.menu-item span {
|
|
186
|
+
flex: 1;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
text-overflow: ellipsis;
|
|
189
|
+
white-space: nowrap;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.menu-item strong {
|
|
193
|
+
color: #1f2937;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.menu-icon {
|
|
197
|
+
width: 1rem;
|
|
198
|
+
height: 1rem;
|
|
199
|
+
color: #6b7280;
|
|
200
|
+
flex-shrink: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.menu-divider {
|
|
204
|
+
height: 1px;
|
|
205
|
+
background: #e5e7eb;
|
|
206
|
+
margin: 0.25rem 0;
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
value: any;
|
|
7
|
+
columnId: string;
|
|
8
|
+
columnHeader: string;
|
|
9
|
+
isNumeric?: boolean;
|
|
10
|
+
};
|
|
11
|
+
events: {
|
|
12
|
+
filterEquals: CustomEvent<{
|
|
13
|
+
columnId: string;
|
|
14
|
+
value: any;
|
|
15
|
+
}>;
|
|
16
|
+
filterNotEquals: CustomEvent<{
|
|
17
|
+
columnId: string;
|
|
18
|
+
value: any;
|
|
19
|
+
}>;
|
|
20
|
+
filterGreaterThan: CustomEvent<{
|
|
21
|
+
columnId: string;
|
|
22
|
+
value: any;
|
|
23
|
+
}>;
|
|
24
|
+
filterLessThan: CustomEvent<{
|
|
25
|
+
columnId: string;
|
|
26
|
+
value: any;
|
|
27
|
+
}>;
|
|
28
|
+
close: CustomEvent<void>;
|
|
29
|
+
} & {
|
|
30
|
+
[evt: string]: CustomEvent<any>;
|
|
31
|
+
};
|
|
32
|
+
slots: {};
|
|
33
|
+
exports?: {} | undefined;
|
|
34
|
+
bindings?: string | undefined;
|
|
35
|
+
};
|
|
36
|
+
export type CellContextMenuProps = typeof __propDef.props;
|
|
37
|
+
export type CellContextMenuEvents = typeof __propDef.events;
|
|
38
|
+
export type CellContextMenuSlots = typeof __propDef.slots;
|
|
39
|
+
export default class CellContextMenu extends SvelteComponent<CellContextMenuProps, CellContextMenuEvents, CellContextMenuSlots> {
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
|
2
|
-
import {
|
|
2
|
+
import { isBrowser } from "../stores/persistence";
|
|
3
3
|
export let column;
|
|
4
4
|
export let isOpen = false;
|
|
5
5
|
export let canSort = true;
|
|
@@ -42,7 +42,7 @@ function handleKeydown(event) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
$: {
|
|
45
|
-
if (
|
|
45
|
+
if (isBrowser) {
|
|
46
46
|
if (isOpen) {
|
|
47
47
|
setTimeout(() => {
|
|
48
48
|
document.addEventListener("mousedown", handleClickOutside);
|
|
@@ -55,7 +55,7 @@ $: {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
onDestroy(() => {
|
|
58
|
-
if (
|
|
58
|
+
if (isBrowser) {
|
|
59
59
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
60
60
|
document.removeEventListener("keydown", handleKeydown);
|
|
61
61
|
}
|
|
@@ -1,11 +1,83 @@
|
|
|
1
1
|
<script>import FilterConditionComponent from "./FilterCondition.svelte";
|
|
2
2
|
export let columns;
|
|
3
|
+
export let data = [];
|
|
4
|
+
export let columnOrder = [];
|
|
5
|
+
export let storageKey = "table-kit";
|
|
3
6
|
export let conditions = [];
|
|
4
7
|
export let onConditionsChange;
|
|
5
8
|
export let logic = "and";
|
|
6
9
|
export let onLogicChange;
|
|
7
10
|
export let isExpanded = false;
|
|
8
11
|
export let onExpandedChange = void 0;
|
|
12
|
+
let columnValuesCache = /* @__PURE__ */ new Map();
|
|
13
|
+
let numericRangeCache = /* @__PURE__ */ new Map();
|
|
14
|
+
let lastDataLength = 0;
|
|
15
|
+
function isNumericColumn(columnId) {
|
|
16
|
+
if (!columnId || !data || data.length === 0) return false;
|
|
17
|
+
let numericCount = 0;
|
|
18
|
+
let sampleCount = 0;
|
|
19
|
+
const sampleSize = Math.min(10, data.length);
|
|
20
|
+
for (const row of data) {
|
|
21
|
+
if (sampleCount >= sampleSize) break;
|
|
22
|
+
const val = row[columnId];
|
|
23
|
+
if (val !== null && val !== void 0 && val !== "") {
|
|
24
|
+
sampleCount++;
|
|
25
|
+
if (typeof val === "number" || typeof val === "string" && !isNaN(Number(val))) {
|
|
26
|
+
numericCount++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return sampleCount > 0 && numericCount / sampleCount >= 0.8;
|
|
31
|
+
}
|
|
32
|
+
function getNumericRange(columnId) {
|
|
33
|
+
if (!columnId || !data || data.length === 0) return null;
|
|
34
|
+
if (data.length !== lastDataLength) {
|
|
35
|
+
numericRangeCache.clear();
|
|
36
|
+
}
|
|
37
|
+
if (numericRangeCache.has(columnId)) {
|
|
38
|
+
return numericRangeCache.get(columnId);
|
|
39
|
+
}
|
|
40
|
+
if (!isNumericColumn(columnId)) {
|
|
41
|
+
numericRangeCache.set(columnId, null);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let min = Infinity;
|
|
45
|
+
let max = -Infinity;
|
|
46
|
+
for (const row of data) {
|
|
47
|
+
const val = row[columnId];
|
|
48
|
+
if (val !== null && val !== void 0 && val !== "") {
|
|
49
|
+
const num = typeof val === "number" ? val : Number(val);
|
|
50
|
+
if (!isNaN(num)) {
|
|
51
|
+
min = Math.min(min, num);
|
|
52
|
+
max = Math.max(max, num);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const range = min !== Infinity ? { min, max } : null;
|
|
57
|
+
numericRangeCache.set(columnId, range);
|
|
58
|
+
return range;
|
|
59
|
+
}
|
|
60
|
+
function getColumnValues(columnId) {
|
|
61
|
+
if (!columnId || !data || data.length === 0) return [];
|
|
62
|
+
if (data.length !== lastDataLength) {
|
|
63
|
+
columnValuesCache.clear();
|
|
64
|
+
numericRangeCache.clear();
|
|
65
|
+
lastDataLength = data.length;
|
|
66
|
+
}
|
|
67
|
+
if (columnValuesCache.has(columnId)) {
|
|
68
|
+
return columnValuesCache.get(columnId);
|
|
69
|
+
}
|
|
70
|
+
const values = /* @__PURE__ */ new Set();
|
|
71
|
+
for (const row of data) {
|
|
72
|
+
const val = row[columnId];
|
|
73
|
+
if (val !== null && val !== void 0 && val !== "") {
|
|
74
|
+
values.add(String(val));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const sortedValues = Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
78
|
+
columnValuesCache.set(columnId, sortedValues);
|
|
79
|
+
return sortedValues;
|
|
80
|
+
}
|
|
9
81
|
function generateId() {
|
|
10
82
|
return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
11
83
|
}
|
|
@@ -99,6 +171,10 @@ $: filterCount = conditions.filter(
|
|
|
99
171
|
<FilterConditionComponent
|
|
100
172
|
{condition}
|
|
101
173
|
{columns}
|
|
174
|
+
{columnOrder}
|
|
175
|
+
{storageKey}
|
|
176
|
+
columnValues={getColumnValues(condition.field)}
|
|
177
|
+
numericRange={getNumericRange(condition.field)}
|
|
102
178
|
onUpdate={(updated) => updateCondition(index, updated)}
|
|
103
179
|
onRemove={() => removeCondition(index)}
|
|
104
180
|
/>
|
|
@@ -4,6 +4,9 @@ import type { FilterCondition, FilterLogic } from '../types';
|
|
|
4
4
|
declare const __propDef: {
|
|
5
5
|
props: {
|
|
6
6
|
columns: ColumnDef<any>[];
|
|
7
|
+
data?: any[];
|
|
8
|
+
columnOrder?: string[];
|
|
9
|
+
storageKey?: string;
|
|
7
10
|
conditions?: FilterCondition[];
|
|
8
11
|
onConditionsChange: (conditions: FilterCondition[]) => void;
|
|
9
12
|
logic?: FilterLogic;
|