@bexis2/bexis2-core-ui 0.4.21 → 0.4.23
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 +14 -0
- package/dist/components/Facets/ShowMore.svelte +32 -52
- package/dist/components/Table/ColumnsMenu.svelte +37 -10
- package/dist/components/Table/TableContent.svelte +160 -21
- package/dist/components/Table/TableFilter.svelte +1 -1
- package/dist/components/Table/TablePagination.svelte +2 -2
- package/dist/components/Table/shared.d.ts +1 -0
- package/dist/components/Table/shared.js +25 -0
- package/dist/components/form/MultiSelect.svelte +1 -1
- package/package.json +1 -1
- package/src/lib/components/Facets/ShowMore.svelte +35 -55
- package/src/lib/components/Table/ColumnsMenu.svelte +38 -9
- package/src/lib/components/Table/TableContent.svelte +189 -21
- package/src/lib/components/Table/TableFilter.svelte +1 -1
- package/src/lib/components/Table/TablePagination.svelte +2 -2
- package/src/lib/components/Table/shared.ts +38 -7
- package/src/lib/components/form/MultiSelect.svelte +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
# bexis-core-ui
|
|
2
|
+
## 0.4.23
|
|
3
|
+
- Table
|
|
4
|
+
- fix resizing issues after page size or page index changes
|
|
5
|
+
- Add Select All and Deselect All in columns menu
|
|
6
|
+
- Convert JSON to CSV and export as CSV
|
|
7
|
+
- Export as JSON to fix special characters and encoding issues
|
|
8
|
+
|
|
9
|
+
## 0.4.22
|
|
10
|
+
- Facets
|
|
11
|
+
- Replace column class function with more efficient solution
|
|
12
|
+
- Sort options in ShowMore alphabetically
|
|
13
|
+
Table
|
|
14
|
+
- Add titles for components for better accessibility
|
|
15
|
+
- Remove z-index from pagination buttons
|
|
2
16
|
|
|
3
17
|
## 0.4.21
|
|
4
18
|
- change footer position in page component
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>export let group;
|
|
2
2
|
export let handleApply;
|
|
3
3
|
export let handleCancel;
|
|
4
|
-
let selected =
|
|
4
|
+
let selected = Object.keys(group.children).sort((a, b) => group.children[a].displayName.localeCompare(group.children[b].displayName)).map((key) => ({ [key]: { ...group.children[key] } })).reduce((acc, val) => ({ ...acc, ...val }), {});
|
|
5
5
|
const selectAll = () => {
|
|
6
6
|
Object.keys(selected).forEach((key) => selected[key].selected = true);
|
|
7
7
|
};
|
|
@@ -15,64 +15,44 @@ const onApply = () => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
const onCancel = () => {
|
|
18
|
-
console.log(selected, group.children);
|
|
19
18
|
selected = structuredClone(group.children);
|
|
20
19
|
handleCancel(group.name);
|
|
21
20
|
};
|
|
22
|
-
const gridClass = (items) => {
|
|
23
|
-
const ceil = Math.ceil(Math.sqrt(items.length));
|
|
24
|
-
const max = Math.max(ceil, Math.floor(items.length / 3));
|
|
25
|
-
const classes = [
|
|
26
|
-
"grid-rows-1",
|
|
27
|
-
"grid-rows-2",
|
|
28
|
-
"grid-rows-3",
|
|
29
|
-
"grid-rows-4",
|
|
30
|
-
"grid-rows-5",
|
|
31
|
-
"grid-rows-6",
|
|
32
|
-
"grid-rows-7",
|
|
33
|
-
"grid-rows-8",
|
|
34
|
-
"grid-rows-9",
|
|
35
|
-
"grid-rows-10",
|
|
36
|
-
"grid-rows-11",
|
|
37
|
-
"grid-rows-12"
|
|
38
|
-
];
|
|
39
|
-
if (max > 12) {
|
|
40
|
-
return "grid-rows-12";
|
|
41
|
-
} else return classes[max - 1 || 1];
|
|
42
|
-
};
|
|
43
21
|
</script>
|
|
44
22
|
|
|
45
|
-
<div class="
|
|
46
|
-
|
|
47
|
-
|
|
23
|
+
<div class="w-full flex justify-center max-w-[800px]">
|
|
24
|
+
<div class="grow max-h-[500px]">
|
|
25
|
+
<div
|
|
26
|
+
class="p-5 rounded-md w-full bg-surface-50 dark:bg-surface-800 border-primary-500 border-2"
|
|
27
|
+
>
|
|
28
|
+
<!-- Header -->
|
|
29
|
+
<h2 class="text-xl font-semibold">{group.displayName}</h2>
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
>
|
|
63
|
-
</label>
|
|
64
|
-
{/each}
|
|
65
|
-
</div>
|
|
31
|
+
<!-- Items -->
|
|
32
|
+
<div class="gap-x-10 space-y-2 py-6 px-[2px] max-h-[500px] columns-[192px] overflow-auto min-h">
|
|
33
|
+
{#each Object.keys(selected) as key}
|
|
34
|
+
<label class="flex gap-3 items-center w-48">
|
|
35
|
+
<input type="checkbox" class="checkbox" bind:checked={selected[key].selected} />
|
|
36
|
+
<span
|
|
37
|
+
title={selected[key].displayName}
|
|
38
|
+
class="whitespace-nowrap break-before-avoid break-after-avoid truncate"
|
|
39
|
+
>{selected[key].displayName}</span
|
|
40
|
+
>
|
|
41
|
+
</label>
|
|
42
|
+
{/each}
|
|
43
|
+
</div>
|
|
66
44
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
45
|
+
<!-- Footer -->
|
|
46
|
+
<div class="flex w-full justify-between gap-5">
|
|
47
|
+
<div class="flex gap-3">
|
|
48
|
+
<button class="btn btn-sm variant-filled-tertiary" on:click={selectNone}>None</button>
|
|
49
|
+
<button class="btn btn-sm variant-filled-tertiary" on:click={selectAll}>All</button>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="flex gap-3">
|
|
52
|
+
<button class="btn btn-sm variant-filled-primary" on:click={onApply}>Apply</button>
|
|
53
|
+
<button class="btn btn-sm variant-filled-secondary" on:click={onCancel}>Cancel</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
76
56
|
</div>
|
|
77
57
|
</div>
|
|
78
58
|
</div>
|
|
@@ -1,38 +1,65 @@
|
|
|
1
|
-
<script>import
|
|
1
|
+
<script>import Fa from "svelte-fa";
|
|
2
|
+
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
|
3
|
+
import { popup } from "@skeletonlabs/skeleton";
|
|
2
4
|
export let columns = [];
|
|
3
5
|
export let tableId;
|
|
4
6
|
const popupCombobox = {
|
|
5
7
|
event: "click",
|
|
6
8
|
target: `${tableId}-columns-menu`,
|
|
7
|
-
placement: "bottom"
|
|
9
|
+
placement: "bottom",
|
|
10
|
+
closeQuery: ""
|
|
11
|
+
};
|
|
12
|
+
const selectAll = () => {
|
|
13
|
+
columns = columns.map((column) => ({ ...column, visible: true }));
|
|
14
|
+
};
|
|
15
|
+
const deselectAll = () => {
|
|
16
|
+
columns = columns.map((column) => ({ ...column, visible: false }));
|
|
17
|
+
columns[0].visible = true;
|
|
8
18
|
};
|
|
9
19
|
</script>
|
|
10
20
|
|
|
11
21
|
<button
|
|
12
22
|
type="button"
|
|
13
|
-
|
|
14
|
-
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
23
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last gap-2"
|
|
15
24
|
aria-label="Open menu to hide/show columns"
|
|
16
|
-
use:popup={popupCombobox}
|
|
25
|
+
use:popup={popupCombobox}><Fa icon={faEye} /> Columns</button
|
|
17
26
|
>
|
|
18
|
-
|
|
19
27
|
<div
|
|
20
|
-
class="bg-white dark:bg-surface-500 p-4 rounded-md shadow-md z-10"
|
|
28
|
+
class="bg-white dark:bg-surface-500 p-4 px-5 rounded-md shadow-md z-10 border border-primary-500"
|
|
21
29
|
data-popup="{tableId}-columns-menu"
|
|
22
30
|
>
|
|
31
|
+
<div class="flex items-center gap-4 pb-5 grow justify-between">
|
|
32
|
+
<button
|
|
33
|
+
on:click|preventDefault={selectAll}
|
|
34
|
+
type="button"
|
|
35
|
+
class="btn p-0 text-sm grow underline text-primary-600"
|
|
36
|
+
>
|
|
37
|
+
Select All
|
|
38
|
+
</button>
|
|
39
|
+
<div class="border border-r border-neutral-200 h-6" />
|
|
40
|
+
<button
|
|
41
|
+
on:click|preventDefault={deselectAll}
|
|
42
|
+
type="button"
|
|
43
|
+
class="btn p-0 text-sm grow underline text-neutral-500"
|
|
44
|
+
>
|
|
45
|
+
Deselect All
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
23
48
|
{#each columns as column}
|
|
24
49
|
<div class="flex gap-3 items-center">
|
|
25
50
|
<label for={column.id} class="cursor-pointer" title={column.label}></label>
|
|
26
51
|
<input
|
|
27
|
-
aria-label=
|
|
52
|
+
aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
28
53
|
type="checkbox"
|
|
29
|
-
|
|
54
|
+
class="checkbox"
|
|
55
|
+
id={column.id}
|
|
30
56
|
bind:checked={column.visible}
|
|
57
|
+
title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
31
58
|
disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
|
|
32
59
|
/>
|
|
33
60
|
<span>{column.label}</span>
|
|
34
61
|
</div>
|
|
35
62
|
{/each}
|
|
36
63
|
|
|
37
|
-
<div class="arrow bg-white dark:bg-surface-500" />
|
|
64
|
+
<div class="arrow bg-white dark:bg-surface-500 border-l border-t border-primary-500" />
|
|
38
65
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<script>import { createEventDispatcher } from "svelte";
|
|
1
|
+
<script>import { afterUpdate, onDestroy, createEventDispatcher } from "svelte";
|
|
2
2
|
import { readable, writable } from "svelte/store";
|
|
3
3
|
import Fa from "svelte-fa";
|
|
4
|
-
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
4
|
+
import { faCompress, faDownload, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
5
5
|
import { createTable, Subscribe, Render, createRender } from "svelte-headless-table";
|
|
6
6
|
import {
|
|
7
7
|
addSortBy,
|
|
@@ -25,10 +25,12 @@ import { columnFilter, searchFilter } from "./filter";
|
|
|
25
25
|
import {
|
|
26
26
|
cellStyle,
|
|
27
27
|
exportAsCsv,
|
|
28
|
+
jsonToCsv,
|
|
28
29
|
fixedWidth,
|
|
29
30
|
normalizeFilters,
|
|
30
31
|
resetResize,
|
|
31
|
-
convertServerColumns
|
|
32
|
+
convertServerColumns,
|
|
33
|
+
minWidth
|
|
32
34
|
} from "./shared";
|
|
33
35
|
import { Receive, Send } from "../../models/Models";
|
|
34
36
|
export let config;
|
|
@@ -63,11 +65,14 @@ let {
|
|
|
63
65
|
} = config;
|
|
64
66
|
let searchValue = "";
|
|
65
67
|
let isFetching = false;
|
|
68
|
+
let tableRef;
|
|
66
69
|
const serverSide = server !== void 0;
|
|
67
70
|
const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
|
|
68
71
|
const filters = writable({});
|
|
69
72
|
const dispatch = createEventDispatcher();
|
|
70
73
|
const actionDispatcher = (obj) => dispatch("action", obj);
|
|
74
|
+
const rowHeights = writable({});
|
|
75
|
+
const colWidths = writable([]);
|
|
71
76
|
const serverItems = serverSide ? writable(0) : void 0;
|
|
72
77
|
const serverItemCount = serverSide ? readable(0, (set) => {
|
|
73
78
|
serverItems.subscribe((val) => set(val));
|
|
@@ -89,7 +94,7 @@ const table = createTable(data, {
|
|
|
89
94
|
serverItemCount
|
|
90
95
|
}),
|
|
91
96
|
expand: addExpandedRows(),
|
|
92
|
-
export: addDataExport({ format: "
|
|
97
|
+
export: addDataExport({ format: "json" })
|
|
93
98
|
});
|
|
94
99
|
const allCols = {};
|
|
95
100
|
$data.forEach((item) => {
|
|
@@ -307,6 +312,94 @@ const sortServer = (order, id) => {
|
|
|
307
312
|
$pageIndex = 0;
|
|
308
313
|
updateTable();
|
|
309
314
|
};
|
|
315
|
+
const getMaxCellHeightInRow = () => {
|
|
316
|
+
if (!tableRef || resizable === "columns" || resizable === "none") return;
|
|
317
|
+
tableRef.querySelectorAll("tbody tr").forEach((row, index) => {
|
|
318
|
+
const cells = row.querySelectorAll("td");
|
|
319
|
+
let maxHeight = optionsComponent ? 56 : 44;
|
|
320
|
+
let minHeight = optionsComponent ? 56 : 44;
|
|
321
|
+
cells.forEach((cell) => {
|
|
322
|
+
const cellHeight = cell.getBoundingClientRect().height;
|
|
323
|
+
if (cellHeight > maxHeight) {
|
|
324
|
+
maxHeight = cellHeight + 2;
|
|
325
|
+
}
|
|
326
|
+
if (cellHeight < minHeight) {
|
|
327
|
+
minHeight = cellHeight + 2;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
rowHeights.update((rh) => {
|
|
331
|
+
const id = +row.id.split(`${tableId}-row-`)[1];
|
|
332
|
+
return {
|
|
333
|
+
...rh,
|
|
334
|
+
[id]: {
|
|
335
|
+
max: maxHeight - 24,
|
|
336
|
+
min: Math.max(minHeight - 24, rowHeight ?? 20)
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
const getMinCellWidthInColumn = () => {
|
|
343
|
+
if (!tableRef || resizable === "rows" || resizable === "none") return;
|
|
344
|
+
if ($colWidths.length === 0) {
|
|
345
|
+
$colWidths = Array.from({ length: $headerRows[0].cells.length }, () => 100);
|
|
346
|
+
}
|
|
347
|
+
colWidths.update((cw) => {
|
|
348
|
+
tableRef?.querySelectorAll("thead tr th span").forEach((cell, index) => {
|
|
349
|
+
cw[index] = cw[index] === 100 ? cell.getBoundingClientRect().width + 12 + 32 : cw[index];
|
|
350
|
+
});
|
|
351
|
+
return cw;
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
const resizeRowsObserver = new ResizeObserver(() => {
|
|
355
|
+
getMaxCellHeightInRow();
|
|
356
|
+
});
|
|
357
|
+
const resizeColumnsObserver = new ResizeObserver(() => {
|
|
358
|
+
getMinCellWidthInColumn();
|
|
359
|
+
});
|
|
360
|
+
const observeFirstCells = () => {
|
|
361
|
+
if (!tableRef) return;
|
|
362
|
+
$pageRows.forEach((row) => {
|
|
363
|
+
const cell = tableRef.querySelector(`#${tableId}-row-${row.id}`);
|
|
364
|
+
if (cell) {
|
|
365
|
+
resizeRowsObserver.observe(cell);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
tableRef.querySelectorAll("tbody tr td:first-child").forEach((cell) => {
|
|
369
|
+
resizeRowsObserver.observe(cell);
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
const observeHeaderColumns = () => {
|
|
373
|
+
if (!tableRef) return;
|
|
374
|
+
tableRef.querySelectorAll("thead tr th").forEach((cell) => {
|
|
375
|
+
resizeColumnsObserver.observe(cell);
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
afterUpdate(() => {
|
|
379
|
+
if (resizable !== "rows" && resizable !== "both") {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const e = tableRef?.querySelector(`#${tableId}-row-${$pageRows[0].id}`);
|
|
383
|
+
if (e) {
|
|
384
|
+
getDimensions();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
onDestroy(() => {
|
|
388
|
+
resizeRowsObserver.disconnect();
|
|
389
|
+
resizeColumnsObserver.disconnect();
|
|
390
|
+
});
|
|
391
|
+
const getDimensions = () => {
|
|
392
|
+
if (!tableRef) return;
|
|
393
|
+
if (resizable === "none") return;
|
|
394
|
+
else if (resizable === "columns") {
|
|
395
|
+
observeHeaderColumns();
|
|
396
|
+
} else if (resizable === "rows") {
|
|
397
|
+
observeFirstCells();
|
|
398
|
+
} else {
|
|
399
|
+
observeHeaderColumns();
|
|
400
|
+
observeFirstCells();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
310
403
|
$: sortKeys = pluginStates.sort.sortKeys;
|
|
311
404
|
$: serverSide && updateTable();
|
|
312
405
|
$: serverSide && sortServer($sortKeys[0]?.order, $sortKeys[0]?.id);
|
|
@@ -337,6 +430,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
337
430
|
title="Search within all table rows"
|
|
338
431
|
bind:value={searchValue}
|
|
339
432
|
placeholder="Search rows..."
|
|
433
|
+
aria-label="Searchbox for searching rows"
|
|
340
434
|
id="{tableId}-search"
|
|
341
435
|
/><button
|
|
342
436
|
type="reset"
|
|
@@ -361,6 +455,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
361
455
|
title="Search"
|
|
362
456
|
id="{tableId}-searchSubmit"
|
|
363
457
|
class="btn variant-filled-primary"
|
|
458
|
+
aria-label="Search"
|
|
364
459
|
on:click|preventDefault={() => {
|
|
365
460
|
if (serverSide && !sendModel) {
|
|
366
461
|
throw new Error('Server-side configuration is missing');
|
|
@@ -375,7 +470,8 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
375
470
|
{/if}
|
|
376
471
|
|
|
377
472
|
<div
|
|
378
|
-
class="flex justify-between items-center w-full {search &&
|
|
473
|
+
class="flex justify-between overflow-x-auto items-center w-full {search &&
|
|
474
|
+
'py-2'} {!search &&
|
|
379
475
|
(shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
|
|
380
476
|
'pb-2'}"
|
|
381
477
|
>
|
|
@@ -389,6 +485,10 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
389
485
|
size="sm"
|
|
390
486
|
checked={fitToScreen}
|
|
391
487
|
id="{tableId}-toggle"
|
|
488
|
+
title={fitToScreen ? 'Fit table data to screen' : `Don't fit table data to screen`}
|
|
489
|
+
aria-label={fitToScreen
|
|
490
|
+
? 'Fit table data to screen'
|
|
491
|
+
: `Don't fit table data to screen`}
|
|
392
492
|
on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
|
|
393
493
|
>
|
|
394
494
|
{/if}
|
|
@@ -398,20 +498,20 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
398
498
|
{#if resizable !== 'none'}
|
|
399
499
|
<button
|
|
400
500
|
type="button"
|
|
401
|
-
|
|
402
|
-
|
|
501
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
|
|
502
|
+
aria-label="Reset sizing of columns and rows"
|
|
403
503
|
on:click|preventDefault={() =>
|
|
404
504
|
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
|
405
|
-
|
|
505
|
+
><Fa icon={faCompress} /> Reset sizing</button
|
|
406
506
|
>
|
|
407
507
|
{/if}
|
|
408
508
|
{#if exportable}
|
|
409
509
|
<button
|
|
410
510
|
type="button"
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
414
|
-
|
|
511
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
|
|
512
|
+
aria-label="Export table data as CSV"
|
|
513
|
+
on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
|
|
514
|
+
><Fa icon={faDownload} /> Export as CSV</button
|
|
415
515
|
>
|
|
416
516
|
{/if}
|
|
417
517
|
{#if shownColumns.length > 0}
|
|
@@ -422,6 +522,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
422
522
|
|
|
423
523
|
<div class="overflow-auto" style="height: {height}px">
|
|
424
524
|
<table
|
|
525
|
+
bind:this={tableRef}
|
|
425
526
|
{...$tableAttrs}
|
|
426
527
|
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
|
427
528
|
id="{tableId}-table"
|
|
@@ -438,14 +539,25 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
438
539
|
let:rowProps
|
|
439
540
|
>
|
|
440
541
|
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
|
|
441
|
-
{#each headerRow.cells as cell (cell.id)}
|
|
542
|
+
{#each headerRow.cells as cell, index (cell.id)}
|
|
442
543
|
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
|
|
443
|
-
<th
|
|
544
|
+
<th
|
|
545
|
+
scope="col"
|
|
546
|
+
class="!p-2"
|
|
547
|
+
{...attrs}
|
|
548
|
+
style={`
|
|
549
|
+
width: ${cell.isData() ? 'auto' : '0'};
|
|
550
|
+
${cellStyle(cell.id, columns)}
|
|
551
|
+
`}
|
|
552
|
+
>
|
|
444
553
|
<div
|
|
445
554
|
class="overflow-auto"
|
|
446
555
|
class:resize-x={(resizable === 'columns' || resizable === 'both') &&
|
|
447
556
|
!fixedWidth(cell.id, columns)}
|
|
448
557
|
id="th-{tableId}-{cell.id}"
|
|
558
|
+
style={`
|
|
559
|
+
min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
|
|
560
|
+
`}
|
|
449
561
|
>
|
|
450
562
|
<div class="flex justify-between items-center">
|
|
451
563
|
<div class="flex gap-1 whitespace-pre-wrap">
|
|
@@ -458,6 +570,11 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
458
570
|
class:cursor-pointer={!props.sort.disabled}
|
|
459
571
|
on:click={props.sort.toggle}
|
|
460
572
|
on:keydown={props.sort.toggle}
|
|
573
|
+
title={props.sort.order === 'asc'
|
|
574
|
+
? `Sort by ${cell.label} column in descending order`
|
|
575
|
+
: props.sort.order === 'desc'
|
|
576
|
+
? `Remove sorting by ${cell.label} column`
|
|
577
|
+
: `Sort by ${cell.label} column in ascending order`}
|
|
461
578
|
>
|
|
462
579
|
{cell.render()}
|
|
463
580
|
</span>
|
|
@@ -491,20 +608,42 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
491
608
|
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
|
492
609
|
{#each row.cells as cell, index (cell?.id)}
|
|
493
610
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
|
494
|
-
<td {...attrs} class="
|
|
611
|
+
<td {...attrs} class="">
|
|
495
612
|
<div
|
|
496
|
-
class="
|
|
613
|
+
class=" h-full {index === 0 &&
|
|
497
614
|
(resizable === 'rows' || resizable === 'both')
|
|
498
|
-
? 'resize-y'
|
|
499
|
-
: ''}"
|
|
615
|
+
? 'resize-y overflow-auto'
|
|
616
|
+
: 'block'}"
|
|
500
617
|
id="{tableId}-{cell.id}-{row.id}"
|
|
618
|
+
style={`
|
|
619
|
+
min-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
|
|
620
|
+
max-height: ${
|
|
621
|
+
index !== 0 && $rowHeights && $rowHeights[+row.id]
|
|
622
|
+
? `${$rowHeights[+row.id].max}px`
|
|
623
|
+
: 'auto'
|
|
624
|
+
};
|
|
625
|
+
height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
|
|
626
|
+
`}
|
|
501
627
|
>
|
|
502
628
|
<!-- Adding config for initial rowHeight, if provided -->
|
|
503
629
|
<div
|
|
504
|
-
class="flex items-
|
|
505
|
-
style=
|
|
630
|
+
class="flex items-start overflow-auto"
|
|
631
|
+
style={`
|
|
632
|
+
max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
|
|
633
|
+
`}
|
|
506
634
|
>
|
|
507
|
-
<div
|
|
635
|
+
<div
|
|
636
|
+
class="grow overflow-auto"
|
|
637
|
+
style={cell.isData()
|
|
638
|
+
? `width: ${
|
|
639
|
+
minWidth(cell.id, columns)
|
|
640
|
+
? minWidth(cell.id, columns)
|
|
641
|
+
: $colWidths[index]
|
|
642
|
+
}px;`
|
|
643
|
+
: 'max-width: min-content;'}
|
|
644
|
+
>
|
|
645
|
+
<Render of={cell.render()} />
|
|
646
|
+
</div>
|
|
508
647
|
</div>
|
|
509
648
|
</div>
|
|
510
649
|
</td>
|
|
@@ -39,7 +39,7 @@ $: goToPreviousPageDisabled = !$hasPreviousPage;
|
|
|
39
39
|
$: $pageSize = pageSizeDropdownValue;
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
|
-
<div class="flex justify-between w-full items-stretch gap-10
|
|
42
|
+
<div class="flex justify-between w-full items-stretch gap-10">
|
|
43
43
|
<div class="flex justify-start">
|
|
44
44
|
<!-- <select
|
|
45
45
|
name="pageSize"
|
|
@@ -53,7 +53,7 @@ $: $pageSize = pageSizeDropdownValue;
|
|
|
53
53
|
</select> -->
|
|
54
54
|
|
|
55
55
|
<button
|
|
56
|
-
aria-label="Open menu to select number of items per page"
|
|
56
|
+
aria-label="Open menu to select number of items to display per page"
|
|
57
57
|
class="btn variant-filled-primary w-20 justify-between"
|
|
58
58
|
use:popup={pageSizePopup}
|
|
59
59
|
>
|
|
@@ -7,6 +7,7 @@ export declare const normalizeFilters: (filters: {
|
|
|
7
7
|
[key: string]: { [key in FilterOptionsEnum]?: number | string | Date; };
|
|
8
8
|
}) => Filter[];
|
|
9
9
|
export declare const exportAsCsv: (tableId: string, exportedData: string) => void;
|
|
10
|
+
export declare const jsonToCsv: (data: string) => string;
|
|
10
11
|
export declare const resetResize: (headerRows: any, pageRows: any, tableId: string, columns: Columns | undefined, resizable: "none" | "rows" | "columns" | "both") => void;
|
|
11
12
|
export declare const missingValuesFn: (key: number | string, missingValues: {
|
|
12
13
|
[key: string | number]: string;
|
|
@@ -52,6 +52,31 @@ export const exportAsCsv = (tableId, exportedData) => {
|
|
|
52
52
|
anchor.click();
|
|
53
53
|
document.body.removeChild(anchor);
|
|
54
54
|
};
|
|
55
|
+
export const jsonToCsv = (data) => {
|
|
56
|
+
const json = JSON.parse(data);
|
|
57
|
+
if (json.length === 0)
|
|
58
|
+
return '';
|
|
59
|
+
// Extract headers (keys)
|
|
60
|
+
const headers = Object.keys(json[0]);
|
|
61
|
+
// Escape and format a single cell
|
|
62
|
+
const escapeCsvCell = (value) => {
|
|
63
|
+
if (value === null || value === undefined)
|
|
64
|
+
return '';
|
|
65
|
+
let cell = String(value);
|
|
66
|
+
// Escape quotes by doubling them, and wrap the value in quotes if it contains special characters
|
|
67
|
+
if (/[",\n]/.test(cell)) {
|
|
68
|
+
cell = `"${cell.replace(/"/g, '""')}"`;
|
|
69
|
+
}
|
|
70
|
+
return cell;
|
|
71
|
+
};
|
|
72
|
+
// Create CSV rows
|
|
73
|
+
const rows = [
|
|
74
|
+
headers.join(','), // Header row
|
|
75
|
+
...json.map((row) => headers.map(header => escapeCsvCell(row[header])).join(',')) // Data rows
|
|
76
|
+
];
|
|
77
|
+
// Join rows with newlines
|
|
78
|
+
return rows.join('\n');
|
|
79
|
+
};
|
|
55
80
|
// Resetting the resized columns and/or rows
|
|
56
81
|
export const resetResize = (headerRows, pageRows, tableId, columns, resizable) => {
|
|
57
82
|
// Run only if resizable is not none
|
package/package.json
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
export let handleApply: (group: SelectedFacetGroup) => {};
|
|
6
6
|
export let handleCancel: (groupName: string) => {};
|
|
7
7
|
|
|
8
|
-
let selected =
|
|
8
|
+
let selected = Object.keys(group.children)
|
|
9
|
+
.sort((a, b) => group.children[a].displayName.localeCompare(group.children[b].displayName))
|
|
10
|
+
.map((key) => ({ [key]: { ...group.children[key] } }))
|
|
11
|
+
.reduce((acc, val) => ({ ...acc, ...val }), {});
|
|
9
12
|
|
|
10
13
|
const selectAll = () => {
|
|
11
14
|
Object.keys(selected).forEach((key) => (selected[key].selected = true));
|
|
@@ -23,67 +26,44 @@
|
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
const onCancel = () => {
|
|
26
|
-
console.log(selected, group.children);
|
|
27
29
|
selected = structuredClone(group.children);
|
|
28
30
|
handleCancel(group.name);
|
|
29
31
|
};
|
|
30
|
-
|
|
31
|
-
const gridClass = (items: any[]) => {
|
|
32
|
-
const ceil = Math.ceil(Math.sqrt(items.length));
|
|
33
|
-
const max = Math.max(ceil, Math.floor(items.length / 3));
|
|
34
|
-
|
|
35
|
-
const classes = [
|
|
36
|
-
'grid-rows-1',
|
|
37
|
-
'grid-rows-2',
|
|
38
|
-
'grid-rows-3',
|
|
39
|
-
'grid-rows-4',
|
|
40
|
-
'grid-rows-5',
|
|
41
|
-
'grid-rows-6',
|
|
42
|
-
'grid-rows-7',
|
|
43
|
-
'grid-rows-8',
|
|
44
|
-
'grid-rows-9',
|
|
45
|
-
'grid-rows-10',
|
|
46
|
-
'grid-rows-11',
|
|
47
|
-
'grid-rows-12'
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
if (max > 12) {
|
|
51
|
-
return 'grid-rows-12';
|
|
52
|
-
} else return classes[max - 1 || 1];
|
|
53
|
-
};
|
|
54
32
|
</script>
|
|
55
33
|
|
|
56
|
-
<div class="
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
<div class="w-full flex justify-center max-w-[800px]">
|
|
35
|
+
<div class="grow max-h-[500px]">
|
|
36
|
+
<div
|
|
37
|
+
class="p-5 rounded-md w-full bg-surface-50 dark:bg-surface-800 border-primary-500 border-2"
|
|
38
|
+
>
|
|
39
|
+
<!-- Header -->
|
|
40
|
+
<h2 class="text-xl font-semibold">{group.displayName}</h2>
|
|
59
41
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
>
|
|
74
|
-
</label>
|
|
75
|
-
{/each}
|
|
76
|
-
</div>
|
|
42
|
+
<!-- Items -->
|
|
43
|
+
<div class="gap-x-10 space-y-2 py-6 px-[2px] max-h-[500px] columns-[192px] overflow-auto min-h">
|
|
44
|
+
{#each Object.keys(selected) as key}
|
|
45
|
+
<label class="flex gap-3 items-center w-48">
|
|
46
|
+
<input type="checkbox" class="checkbox" bind:checked={selected[key].selected} />
|
|
47
|
+
<span
|
|
48
|
+
title={selected[key].displayName}
|
|
49
|
+
class="whitespace-nowrap break-before-avoid break-after-avoid truncate"
|
|
50
|
+
>{selected[key].displayName}</span
|
|
51
|
+
>
|
|
52
|
+
</label>
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
56
|
+
<!-- Footer -->
|
|
57
|
+
<div class="flex w-full justify-between gap-5">
|
|
58
|
+
<div class="flex gap-3">
|
|
59
|
+
<button class="btn btn-sm variant-filled-tertiary" on:click={selectNone}>None</button>
|
|
60
|
+
<button class="btn btn-sm variant-filled-tertiary" on:click={selectAll}>All</button>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="flex gap-3">
|
|
63
|
+
<button class="btn btn-sm variant-filled-primary" on:click={onApply}>Apply</button>
|
|
64
|
+
<button class="btn btn-sm variant-filled-secondary" on:click={onCancel}>Cancel</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
87
67
|
</div>
|
|
88
68
|
</div>
|
|
89
69
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Fa from 'svelte-fa';
|
|
3
|
+
import { faEye } from '@fortawesome/free-solid-svg-icons';
|
|
2
4
|
import { popup } from '@skeletonlabs/skeleton';
|
|
3
5
|
import type { PopupSettings } from '@skeletonlabs/skeleton';
|
|
4
6
|
|
|
@@ -8,35 +10,62 @@
|
|
|
8
10
|
const popupCombobox: PopupSettings = {
|
|
9
11
|
event: 'click',
|
|
10
12
|
target: `${tableId}-columns-menu`,
|
|
11
|
-
placement: 'bottom'
|
|
13
|
+
placement: 'bottom',
|
|
14
|
+
closeQuery: ''
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const selectAll = () => {
|
|
18
|
+
columns = columns.map((column) => ({ ...column, visible: true }));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const deselectAll = () => {
|
|
22
|
+
columns = columns.map((column) => ({ ...column, visible: false }));
|
|
23
|
+
columns[0].visible = true;
|
|
12
24
|
};
|
|
13
25
|
</script>
|
|
14
26
|
|
|
15
27
|
<button
|
|
16
28
|
type="button"
|
|
17
|
-
|
|
18
|
-
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
29
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last gap-2"
|
|
19
30
|
aria-label="Open menu to hide/show columns"
|
|
20
|
-
use:popup={popupCombobox}
|
|
31
|
+
use:popup={popupCombobox}><Fa icon={faEye} /> Columns</button
|
|
21
32
|
>
|
|
22
|
-
|
|
23
33
|
<div
|
|
24
|
-
class="bg-white dark:bg-surface-500 p-4 rounded-md shadow-md z-10"
|
|
34
|
+
class="bg-white dark:bg-surface-500 p-4 px-5 rounded-md shadow-md z-10 border border-primary-500"
|
|
25
35
|
data-popup="{tableId}-columns-menu"
|
|
26
36
|
>
|
|
37
|
+
<div class="flex items-center gap-4 pb-5 grow justify-between">
|
|
38
|
+
<button
|
|
39
|
+
on:click|preventDefault={selectAll}
|
|
40
|
+
type="button"
|
|
41
|
+
class="btn p-0 text-sm grow underline text-primary-600"
|
|
42
|
+
>
|
|
43
|
+
Select All
|
|
44
|
+
</button>
|
|
45
|
+
<div class="border border-r border-neutral-200 h-6" />
|
|
46
|
+
<button
|
|
47
|
+
on:click|preventDefault={deselectAll}
|
|
48
|
+
type="button"
|
|
49
|
+
class="btn p-0 text-sm grow underline text-neutral-500"
|
|
50
|
+
>
|
|
51
|
+
Deselect All
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
27
54
|
{#each columns as column}
|
|
28
55
|
<div class="flex gap-3 items-center">
|
|
29
56
|
<label for={column.id} class="cursor-pointer" title={column.label}></label>
|
|
30
57
|
<input
|
|
31
|
-
aria-label=
|
|
58
|
+
aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
32
59
|
type="checkbox"
|
|
33
|
-
|
|
60
|
+
class="checkbox"
|
|
61
|
+
id={column.id}
|
|
34
62
|
bind:checked={column.visible}
|
|
63
|
+
title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
35
64
|
disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
|
|
36
65
|
/>
|
|
37
66
|
<span>{column.label}</span>
|
|
38
67
|
</div>
|
|
39
68
|
{/each}
|
|
40
69
|
|
|
41
|
-
<div class="arrow bg-white dark:bg-surface-500" />
|
|
70
|
+
<div class="arrow bg-white dark:bg-surface-500 border-l border-t border-primary-500" />
|
|
42
71
|
</div>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { createEventDispatcher } from 'svelte';
|
|
2
|
+
import { afterUpdate, onDestroy, createEventDispatcher } from 'svelte';
|
|
3
3
|
import { readable, writable } from 'svelte/store';
|
|
4
4
|
|
|
5
5
|
import Fa from 'svelte-fa';
|
|
6
|
-
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
6
|
+
import { faCompress, faDownload, faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
7
7
|
import { createTable, Subscribe, Render, createRender } from 'svelte-headless-table';
|
|
8
8
|
import {
|
|
9
9
|
addSortBy,
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
import {
|
|
31
31
|
cellStyle,
|
|
32
32
|
exportAsCsv,
|
|
33
|
+
jsonToCsv,
|
|
33
34
|
fixedWidth,
|
|
34
35
|
normalizeFilters,
|
|
35
36
|
resetResize,
|
|
36
|
-
convertServerColumns
|
|
37
|
+
convertServerColumns,
|
|
38
|
+
minWidth
|
|
37
39
|
} from './shared';
|
|
38
40
|
import { Receive, Send } from '$models/Models';
|
|
39
41
|
import type { TableConfig } from '$models/Models';
|
|
@@ -61,6 +63,8 @@
|
|
|
61
63
|
|
|
62
64
|
let searchValue = '';
|
|
63
65
|
let isFetching = false;
|
|
66
|
+
let tableRef: HTMLTableElement;
|
|
67
|
+
|
|
64
68
|
const serverSide = server !== undefined;
|
|
65
69
|
const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
|
|
66
70
|
|
|
@@ -75,6 +79,11 @@
|
|
|
75
79
|
const dispatch = createEventDispatcher();
|
|
76
80
|
const actionDispatcher = (obj) => dispatch('action', obj);
|
|
77
81
|
|
|
82
|
+
// Stores to hold the width and height information for resizing
|
|
83
|
+
const rowHeights = writable<{ [key: number]: { max: number; min: number } }>({});
|
|
84
|
+
const colWidths = writable<number[]>([]);
|
|
85
|
+
|
|
86
|
+
// Server-side variables
|
|
78
87
|
const serverItems = serverSide ? writable<Number>(0) : undefined;
|
|
79
88
|
const serverItemCount = serverSide
|
|
80
89
|
? readable<Number>(0, (set) => {
|
|
@@ -100,7 +109,7 @@
|
|
|
100
109
|
serverItemCount
|
|
101
110
|
} as PaginationConfig),
|
|
102
111
|
expand: addExpandedRows(),
|
|
103
|
-
export: addDataExport({ format: '
|
|
112
|
+
export: addDataExport({ format: 'json' })
|
|
104
113
|
});
|
|
105
114
|
|
|
106
115
|
// A variable to hold all the keys
|
|
@@ -375,6 +384,119 @@
|
|
|
375
384
|
updateTable();
|
|
376
385
|
};
|
|
377
386
|
|
|
387
|
+
const getMaxCellHeightInRow = () => {
|
|
388
|
+
if (!tableRef || resizable === 'columns' || resizable === 'none') return;
|
|
389
|
+
|
|
390
|
+
tableRef.querySelectorAll('tbody tr').forEach((row, index) => {
|
|
391
|
+
const cells = row.querySelectorAll('td');
|
|
392
|
+
|
|
393
|
+
let maxHeight = optionsComponent ? 56 : 44;
|
|
394
|
+
let minHeight = optionsComponent ? 56 : 44;
|
|
395
|
+
|
|
396
|
+
cells.forEach((cell) => {
|
|
397
|
+
const cellHeight = cell.getBoundingClientRect().height;
|
|
398
|
+
// + 2 pixels for rendering borders correctly
|
|
399
|
+
if (cellHeight > maxHeight) {
|
|
400
|
+
maxHeight = cellHeight + 2;
|
|
401
|
+
}
|
|
402
|
+
if (cellHeight < minHeight) {
|
|
403
|
+
minHeight = cellHeight + 2;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
rowHeights.update((rh) => {
|
|
408
|
+
const id = +row.id.split(`${tableId}-row-`)[1];
|
|
409
|
+
return {
|
|
410
|
+
...rh,
|
|
411
|
+
[id]: {
|
|
412
|
+
max: maxHeight - 24,
|
|
413
|
+
min: Math.max(minHeight - 24, rowHeight ?? 20)
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const getMinCellWidthInColumn = () => {
|
|
421
|
+
if (!tableRef || resizable === 'rows' || resizable === 'none') return;
|
|
422
|
+
|
|
423
|
+
// Initialize the colWidths array if it is empty
|
|
424
|
+
if ($colWidths.length === 0) {
|
|
425
|
+
$colWidths = Array.from({ length: $headerRows[0].cells.length }, () => 100);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
colWidths.update((cw) => {
|
|
429
|
+
tableRef?.querySelectorAll('thead tr th span').forEach((cell, index) => {
|
|
430
|
+
// + 12 pixels for padding and + 32 pixels for filter icon
|
|
431
|
+
// If the column width is 100, which means it has not been initialized, then calculate the width
|
|
432
|
+
cw[index] = cw[index] === 100 ? cell.getBoundingClientRect().width + 12 + 32 : cw[index];
|
|
433
|
+
});
|
|
434
|
+
return cw;
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const resizeRowsObserver = new ResizeObserver(() => {
|
|
439
|
+
getMaxCellHeightInRow();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const resizeColumnsObserver = new ResizeObserver(() => {
|
|
443
|
+
getMinCellWidthInColumn();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const observeFirstCells = () => {
|
|
447
|
+
if (!tableRef) return;
|
|
448
|
+
|
|
449
|
+
$pageRows.forEach((row) => {
|
|
450
|
+
const cell = tableRef.querySelector(`#${tableId}-row-${row.id}`);
|
|
451
|
+
if (cell) {
|
|
452
|
+
resizeRowsObserver.observe(cell);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
tableRef.querySelectorAll('tbody tr td:first-child').forEach((cell) => {
|
|
457
|
+
resizeRowsObserver.observe(cell);
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const observeHeaderColumns = () => {
|
|
462
|
+
if (!tableRef) return;
|
|
463
|
+
|
|
464
|
+
tableRef.querySelectorAll('thead tr th').forEach((cell) => {
|
|
465
|
+
resizeColumnsObserver.observe(cell);
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
afterUpdate(() => {
|
|
470
|
+
if (resizable !== 'rows' && resizable !== 'both') {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// Making sure tableRef is up to date and contains the new rows
|
|
474
|
+
// If it contains even one element, it means it contains them all
|
|
475
|
+
const e = tableRef?.querySelector(`#${tableId}-row-${$pageRows[0].id}`);
|
|
476
|
+
if (e) {
|
|
477
|
+
getDimensions();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Remove the resize observer when the component is destroyed for performance reasons
|
|
482
|
+
onDestroy(() => {
|
|
483
|
+
resizeRowsObserver.disconnect();
|
|
484
|
+
resizeColumnsObserver.disconnect();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const getDimensions = () => {
|
|
488
|
+
if (!tableRef) return;
|
|
489
|
+
if (resizable === 'none') return;
|
|
490
|
+
else if (resizable === 'columns') {
|
|
491
|
+
observeHeaderColumns();
|
|
492
|
+
} else if (resizable === 'rows') {
|
|
493
|
+
observeFirstCells();
|
|
494
|
+
} else {
|
|
495
|
+
observeHeaderColumns();
|
|
496
|
+
observeFirstCells();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
378
500
|
$: sortKeys = pluginStates.sort.sortKeys;
|
|
379
501
|
$: serverSide && updateTable();
|
|
380
502
|
$: serverSide && sortServer($sortKeys[0]?.order, $sortKeys[0]?.id);
|
|
@@ -405,6 +527,7 @@
|
|
|
405
527
|
title="Search within all table rows"
|
|
406
528
|
bind:value={searchValue}
|
|
407
529
|
placeholder="Search rows..."
|
|
530
|
+
aria-label="Searchbox for searching rows"
|
|
408
531
|
id="{tableId}-search"
|
|
409
532
|
/><button
|
|
410
533
|
type="reset"
|
|
@@ -429,6 +552,7 @@
|
|
|
429
552
|
title="Search"
|
|
430
553
|
id="{tableId}-searchSubmit"
|
|
431
554
|
class="btn variant-filled-primary"
|
|
555
|
+
aria-label="Search"
|
|
432
556
|
on:click|preventDefault={() => {
|
|
433
557
|
if (serverSide && !sendModel) {
|
|
434
558
|
throw new Error('Server-side configuration is missing');
|
|
@@ -443,7 +567,8 @@
|
|
|
443
567
|
{/if}
|
|
444
568
|
|
|
445
569
|
<div
|
|
446
|
-
class="flex justify-between items-center w-full {search &&
|
|
570
|
+
class="flex justify-between overflow-x-auto items-center w-full {search &&
|
|
571
|
+
'py-2'} {!search &&
|
|
447
572
|
(shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
|
|
448
573
|
'pb-2'}"
|
|
449
574
|
>
|
|
@@ -457,6 +582,10 @@
|
|
|
457
582
|
size="sm"
|
|
458
583
|
checked={fitToScreen}
|
|
459
584
|
id="{tableId}-toggle"
|
|
585
|
+
title={fitToScreen ? 'Fit table data to screen' : `Don't fit table data to screen`}
|
|
586
|
+
aria-label={fitToScreen
|
|
587
|
+
? 'Fit table data to screen'
|
|
588
|
+
: `Don't fit table data to screen`}
|
|
460
589
|
on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
|
|
461
590
|
>
|
|
462
591
|
{/if}
|
|
@@ -466,20 +595,20 @@
|
|
|
466
595
|
{#if resizable !== 'none'}
|
|
467
596
|
<button
|
|
468
597
|
type="button"
|
|
469
|
-
|
|
470
|
-
|
|
598
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
|
|
599
|
+
aria-label="Reset sizing of columns and rows"
|
|
471
600
|
on:click|preventDefault={() =>
|
|
472
601
|
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
|
473
|
-
|
|
602
|
+
><Fa icon={faCompress} /> Reset sizing</button
|
|
474
603
|
>
|
|
475
604
|
{/if}
|
|
476
605
|
{#if exportable}
|
|
477
606
|
<button
|
|
478
607
|
type="button"
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
482
|
-
|
|
608
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
|
|
609
|
+
aria-label="Export table data as CSV"
|
|
610
|
+
on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
|
|
611
|
+
><Fa icon={faDownload} /> Export as CSV</button
|
|
483
612
|
>
|
|
484
613
|
{/if}
|
|
485
614
|
{#if shownColumns.length > 0}
|
|
@@ -490,6 +619,7 @@
|
|
|
490
619
|
|
|
491
620
|
<div class="overflow-auto" style="height: {height}px">
|
|
492
621
|
<table
|
|
622
|
+
bind:this={tableRef}
|
|
493
623
|
{...$tableAttrs}
|
|
494
624
|
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
|
495
625
|
id="{tableId}-table"
|
|
@@ -506,14 +636,25 @@
|
|
|
506
636
|
let:rowProps
|
|
507
637
|
>
|
|
508
638
|
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
|
|
509
|
-
{#each headerRow.cells as cell (cell.id)}
|
|
639
|
+
{#each headerRow.cells as cell, index (cell.id)}
|
|
510
640
|
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
|
|
511
|
-
<th
|
|
641
|
+
<th
|
|
642
|
+
scope="col"
|
|
643
|
+
class="!p-2"
|
|
644
|
+
{...attrs}
|
|
645
|
+
style={`
|
|
646
|
+
width: ${cell.isData() ? 'auto' : '0'};
|
|
647
|
+
${cellStyle(cell.id, columns)}
|
|
648
|
+
`}
|
|
649
|
+
>
|
|
512
650
|
<div
|
|
513
651
|
class="overflow-auto"
|
|
514
652
|
class:resize-x={(resizable === 'columns' || resizable === 'both') &&
|
|
515
653
|
!fixedWidth(cell.id, columns)}
|
|
516
654
|
id="th-{tableId}-{cell.id}"
|
|
655
|
+
style={`
|
|
656
|
+
min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
|
|
657
|
+
`}
|
|
517
658
|
>
|
|
518
659
|
<div class="flex justify-between items-center">
|
|
519
660
|
<div class="flex gap-1 whitespace-pre-wrap">
|
|
@@ -526,6 +667,11 @@
|
|
|
526
667
|
class:cursor-pointer={!props.sort.disabled}
|
|
527
668
|
on:click={props.sort.toggle}
|
|
528
669
|
on:keydown={props.sort.toggle}
|
|
670
|
+
title={props.sort.order === 'asc'
|
|
671
|
+
? `Sort by ${cell.label} column in descending order`
|
|
672
|
+
: props.sort.order === 'desc'
|
|
673
|
+
? `Remove sorting by ${cell.label} column`
|
|
674
|
+
: `Sort by ${cell.label} column in ascending order`}
|
|
529
675
|
>
|
|
530
676
|
{cell.render()}
|
|
531
677
|
</span>
|
|
@@ -559,20 +705,42 @@
|
|
|
559
705
|
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
|
560
706
|
{#each row.cells as cell, index (cell?.id)}
|
|
561
707
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
|
562
|
-
<td {...attrs} class="
|
|
708
|
+
<td {...attrs} class="">
|
|
563
709
|
<div
|
|
564
|
-
class="
|
|
710
|
+
class=" h-full {index === 0 &&
|
|
565
711
|
(resizable === 'rows' || resizable === 'both')
|
|
566
|
-
? 'resize-y'
|
|
567
|
-
: ''}"
|
|
712
|
+
? 'resize-y overflow-auto'
|
|
713
|
+
: 'block'}"
|
|
568
714
|
id="{tableId}-{cell.id}-{row.id}"
|
|
715
|
+
style={`
|
|
716
|
+
min-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
|
|
717
|
+
max-height: ${
|
|
718
|
+
index !== 0 && $rowHeights && $rowHeights[+row.id]
|
|
719
|
+
? `${$rowHeights[+row.id].max}px`
|
|
720
|
+
: 'auto'
|
|
721
|
+
};
|
|
722
|
+
height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
|
|
723
|
+
`}
|
|
569
724
|
>
|
|
570
725
|
<!-- Adding config for initial rowHeight, if provided -->
|
|
571
726
|
<div
|
|
572
|
-
class="flex items-
|
|
573
|
-
style=
|
|
727
|
+
class="flex items-start overflow-auto"
|
|
728
|
+
style={`
|
|
729
|
+
max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
|
|
730
|
+
`}
|
|
574
731
|
>
|
|
575
|
-
<div
|
|
732
|
+
<div
|
|
733
|
+
class="grow overflow-auto"
|
|
734
|
+
style={cell.isData()
|
|
735
|
+
? `width: ${
|
|
736
|
+
minWidth(cell.id, columns)
|
|
737
|
+
? minWidth(cell.id, columns)
|
|
738
|
+
: $colWidths[index]
|
|
739
|
+
}px;`
|
|
740
|
+
: 'max-width: min-content;'}
|
|
741
|
+
>
|
|
742
|
+
<Render of={cell.render()} />
|
|
743
|
+
</div>
|
|
576
744
|
</div>
|
|
577
745
|
</div>
|
|
578
746
|
</td>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
$: $pageSize = pageSizeDropdownValue;
|
|
50
50
|
</script>
|
|
51
51
|
|
|
52
|
-
<div class="flex justify-between w-full items-stretch gap-10
|
|
52
|
+
<div class="flex justify-between w-full items-stretch gap-10">
|
|
53
53
|
<div class="flex justify-start">
|
|
54
54
|
<!-- <select
|
|
55
55
|
name="pageSize"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
</select> -->
|
|
64
64
|
|
|
65
65
|
<button
|
|
66
|
-
aria-label="Open menu to select number of items per page"
|
|
66
|
+
aria-label="Open menu to select number of items to display per page"
|
|
67
67
|
class="btn variant-filled-primary w-20 justify-between"
|
|
68
68
|
use:popup={pageSizePopup}
|
|
69
69
|
>
|
|
@@ -63,6 +63,37 @@ export const exportAsCsv = (tableId: string, exportedData: string) => {
|
|
|
63
63
|
document.body.removeChild(anchor);
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
export const jsonToCsv = (data: string): string => {
|
|
67
|
+
const json = JSON.parse(data);
|
|
68
|
+
|
|
69
|
+
if (json.length === 0) return '';
|
|
70
|
+
|
|
71
|
+
// Extract headers (keys)
|
|
72
|
+
const headers = Object.keys(json[0]);
|
|
73
|
+
|
|
74
|
+
// Escape and format a single cell
|
|
75
|
+
const escapeCsvCell = (value: any): string => {
|
|
76
|
+
if (value === null || value === undefined) return '';
|
|
77
|
+
let cell = String(value);
|
|
78
|
+
// Escape quotes by doubling them, and wrap the value in quotes if it contains special characters
|
|
79
|
+
if (/[",\n]/.test(cell)) {
|
|
80
|
+
cell = `"${cell.replace(/"/g, '""')}"`;
|
|
81
|
+
}
|
|
82
|
+
return cell;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Create CSV rows
|
|
86
|
+
const rows = [
|
|
87
|
+
headers.join(','), // Header row
|
|
88
|
+
...json.map((row) =>
|
|
89
|
+
headers.map(header => escapeCsvCell(row[header])).join(',')
|
|
90
|
+
) // Data rows
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// Join rows with newlines
|
|
94
|
+
return rows.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
// Resetting the resized columns and/or rows
|
|
67
98
|
export const resetResize = (
|
|
68
99
|
headerRows: any,
|
|
@@ -114,15 +145,15 @@ export const missingValuesFn = (
|
|
|
114
145
|
const foundKey =
|
|
115
146
|
typeof key === 'number' && key.toString().includes('e')
|
|
116
147
|
? Object.keys(missingValues).find((item) => {
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
return (item as string).toLowerCase() === key.toString().toLowerCase();
|
|
149
|
+
})
|
|
119
150
|
: typeof key === 'string' && parseInt(key).toString().length !== key.length && new Date(key)
|
|
120
|
-
|
|
151
|
+
? Object.keys(missingValues).find(
|
|
121
152
|
(item) => new Date(item).getTime() === new Date(key).getTime()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
)
|
|
154
|
+
: key in missingValues
|
|
155
|
+
? key
|
|
156
|
+
: undefined;
|
|
126
157
|
|
|
127
158
|
return foundKey ? missingValues[foundKey] : key;
|
|
128
159
|
};
|