@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.
- package/ftable.esm.js +76 -34
- package/ftable.js +76 -34
- package/ftable.min.js +2 -2
- package/ftable.umd.js +76 -34
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +42 -21
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +57 -22
- package/themes/lightcolor/blue/ftable.css +42 -21
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +42 -21
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +42 -21
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +42 -21
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +42 -21
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +42 -21
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +42 -21
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +42 -21
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +42 -21
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +42 -21
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +42 -21
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +42 -21
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +42 -21
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +42 -21
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +42 -21
- 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
|
-
//
|
|
1894
|
-
this.
|
|
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
|
-
|
|
2212
|
-
const headers = Array.from(this.elements.table.querySelectorAll('thead th
|
|
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.
|
|
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
|
-
|
|
2262
|
-
|
|
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
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
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
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
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 += ' ';
|
|
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
|
-
//
|
|
1899
|
-
this.
|
|
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
|
-
|
|
2217
|
-
const headers = Array.from(this.elements.table.querySelectorAll('thead th
|
|
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.
|
|
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
|
-
|
|
2267
|
-
|
|
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
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
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
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
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;' });
|