Skip to content

Commit 8cdacbf

Browse files
authored
Merge pull request #113 from BEXIS2/table
Table
2 parents b97c5f1 + e6e6b2b commit 8cdacbf

File tree

5 files changed

+301
-36
lines changed

5 files changed

+301
-36
lines changed
Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import Fa from 'svelte-fa';
3+
import { faEye } from '@fortawesome/free-solid-svg-icons';
24
import { popup } from '@skeletonlabs/skeleton';
35
import type { PopupSettings } from '@skeletonlabs/skeleton';
46
@@ -8,29 +10,55 @@
810
const popupCombobox: PopupSettings = {
911
event: 'click',
1012
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;
1224
};
1325
</script>
1426

1527
<button
1628
type="button"
17-
title="Hide or show columns"
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"
1930
aria-label="Open menu to hide/show columns"
20-
use:popup={popupCombobox}>Columns</button
31+
use:popup={popupCombobox}><Fa icon={faEye} /> Columns</button
2132
>
22-
2333
<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"
2535
data-popup="{tableId}-columns-menu"
2636
>
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>
2754
{#each columns as column}
2855
<div class="flex gap-3 items-center">
2956
<label for={column.id} class="cursor-pointer" title={column.label}></label>
3057
<input
3158
aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
3259
type="checkbox"
33-
id = {column.id}
60+
class="checkbox"
61+
id={column.id}
3462
bind:checked={column.visible}
3563
title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
3664
disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
@@ -39,5 +67,5 @@
3967
</div>
4068
{/each}
4169

42-
<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" />
4371
</div>

src/lib/components/Table/TableContent.svelte

Lines changed: 176 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<script lang="ts">
2-
import { createEventDispatcher } from 'svelte';
2+
import { afterUpdate, onDestroy, createEventDispatcher } from 'svelte';
33
import { readable, writable } from 'svelte/store';
44
55
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';
77
import { createTable, Subscribe, Render, createRender } from 'svelte-headless-table';
88
import {
99
addSortBy,
@@ -30,10 +30,12 @@
3030
import {
3131
cellStyle,
3232
exportAsCsv,
33+
jsonToCsv,
3334
fixedWidth,
3435
normalizeFilters,
3536
resetResize,
36-
convertServerColumns
37+
convertServerColumns,
38+
minWidth
3739
} from './shared';
3840
import { Receive, Send } from '$models/Models';
3941
import type { TableConfig } from '$models/Models';
@@ -61,6 +63,8 @@
6163
6264
let searchValue = '';
6365
let isFetching = false;
66+
let tableRef: HTMLTableElement;
67+
6468
const serverSide = server !== undefined;
6569
const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
6670
@@ -75,6 +79,11 @@
7579
const dispatch = createEventDispatcher();
7680
const actionDispatcher = (obj) => dispatch('action', obj);
7781
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
7887
const serverItems = serverSide ? writable<Number>(0) : undefined;
7988
const serverItemCount = serverSide
8089
? readable<Number>(0, (set) => {
@@ -100,7 +109,7 @@
100109
serverItemCount
101110
} as PaginationConfig),
102111
expand: addExpandedRows(),
103-
export: addDataExport({ format: 'csv' })
112+
export: addDataExport({ format: 'json' })
104113
});
105114
106115
// A variable to hold all the keys
@@ -375,6 +384,119 @@
375384
updateTable();
376385
};
377386
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+
378500
$: sortKeys = pluginStates.sort.sortKeys;
379501
$: serverSide && updateTable();
380502
$: serverSide && sortServer($sortKeys[0]?.order, $sortKeys[0]?.id);
@@ -445,7 +567,8 @@
445567
{/if}
446568

447569
<div
448-
class="flex justify-between items-center w-full {search && 'py-2'} {!search &&
570+
class="flex justify-between overflow-x-auto items-center w-full {search &&
571+
'py-2'} {!search &&
449572
(shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
450573
'pb-2'}"
451574
>
@@ -472,22 +595,20 @@
472595
{#if resizable !== 'none'}
473596
<button
474597
type="button"
475-
title="Reset column and row sizing"
476-
class="btn btn-sm variant-filled-primary rounded-full order-last"
598+
class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
477599
aria-label="Reset sizing of columns and rows"
478600
on:click|preventDefault={() =>
479601
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
480-
>Reset sizing</button
602+
><Fa icon={faCompress} /> Reset sizing</button
481603
>
482604
{/if}
483605
{#if exportable}
484606
<button
485607
type="button"
486-
title="Export table data as CSV"
487-
class="btn btn-sm variant-filled-primary rounded-full order-last"
608+
class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
488609
aria-label="Export table data as CSV"
489-
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
490-
>Export as CSV</button
610+
on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
611+
><Fa icon={faDownload} /> Export as CSV</button
491612
>
492613
{/if}
493614
{#if shownColumns.length > 0}
@@ -498,6 +619,7 @@
498619

499620
<div class="overflow-auto" style="height: {height}px">
500621
<table
622+
bind:this={tableRef}
501623
{...$tableAttrs}
502624
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
503625
id="{tableId}-table"
@@ -514,14 +636,25 @@
514636
let:rowProps
515637
>
516638
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
517-
{#each headerRow.cells as cell (cell.id)}
639+
{#each headerRow.cells as cell, index (cell.id)}
518640
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
519-
<th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
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+
>
520650
<div
521651
class="overflow-auto"
522652
class:resize-x={(resizable === 'columns' || resizable === 'both') &&
523653
!fixedWidth(cell.id, columns)}
524654
id="th-{tableId}-{cell.id}"
655+
style={`
656+
min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
657+
`}
525658
>
526659
<div class="flex justify-between items-center">
527660
<div class="flex gap-1 whitespace-pre-wrap">
@@ -572,20 +705,42 @@
572705
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
573706
{#each row.cells as cell, index (cell?.id)}
574707
<Subscribe attrs={cell.attrs()} let:attrs>
575-
<td {...attrs} class="!p-2">
708+
<td {...attrs} class="">
576709
<div
577-
class=" overflow-auto h-max {index === 0 &&
710+
class=" h-full {index === 0 &&
578711
(resizable === 'rows' || resizable === 'both')
579-
? 'resize-y'
580-
: ''}"
712+
? 'resize-y overflow-auto'
713+
: 'block'}"
581714
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+
`}
582724
>
583725
<!-- Adding config for initial rowHeight, if provided -->
584726
<div
585-
class="flex items-center overflow-auto"
586-
style="height: {rowHeight ? `${rowHeight}px` : 'auto'};"
727+
class="flex items-start overflow-auto"
728+
style={`
729+
max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
730+
`}
587731
>
588-
<div class="grow h-full"><Render of={cell.render()} /></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>
589744
</div>
590745
</div>
591746
</td>

0 commit comments

Comments
 (0)