|
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 | 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 | 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 | 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 | 109 | serverItemCount
|
101 | 110 | } as PaginationConfig),
|
102 | 111 | expand: addExpandedRows(),
|
103 |
| - export: addDataExport({ format: 'csv' }) |
| 112 | + export: addDataExport({ format: 'json' }) |
104 | 113 | });
|
105 | 114 |
|
106 | 115 | // A variable to hold all the keys
|
|
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);
|
|
445 | 567 | {/if}
|
446 | 568 |
|
447 | 569 | <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 && |
449 | 572 | (shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
|
450 | 573 | 'pb-2'}"
|
451 | 574 | >
|
|
472 | 595 | {#if resizable !== 'none'}
|
473 | 596 | <button
|
474 | 597 | 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" |
477 | 599 | aria-label="Reset sizing of columns and rows"
|
478 | 600 | on:click|preventDefault={() =>
|
479 | 601 | resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
480 |
| - >Reset sizing</button |
| 602 | + ><Fa icon={faCompress} /> Reset sizing</button |
481 | 603 | >
|
482 | 604 | {/if}
|
483 | 605 | {#if exportable}
|
484 | 606 | <button
|
485 | 607 | 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" |
488 | 609 | 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 |
491 | 612 | >
|
492 | 613 | {/if}
|
493 | 614 | {#if shownColumns.length > 0}
|
|
498 | 619 |
|
499 | 620 | <div class="overflow-auto" style="height: {height}px">
|
500 | 621 | <table
|
| 622 | + bind:this={tableRef} |
501 | 623 | {...$tableAttrs}
|
502 | 624 | class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
503 | 625 | id="{tableId}-table"
|
|
514 | 636 | let:rowProps
|
515 | 637 | >
|
516 | 638 | <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)} |
518 | 640 | <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 | + > |
520 | 650 | <div
|
521 | 651 | class="overflow-auto"
|
522 | 652 | class:resize-x={(resizable === 'columns' || resizable === 'both') &&
|
523 | 653 | !fixedWidth(cell.id, columns)}
|
524 | 654 | id="th-{tableId}-{cell.id}"
|
| 655 | + style={` |
| 656 | + min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px; |
| 657 | + `} |
525 | 658 | >
|
526 | 659 | <div class="flex justify-between items-center">
|
527 | 660 | <div class="flex gap-1 whitespace-pre-wrap">
|
|
572 | 705 | <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
573 | 706 | {#each row.cells as cell, index (cell?.id)}
|
574 | 707 | <Subscribe attrs={cell.attrs()} let:attrs>
|
575 |
| - <td {...attrs} class="!p-2"> |
| 708 | + <td {...attrs} class=""> |
576 | 709 | <div
|
577 |
| - class=" overflow-auto h-max {index === 0 && |
| 710 | + class=" h-full {index === 0 && |
578 | 711 | (resizable === 'rows' || resizable === 'both')
|
579 |
| - ? 'resize-y' |
580 |
| - : ''}" |
| 712 | + ? 'resize-y overflow-auto' |
| 713 | + : 'block'}" |
581 | 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 | + `} |
582 | 724 | >
|
583 | 725 | <!-- Adding config for initial rowHeight, if provided -->
|
584 | 726 | <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 | + `} |
587 | 731 | >
|
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> |
589 | 744 | </div>
|
590 | 745 | </div>
|
591 | 746 | </td>
|
|
0 commit comments