@softwareone/spi-sv5-library 1.16.1 → 1.17.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/components/clipboard/Clipboard.svelte +74 -0
- package/dist/components/clipboard/Clipboard.svelte.d.ts +9 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/table/Table.svelte +68 -8
- package/dist/components/table/Table.svelte.d.ts +2 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/local-storage.d.ts +6 -0
- package/dist/utils/local-storage.js +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Input, addToast } from '../../index.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label?: string;
|
|
6
|
+
notification?: string;
|
|
7
|
+
iconOnly?: boolean;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { label, notification = label, iconOnly = false, value }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const copyToClipboard = async (value: string, notification?: string) => {
|
|
14
|
+
await navigator.clipboard.writeText(value);
|
|
15
|
+
addToast({
|
|
16
|
+
message: `${notification || 'Item'} copied to clipboard.`,
|
|
17
|
+
type: 'neutral'
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class={['clipboard', label && 'clipboard-labeled']}>
|
|
23
|
+
{#if label}
|
|
24
|
+
<span class="clipboard-label">{label}</span>
|
|
25
|
+
{/if}
|
|
26
|
+
<div class={[!iconOnly && 'clipboard-row-with-input']}>
|
|
27
|
+
{#if !iconOnly}
|
|
28
|
+
<div class="clipboard-input">
|
|
29
|
+
<Input
|
|
30
|
+
placeholder={label}
|
|
31
|
+
aria-label={label}
|
|
32
|
+
{value}
|
|
33
|
+
readonly
|
|
34
|
+
disableValidationColor
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
{/if}
|
|
38
|
+
<Button
|
|
39
|
+
variant="outline"
|
|
40
|
+
aria-label={notification ?? label ?? 'Copy to clipboard'}
|
|
41
|
+
onclick={() => copyToClipboard(value, notification)}
|
|
42
|
+
>
|
|
43
|
+
<span class="material-icons" style:font-size="var(--spi-font-size-sm)"
|
|
44
|
+
>content_copy</span
|
|
45
|
+
>
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.clipboard {
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.clipboard-labeled {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: var(--spi-size-2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.clipboard-label {
|
|
62
|
+
font-size: var(--spi-font-size-sm);
|
|
63
|
+
font-weight: var(--spi-font-weight-medium);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.clipboard-row-with-input {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: var(--spi-size-2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.clipboard-input {
|
|
72
|
+
flex: 1;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -3,6 +3,7 @@ export { default as Avatar } from './avatar/Avatar.svelte';
|
|
|
3
3
|
export { default as Button } from './button/Button.svelte';
|
|
4
4
|
export { default as Card } from './card/Card.svelte';
|
|
5
5
|
export { default as Chips } from './chips/Chips.svelte';
|
|
6
|
+
export { default as Clipboard } from './clipboard/Clipboard.svelte';
|
|
6
7
|
export { default as ConfirmationModal } from './confirmation/Confirmation.svelte';
|
|
7
8
|
export { default as AttachFile } from './controls/attach-file/AttachFile.svelte';
|
|
8
9
|
export { default as Input } from './controls/input/Input.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Avatar } from './avatar/Avatar.svelte';
|
|
|
3
3
|
export { default as Button } from './button/Button.svelte';
|
|
4
4
|
export { default as Card } from './card/Card.svelte';
|
|
5
5
|
export { default as Chips } from './chips/Chips.svelte';
|
|
6
|
+
export { default as Clipboard } from './clipboard/Clipboard.svelte';
|
|
6
7
|
export { default as ConfirmationModal } from './confirmation/Confirmation.svelte';
|
|
7
8
|
export { default as AttachFile } from './controls/attach-file/AttachFile.svelte';
|
|
8
9
|
export { default as Input } from './controls/input/Input.svelte';
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object">
|
|
2
|
+
import { browser } from '$app/environment';
|
|
2
3
|
import { goto } from '$app/navigation';
|
|
3
4
|
import { page } from '$app/state';
|
|
4
|
-
import { type Snippet } from 'svelte';
|
|
5
|
+
import { onMount, type Snippet } from 'svelte';
|
|
5
6
|
|
|
6
7
|
import { Button, Search } from '../../index.js';
|
|
8
|
+
import { LocalStorage } from '../../utils/local-storage.js';
|
|
7
9
|
import {
|
|
8
10
|
createSvelteTable,
|
|
9
11
|
getCoreRowModel,
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
createCheckedColumn,
|
|
41
43
|
isCursorPagination,
|
|
42
44
|
isValidPage,
|
|
43
|
-
isValidPageSize
|
|
45
|
+
isValidPageSize,
|
|
46
|
+
sanitizeFilters
|
|
44
47
|
} from './utils.js';
|
|
45
48
|
|
|
46
49
|
interface BaseProps<T> {
|
|
@@ -54,6 +57,8 @@
|
|
|
54
57
|
enableColumnSearch?: boolean;
|
|
55
58
|
enableColumnVisibility?: boolean;
|
|
56
59
|
enableAdvancedFilter?: boolean;
|
|
60
|
+
stickyFilter?: boolean;
|
|
61
|
+
stickyFilterKey?: string;
|
|
57
62
|
serverSide?: boolean;
|
|
58
63
|
pagination?: Pagination;
|
|
59
64
|
initialPage?: number;
|
|
@@ -101,6 +106,8 @@
|
|
|
101
106
|
enableColumnSearch = false,
|
|
102
107
|
enableColumnVisibility = true,
|
|
103
108
|
enableAdvancedFilter = false,
|
|
109
|
+
stickyFilter = false,
|
|
110
|
+
stickyFilterKey,
|
|
104
111
|
serverSide = false,
|
|
105
112
|
pagination,
|
|
106
113
|
initialPage,
|
|
@@ -118,6 +125,7 @@
|
|
|
118
125
|
}: Props<T> = $props();
|
|
119
126
|
|
|
120
127
|
const tableManualPagination = serverSide ? setPaginationTableContext() : undefined;
|
|
128
|
+
const STICKY_FILTER_STORAGE_PREFIX = 'spi-table-sticky-filters';
|
|
121
129
|
|
|
122
130
|
let globalFilter = $state<string>('');
|
|
123
131
|
let sorting = $state<SortingState>([]);
|
|
@@ -272,17 +280,58 @@
|
|
|
272
280
|
rowSelection = {};
|
|
273
281
|
};
|
|
274
282
|
|
|
283
|
+
const getStickyFilterStorageKey = () => {
|
|
284
|
+
const key = stickyFilterKey?.trim() ? stickyFilterKey : page.url.pathname;
|
|
285
|
+
return `${STICKY_FILTER_STORAGE_PREFIX}:${key}`;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const isStickyFilterActive = () => browser && stickyFilter;
|
|
289
|
+
|
|
290
|
+
const readStickyFilterValues = (): string[] =>
|
|
291
|
+
isStickyFilterActive()
|
|
292
|
+
? (LocalStorage.getItem<string[]>(getStickyFilterStorageKey()) ?? [])
|
|
293
|
+
: [];
|
|
294
|
+
|
|
295
|
+
const writeStickyFilterValues = (filters: string[]) => {
|
|
296
|
+
if (!isStickyFilterActive()) return;
|
|
297
|
+
|
|
298
|
+
const storageKey = getStickyFilterStorageKey();
|
|
299
|
+
if (!filters.length) {
|
|
300
|
+
LocalStorage.removeItem(storageKey);
|
|
301
|
+
} else {
|
|
302
|
+
LocalStorage.setItem(storageKey, filters);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const restoreStickyFilters = () => {
|
|
307
|
+
if (!isStickyFilterActive() || !enableAdvancedFilter) return;
|
|
308
|
+
|
|
309
|
+
const currentFilters = page.url.searchParams.getAll('filter');
|
|
310
|
+
if (currentFilters.length) {
|
|
311
|
+
writeStickyFilterValues(sanitizeFilters(currentFilters).map(buildFilterValue));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const storedFilters = sanitizeFilters(readStickyFilterValues()).map(buildFilterValue);
|
|
316
|
+
if (!storedFilters.length) return;
|
|
317
|
+
|
|
318
|
+
const searchParams = new URLSearchParams(page.url.searchParams);
|
|
319
|
+
storedFilters.forEach((filterValue) => {
|
|
320
|
+
searchParams.append('filter', filterValue);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
goto(buildUrl(searchParams), { replaceState: true, noScroll: true, keepFocus: true });
|
|
324
|
+
};
|
|
325
|
+
|
|
275
326
|
const changeFilters = (filters: Filter[]) => {
|
|
276
327
|
const searchParams = new URLSearchParams(page.url.searchParams);
|
|
277
328
|
searchParams.delete('filter');
|
|
278
329
|
|
|
279
|
-
filters.
|
|
280
|
-
|
|
281
|
-
searchParams.append('filter', filterValue);
|
|
282
|
-
});
|
|
330
|
+
const filterValues = filters.map(buildFilterValue);
|
|
331
|
+
filterValues.forEach((filterValue) => searchParams.append('filter', filterValue));
|
|
283
332
|
|
|
284
|
-
|
|
285
|
-
goto(
|
|
333
|
+
writeStickyFilterValues(filterValues);
|
|
334
|
+
goto(buildUrl(searchParams));
|
|
286
335
|
};
|
|
287
336
|
|
|
288
337
|
const buildFilterValue = (filter: Filter) => {
|
|
@@ -295,6 +344,13 @@
|
|
|
295
344
|
return queryString ? `${page.url.pathname}?${queryString}` : page.url.pathname;
|
|
296
345
|
};
|
|
297
346
|
|
|
347
|
+
$effect(() => {
|
|
348
|
+
columnFilters = sanitizeFilters(page.url.searchParams.getAll('filter')).map((filter) => ({
|
|
349
|
+
id: filter.column,
|
|
350
|
+
value: filter.value
|
|
351
|
+
}));
|
|
352
|
+
});
|
|
353
|
+
|
|
298
354
|
$effect(() => {
|
|
299
355
|
if (serverSide && pagination) {
|
|
300
356
|
$tableManualPagination = { ...pagination };
|
|
@@ -310,6 +366,10 @@
|
|
|
310
366
|
if (!enableChecked) return;
|
|
311
367
|
onclearselection?.(clearSelection);
|
|
312
368
|
});
|
|
369
|
+
|
|
370
|
+
onMount(() => {
|
|
371
|
+
restoreStickyFilters();
|
|
372
|
+
});
|
|
313
373
|
</script>
|
|
314
374
|
|
|
315
375
|
<section class="table-container">
|
|
@@ -13,6 +13,8 @@ interface BaseProps<T> {
|
|
|
13
13
|
enableColumnSearch?: boolean;
|
|
14
14
|
enableColumnVisibility?: boolean;
|
|
15
15
|
enableAdvancedFilter?: boolean;
|
|
16
|
+
stickyFilter?: boolean;
|
|
17
|
+
stickyFilterKey?: string;
|
|
16
18
|
serverSide?: boolean;
|
|
17
19
|
pagination?: Pagination;
|
|
18
20
|
initialPage?: number;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const LocalStorage = {
|
|
2
|
+
getItem: (key) => {
|
|
3
|
+
const item = localStorage.getItem(key);
|
|
4
|
+
if (item === null)
|
|
5
|
+
return null;
|
|
6
|
+
const cachedItem = JSON.parse(item);
|
|
7
|
+
if (cachedItem.expiresAt && Date.now() > cachedItem.expiresAt) {
|
|
8
|
+
localStorage.removeItem(key);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return cachedItem.value;
|
|
12
|
+
},
|
|
13
|
+
setItem: (key, value, expiresInMs) => {
|
|
14
|
+
const stored = {
|
|
15
|
+
value,
|
|
16
|
+
expiresAt: expiresInMs ? Date.now() + expiresInMs : undefined
|
|
17
|
+
};
|
|
18
|
+
localStorage.setItem(key, JSON.stringify(stored));
|
|
19
|
+
},
|
|
20
|
+
removeItem: (key) => {
|
|
21
|
+
localStorage.removeItem(key);
|
|
22
|
+
},
|
|
23
|
+
clear: () => {
|
|
24
|
+
localStorage.clear();
|
|
25
|
+
}
|
|
26
|
+
};
|