@bexis2/bexis2-core-ui 0.4.22 → 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 CHANGED
@@ -1,4 +1,11 @@
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
+
2
9
  ## 0.4.22
3
10
  - Facets
4
11
  - Replace column class function with more efficient solution
@@ -1,32 +1,58 @@
1
- <script>import { popup } from "@skeletonlabs/skeleton";
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
- title="Hide or show columns"
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}>Columns</button
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
52
  aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
28
53
  type="checkbox"
29
- id = {column.id}
54
+ class="checkbox"
55
+ id={column.id}
30
56
  bind:checked={column.visible}
31
57
  title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
32
58
  disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
@@ -35,5 +61,5 @@ const popupCombobox = {
35
61
  </div>
36
62
  {/each}
37
63
 
38
- <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" />
39
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: "csv" })
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);
@@ -377,7 +470,8 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
377
470
  {/if}
378
471
 
379
472
  <div
380
- class="flex justify-between items-center w-full {search && 'py-2'} {!search &&
473
+ class="flex justify-between overflow-x-auto items-center w-full {search &&
474
+ 'py-2'} {!search &&
381
475
  (shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
382
476
  'pb-2'}"
383
477
  >
@@ -404,22 +498,20 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
404
498
  {#if resizable !== 'none'}
405
499
  <button
406
500
  type="button"
407
- title="Reset column and row sizing"
408
- class="btn btn-sm variant-filled-primary rounded-full order-last"
501
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
409
502
  aria-label="Reset sizing of columns and rows"
410
503
  on:click|preventDefault={() =>
411
504
  resetResize($headerRows, $pageRows, tableId, columns, resizable)}
412
- >Reset sizing</button
505
+ ><Fa icon={faCompress} /> Reset sizing</button
413
506
  >
414
507
  {/if}
415
508
  {#if exportable}
416
509
  <button
417
510
  type="button"
418
- title="Export table data as CSV"
419
- class="btn btn-sm variant-filled-primary rounded-full order-last"
511
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
420
512
  aria-label="Export table data as CSV"
421
- on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
422
- >Export as CSV</button
513
+ on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
514
+ ><Fa icon={faDownload} /> Export as CSV</button
423
515
  >
424
516
  {/if}
425
517
  {#if shownColumns.length > 0}
@@ -430,6 +522,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
430
522
 
431
523
  <div class="overflow-auto" style="height: {height}px">
432
524
  <table
525
+ bind:this={tableRef}
433
526
  {...$tableAttrs}
434
527
  class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
435
528
  id="{tableId}-table"
@@ -446,14 +539,25 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
446
539
  let:rowProps
447
540
  >
448
541
  <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
449
- {#each headerRow.cells as cell (cell.id)}
542
+ {#each headerRow.cells as cell, index (cell.id)}
450
543
  <Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
451
- <th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
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
+ >
452
553
  <div
453
554
  class="overflow-auto"
454
555
  class:resize-x={(resizable === 'columns' || resizable === 'both') &&
455
556
  !fixedWidth(cell.id, columns)}
456
557
  id="th-{tableId}-{cell.id}"
558
+ style={`
559
+ min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
560
+ `}
457
561
  >
458
562
  <div class="flex justify-between items-center">
459
563
  <div class="flex gap-1 whitespace-pre-wrap">
@@ -504,20 +608,42 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
504
608
  <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
505
609
  {#each row.cells as cell, index (cell?.id)}
506
610
  <Subscribe attrs={cell.attrs()} let:attrs>
507
- <td {...attrs} class="!p-2">
611
+ <td {...attrs} class="">
508
612
  <div
509
- class=" overflow-auto h-max {index === 0 &&
613
+ class=" h-full {index === 0 &&
510
614
  (resizable === 'rows' || resizable === 'both')
511
- ? 'resize-y'
512
- : ''}"
615
+ ? 'resize-y overflow-auto'
616
+ : 'block'}"
513
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
+ `}
514
627
  >
515
628
  <!-- Adding config for initial rowHeight, if provided -->
516
629
  <div
517
- class="flex items-center overflow-auto"
518
- style="height: {rowHeight ? `${rowHeight}px` : 'auto'};"
630
+ class="flex items-start overflow-auto"
631
+ style={`
632
+ max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
633
+ `}
519
634
  >
520
- <div class="grow h-full"><Render of={cell.render()} /></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>
521
647
  </div>
522
648
  </div>
523
649
  </td>
@@ -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
@@ -71,7 +71,7 @@
71
71
  }
72
72
 
73
73
  if (!complexSource && !complexTarget && isLoaded && !isMulti) {
74
- console.log('🚀 ~ updateTarget ~ selection:', selection);
74
+ //console.log('🚀 ~ updateTarget ~ selection:', selection);
75
75
  if (selection) {
76
76
  target = selection.value;
77
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bexis2/bexis2-core-ui",
3
- "version": "0.4.22",
3
+ "version": "0.4.23",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -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,29 +10,55 @@
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
- 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"
19
30
  aria-label="Open menu to hide/show columns"
20
- use:popup={popupCombobox}>Columns</button
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
58
  aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
32
59
  type="checkbox"
33
- id = {column.id}
60
+ class="checkbox"
61
+ id={column.id}
34
62
  bind:checked={column.visible}
35
63
  title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
36
64
  disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
@@ -39,5 +67,5 @@
39
67
  </div>
40
68
  {/each}
41
69
 
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" />
43
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: 'csv' })
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);
@@ -445,7 +567,8 @@
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,22 +595,20 @@
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,6 +619,7 @@
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,14 +636,25 @@
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,20 +705,42 @@
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>
@@ -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
- return (item as string).toLowerCase() === key.toString().toLowerCase();
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
- ? Object.keys(missingValues).find(
151
+ ? Object.keys(missingValues).find(
121
152
  (item) => new Date(item).getTime() === new Date(key).getTime()
122
- )
123
- : key in missingValues
124
- ? key
125
- : undefined;
153
+ )
154
+ : key in missingValues
155
+ ? key
156
+ : undefined;
126
157
 
127
158
  return foundKey ? missingValues[foundKey] : key;
128
159
  };
@@ -71,7 +71,7 @@
71
71
  }
72
72
 
73
73
  if (!complexSource && !complexTarget && isLoaded && !isMulti) {
74
- console.log('🚀 ~ updateTarget ~ selection:', selection);
74
+ //console.log('🚀 ~ updateTarget ~ selection:', selection);
75
75
  if (selection) {
76
76
  target = selection.value;
77
77
  }