diff --git a/src/lib/components/Facets/Facets.svelte b/src/lib/components/Facets/Facets.svelte index 5783d901..0b38eb06 100644 --- a/src/lib/components/Facets/Facets.svelte +++ b/src/lib/components/Facets/Facets.svelte @@ -191,10 +191,14 @@ hyphenOpacity="opacity-0" bind:checked={selectedGroups[group.name]} bind:group={selectedGroups} + regionSymbol="shrink-0" > -

- {group.displayName}{group.count !== undefined ? ` (${group.count})` : ''} -

+
+

+ {group.displayName} +

+ {group.count !== undefined ? ` (${group.count})` : ''} +
@@ -210,9 +214,9 @@ selection multiple > -
-

- {item.displayName} +

+

+ {item.displayName}

({item.count})
@@ -237,9 +241,9 @@ selection multiple > -
-

- {item.displayName} +

+

+ {item.displayName}

({item.count})
diff --git a/src/lib/components/Table/TableContent.svelte b/src/lib/components/Table/TableContent.svelte index d8ac2ebf..a8f659fb 100644 --- a/src/lib/components/Table/TableContent.svelte +++ b/src/lib/components/Table/TableContent.svelte @@ -1,5 +1,5 @@ @@ -598,20 +506,23 @@ class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center" aria-label="Reset sizing of columns and rows" on:click|preventDefault={() => - resetResize($headerRows, $pageRows, tableId, columns, resizable)} + utils.resetResize($headerRows, $pageRows, tableId, columns, resizable)} > Reset sizing {/if} + {#if exportable} {/if} - {#if shownColumns.length > 0} + + {#if showColumnsMenu && shownColumns.length > 0} {/if}
@@ -644,16 +555,20 @@ {...attrs} style={` width: ${cell.isData() ? 'auto' : '0'}; - ${cellStyle(cell.id, columns)} + ${utils.cellStyle(cell.id, columns)} `} >
@@ -712,15 +627,7 @@ ? 'resize-y overflow-auto' : 'block'}" id="{tableId}-{cell.id}-{row.id}" - style={` - min-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'}; - max-height: ${ - index !== 0 && $rowHeights && $rowHeights[+row.id] - ? `${$rowHeights[+row.id].max}px` - : 'auto' - }; - height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'}; - `} + style={utils.getResizeStyles($rowHeights, row.id, index)} >
{:else} - + {/if} {/if}
diff --git a/src/lib/components/Table/TablePagination.svelte b/src/lib/components/Table/TablePagination.svelte index 5266d246..388e2f74 100644 --- a/src/lib/components/Table/TablePagination.svelte +++ b/src/lib/components/Table/TablePagination.svelte @@ -9,10 +9,14 @@ } from '@fortawesome/free-solid-svg-icons'; import { ListBox, ListBoxItem, popup, type PopupSettings } from '@skeletonlabs/skeleton'; + export let itemCount; export let pageConfig; export let pageSizes; + export let pageIndexStringType; export let id; + let indexInformation = ''; + const { pageIndex, pageCount, pageSize, hasNextPage, hasPreviousPage } = pageConfig; const goToFirstPage = () => ($pageIndex = 0); @@ -42,11 +46,23 @@ closeQuery: '.listbox-item' }; + const getIndexInfomationString = () => { + if (pageIndexStringType === 'pages') { + return $pageCount > 0 ? `Page ${$pageIndex + 1} of ${$pageCount}` : 'No pages'; + } else { + return itemCount === 0 ? 'No items' : `Displaying items ${$pageIndex * $pageSize + 1} - ${Math.min( + ($pageIndex + 1) * $pageSize, + itemCount + )} of ${Math.min($pageCount * $pageSize, itemCount)}`; + } + }; + $: goToFirstPageDisabled = !$pageIndex; $: goToLastPageDisabled = $pageIndex == $pageCount - 1; $: goToNextPageDisabled = !$hasNextPage; $: goToPreviousPageDisabled = !$hasPreviousPage; $: $pageSize = pageSizeDropdownValue; + $: $pageCount, $pageIndex, $pageSize, (indexInformation = getIndexInfomationString());
@@ -124,12 +140,6 @@ >
- - {#if $pageCount > 0} - Page {$pageIndex + 1} of {$pageCount} - {:else} - No pages - {/if} - + {indexInformation}
diff --git a/src/lib/components/Table/TablePaginationServer.svelte b/src/lib/components/Table/TablePaginationServer.svelte index 8c35236b..c97cdc61 100644 --- a/src/lib/components/Table/TablePaginationServer.svelte +++ b/src/lib/components/Table/TablePaginationServer.svelte @@ -10,6 +10,7 @@ export let id; // Unique table ID export let pageIndex; export let pageSize; + export let pageIndexStringType; export let pageSizes; // Available page sizes export let serverItemCount; // Total number of items expected from the server. `serverSide` must be true on table config. export let updateTable; // Function to update table @@ -20,6 +21,9 @@ let goToNextPageDisabled = true; let goToPreviousPageDisabled = true; + // Index information string + let indexInformation = ''; + // Handles the input change event const handleChange = (e) => { const value = e.target.value; @@ -58,12 +62,23 @@ updateTable(); }; + const getIndexInfomationString = () => { + if (pageIndexStringType === 'pages') { + return pageCount > 0 ? `Page ${$pageIndex + 1} of ${pageCount}` : 'No pages'; + } else { + return `Showing ${$pageIndex * $pageSize + 1} - ${($pageIndex + 1) * $pageSize} of ${ + pageCount * $pageSize + } items`; + } + }; + $: pageCount = Math.ceil($serverItemCount / $pageSize); $: goToFirstPageDisabled = !$pageIndex; $: goToLastPageDisabled = $pageIndex == pageCount - 1; $: goToNextPageDisabled = $pageIndex == pageCount - 1; $: goToPreviousPageDisabled = !$pageIndex; $: $pageSize && updateTable(); // Update query when page size changes + $: pageCount, $pageIndex, $pageSize, (indexInformation = getIndexInfomationString()); updateTable(); @@ -127,15 +142,7 @@
- {#if pageCount > 0} - {#if pageCount == 1} - 1 page - {:else} - {pageCount} pages - {/if} - {:else} - No pages - {/if} + {indexInformation}
diff --git a/src/lib/components/Table/shared.ts b/src/lib/components/Table/utils.ts similarity index 59% rename from src/lib/components/Table/shared.ts rename to src/lib/components/Table/utils.ts index b4e792a7..100f3c80 100644 --- a/src/lib/components/Table/shared.ts +++ b/src/lib/components/Table/utils.ts @@ -1,7 +1,10 @@ import dateFormat from 'dateformat'; +import { SvelteComponent } from 'svelte'; +import type { Writable } from 'svelte/store'; +import { Send, Receive } from '$models/Models'; import type { FilterOptionsEnum } from '$models/Enums'; -import type { Columns, Filter, ServerColumn } from '$models/Models'; +import type { Columns, Filter, ServerColumn, ServerConfig } from '$models/Models'; // Function to determine minWidth for a column to simplify the logic in the HTML export const minWidth = (id: string, columns: Columns | undefined) => { @@ -158,6 +161,76 @@ export const missingValuesFn = ( return foundKey ? missingValues[foundKey] : key; }; +// Function to update the server-side table data +export const updateTable = async ( + pageSize: number, + pageIndex: number, + server: ServerConfig | undefined, + filters: { + [key: string]: { [key in FilterOptionsEnum]?: number | string | Date } + }, + data: Writable, + serverItems: Writable | undefined, + columns: Columns | undefined, + dispatch: any +) => { + const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {}; + + if (!sendModel) throw new Error('Server-side configuration is missing'); + + sendModel.limit = pageSize; + sendModel.offset = pageSize * pageIndex; + sendModel.version = versionId || -1; + sendModel.id = entityId || -1; + sendModel.filter = normalizeFilters(filters); + + let fetchData; + + try { + fetchData = await fetch(baseUrl || '', { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify(sendModel) + }); + } catch (error) { + throw new Error(`Network error: ${(error as Error).message}`); + } + + if (!fetchData.ok) { + throw new Error('Failed to fetch data'); + } + + const response: Receive = await fetchData.json(); + + // Format server columns to the client columns + if (response.columns !== undefined) { + columns = convertServerColumns(response.columns, columns); + + const clientCols = response.columns.reduce((acc, col) => { + acc[col.key] = col.column; + return acc; + }, {}); + + const tmpArr: any[] = []; + + response.data.forEach((row, index) => { + const tmp: { [key: string]: any } = {}; + Object.keys(row).forEach((key) => { + tmp[clientCols[key]] = row[key]; + }); + tmpArr.push(tmp); + }); + dispatch('fetch', columns); + data.set(tmpArr); + } + + serverItems?.set(response.count); + + return response; +}; + export const convertServerColumns = ( serverColumns: ServerColumn[], columns: Columns | undefined @@ -215,3 +288,86 @@ export const convertServerColumns = ( return columnsConfig; }; + +// Calculates the maximum height of the cells in each row +export const getMaxCellHeightInRow = ( + tableRef: HTMLTableElement, + resizable: 'columns' | 'rows' | 'none' | 'both', + optionsComponent: typeof SvelteComponent | undefined, + rowHeights: Writable<{ [key: number]: { max: number; min: number } }>, + tableId: string, + rowHeight: number | null +) => { + if (!tableRef || resizable === 'columns' || resizable === 'none') return; + + tableRef.querySelectorAll('tbody tr').forEach((row, index) => { + const cells = row.querySelectorAll('td'); + + let maxHeight = optionsComponent ? 56 : 44; + let minHeight = optionsComponent ? 56 : 44; + + cells.forEach((cell) => { + const cellHeight = cell.getBoundingClientRect().height; + // + 2 pixels for rendering borders correctly + if (cellHeight > maxHeight) { + maxHeight = cellHeight + 2; + } + if (cellHeight < minHeight) { + minHeight = cellHeight + 2; + } + }); + + rowHeights.update((rh) => { + const id = +row.id.split(`${tableId}-row-`)[1]; + return { + ...rh, + [id]: { + max: maxHeight - 24, + min: Math.max(minHeight - 24, rowHeight ?? 20) + } + }; + }); + }); +}; + +// Calculates the minimum width of the cells in each column +export const getMinCellWidthInColumn = ( + tableRef: HTMLTableElement, + colWidths: Writable, + headerRowsLength: number, + resizable: 'columns' | 'rows' | 'none' | 'both' +) => { + if (!tableRef || resizable === 'rows' || resizable === 'none') return; + + // Initialize the column widths if they are not already initialized + colWidths.update((cw) => { + if (cw.length === 0) { + return Array.from({ length: headerRowsLength }, () => 100); + } + return cw; + }); + + colWidths.update((cw) => { + tableRef?.querySelectorAll('thead tr th span').forEach((cell, index) => { + // + 12 pixels for padding and + 32 pixels for filter icon + // If the column width is 100, which means it has not been initialized, then calculate the width + cw[index] = cw[index] === 100 ? cell.getBoundingClientRect().width + 12 + 32 : cw[index]; + }); + return cw; + }); +}; + +export const getResizeStyles = ( + rowHeights: { [key: number]: { max: number; min: number } }, + id: string | number, + index: number +) => { + return ` + min-height: ${rowHeights && rowHeights[+id] ? `${rowHeights[+id].min}px` : 'auto'}; + max-height: ${index !== 0 && rowHeights && rowHeights[+id] + ? `${rowHeights[+id].max}px` + : 'auto' + }; + height: ${rowHeights && rowHeights[+id] ? `${rowHeights[+id].min}px` : 'auto'}; + `; +} \ No newline at end of file diff --git a/src/lib/models/Models.ts b/src/lib/models/Models.ts index bff6997d..eced55f4 100644 --- a/src/lib/models/Models.ts +++ b/src/lib/models/Models.ts @@ -117,6 +117,7 @@ export interface TableConfig { id: string; data: Writable; resizable?: 'none' | 'rows' | 'columns' | 'both'; // none by default + showColumnsMenu?: boolean; // false by default toggle?: boolean; // false by default search?: boolean; // true by default fitToScreen?: boolean; // true by default @@ -126,6 +127,7 @@ export interface TableConfig { exportable?: boolean; // false by default pageSizes?: number[]; // [5, 10, 20, 50, 100] by default defaultPageSize?: number; // 10 by default + pageIndexStringType?: 'items' | 'pages'; // pages by default optionsComponent?: typeof SvelteComponent; server?: ServerConfig; diff --git a/src/routes/components/table/data/codeBlocks.ts b/src/routes/components/table/data/codeBlocks.ts index 5cfbb28b..39970c1c 100644 --- a/src/routes/components/table/data/codeBlocks.ts +++ b/src/routes/components/table/data/codeBlocks.ts @@ -146,6 +146,7 @@ export const usersBDHTML = ` const usersBDConfig: TableConfig = { id: 'usersBD', data: usersBDStore, + pageIndexStringType: 'items', columns: { dateOfBirth: { header: 'Date of birth', @@ -203,15 +204,20 @@ export interface TableConfig { id: string; data: Writable; resizable?: 'rows' | 'columns' | 'both'; // none by default + showColumnsMenu?: boolean; // false by default toggle?: boolean; // false by default + search?: boolean; // true by default fitToScreen?: boolean; // true by default height?: null | number; // null by default rowHeight?: number; // auto by default columns?: Columns; exportable?: boolean; // false by default - pageSizes?: number[]; // [5, 10, 15, 20] by default + pageSizes?: number[]; // [5, 10, 20, 50, 100] by default defaultPageSize?: number; // 10 by default + pageIndexStringType?: 'items' | 'pages'; // pages by default optionsComponent?: typeof SvelteComponent; + + server?: ServerConfig; }`; export const columnsTypeCode = ` @@ -332,6 +338,7 @@ export const websitesHTML = ` const websitesConfig: TableConfig = { id: 'websites', data: websitesStore, + showColumnsMenu: true, toggle: true, fitToScreen: false, columns: { @@ -707,13 +714,13 @@ export const serverSideTableHTML = ` const serverTableConfig: TableConfig = { id: 'serverTable', // a unique id for the table - entityId: 3, // dataset ID - versionId: -1, // vesion ID data: tableStore, // store to hold and retrieve data - serverSide: true, // serverSide needs to be set to true - // URL for the table to be fetched from - URL: 'https://dev.bexis2.uni-jena.de/api/datatable/', - token: '' // API token to access the datasets + server: { + // URL for the table to be fetched from + baseUrl: 'https://dev.bexis2.uni-jena.de/api/datatable/', + entityId: 1, // dataset ID + versionId: -1, // version ID + } }; diff --git a/src/routes/components/table/docs/TableConfigDocs.svelte b/src/routes/components/table/docs/TableConfigDocs.svelte index e34d0ac3..a5d13ed7 100644 --- a/src/routes/components/table/docs/TableConfigDocs.svelte +++ b/src/routes/components/table/docs/TableConfigDocs.svelte @@ -19,7 +19,7 @@ >
Underlined attributes are required.
- +
@@ -42,32 +42,31 @@

A writable store of the type T[]T[]. Any changes in the store will be reflected in the table.

-
search:
-
boolean
+
resizable:
+
"rows", "columns" or "both"

- Whether the table should have a search bar. true by default. + Whether rows, columns or both should be resizable. Not resizable by default.

-
exportable:
+
showColumnsMenu:
boolean

- Whether the table should be exportable to CSV. false by default.

@@ -80,35 +79,26 @@

- Whether the fitToScreen toggle should be visible. - false by default. -

- - -
-
-
resizable:
-
"rows", "columns" or "both"
-
- -

- Whether rows, columns or both should be resizable. Not resizable by default. + false by default.

-
rowHeight:
-
number
+
search:
+
boolean

- Sets height for the rows in pixels. If resizable is set to "both" or "rows", this value can be interpreted as minimum height for the rows. + Whether the table should have a search bar. true by default.

@@ -136,15 +126,43 @@
-
optionsComponent:
-
{`SvelteComponent`}
+
rowHeight:
+
number

- Custom Svelte component to apply actions on a specific row. Table will not have an options - column if no optionsComponent was provided. + Sets height for the rows in pixels. If resizable is set to "both" or "rows", this value can be interpreted as minimum height for the rows. +

+
+ +
+
+
columns:
+
{`Columns`}
+
+ +

+ An object with configuration for specific columns. Columns + object is described below. +

+
+ +
+
+
exportable:
+
boolean
+
+ +

+ Whether the table should be exportable to CSV. false by default.

@@ -155,7 +173,8 @@

- An array of page sizes to be used for the table. By default, page sizes are 5, 10, 20, 50, 100. + An array of page sizes to be used for the table. By default, page sizes are 5, 10, 20, 50, + 100.

@@ -173,15 +192,31 @@
-
columns:
-
{`Columns`}
+
pageIndexStringType:
+
"items" or "pages"

- An object with configuration for specific columns. Columns"pages" - object is described below. + by default. +

+
+ +
+
+
optionsComponent:
+
{`SvelteComponent`}
+
+ +

+ Custom Svelte component to apply actions on a specific row. Table will not have an options + column if no optionsComponent was provided.

diff --git a/src/routes/components/table/examples/TableDate.svelte b/src/routes/components/table/examples/TableDate.svelte index b26f9ed8..531c387a 100644 --- a/src/routes/components/table/examples/TableDate.svelte +++ b/src/routes/components/table/examples/TableDate.svelte @@ -9,6 +9,7 @@ const usersBDConfig: TableConfig = { id: 'usersBD', data: usersBDStore, + pageIndexStringType: 'items', columns: { dateOfBirth: { header: 'Date of birth', @@ -24,7 +25,7 @@
- + diff --git a/src/routes/components/table/examples/TableServer.svelte b/src/routes/components/table/examples/TableServer.svelte index 74fb25e5..6a870cf9 100644 --- a/src/routes/components/table/examples/TableServer.svelte +++ b/src/routes/components/table/examples/TableServer.svelte @@ -22,13 +22,11 @@ serverTableConfig = { id: 'serverTable', // a unique id for the table data: tableStore, // store to hold and retrieve data - // URL for the table to be fetched from - pageSizes: [10, 25, 50, 100], - server: { + // URL for the table to be fetched from baseUrl: 'https://dev.bexis2.uni-jena.de/api/datatable/', entityId: 1, // dataset ID - versionId: -1, // version ID + versionId: -1 // version ID } }; }); diff --git a/src/routes/components/table/examples/TableURLs.svelte b/src/routes/components/table/examples/TableURLs.svelte index 4f6582fa..b9f2fadd 100644 --- a/src/routes/components/table/examples/TableURLs.svelte +++ b/src/routes/components/table/examples/TableURLs.svelte @@ -10,6 +10,7 @@ const websitesConfig: TableConfig = { id: 'websites', data: websitesStore, + showColumnsMenu: true, toggle: true, fitToScreen: false, columns: { @@ -31,7 +32,7 @@
- +