@liedekef/ftable 1.1.15 → 1.1.17

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.
Files changed (38) hide show
  1. package/ftable.esm.js +76 -34
  2. package/ftable.js +76 -34
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +76 -34
  5. package/package.json +1 -1
  6. package/themes/basic/ftable_basic.css +42 -21
  7. package/themes/basic/ftable_basic.min.css +1 -1
  8. package/themes/ftable_theme_base.less +57 -22
  9. package/themes/lightcolor/blue/ftable.css +42 -21
  10. package/themes/lightcolor/blue/ftable.min.css +1 -1
  11. package/themes/lightcolor/gray/ftable.css +42 -21
  12. package/themes/lightcolor/gray/ftable.min.css +1 -1
  13. package/themes/lightcolor/green/ftable.css +42 -21
  14. package/themes/lightcolor/green/ftable.min.css +1 -1
  15. package/themes/lightcolor/orange/ftable.css +42 -21
  16. package/themes/lightcolor/orange/ftable.min.css +1 -1
  17. package/themes/lightcolor/red/ftable.css +42 -21
  18. package/themes/lightcolor/red/ftable.min.css +1 -1
  19. package/themes/metro/blue/ftable.css +42 -21
  20. package/themes/metro/blue/ftable.min.css +1 -1
  21. package/themes/metro/brown/ftable.css +42 -21
  22. package/themes/metro/brown/ftable.min.css +1 -1
  23. package/themes/metro/crimson/ftable.css +42 -21
  24. package/themes/metro/crimson/ftable.min.css +1 -1
  25. package/themes/metro/darkgray/ftable.css +42 -21
  26. package/themes/metro/darkgray/ftable.min.css +1 -1
  27. package/themes/metro/darkorange/ftable.css +42 -21
  28. package/themes/metro/darkorange/ftable.min.css +1 -1
  29. package/themes/metro/green/ftable.css +42 -21
  30. package/themes/metro/green/ftable.min.css +1 -1
  31. package/themes/metro/lightgray/ftable.css +42 -21
  32. package/themes/metro/lightgray/ftable.min.css +1 -1
  33. package/themes/metro/pink/ftable.css +42 -21
  34. package/themes/metro/pink/ftable.min.css +1 -1
  35. package/themes/metro/purple/ftable.css +42 -21
  36. package/themes/metro/purple/ftable.min.css +1 -1
  37. package/themes/metro/red/ftable.css +42 -21
  38. package/themes/metro/red/ftable.min.css +1 -1
package/ftable.esm.js CHANGED
@@ -1493,6 +1493,7 @@ class FTable extends FTableEventEmitter {
1493
1493
 
1494
1494
  this.bindEvents();
1495
1495
 
1496
+ this.updateSortingHeaders();
1496
1497
  this.renderSortingInfo();
1497
1498
 
1498
1499
  // Add essential CSS if not already present
@@ -1505,7 +1506,7 @@ class FTable extends FTableEventEmitter {
1505
1506
  initColumnWidths() {
1506
1507
  const visibleFields = this.columnList.filter(fieldName => {
1507
1508
  const field = this.options.fields[fieldName];
1508
- return field.visibility !== 'hidden';
1509
+ return field.visibility !== 'hidden' && field.visibility !== 'separator';
1509
1510
  });
1510
1511
 
1511
1512
  const count = visibleFields.length;
@@ -1524,7 +1525,7 @@ class FTable extends FTableEventEmitter {
1524
1525
  th: this.elements.table.querySelector(`[data-field-name="${fieldName}"]`),
1525
1526
  field: this.options.fields[fieldName]
1526
1527
  }))
1527
- .filter(item => item.th && item.field.visibility !== 'hidden');
1528
+ .filter(item => item.th && item.field.visibility !== 'hidden' && item.field.visibility !== 'separator');
1528
1529
 
1529
1530
  if (visibleHeaders.length === 0) return;
1530
1531
 
@@ -1836,7 +1837,7 @@ class FTable extends FTableEventEmitter {
1836
1837
  // Add selecting column if enabled
1837
1838
  if (this.options.selecting && this.options.selectingCheckboxes) {
1838
1839
  const selectHeader = FTableDOMHelper.create('th', {
1839
- className: `ftable-column-header ftable-column-header-select`,
1840
+ className: `ftable-command-column-header ftable-column-header-select`,
1840
1841
  parent: headerRow
1841
1842
  });
1842
1843
 
@@ -1883,16 +1884,12 @@ class FTable extends FTableEventEmitter {
1883
1884
 
1884
1885
  // Make sortable if enabled
1885
1886
  if (this.options.sorting && field.sorting !== false) {
1886
- // Add some empty spaces after the text so the background icon has room next to it
1887
- // one could play with css and ::after, but then the width calculation of columns borks, resize bar is off etc ...
1888
- //textHeader.innerHTML += '     ';
1889
1887
  FTableDOMHelper.addClass(textHeader, 'ftable-sortable-text'); // Add class for spacing
1890
1888
  FTableDOMHelper.addClass(th, 'ftable-column-header-sortable');
1891
1889
  th.addEventListener('click', (e) => {
1892
1890
  e.preventDefault();
1893
- // Store event for multiSortingCtrlKey logic
1894
- this.lastSortEvent = e;
1895
- this.sortByColumn(fieldName);
1891
+ const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey for Mac
1892
+ this.sortByColumn(fieldName, isCtrlPressed);
1896
1893
  });
1897
1894
  }
1898
1895
 
@@ -2208,8 +2205,8 @@ class FTable extends FTableEventEmitter {
2208
2205
  this.load();
2209
2206
  }
2210
2207
 
2211
- getNextVisibleHeader(th) {
2212
- const headers = Array.from(this.elements.table.querySelectorAll('thead th:not(.ftable-command-column-header, .ftable-toolbarsearch-column-header)'));
2208
+ getNextResizableHeader(th) {
2209
+ const headers = Array.from(this.elements.table.querySelectorAll('thead th.ftable-column-header-resizable'));
2213
2210
  const index = headers.indexOf(th);
2214
2211
  for (let i = index + 1; i < headers.length; i++) {
2215
2212
  if (headers[i].offsetParent !== null) { // visible
@@ -2220,6 +2217,7 @@ class FTable extends FTableEventEmitter {
2220
2217
  }
2221
2218
 
2222
2219
  makeColumnResizable(th, container) {
2220
+ FTableDOMHelper.addClass(th, 'ftable-column-header-resizable');
2223
2221
  if (!this.elements.resizeBar) {
2224
2222
  this.elements.resizeBar = FTableDOMHelper.create('div', {
2225
2223
  className: 'ftable-column-resize-bar',
@@ -2253,13 +2251,13 @@ class FTable extends FTableEventEmitter {
2253
2251
  startWidth = th.offsetWidth;
2254
2252
 
2255
2253
  // Find next visible column
2256
- nextTh = this.getNextVisibleHeader(th);
2254
+ nextTh = this.getNextResizableHeader(th);
2257
2255
  if (nextTh) {
2258
2256
  nextStartWidth = nextTh.offsetWidth;
2259
2257
  const fieldName = nextTh.dataset.fieldName;
2260
2258
  nextField = this.options.fields[fieldName];
2261
- } else {
2262
- return;
2259
+ // } else {
2260
+ // return;
2263
2261
  }
2264
2262
 
2265
2263
  // Position resize bar
@@ -3109,7 +3107,7 @@ class FTable extends FTableEventEmitter {
3109
3107
  if (field.visibility === 'fixed') {
3110
3108
  return;
3111
3109
  }
3112
- if (field.visibility === 'hidden') {
3110
+ if (field.visibility === 'hidden' || field.visibility === 'separator') {
3113
3111
  FTableDOMHelper.hide(cell);
3114
3112
  }
3115
3113
  }
@@ -3685,7 +3683,7 @@ class FTable extends FTableEventEmitter {
3685
3683
  }
3686
3684
 
3687
3685
  // Sorting Methods
3688
- sortByColumn(fieldName) {
3686
+ sortByColumn(fieldName, isCtrlPressed = false) {
3689
3687
  const field = this.options.fields[fieldName];
3690
3688
 
3691
3689
  if (!field || field.sorting === false) return;
@@ -3706,9 +3704,6 @@ class FTable extends FTableEventEmitter {
3706
3704
  this.state.sorting.push({ fieldName, direction: newDirection });
3707
3705
  }
3708
3706
 
3709
- // Handle multiSortingCtrlKey: did user press Ctrl/Cmd?
3710
- const isCtrlPressed = this.lastSortEvent?.ctrlKey || this.lastSortEvent?.metaKey; // metaKey for Mac
3711
-
3712
3707
  if (this.options.multiSorting) {
3713
3708
  // If multiSorting is enabled, respect multiSortingCtrlKey
3714
3709
  if (this.options.multiSortingCtrlKey && !isCtrlPressed) {
@@ -4069,23 +4064,70 @@ class FTable extends FTableEventEmitter {
4069
4064
 
4070
4065
  // CSV Export functionality
4071
4066
  exportToCSV(filename = 'table-data.csv') {
4072
- const headers = this.columnList.map(fieldName => {
4073
- const field = this.options.fields[fieldName];
4074
- return field.title || fieldName;
4075
- });
4067
+ // Create a clean table clone from the DOM
4068
+ const tableClone = this.elements.table.cloneNode(true);
4069
+ const csvRows = [];
4070
+
4071
+ // Helper to format CSV cell (escape quotes, wrap in quotes)
4072
+ const formatCSV = (text) => {
4073
+ const str = String(text || '').replace(/"/g, '""');
4074
+ return `"${str}"`;
4075
+ };
4076
4076
 
4077
- const rows = this.state.records.map(record => {
4078
- return this.columnList.map(fieldName => {
4079
- const value = this.getDisplayText(record, fieldName);
4080
- // Escape CSV values
4081
- return `"${String(value).replace(/"/g, '""')}"`;
4082
- });
4083
- });
4077
+ // 1. Extract headers from visible, non-command columns
4078
+ const headerCells = tableClone.querySelectorAll('thead th');
4079
+ const headerRow = [];
4080
+ for (const th of headerCells) {
4081
+ if (th.classList.contains('ftable-command-column-header') ||
4082
+ th.classList.contains('ftable-toolbarsearch-column-header') ||
4083
+ th.style.display === 'none') {
4084
+ continue;
4085
+ }
4086
+ const text = th.textContent.trim();
4087
+ headerRow.push(formatCSV(text));
4088
+ }
4089
+ csvRows.push(headerRow.join(','));
4090
+
4091
+ // 2. Extract rows from visible, non-command cells
4092
+ const dataRows = tableClone.querySelectorAll('tbody tr');
4093
+ for (const tr of dataRows) {
4094
+ // Skip hidden rows
4095
+ if (tr.style.display === 'none') {
4096
+ continue;
4097
+ }
4098
+
4099
+ const rowCells = tr.querySelectorAll('td');
4100
+ const csvRow = [];
4101
+ let hasData = false;
4102
+
4103
+ for (const td of rowCells) {
4104
+ if (td.classList.contains('ftable-command-column') ||
4105
+ td.style.display === 'none') {
4106
+ continue;
4107
+ }
4108
+
4109
+ // Clean up: remove buttons, images
4110
+ if (td.querySelector('img, button, input, select')) {
4111
+ td.innerHTML = td.textContent; // Strip HTML
4112
+ }
4113
+
4114
+ // Replace <br> with \n
4115
+ const html = td.innerHTML;
4116
+ const withLineBreaks = html.replace(/<br\s*\/?>/gi, '\n');
4117
+ const tempDiv = document.createElement('div');
4118
+ tempDiv.innerHTML = withLineBreaks;
4119
+ const text = tempDiv.textContent || '';
4120
+
4121
+ csvRow.push(formatCSV(text));
4122
+ hasData = true;
4123
+ }
4124
+
4125
+ if (hasData) {
4126
+ csvRows.push(csvRow.join(','));
4127
+ }
4128
+ }
4084
4129
 
4085
- const csvContent = [
4086
- headers.map(h => `"${h}"`).join(','),
4087
- ...rows.map(row => row.join(','))
4088
- ].join('\n');
4130
+ const csvContent = csvRows.join('\n');
4089
4131
 
4090
4132
  // Create and trigger download
4091
4133
  const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
package/ftable.js CHANGED
@@ -1498,6 +1498,7 @@ class FTable extends FTableEventEmitter {
1498
1498
 
1499
1499
  this.bindEvents();
1500
1500
 
1501
+ this.updateSortingHeaders();
1501
1502
  this.renderSortingInfo();
1502
1503
 
1503
1504
  // Add essential CSS if not already present
@@ -1510,7 +1511,7 @@ class FTable extends FTableEventEmitter {
1510
1511
  initColumnWidths() {
1511
1512
  const visibleFields = this.columnList.filter(fieldName => {
1512
1513
  const field = this.options.fields[fieldName];
1513
- return field.visibility !== 'hidden';
1514
+ return field.visibility !== 'hidden' && field.visibility !== 'separator';
1514
1515
  });
1515
1516
 
1516
1517
  const count = visibleFields.length;
@@ -1529,7 +1530,7 @@ class FTable extends FTableEventEmitter {
1529
1530
  th: this.elements.table.querySelector(`[data-field-name="${fieldName}"]`),
1530
1531
  field: this.options.fields[fieldName]
1531
1532
  }))
1532
- .filter(item => item.th && item.field.visibility !== 'hidden');
1533
+ .filter(item => item.th && item.field.visibility !== 'hidden' && item.field.visibility !== 'separator');
1533
1534
 
1534
1535
  if (visibleHeaders.length === 0) return;
1535
1536
 
@@ -1841,7 +1842,7 @@ class FTable extends FTableEventEmitter {
1841
1842
  // Add selecting column if enabled
1842
1843
  if (this.options.selecting && this.options.selectingCheckboxes) {
1843
1844
  const selectHeader = FTableDOMHelper.create('th', {
1844
- className: `ftable-column-header ftable-column-header-select`,
1845
+ className: `ftable-command-column-header ftable-column-header-select`,
1845
1846
  parent: headerRow
1846
1847
  });
1847
1848
 
@@ -1888,16 +1889,12 @@ class FTable extends FTableEventEmitter {
1888
1889
 
1889
1890
  // Make sortable if enabled
1890
1891
  if (this.options.sorting && field.sorting !== false) {
1891
- // Add some empty spaces after the text so the background icon has room next to it
1892
- // one could play with css and ::after, but then the width calculation of columns borks, resize bar is off etc ...
1893
- //textHeader.innerHTML += '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
1894
1892
  FTableDOMHelper.addClass(textHeader, 'ftable-sortable-text'); // Add class for spacing
1895
1893
  FTableDOMHelper.addClass(th, 'ftable-column-header-sortable');
1896
1894
  th.addEventListener('click', (e) => {
1897
1895
  e.preventDefault();
1898
- // Store event for multiSortingCtrlKey logic
1899
- this.lastSortEvent = e;
1900
- this.sortByColumn(fieldName);
1896
+ const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey for Mac
1897
+ this.sortByColumn(fieldName, isCtrlPressed);
1901
1898
  });
1902
1899
  }
1903
1900
 
@@ -2213,8 +2210,8 @@ class FTable extends FTableEventEmitter {
2213
2210
  this.load();
2214
2211
  }
2215
2212
 
2216
- getNextVisibleHeader(th) {
2217
- const headers = Array.from(this.elements.table.querySelectorAll('thead th:not(.ftable-command-column-header, .ftable-toolbarsearch-column-header)'));
2213
+ getNextResizableHeader(th) {
2214
+ const headers = Array.from(this.elements.table.querySelectorAll('thead th.ftable-column-header-resizable'));
2218
2215
  const index = headers.indexOf(th);
2219
2216
  for (let i = index + 1; i < headers.length; i++) {
2220
2217
  if (headers[i].offsetParent !== null) { // visible
@@ -2225,6 +2222,7 @@ class FTable extends FTableEventEmitter {
2225
2222
  }
2226
2223
 
2227
2224
  makeColumnResizable(th, container) {
2225
+ FTableDOMHelper.addClass(th, 'ftable-column-header-resizable');
2228
2226
  if (!this.elements.resizeBar) {
2229
2227
  this.elements.resizeBar = FTableDOMHelper.create('div', {
2230
2228
  className: 'ftable-column-resize-bar',
@@ -2258,13 +2256,13 @@ class FTable extends FTableEventEmitter {
2258
2256
  startWidth = th.offsetWidth;
2259
2257
 
2260
2258
  // Find next visible column
2261
- nextTh = this.getNextVisibleHeader(th);
2259
+ nextTh = this.getNextResizableHeader(th);
2262
2260
  if (nextTh) {
2263
2261
  nextStartWidth = nextTh.offsetWidth;
2264
2262
  const fieldName = nextTh.dataset.fieldName;
2265
2263
  nextField = this.options.fields[fieldName];
2266
- } else {
2267
- return;
2264
+ // } else {
2265
+ // return;
2268
2266
  }
2269
2267
 
2270
2268
  // Position resize bar
@@ -3114,7 +3112,7 @@ class FTable extends FTableEventEmitter {
3114
3112
  if (field.visibility === 'fixed') {
3115
3113
  return;
3116
3114
  }
3117
- if (field.visibility === 'hidden') {
3115
+ if (field.visibility === 'hidden' || field.visibility === 'separator') {
3118
3116
  FTableDOMHelper.hide(cell);
3119
3117
  }
3120
3118
  }
@@ -3690,7 +3688,7 @@ class FTable extends FTableEventEmitter {
3690
3688
  }
3691
3689
 
3692
3690
  // Sorting Methods
3693
- sortByColumn(fieldName) {
3691
+ sortByColumn(fieldName, isCtrlPressed = false) {
3694
3692
  const field = this.options.fields[fieldName];
3695
3693
 
3696
3694
  if (!field || field.sorting === false) return;
@@ -3711,9 +3709,6 @@ class FTable extends FTableEventEmitter {
3711
3709
  this.state.sorting.push({ fieldName, direction: newDirection });
3712
3710
  }
3713
3711
 
3714
- // Handle multiSortingCtrlKey: did user press Ctrl/Cmd?
3715
- const isCtrlPressed = this.lastSortEvent?.ctrlKey || this.lastSortEvent?.metaKey; // metaKey for Mac
3716
-
3717
3712
  if (this.options.multiSorting) {
3718
3713
  // If multiSorting is enabled, respect multiSortingCtrlKey
3719
3714
  if (this.options.multiSortingCtrlKey && !isCtrlPressed) {
@@ -4074,23 +4069,70 @@ class FTable extends FTableEventEmitter {
4074
4069
 
4075
4070
  // CSV Export functionality
4076
4071
  exportToCSV(filename = 'table-data.csv') {
4077
- const headers = this.columnList.map(fieldName => {
4078
- const field = this.options.fields[fieldName];
4079
- return field.title || fieldName;
4080
- });
4072
+ // Create a clean table clone from the DOM
4073
+ const tableClone = this.elements.table.cloneNode(true);
4074
+ const csvRows = [];
4075
+
4076
+ // Helper to format CSV cell (escape quotes, wrap in quotes)
4077
+ const formatCSV = (text) => {
4078
+ const str = String(text || '').replace(/"/g, '""');
4079
+ return `"${str}"`;
4080
+ };
4081
4081
 
4082
- const rows = this.state.records.map(record => {
4083
- return this.columnList.map(fieldName => {
4084
- const value = this.getDisplayText(record, fieldName);
4085
- // Escape CSV values
4086
- return `"${String(value).replace(/"/g, '""')}"`;
4087
- });
4088
- });
4082
+ // 1. Extract headers from visible, non-command columns
4083
+ const headerCells = tableClone.querySelectorAll('thead th');
4084
+ const headerRow = [];
4085
+ for (const th of headerCells) {
4086
+ if (th.classList.contains('ftable-command-column-header') ||
4087
+ th.classList.contains('ftable-toolbarsearch-column-header') ||
4088
+ th.style.display === 'none') {
4089
+ continue;
4090
+ }
4091
+ const text = th.textContent.trim();
4092
+ headerRow.push(formatCSV(text));
4093
+ }
4094
+ csvRows.push(headerRow.join(','));
4095
+
4096
+ // 2. Extract rows from visible, non-command cells
4097
+ const dataRows = tableClone.querySelectorAll('tbody tr');
4098
+ for (const tr of dataRows) {
4099
+ // Skip hidden rows
4100
+ if (tr.style.display === 'none') {
4101
+ continue;
4102
+ }
4103
+
4104
+ const rowCells = tr.querySelectorAll('td');
4105
+ const csvRow = [];
4106
+ let hasData = false;
4107
+
4108
+ for (const td of rowCells) {
4109
+ if (td.classList.contains('ftable-command-column') ||
4110
+ td.style.display === 'none') {
4111
+ continue;
4112
+ }
4113
+
4114
+ // Clean up: remove buttons, images
4115
+ if (td.querySelector('img, button, input, select')) {
4116
+ td.innerHTML = td.textContent; // Strip HTML
4117
+ }
4118
+
4119
+ // Replace <br> with \n
4120
+ const html = td.innerHTML;
4121
+ const withLineBreaks = html.replace(/<br\s*\/?>/gi, '\n');
4122
+ const tempDiv = document.createElement('div');
4123
+ tempDiv.innerHTML = withLineBreaks;
4124
+ const text = tempDiv.textContent || '';
4125
+
4126
+ csvRow.push(formatCSV(text));
4127
+ hasData = true;
4128
+ }
4129
+
4130
+ if (hasData) {
4131
+ csvRows.push(csvRow.join(','));
4132
+ }
4133
+ }
4089
4134
 
4090
- const csvContent = [
4091
- headers.map(h => `"${h}"`).join(','),
4092
- ...rows.map(row => row.join(','))
4093
- ].join('\n');
4135
+ const csvContent = csvRows.join('\n');
4094
4136
 
4095
4137
  // Create and trigger download
4096
4138
  const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });