@softwareone/spi-sv5-library 1.10.4 → 1.11.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/dist/Button/Button.svelte +2 -1
- package/dist/Card/Card.svelte +1 -1
- package/dist/{AttachFile → Controls/AttachFile}/AttachFile.svelte +36 -21
- package/dist/{AttachFile → Controls/AttachFile}/AttachFile.svelte.d.ts +2 -1
- package/dist/{AttachFile → Controls/AttachFile}/Warnings.svelte +1 -1
- package/dist/Controls/Input/Input.svelte +2 -4
- package/dist/Controls/Input/Input.svelte.d.ts +2 -4
- package/dist/Controls/Label/Label.svelte +3 -8
- package/dist/Controls/Label/Label.svelte.d.ts +1 -7
- package/dist/Controls/Label/labelState.svelte.d.ts +7 -0
- package/dist/Controls/Label/labelState.svelte.js +1 -0
- package/dist/Controls/Select/Select.svelte +2 -5
- package/dist/Controls/Select/Select.svelte.d.ts +2 -5
- package/dist/Controls/TextArea/TextArea.svelte +2 -4
- package/dist/Controls/TextArea/TextArea.svelte.d.ts +2 -4
- package/dist/Controls/Toggle/Toggle.svelte +3 -5
- package/dist/Controls/Toggle/Toggle.svelte.d.ts +2 -4
- package/dist/Table/AdvancedFilter.svelte +385 -0
- package/dist/Table/AdvancedFilter.svelte.d.ts +29 -0
- package/dist/Table/Header.svelte +38 -5
- package/dist/Table/Table.svelte +78 -35
- package/dist/Table/Table.svelte.d.ts +2 -1
- package/dist/Table/consts.d.ts +6 -0
- package/dist/Table/consts.js +5 -0
- package/dist/Table/excel.js +11 -1
- package/dist/Table/index.d.ts +2 -1
- package/dist/Table/index.js +2 -1
- package/dist/Table/types.js +1 -1
- package/dist/Table/util.d.ts +2 -0
- package/dist/Table/util.js +21 -3
- package/dist/Tabs/Tabs.svelte +5 -2
- package/dist/Tabs/tabsState.svelte.d.ts +7 -5
- package/dist/Tabs/tabsState.svelte.js +3 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +1 -1
- /package/dist/{AttachFile → Controls/AttachFile}/FileManager.svelte +0 -0
- /package/dist/{AttachFile → Controls/AttachFile}/FileManager.svelte.d.ts +0 -0
- /package/dist/{AttachFile → Controls/AttachFile}/Warnings.svelte.d.ts +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
|
|
4
|
+
import { Button, Input, Select, type SelectOption } from '../index.js';
|
|
5
|
+
import type { Column } from './adapter/index.js';
|
|
6
|
+
import { Operator } from './consts.js';
|
|
7
|
+
import { type Filter } from './types.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
columns: Column<T, unknown>[];
|
|
11
|
+
onfilterschange?: (filters: Filter[]) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { columns, onfilterschange }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let isOpen = $state(false);
|
|
17
|
+
let editingFilters = $state<Filter[]>([]);
|
|
18
|
+
const operatorOptions: Operator[] = Object.values(Operator);
|
|
19
|
+
|
|
20
|
+
const filters = $derived.by<Filter[]>(() => {
|
|
21
|
+
const filterParams = page.url.searchParams.getAll('filter');
|
|
22
|
+
return filterParams
|
|
23
|
+
.filter((param) => param.trim())
|
|
24
|
+
.map(parseFilterParam)
|
|
25
|
+
.filter((filter): filter is Filter => filter !== null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const hasActiveFilters = $derived(filters.length > 0);
|
|
29
|
+
const columnOptions = $derived<SelectOption[]>(
|
|
30
|
+
columns
|
|
31
|
+
.filter((column) => !column.columnDef.meta?.hideColumnFilter)
|
|
32
|
+
.map((column) => ({
|
|
33
|
+
value: column.id,
|
|
34
|
+
label: (column.columnDef.header as string) || column.id
|
|
35
|
+
}))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const addFilterRow = () => {
|
|
39
|
+
editingFilters = [...editingFilters, { column: '', value: '', operator: Operator.And }];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const removeFilterRow = (index: number) => {
|
|
43
|
+
const remainingFilters = editingFilters.filter((_, filterIndex) => filterIndex !== index);
|
|
44
|
+
|
|
45
|
+
editingFilters =
|
|
46
|
+
remainingFilters.length === 1
|
|
47
|
+
? [{ ...remainingFilters[0], operator: undefined }]
|
|
48
|
+
: remainingFilters;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const applyFilters = () => {
|
|
52
|
+
const validFilters = getValidFilters();
|
|
53
|
+
handleFilters(validFilters);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const clearAllFilters = () => {
|
|
57
|
+
editingFilters = [];
|
|
58
|
+
handleFilters(editingFilters);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleFilters = (filters: Filter[]) => {
|
|
62
|
+
onfilterschange?.(filters);
|
|
63
|
+
isOpen = false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const toggleModal = () => {
|
|
67
|
+
isOpen = !isOpen;
|
|
68
|
+
isOpen ? initializeEditingFilters() : clearFilters();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const initializeEditingFilters = () => {
|
|
72
|
+
if (!editingFilters.length) {
|
|
73
|
+
editingFilters = filters.length
|
|
74
|
+
? filters.map((filter) => ({ ...filter }))
|
|
75
|
+
: [{ column: '', value: '', operator: undefined }];
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const clearFilters = () => {
|
|
80
|
+
const validFilters = getValidFilters();
|
|
81
|
+
if (validFilters.length === 0 && filters.length > 0) {
|
|
82
|
+
clearAllFilters();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getValidFilters = () => {
|
|
87
|
+
return editingFilters.filter((filter) => filter.column && filter.value.trim());
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const parseFilterParam = (filterValue: string) => {
|
|
91
|
+
const parts = filterValue.split(':');
|
|
92
|
+
if (parts.length === 3) return parseSimpleFilter(parts);
|
|
93
|
+
if (parts.length === 4) return parseOperatorFilter(parts);
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const parseSimpleFilter = (parts: string[]): Filter | null => {
|
|
98
|
+
const [column, operator, value] = parts;
|
|
99
|
+
return isValidFilter(column, operator, value) ? { column, value, operator: undefined } : null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const parseOperatorFilter = (parts: string[]): Filter | null => {
|
|
103
|
+
const [operator, column, eq, value] = parts;
|
|
104
|
+
return isValidFilter(column, eq, value) && isValidOperator(operator)
|
|
105
|
+
? { column, value, operator }
|
|
106
|
+
: null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const isValidFilter = (column: string, operator: string, value: string) => {
|
|
110
|
+
return operator === 'eq' && !!column && !!value;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isValidOperator = (operator: string): operator is Operator => {
|
|
114
|
+
return operatorOptions.includes(operator as Operator);
|
|
115
|
+
};
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<div class="filter-container">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class={['filter-button', hasActiveFilters && 'filter-button--active']}
|
|
122
|
+
onclick={toggleModal}
|
|
123
|
+
>
|
|
124
|
+
<span class="material-icons">filter_list</span>
|
|
125
|
+
{#if hasActiveFilters}
|
|
126
|
+
<strong>{filters.length} {filters.length === 1 ? 'Filter' : 'Filters'}</strong>
|
|
127
|
+
{:else}
|
|
128
|
+
<p>Filters</p>
|
|
129
|
+
{/if}
|
|
130
|
+
</button>
|
|
131
|
+
|
|
132
|
+
{#if isOpen}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
class="filter-overlay"
|
|
136
|
+
onclick={toggleModal}
|
|
137
|
+
aria-label="Close filter modal"
|
|
138
|
+
></button>
|
|
139
|
+
|
|
140
|
+
<div
|
|
141
|
+
class="filter-modal"
|
|
142
|
+
role="dialog"
|
|
143
|
+
tabindex="-1"
|
|
144
|
+
aria-modal="true"
|
|
145
|
+
aria-labelledby="filter-dialog-title"
|
|
146
|
+
>
|
|
147
|
+
<div class="filter-header">
|
|
148
|
+
<h2 id="filter-dialog-title" class="filter-title">Filters</h2>
|
|
149
|
+
<button type="button" onclick={toggleModal} class="filter-close-button" aria-label="Close">
|
|
150
|
+
<span class="material-icons">close</span>
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="filter-body">
|
|
155
|
+
{#each editingFilters as filter, index (index)}
|
|
156
|
+
<div class="filter-row">
|
|
157
|
+
{#if index > 0}
|
|
158
|
+
<div class="filter-operator">
|
|
159
|
+
<Select
|
|
160
|
+
options={operatorOptions}
|
|
161
|
+
disableValidationColor
|
|
162
|
+
bind:value={filter.operator}
|
|
163
|
+
hideClearButton
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
{:else}
|
|
167
|
+
<div class="filter-operator"></div>
|
|
168
|
+
{/if}
|
|
169
|
+
|
|
170
|
+
<div class="filter-column">
|
|
171
|
+
<Select
|
|
172
|
+
options={columnOptions}
|
|
173
|
+
disableValidationColor
|
|
174
|
+
bind:value={filter.column}
|
|
175
|
+
placeholder="Select column"
|
|
176
|
+
searchable
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="filter-value">
|
|
181
|
+
<Input
|
|
182
|
+
id="filter-value-{index}"
|
|
183
|
+
disableValidationColor
|
|
184
|
+
type="text"
|
|
185
|
+
bind:value={filter.value}
|
|
186
|
+
placeholder="Enter value"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onclick={() => removeFilterRow(index)}
|
|
193
|
+
class="filter-delete-button"
|
|
194
|
+
aria-label="Remove filter"
|
|
195
|
+
>
|
|
196
|
+
<span class="material-icons-outlined">delete</span>
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
{/each}
|
|
200
|
+
|
|
201
|
+
<div class="filter-actions">
|
|
202
|
+
<Button variant="outline-none" type="button" onclick={addFilterRow}>Add condition</Button>
|
|
203
|
+
{#if editingFilters.length}
|
|
204
|
+
<Button variant="outline-none" type="button" onclick={clearAllFilters}>
|
|
205
|
+
Reset filters
|
|
206
|
+
</Button>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{#if editingFilters.length}
|
|
212
|
+
<div class="filter-footer">
|
|
213
|
+
<Button
|
|
214
|
+
onclick={applyFilters}
|
|
215
|
+
disabled={editingFilters.every((filter) => !filter.column || !filter.value.trim())}
|
|
216
|
+
>
|
|
217
|
+
Apply Filters
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
</div>
|
|
222
|
+
{/if}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<style>
|
|
226
|
+
.filter-container {
|
|
227
|
+
--color-primary: #472aff;
|
|
228
|
+
--color-primary-light: #eaecff;
|
|
229
|
+
--color-white: #fff;
|
|
230
|
+
--color-text-primary: #25282d;
|
|
231
|
+
--color-text-dark: #111827;
|
|
232
|
+
--color-text-secondary: #6b7280;
|
|
233
|
+
--color-text-hover: #374151;
|
|
234
|
+
--color-bg-hover: #f4f6f8;
|
|
235
|
+
--color-border: #e5e7eb;
|
|
236
|
+
--spacing-xs: 4px;
|
|
237
|
+
--spacing-sm: 8px;
|
|
238
|
+
--spacing-md: 12px;
|
|
239
|
+
--spacing-lg: 16px;
|
|
240
|
+
--spacing-xl: 24px;
|
|
241
|
+
--font-size-sm: 14px;
|
|
242
|
+
--font-size-md: 18px;
|
|
243
|
+
--font-size-icon: 20px;
|
|
244
|
+
--border-radius-sm: 8px;
|
|
245
|
+
--border-radius-circle: 50%;
|
|
246
|
+
--button-size: 40px;
|
|
247
|
+
--delete-button-size: 48px;
|
|
248
|
+
--modal-width: 640px;
|
|
249
|
+
--operator-column-width: 80px;
|
|
250
|
+
--transition-speed: 0.2s;
|
|
251
|
+
|
|
252
|
+
position: relative;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.filter-button {
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: var(--spacing-sm);
|
|
259
|
+
padding: var(--spacing-sm) var(--spacing-lg);
|
|
260
|
+
border: none;
|
|
261
|
+
border-radius: var(--border-radius-sm);
|
|
262
|
+
background: var(--color-white);
|
|
263
|
+
font-size: var(--font-size-sm);
|
|
264
|
+
color: var(--color-text-primary);
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
transition: background-color var(--transition-speed) ease-in-out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.filter-button:hover {
|
|
270
|
+
background: var(--color-bg-hover);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.filter-button--active {
|
|
274
|
+
background: var(--color-primary-light);
|
|
275
|
+
color: var(--color-primary);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.filter-overlay {
|
|
279
|
+
position: fixed;
|
|
280
|
+
inset: 0;
|
|
281
|
+
z-index: 9;
|
|
282
|
+
background: transparent;
|
|
283
|
+
border: none;
|
|
284
|
+
cursor: default;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.filter-modal {
|
|
288
|
+
position: absolute;
|
|
289
|
+
top: calc(100% + var(--spacing-sm));
|
|
290
|
+
right: 0;
|
|
291
|
+
z-index: 10;
|
|
292
|
+
width: var(--modal-width);
|
|
293
|
+
padding: var(--spacing-xl);
|
|
294
|
+
border: 1px solid var(--color-border);
|
|
295
|
+
border-radius: var(--border-radius-sm);
|
|
296
|
+
background: var(--color-white);
|
|
297
|
+
box-shadow:
|
|
298
|
+
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
299
|
+
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.filter-header {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: space-between;
|
|
306
|
+
margin-bottom: var(--spacing-xl);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.filter-title {
|
|
310
|
+
font-size: var(--font-size-md);
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
color: var(--color-text-dark);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.filter-close-button {
|
|
316
|
+
display: flex;
|
|
317
|
+
align-items: center;
|
|
318
|
+
padding: var(--spacing-xs);
|
|
319
|
+
border: none;
|
|
320
|
+
background: transparent;
|
|
321
|
+
color: var(--color-text-secondary);
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
transition: color var(--transition-speed) ease-in-out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.filter-close-button:hover {
|
|
327
|
+
color: var(--color-text-hover);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.filter-body {
|
|
331
|
+
display: flex;
|
|
332
|
+
flex-direction: column;
|
|
333
|
+
gap: var(--spacing-lg);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.filter-row {
|
|
337
|
+
display: grid;
|
|
338
|
+
grid-template-columns: var(--operator-column-width) 1fr 1fr var(--delete-button-size);
|
|
339
|
+
align-items: end;
|
|
340
|
+
gap: var(--spacing-md);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.filter-operator,
|
|
344
|
+
.filter-column,
|
|
345
|
+
.filter-value {
|
|
346
|
+
display: flex;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.filter-delete-button {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
padding: var(--spacing-sm);
|
|
354
|
+
border: none;
|
|
355
|
+
background: transparent;
|
|
356
|
+
color: var(--color-text-primary);
|
|
357
|
+
border-radius: var(--border-radius-circle);
|
|
358
|
+
width: var(--button-size);
|
|
359
|
+
height: var(--button-size);
|
|
360
|
+
cursor: pointer;
|
|
361
|
+
transition: background-color var(--transition-speed) ease-in-out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.filter-delete-button:hover {
|
|
365
|
+
background: var(--color-bg-hover);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.filter-actions {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
justify-content: space-between;
|
|
372
|
+
margin-top: var(--spacing-sm);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.filter-footer {
|
|
376
|
+
display: flex;
|
|
377
|
+
justify-content: flex-end;
|
|
378
|
+
margin-top: var(--spacing-lg);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.material-icons,
|
|
382
|
+
.material-icons-outlined {
|
|
383
|
+
font-size: var(--font-size-icon);
|
|
384
|
+
}
|
|
385
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Column } from './adapter/index.js';
|
|
2
|
+
import { type Filter } from './types.js';
|
|
3
|
+
declare function $$render<T>(): {
|
|
4
|
+
props: {
|
|
5
|
+
columns: Column<T, unknown>[];
|
|
6
|
+
onfilterschange?: (filters: Filter[]) => void;
|
|
7
|
+
};
|
|
8
|
+
exports: {};
|
|
9
|
+
bindings: "";
|
|
10
|
+
slots: {};
|
|
11
|
+
events: {};
|
|
12
|
+
};
|
|
13
|
+
declare class __sveltets_Render<T> {
|
|
14
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
15
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
16
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
17
|
+
bindings(): "";
|
|
18
|
+
exports(): {};
|
|
19
|
+
}
|
|
20
|
+
interface $$IsomorphicComponent {
|
|
21
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
22
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
23
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
24
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
25
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
26
|
+
}
|
|
27
|
+
declare const AdvancedFilter: $$IsomorphicComponent;
|
|
28
|
+
type AdvancedFilter<T> = InstanceType<typeof AdvancedFilter<T>>;
|
|
29
|
+
export default AdvancedFilter;
|
package/dist/Table/Header.svelte
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends RowData">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
|
|
2
4
|
import { Search } from '../index.js';
|
|
3
5
|
import { FlexRender, type HeaderGroup, type RowData } from './adapter/index.js';
|
|
4
6
|
|
|
@@ -7,13 +9,24 @@
|
|
|
7
9
|
enableColumnSearch: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
let { headerGroups, enableColumnSearch }: Props = $props();
|
|
13
|
+
|
|
10
14
|
const getSortIcon: Record<string, string> = {
|
|
11
15
|
asc: 'arrow_upward',
|
|
12
16
|
desc: 'arrow_downward',
|
|
13
17
|
false: 'swap_vert'
|
|
14
18
|
};
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
const isColumnFiltered = (columnId: string): boolean => {
|
|
21
|
+
const filterParams = page.url.searchParams.getAll('filter');
|
|
22
|
+
return filterParams.some((filterValue) => {
|
|
23
|
+
const parts = filterValue.split(':');
|
|
24
|
+
return (
|
|
25
|
+
(parts.length === 3 && parts[0] === columnId) ||
|
|
26
|
+
(parts.length === 4 && parts[1] === columnId)
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
17
30
|
</script>
|
|
18
31
|
|
|
19
32
|
<thead class="container">
|
|
@@ -27,9 +40,11 @@
|
|
|
27
40
|
{@const hideColumnFilter = header.column.columnDef.meta?.hideColumnFilter}
|
|
28
41
|
{@const justifyContent = alignColumn ?? 'left'}
|
|
29
42
|
{@const isSorted = header.column.getIsSorted().toString() !== 'false'}
|
|
43
|
+
{@const isFiltered = isColumnFiltered(header.column.id)}
|
|
44
|
+
{@const isActive = isSorted || isFiltered}
|
|
30
45
|
<th
|
|
31
46
|
colSpan={header.colSpan}
|
|
32
|
-
class={['table-header-cell', className,
|
|
47
|
+
class={['table-header-cell', className, isActive && 'table-header-cell--active']}
|
|
33
48
|
style:width={columnWidth}
|
|
34
49
|
style={columnStyle}
|
|
35
50
|
>
|
|
@@ -61,6 +76,12 @@
|
|
|
61
76
|
>
|
|
62
77
|
</div>
|
|
63
78
|
{/if}
|
|
79
|
+
|
|
80
|
+
{#if isFiltered}
|
|
81
|
+
<div class="table-header-filtered-icon">
|
|
82
|
+
<span class="material-icons-outlined">filter_alt</span>
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
64
85
|
</button>
|
|
65
86
|
|
|
66
87
|
{#if enableColumnSearch && !hideColumnFilter}
|
|
@@ -82,6 +103,7 @@
|
|
|
82
103
|
<style>
|
|
83
104
|
.container {
|
|
84
105
|
--color-primary: #472aff;
|
|
106
|
+
--color-primary-light: #eaecff;
|
|
85
107
|
--color-black: #000000;
|
|
86
108
|
--color-gray-100: #f3f4f6;
|
|
87
109
|
--color-gray-200: #e5e7eb;
|
|
@@ -112,8 +134,8 @@
|
|
|
112
134
|
background: var(--color-gray-100);
|
|
113
135
|
}
|
|
114
136
|
|
|
115
|
-
.table-header-cell--
|
|
116
|
-
background: var(--color-
|
|
137
|
+
.table-header-cell--active {
|
|
138
|
+
background: var(--color-primary-light);
|
|
117
139
|
color: var(--color-primary);
|
|
118
140
|
}
|
|
119
141
|
|
|
@@ -158,7 +180,18 @@
|
|
|
158
180
|
margin-top: 4px;
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
.table-header-sort-icon .
|
|
183
|
+
.table-header-sort-icon:has(+ .table-header-filtered-icon) {
|
|
184
|
+
right: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.table-header-filtered-icon {
|
|
188
|
+
position: absolute;
|
|
189
|
+
right: -13px;
|
|
190
|
+
margin-top: 4px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.table-header-sort-icon .material-icons,
|
|
194
|
+
.material-icons-outlined {
|
|
162
195
|
font-size: var(--font-size-sm);
|
|
163
196
|
}
|
|
164
197
|
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts" generics="T">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import { page } from '$app/state';
|
|
2
4
|
import { type Snippet } from 'svelte';
|
|
3
5
|
|
|
4
6
|
import { Button, Search } from '../index.js';
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
type Updater,
|
|
19
21
|
type VisibilityState
|
|
20
22
|
} from './adapter/index.js';
|
|
23
|
+
import AdvancedFilter from './AdvancedFilter.svelte';
|
|
21
24
|
import Body from './Body.svelte';
|
|
22
25
|
import ColumnVisibilityDropdown from './ColumnVisibilityDropdown.svelte';
|
|
23
26
|
import { DEFAULT_MIN_PAGE_LIMIT, DEFAULT_PAGE_LIMIT } from './consts.js';
|
|
@@ -27,8 +30,8 @@
|
|
|
27
30
|
import Footer from './Footer.svelte';
|
|
28
31
|
import Header from './Header.svelte';
|
|
29
32
|
import Skeleton from './Skeleton.svelte';
|
|
30
|
-
import type
|
|
31
|
-
import { createCheckedColumn } from './util.js';
|
|
33
|
+
import { type Filter, type Pagination } from './types.js';
|
|
34
|
+
import { createCheckedColumn, getPage, getPageLimit } from './util.js';
|
|
32
35
|
|
|
33
36
|
interface Props {
|
|
34
37
|
columns: ColumnDef<T, any>[];
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
enableGlobalSearch?: boolean;
|
|
41
44
|
enableColumnSearch?: boolean;
|
|
42
45
|
enableColumnVisibility?: boolean;
|
|
46
|
+
enableAdvancedFilter?: boolean;
|
|
43
47
|
manualPagination?: boolean;
|
|
44
48
|
pagination?: Pagination;
|
|
45
49
|
excelSetting?: ExcelSetting;
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
enableGlobalSearch = true,
|
|
60
64
|
enableColumnSearch = false,
|
|
61
65
|
enableColumnVisibility = true,
|
|
66
|
+
enableAdvancedFilter = false,
|
|
62
67
|
manualPagination = false,
|
|
63
68
|
pagination,
|
|
64
69
|
excelSetting = {
|
|
@@ -71,33 +76,6 @@
|
|
|
71
76
|
bulkActions
|
|
72
77
|
}: Props = $props();
|
|
73
78
|
|
|
74
|
-
const getInitialColumnVisibility = (): VisibilityState => {
|
|
75
|
-
const visibility: VisibilityState = {};
|
|
76
|
-
columns.forEach((column) => {
|
|
77
|
-
const columnId =
|
|
78
|
-
column.id ?? ('accessorKey' in column ? String(column.accessorKey) : undefined);
|
|
79
|
-
if (columnId && column.meta?.isVisible === false) {
|
|
80
|
-
visibility[columnId] = false;
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
return visibility;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const getInitialPaginationState = (): PaginationState => {
|
|
87
|
-
if (!manualPagination) {
|
|
88
|
-
return {
|
|
89
|
-
pageIndex: 0,
|
|
90
|
-
pageSize: minPageSize ? DEFAULT_MIN_PAGE_LIMIT : DEFAULT_PAGE_LIMIT
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const pageSize = DEFAULT_PAGE_LIMIT;
|
|
95
|
-
const pageIndex = 0;
|
|
96
|
-
|
|
97
|
-
return { pageIndex, pageSize };
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const tableManualPagination = manualPagination ? setPaginationTableContext() : undefined;
|
|
101
79
|
let globalFilter = $state<string>('');
|
|
102
80
|
let sorting = $state<SortingState>([]);
|
|
103
81
|
let columnFilters = $state<ColumnFiltersState>([]);
|
|
@@ -106,6 +84,8 @@
|
|
|
106
84
|
let columnVisibility = $state<VisibilityState>(getInitialColumnVisibility());
|
|
107
85
|
let paginationState = $state<PaginationState>(getInitialPaginationState());
|
|
108
86
|
let tableData = $derived(data);
|
|
87
|
+
const tableManualPagination = manualPagination ? setPaginationTableContext() : undefined;
|
|
88
|
+
const hasData = $derived(data.length > 0);
|
|
109
89
|
|
|
110
90
|
const table = createSvelteTable({
|
|
111
91
|
get data() {
|
|
@@ -172,7 +152,37 @@
|
|
|
172
152
|
sortDescFirst: false
|
|
173
153
|
});
|
|
174
154
|
|
|
175
|
-
const
|
|
155
|
+
const hasFilteredRows = $derived(table.getRowModel().rows.length > 0);
|
|
156
|
+
const selectedRows = $derived(table.getSelectedRowModel().rows);
|
|
157
|
+
|
|
158
|
+
function getInitialColumnVisibility(): VisibilityState {
|
|
159
|
+
const visibility: VisibilityState = {};
|
|
160
|
+
columns.forEach((column) => {
|
|
161
|
+
const columnId =
|
|
162
|
+
column.id ?? ('accessorKey' in column ? String(column.accessorKey) : undefined);
|
|
163
|
+
if (columnId && column.meta?.isVisible === false) {
|
|
164
|
+
visibility[columnId] = false;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return visibility;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getInitialPaginationState(): PaginationState {
|
|
171
|
+
if (!manualPagination) {
|
|
172
|
+
return {
|
|
173
|
+
pageIndex: 0,
|
|
174
|
+
pageSize: minPageSize ? DEFAULT_MIN_PAGE_LIMIT : DEFAULT_PAGE_LIMIT
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const currentPage = getPage(page.url.searchParams);
|
|
179
|
+
const pageSize = getPageLimit(page.url.searchParams);
|
|
180
|
+
const pageIndex = Math.max(0, currentPage - 1);
|
|
181
|
+
|
|
182
|
+
return { pageIndex, pageSize };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function changeManualPagination(updatedPaginationState: PaginationState) {
|
|
176
186
|
const pageSizeChanged = updatedPaginationState.pageSize !== paginationState.pageSize;
|
|
177
187
|
|
|
178
188
|
if (pageSizeChanged) {
|
|
@@ -182,11 +192,30 @@
|
|
|
182
192
|
const currentPageNumber = updatedPaginationState.pageIndex + 1;
|
|
183
193
|
onpagechange?.(updatedPaginationState.pageSize, currentPageNumber);
|
|
184
194
|
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const changeFilters = (filters: Filter[]) => {
|
|
198
|
+
const searchParams = new URLSearchParams(page.url.searchParams);
|
|
199
|
+
searchParams.delete('filter');
|
|
200
|
+
|
|
201
|
+
filters.forEach((filter) => {
|
|
202
|
+
const filterValue = buildFilterValue(filter);
|
|
203
|
+
searchParams.append('filter', filterValue);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const url = buildUrl(searchParams);
|
|
207
|
+
goto(url);
|
|
185
208
|
};
|
|
186
209
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
const buildFilterValue = (filter: Filter) => {
|
|
211
|
+
const operator = filter.operator ? `${filter.operator}:` : '';
|
|
212
|
+
return `${operator}${filter.column}:eq:${filter.value}`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const buildUrl = (searchParams: URLSearchParams) => {
|
|
216
|
+
const queryString = searchParams.toString();
|
|
217
|
+
return queryString ? `${page.url.pathname}?${queryString}` : page.url.pathname;
|
|
218
|
+
};
|
|
190
219
|
|
|
191
220
|
$effect(() => {
|
|
192
221
|
if (manualPagination && pagination) {
|
|
@@ -216,8 +245,16 @@
|
|
|
216
245
|
]}
|
|
217
246
|
>
|
|
218
247
|
{#if enableGlobalSearch}
|
|
219
|
-
<div class="table-search">
|
|
220
|
-
<
|
|
248
|
+
<div class="table-search-container">
|
|
249
|
+
<div class="table-search">
|
|
250
|
+
<Search bind:value={globalFilter} />
|
|
251
|
+
</div>
|
|
252
|
+
{#if enableAdvancedFilter}
|
|
253
|
+
<AdvancedFilter
|
|
254
|
+
columns={table.getAllLeafColumns().filter((column) => column.getCanHide())}
|
|
255
|
+
onfilterschange={changeFilters}
|
|
256
|
+
/>
|
|
257
|
+
{/if}
|
|
221
258
|
</div>
|
|
222
259
|
{/if}
|
|
223
260
|
|
|
@@ -303,6 +340,12 @@
|
|
|
303
340
|
width: var(--search-width);
|
|
304
341
|
}
|
|
305
342
|
|
|
343
|
+
.table-search-container {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
gap: var(--spacing-md);
|
|
347
|
+
}
|
|
348
|
+
|
|
306
349
|
.table-actions {
|
|
307
350
|
display: flex;
|
|
308
351
|
gap: var(--spacing-md);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Snippet } from 'svelte';
|
|
2
2
|
import { type ColumnDef } from './adapter/index.js';
|
|
3
3
|
import type { ExcelSetting } from './excel-setting.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type Pagination } from './types.js';
|
|
5
5
|
declare function $$render<T>(): {
|
|
6
6
|
props: {
|
|
7
7
|
columns: ColumnDef<T, any>[];
|
|
@@ -13,6 +13,7 @@ declare function $$render<T>(): {
|
|
|
13
13
|
enableGlobalSearch?: boolean;
|
|
14
14
|
enableColumnSearch?: boolean;
|
|
15
15
|
enableColumnVisibility?: boolean;
|
|
16
|
+
enableAdvancedFilter?: boolean;
|
|
16
17
|
manualPagination?: boolean;
|
|
17
18
|
pagination?: Pagination;
|
|
18
19
|
excelSetting?: ExcelSetting;
|