handsontable 0.0.0-next-b96fb9f-20231207 → 0.0.0-next-3d099da-20231208

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of handsontable might be problematic. Click here for more details.

Files changed (122) hide show
  1. package/3rdparty/walkontable/src/calculator/index.js +6 -11
  2. package/3rdparty/walkontable/src/calculator/index.mjs +3 -5
  3. package/3rdparty/walkontable/src/calculator/viewportColumns.js +122 -1
  4. package/3rdparty/walkontable/src/calculator/viewportColumns.mjs +124 -2
  5. package/3rdparty/walkontable/src/calculator/viewportRows.js +1 -1
  6. package/3rdparty/walkontable/src/calculator/viewportRows.mjs +3 -2
  7. package/3rdparty/walkontable/src/core/_base.js +12 -0
  8. package/3rdparty/walkontable/src/core/_base.mjs +12 -0
  9. package/3rdparty/walkontable/src/core/core.js +2 -0
  10. package/3rdparty/walkontable/src/core/core.mjs +2 -0
  11. package/3rdparty/walkontable/src/facade/core.js +9 -0
  12. package/3rdparty/walkontable/src/facade/core.mjs +9 -0
  13. package/3rdparty/walkontable/src/index.js +4 -3
  14. package/3rdparty/walkontable/src/index.mjs +2 -1
  15. package/3rdparty/walkontable/src/overlays.js +3 -0
  16. package/3rdparty/walkontable/src/overlays.mjs +4 -0
  17. package/3rdparty/walkontable/src/renderer/colGroup.js +0 -10
  18. package/3rdparty/walkontable/src/renderer/colGroup.mjs +0 -10
  19. package/3rdparty/walkontable/src/renderer/rows.js +3 -4
  20. package/3rdparty/walkontable/src/renderer/rows.mjs +3 -4
  21. package/3rdparty/walkontable/src/selection/manager.js +1 -0
  22. package/3rdparty/walkontable/src/selection/manager.mjs +1 -0
  23. package/3rdparty/walkontable/src/settings.js +0 -3
  24. package/3rdparty/walkontable/src/settings.mjs +0 -2
  25. package/3rdparty/walkontable/src/table.js +1 -0
  26. package/3rdparty/walkontable/src/table.mjs +1 -0
  27. package/3rdparty/walkontable/src/utils/column.js +12 -27
  28. package/3rdparty/walkontable/src/utils/column.mjs +12 -27
  29. package/3rdparty/walkontable/src/viewport.js +17 -22
  30. package/3rdparty/walkontable/src/viewport.mjs +18 -23
  31. package/base.js +2 -4
  32. package/base.mjs +2 -2
  33. package/core/focusCatcher/index.js +6 -44
  34. package/core/focusCatcher/index.mjs +6 -44
  35. package/core.js +11 -0
  36. package/core.mjs +11 -0
  37. package/dataMap/dataMap.js +0 -1
  38. package/dataMap/metaManager/metaSchema.js +2 -28
  39. package/dataMap/metaManager/metaSchema.mjs +2 -28
  40. package/dataMap/metaManager/mods/extendMetaProperties.js +0 -12
  41. package/dataMap/metaManager/mods/extendMetaProperties.mjs +0 -12
  42. package/dist/handsontable.css +2 -2
  43. package/dist/handsontable.full.css +2 -2
  44. package/dist/handsontable.full.js +3903 -2640
  45. package/dist/handsontable.full.min.css +2 -2
  46. package/dist/handsontable.full.min.js +70 -66
  47. package/dist/handsontable.js +3906 -2643
  48. package/dist/handsontable.min.css +2 -2
  49. package/dist/handsontable.min.js +39 -35
  50. package/editorManager.js +4 -3
  51. package/editorManager.mjs +4 -3
  52. package/editors/autocompleteEditor/autocompleteEditor.js +2 -0
  53. package/editors/autocompleteEditor/autocompleteEditor.mjs +2 -0
  54. package/editors/handsontableEditor/handsontableEditor.js +1 -0
  55. package/editors/handsontableEditor/handsontableEditor.mjs +1 -0
  56. package/editors/textEditor/textEditor.js +4 -0
  57. package/editors/textEditor/textEditor.mjs +4 -0
  58. package/helpers/mixed.js +1 -1
  59. package/helpers/mixed.mjs +1 -1
  60. package/package.json +1 -1
  61. package/pluginHooks.d.ts +29 -6
  62. package/pluginHooks.js +123 -65
  63. package/pluginHooks.mjs +122 -62
  64. package/plugins/copyPaste/clipboardData/clipboardData.js +588 -0
  65. package/plugins/copyPaste/clipboardData/clipboardData.mjs +584 -0
  66. package/plugins/copyPaste/clipboardData/copyClipboardData.js +69 -0
  67. package/plugins/copyPaste/clipboardData/copyClipboardData.mjs +65 -0
  68. package/plugins/copyPaste/clipboardData/index.js +9 -0
  69. package/plugins/copyPaste/clipboardData/index.mjs +4 -0
  70. package/plugins/copyPaste/clipboardData/pasteClipboardData.js +81 -0
  71. package/plugins/copyPaste/clipboardData/pasteClipboardData.mjs +77 -0
  72. package/plugins/copyPaste/copyPaste.js +51 -129
  73. package/plugins/copyPaste/copyPaste.mjs +54 -132
  74. package/plugins/copyPaste/copyableRanges.js +7 -43
  75. package/plugins/copyPaste/copyableRanges.mjs +7 -42
  76. package/plugins/copyPaste/pasteEvent.mjs +1 -1
  77. package/plugins/customBorders/customBorders.js +5 -0
  78. package/plugins/customBorders/customBorders.mjs +5 -0
  79. package/plugins/customBorders/utils.js +1 -0
  80. package/plugins/customBorders/utils.mjs +1 -0
  81. package/plugins/filters/ui/radioInput.js +1 -1
  82. package/plugins/filters/ui/radioInput.mjs +1 -1
  83. package/plugins/formulas/formulas.js +2 -0
  84. package/plugins/formulas/formulas.mjs +2 -0
  85. package/plugins/formulas/indexSyncer/axisSyncer.js +1 -0
  86. package/plugins/formulas/indexSyncer/axisSyncer.mjs +1 -0
  87. package/plugins/manualColumnResize/manualColumnResize.js +1 -0
  88. package/plugins/manualColumnResize/manualColumnResize.mjs +1 -0
  89. package/plugins/mergeCells/mergeCells.js +127 -1
  90. package/plugins/mergeCells/mergeCells.mjs +127 -1
  91. package/plugins/nestedHeaders/nestedHeaders.js +87 -41
  92. package/plugins/nestedHeaders/nestedHeaders.mjs +88 -42
  93. package/plugins/nestedHeaders/stateManager/headersTree.js +1 -0
  94. package/plugins/nestedHeaders/stateManager/headersTree.mjs +1 -0
  95. package/plugins/undoRedo/undoRedo.js +2 -0
  96. package/plugins/undoRedo/undoRedo.mjs +2 -0
  97. package/renderers/autocompleteRenderer/autocompleteRenderer.js +1 -0
  98. package/renderers/autocompleteRenderer/autocompleteRenderer.mjs +1 -0
  99. package/renderers/checkboxRenderer/checkboxRenderer.js +2 -0
  100. package/renderers/checkboxRenderer/checkboxRenderer.mjs +2 -0
  101. package/selection/highlight/highlight.js +1 -0
  102. package/selection/highlight/highlight.mjs +1 -0
  103. package/selection/index.js +3 -1
  104. package/selection/index.mjs +2 -2
  105. package/selection/utils.js +34 -0
  106. package/selection/utils.mjs +33 -0
  107. package/settings.d.ts +0 -1
  108. package/tableView.js +2 -1
  109. package/tableView.mjs +2 -1
  110. package/translations/indexMapper.js +1 -2
  111. package/utils/parseTable.js +538 -84
  112. package/utils/parseTable.mjs +534 -83
  113. package/validators/timeValidator/timeValidator.js +1 -0
  114. package/validators/timeValidator/timeValidator.mjs +1 -0
  115. package/3rdparty/walkontable/src/calculator/renderAllColumns.js +0 -50
  116. package/3rdparty/walkontable/src/calculator/renderAllColumns.mjs +0 -46
  117. package/3rdparty/walkontable/src/calculator/renderAllRows.js +0 -50
  118. package/3rdparty/walkontable/src/calculator/renderAllRows.mjs +0 -46
  119. package/3rdparty/walkontable/src/utils/columnStretching.js +0 -219
  120. package/3rdparty/walkontable/src/utils/columnStretching.mjs +0 -215
  121. package/plugins/copyPaste/clipboardData.js +0 -18
  122. package/plugins/copyPaste/clipboardData.mjs +0 -14
@@ -1,5 +1,8 @@
1
1
  import "core-js/modules/es.array.push.js";
2
+ import "core-js/modules/es.string.replace-all.js";
2
3
  import { isEmpty } from "./../helpers/mixed.mjs";
4
+ import { isObject } from "./../helpers/object.mjs";
5
+ import { rangeEach } from "../helpers/number.mjs";
3
6
  const ESCAPED_HTML_CHARS = {
4
7
  ' ': '\x20',
5
8
  '&': '&',
@@ -18,102 +21,549 @@ function isHTMLTable(element) {
18
21
  return (element && element.nodeName || '') === 'TABLE';
19
22
  }
20
23
 
24
+ /**
25
+ * Parses empty values to an empty string or leave them untouched otherwise.
26
+ *
27
+ * @private
28
+ * @param {string} cellValue Parsed cell value.
29
+ * @returns {string}
30
+ */
31
+ function parseEmptyValues(cellValue) {
32
+ if (isEmpty(cellValue)) {
33
+ return '';
34
+ }
35
+ return cellValue;
36
+ }
37
+
21
38
  /**
22
39
  * Converts Handsontable into HTMLTableElement.
23
40
  *
24
- * @param {Core} instance The Handsontable instance.
41
+ * @param {Core} hotInstance The Handsontable instance.
25
42
  * @returns {string} OuterHTML of the HTMLTableElement.
26
43
  */
27
- export function instanceToHTML(instance) {
28
- const hasColumnHeaders = instance.hasColHeaders();
29
- const hasRowHeaders = instance.hasRowHeaders();
30
- const coords = [hasColumnHeaders ? -1 : 0, hasRowHeaders ? -1 : 0, instance.countRows() - 1, instance.countCols() - 1];
31
- const data = instance.getData(...coords);
32
- const countRows = data.length;
33
- const countCols = countRows > 0 ? data[0].length : 0;
34
- const TABLE = ['<table>', '</table>'];
35
- const THEAD = hasColumnHeaders ? ['<thead>', '</thead>'] : [];
36
- const TBODY = ['<tbody>', '</tbody>'];
37
- const rowModifier = hasRowHeaders ? 1 : 0;
38
- const columnModifier = hasColumnHeaders ? 1 : 0;
39
- for (let row = 0; row < countRows; row += 1) {
40
- const isColumnHeadersRow = hasColumnHeaders && row === 0;
41
- const CELLS = [];
42
- for (let column = 0; column < countCols; column += 1) {
43
- const isRowHeadersColumn = !isColumnHeadersRow && hasRowHeaders && column === 0;
44
- let cell = '';
45
- if (isColumnHeadersRow) {
46
- cell = `<th>${instance.getColHeader(column - rowModifier)}</th>`;
47
- } else if (isRowHeadersColumn) {
48
- cell = `<th>${instance.getRowHeader(row - columnModifier)}</th>`;
49
- } else {
50
- const cellData = data[row][column];
44
+ export function instanceToHTML(hotInstance) {
45
+ const startColumn = hotInstance.hasRowHeaders() ? -1 : 0;
46
+ const startRow = hotInstance.hasColHeaders() ? -1 : 0;
47
+ const rows = Array.from({
48
+ length: hotInstance.countRows() + Math.abs(startRow)
49
+ }, (_, i) => i + startRow);
50
+ const columns = Array.from({
51
+ length: hotInstance.countCols() + Math.abs(startColumn)
52
+ }, (_, i) => i + startColumn);
53
+ return getHTMLByCoords(hotInstance, {
54
+ rows,
55
+ columns
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Converts Handsontable's coordinates into HTMLTableElement.
61
+ *
62
+ * @param {Core} hotInstance The Handsontable instance.
63
+ * @param {object} config Configuration for building HTMLTableElement.
64
+ * @param {Array<number>} config.rows List of row indexes which should be taken into account when creating the table.
65
+ * @param {Array<number>} config.columns List of column indexes which should be taken into account when creating the table.
66
+ * @returns {string} OuterHTML of the HTMLTableElement.
67
+ */
68
+ export function getHTMLByCoords(hotInstance, config) {
69
+ return ['<table>',
70
+ // Needed for desktop Excel on MacOS while pasting any elements with rowspan/colspan.
71
+ '<!--StartFragment-->', ...getHeadersHTMLByCoords(hotInstance, config), ...getBodyHTMLByCoords(hotInstance, config),
72
+ // Needed for desktop Excel on MacOS while pasting any elements with rowspan/colspan.
73
+ '<!--EndFragment-->', '</table>'].join('');
74
+ }
75
+
76
+ /**
77
+ * Converts Handsontable's coordinates into list of cell values.
78
+ *
79
+ * @param {Core} hotInstance The Handsontable instance.
80
+ * @param {object} config Configuration for building the cell value list.
81
+ * @param {Array<number>} config.rows List of row indexes which should be taken into account when creating the
82
+ * cell value list.
83
+ * @param {Array<number>} config.columns List of column indexes which should be taken into account when creating the
84
+ * cell value list.
85
+ * @returns {Array<Array<string>>} List of displayed cell values.
86
+ */
87
+ export function getDataByCoords(hotInstance, config) {
88
+ return [...getHeadersDataByCoords(hotInstance, config), ...getBodyDataByCoords(hotInstance, config)];
89
+ }
90
+
91
+ /**
92
+ * Converts config into HTMLTableElement.
93
+ *
94
+ * @param {object} config Configuration for building HTMLTableElement.
95
+ * @param {Array<number>} [config.excludedRows] List of row indexes which should be excluded when creating the table.
96
+ * @param {Array<number>} [config.excludedColumns] List of column indexes which should be excluded when creating the table.
97
+ * @param {Array<Array<string>>} [config.data] List of cell data.
98
+ * @param {Array<object>} [config.mergeCells] List of merged cells.
99
+ * @param {Array<Array<string|object>>} [config.nestedHeaders] List of headers and corresponding information about some
100
+ * nested elements.
101
+ * @param {Array<string>} [config.colHeaders] List of first level header values.
102
+ * @returns {string} OuterHTML of the HTMLTableElement.
103
+ */
104
+ export function getHTMLFromConfig(config) {
105
+ return ['<table>',
106
+ // Needed for desktop Excel on MacOS while pasting any elements with rowspan/colspan.
107
+ '<!--StartFragment-->', ...getHeadersHTMLByConfig(config), ...getBodyHTMLByConfig(config),
108
+ // Needed for desktop Excel on MacOS while pasting any elements with rowspan/colspan.
109
+ '<!--EndFragment-->', '</table>'].join('');
110
+ }
111
+
112
+ /**
113
+ * Get list of filtered nested headers.
114
+ *
115
+ * @param {Array<Array<string|object>>} nestedHeaders List of nested headers which will be filtered.
116
+ * @param {Array<number>} excludedHeaders List of headers which should be excluded when creating the HTMLTableElement.tHead.
117
+ * @param {Array<number>} excludedColumns List of column indexes which should be excluded when creating the HTMLTableElement.tHead.
118
+ * @returns {*}
119
+ */
120
+ function getFilteredNestedHeaders(nestedHeaders, excludedHeaders, excludedColumns) {
121
+ return nestedHeaders.reduce((listOfHeaders, headerValues, rowIndex) => {
122
+ if (excludedHeaders.includes(rowIndex - nestedHeaders.length)) {
123
+ return listOfHeaders;
124
+ }
125
+ const filteredNestedHeader = headerValues.filter((columnData, columnIndex) => excludedColumns.includes(columnIndex) === false);
126
+ if (filteredNestedHeader.length > 0) {
127
+ return listOfHeaders.concat([filteredNestedHeader]);
128
+ }
129
+ return listOfHeaders;
130
+ }, []);
131
+ }
132
+
133
+ /**
134
+ * Get HTML for nested headers.
135
+ *
136
+ * @param {Array<Array<string|object>>} nestedHeaders List of nested headers which will be filtered.
137
+ * @param {Array<number>} excludedHeaders List of headers which should be excluded when creating the HTMLTableElement.tHead.
138
+ * @param {Array<number>} excludedColumns List of column indexes which should be excluded when creating the HTMLTableElement.tHead.
139
+ * @returns {Array<string>}
140
+ */
141
+ function getNestedHeadersHTML(nestedHeaders, excludedHeaders, excludedColumns) {
142
+ const headersHTML = [];
143
+ getFilteredNestedHeaders(nestedHeaders, excludedHeaders, excludedColumns).forEach(listOfHeaders => {
144
+ const rowHTML = ['<tr>'];
145
+ for (let i = 0; i < listOfHeaders.length; i += 1) {
146
+ const header = listOfHeaders[i];
147
+ let headerValue = header;
148
+ let colspanAttribute = '';
149
+ if (isObject(header)) {
150
+ const {
151
+ colspan,
152
+ label
153
+ } = header;
154
+ headerValue = label;
155
+ colspanAttribute = ` colspan=${colspan}`;
156
+ }
157
+ rowHTML.push(`<th${colspanAttribute}>${encodeHTMLEntities(parseEmptyValues(headerValue))}</th>`);
158
+ }
159
+ rowHTML.push('</tr>');
160
+ headersHTML.push(...rowHTML);
161
+ });
162
+ return headersHTML;
163
+ }
164
+
165
+ /**
166
+ * Get HTML for first level header.
167
+ *
168
+ * @param {Array<string>} columnHeaders List of header values which will be filtered.
169
+ * @param {Array<number>} excludedHeaders List of headers which should be excluded when creating the HTMLTableElement.tHead.
170
+ * @param {Array<number>} excludedColumns List of column indexes which should be excluded when creating the HTMLTableElement.tHead.
171
+ * @returns {*[]}
172
+ */
173
+ function getSimpleHeadersHTML(columnHeaders, excludedHeaders, excludedColumns) {
174
+ if (excludedHeaders.includes(-1)) {
175
+ return [];
176
+ }
177
+ const filteredColumnHeaders = columnHeaders.filter((columnHeaderValue, columnIndex) => excludedColumns.includes(columnIndex) === false);
178
+ if (filteredColumnHeaders.length === 0) {
179
+ return [];
180
+ }
181
+ return ['<tr>', ...filteredColumnHeaders.map(columnHeader => `<th>${encodeHTMLEntities(parseEmptyValues(columnHeader))}</th>`), '</tr>'];
182
+ }
183
+
184
+ /**
185
+ * Get list of cells filtered by list of excluded rows and columns.
186
+ *
187
+ * @private
188
+ * @param {Array<Array<string>>} data List of cells values which will be filtered.
189
+ * @param {Array<number>} excludedRows List of row indexes which should be excluded when creating the HTMLTableElement.tHead.
190
+ * @param {Array<number>} excludedColumns List of column indexes which should be excluded when creating the HTMLTableElement.tHead.
191
+ * @returns {Array<string>} List of cell values.
192
+ */
193
+ function getFilteredCells(data, excludedRows, excludedColumns) {
194
+ if (Array.isArray(data) === false) {
195
+ return [];
196
+ }
197
+ return data.reduce((listOfCells, rowData, rowIndex) => {
198
+ if (excludedRows.includes(rowIndex)) {
199
+ return listOfCells;
200
+ }
201
+ const filteredRowData = rowData.filter((cellData, columnIndex) => excludedColumns.includes(columnIndex) === false);
202
+ if (filteredRowData.length > 0) {
203
+ return listOfCells.concat([filteredRowData]);
204
+ }
205
+ return listOfCells;
206
+ }, []);
207
+ }
208
+
209
+ /**
210
+ * Prepare information about merged areas to reduce complexity of calculations.
211
+ *
212
+ * @private
213
+ * @param {Array<object>} mergedCellsConfig List of merged cells.
214
+ * @returns {{mergedCellsMap: Map<any, any>, mergedArea: Set<any>}}
215
+ */
216
+ function getMergedCellsInformation(mergedCellsConfig) {
217
+ const mergedCellsMap = new Map();
218
+ const mergedArea = new Set();
219
+ let mergedRows = 1;
220
+ let mergedColumns = 1;
221
+ mergedCellsConfig === null || mergedCellsConfig === void 0 || mergedCellsConfig.forEach(mergeArea => {
222
+ const {
223
+ row,
224
+ col,
225
+ rowspan,
226
+ colspan
227
+ } = mergeArea;
228
+ mergedCellsMap.set(`${row}x${col}`, {
229
+ rowspan,
230
+ colspan
231
+ });
232
+ if (Number.isInteger(rowspan)) {
233
+ mergedRows = rowspan;
234
+ }
235
+ if (Number.isInteger(colspan)) {
236
+ mergedColumns = colspan;
237
+ }
238
+ rangeEach(row, row + mergedRows - 1, rowIndex => {
239
+ rangeEach(col, col + mergedColumns - 1, columnIndex => {
240
+ // Other than start point.
241
+ if (rowIndex !== row || columnIndex !== col) {
242
+ mergedArea.add(`${rowIndex}x${columnIndex}`);
243
+ }
244
+ });
245
+ });
246
+ });
247
+ return {
248
+ mergedCellsMap,
249
+ mergedArea
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Converts config with information about cells into HTMLTableElement.tBodies.
255
+ *
256
+ * @private
257
+ * @param {object} config Configuration for building HTMLTableElement.tBodies.
258
+ * @param {Array<Array<string>>} config.data List of cell data.
259
+ * @param {Array<number>} [config.excludedRows] List of row indexes which should be excluded when creating the HTMLTableElement.tBodies.
260
+ * @param {Array<number>} [config.excludedColumns] List of column indexes which should be excluded when creating the HTMLTableElement.tBodies.
261
+ * @param {Array<object>} [config.mergeCells] List of merged cells.
262
+ * @returns {Array<string>} List of HTMLElements stored as strings.
263
+ */
264
+ function getBodyHTMLByConfig(config) {
265
+ const excludedColumns = config.excludedColumns || [];
266
+ const excludedRows = config.excludedRows || [];
267
+ const {
268
+ data,
269
+ mergeCells
270
+ } = config;
271
+ const ignoredCellRows = excludedRows.filter(rowIndex => rowIndex >= 0);
272
+ const filteredData = getFilteredCells(data, ignoredCellRows, excludedColumns);
273
+ const cells = [];
274
+ if (filteredData.length === 0) {
275
+ return [];
276
+ }
277
+ const {
278
+ mergedCellsMap,
279
+ mergedArea
280
+ } = getMergedCellsInformation(mergeCells);
281
+ filteredData.forEach((rowData, rowIndex) => {
282
+ const rowHTML = ['<tr>'];
283
+ rowData.forEach((cellData, columnIndex) => {
284
+ const attrs = [];
285
+ const checkedMergeCoordinate = `${rowIndex}x${columnIndex}`;
286
+ const mergeParent = mergedCellsMap.get(checkedMergeCoordinate);
287
+ if (mergeParent !== undefined) {
51
288
  const {
52
- hidden,
53
289
  rowspan,
54
290
  colspan
55
- } = instance.getCellMeta(row - columnModifier, column - rowModifier);
56
- if (!hidden) {
57
- const attrs = [];
58
- if (rowspan) {
59
- attrs.push(`rowspan="${rowspan}"`);
60
- }
61
- if (colspan) {
62
- attrs.push(`colspan="${colspan}"`);
63
- }
64
- if (isEmpty(cellData)) {
65
- cell = `<td ${attrs.join(' ')}></td>`;
66
- } else {
67
- const value = cellData.toString().replace('<', '&lt;').replace('>', '&gt;').replace(/(<br(\s*|\/)>(\r\n|\n)?|\r\n|\n)/g, '<br>\r\n').replace(/\x20/gi, '&nbsp;').replace(/\t/gi, '&#9;');
68
- cell = `<td ${attrs.join(' ')}>${value}</td>`;
69
- }
291
+ } = mergeParent;
292
+ if (Number.isInteger(rowspan) && rowspan > 1) {
293
+ attrs.push(` rowspan="${rowspan}"`);
294
+ }
295
+ if (Number.isInteger(colspan) && colspan > 1) {
296
+ attrs.push(` colspan="${colspan}"`);
70
297
  }
298
+ } else if (mergedArea.has(checkedMergeCoordinate)) {
299
+ return;
71
300
  }
72
- CELLS.push(cell);
73
- }
74
- const TR = ['<tr>', ...CELLS, '</tr>'].join('');
75
- if (isColumnHeadersRow) {
76
- THEAD.splice(1, 0, TR);
77
- } else {
78
- TBODY.splice(-1, 0, TR);
79
- }
301
+ rowHTML.push(`<td${attrs.join('')}>${encodeHTMLEntities(parseEmptyValues(cellData))}</td>`);
302
+ });
303
+ rowHTML.push('</tr>');
304
+ cells.push(...rowHTML);
305
+ });
306
+ return ['<tbody>', ...cells, '</tbody>'];
307
+ }
308
+
309
+ /**
310
+ * Converts config with information about headers into HTMLTableElement.tHead.
311
+ *
312
+ * @private
313
+ * @param {object} config Configuration for building HTMLTableElement.tHead.
314
+ * @param {Array<Array<string|object>>} [config.nestedHeaders] List of headers and corresponding information about some
315
+ * nested elements.
316
+ * @param {Array<string>} [config.colHeaders] List of first level header values.
317
+ * @param {Array<number>} [config.excludedRows] List of row indexes which should be excluded when creating the HTMLTableElement.tHead.
318
+ * @param {Array<number>} [config.excludedColumns] List of column indexes which should be excluded when creating the HTMLTableElement.tHead.
319
+ * @returns {Array<string>} List of HTMLElements stored as strings.
320
+ */
321
+ function getHeadersHTMLByConfig(config) {
322
+ const headersHTML = [];
323
+ const excludedColumns = Array.isArray(config === null || config === void 0 ? void 0 : config.excludedColumns) ? config.excludedColumns : [];
324
+ const excludedRows = Array.isArray(config === null || config === void 0 ? void 0 : config.excludedRows) ? config.excludedRows : [];
325
+ const {
326
+ nestedHeaders,
327
+ colHeaders
328
+ } = config;
329
+ const excludedHeaders = excludedRows.filter(rowIndex => rowIndex < 0);
330
+ if (Array.isArray(nestedHeaders)) {
331
+ headersHTML.push(...getNestedHeadersHTML(nestedHeaders, excludedHeaders, excludedColumns));
332
+ } else if (Array.isArray(colHeaders)) {
333
+ headersHTML.push(...getSimpleHeadersHTML(colHeaders, excludedHeaders, excludedColumns));
80
334
  }
81
- TABLE.splice(1, 0, THEAD.join(''), TBODY.join(''));
82
- return TABLE.join('');
335
+ if (headersHTML.length > 0) {
336
+ return ['<thead>', ...headersHTML, '</thead>'];
337
+ }
338
+ return [];
83
339
  }
84
340
 
85
341
  /**
86
- * Converts 2D array into HTMLTableElement.
342
+ * Converts config with information about cells and headers into list of values.
87
343
  *
88
- * @param {Array} input Input array which will be converted to HTMLTable.
89
- * @returns {string} OuterHTML of the HTMLTableElement.
344
+ * @param {object} config Configuration for building list of values.
345
+ * @param {Array<number>} [config.excludedRows] List of row indexes which should be excluded when creating the value list.
346
+ * @param {Array<number>} [config.excludedColumns] List of column indexes which should be excluded when creating the value list.
347
+ * @param {Array<Array<string|object>>} [config.nestedHeaders] List of headers and information about some nested elements.
348
+ * @param {Array<string>} [config.colHeaders] List of first level header values.
349
+ * @returns {string[][]} List of values.
90
350
  */
91
- // eslint-disable-next-line no-restricted-globals
92
- export function _dataToHTML(input) {
93
- const inputLen = input.length;
94
- const result = ['<table>'];
95
- for (let row = 0; row < inputLen; row += 1) {
96
- const rowData = input[row];
97
- const columnsLen = rowData.length;
98
- const columnsResult = [];
99
- if (row === 0) {
100
- result.push('<tbody>');
101
- }
102
- for (let column = 0; column < columnsLen; column += 1) {
103
- const cellData = rowData[column];
104
- const parsedCellData = isEmpty(cellData) ? '' : cellData.toString().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/(<br(\s*|\/)>(\r\n|\n)?|\r\n|\n)/g, '<br>\r\n').replace(/\x20{2,}/gi, substring => {
105
- // The way how Excel serializes data with at least two spaces.
106
- return `<span style="mso-spacerun: yes">${'&nbsp;'.repeat(substring.length - 1)} </span>`;
107
- }).replace(/\t/gi, '&#9;');
108
- columnsResult.push(`<td>${parsedCellData}</td>`);
351
+ export function getDataWithHeadersByConfig(config) {
352
+ const dataWithHeaders = [];
353
+ const excludedColumns = Array.isArray(config === null || config === void 0 ? void 0 : config.excludedColumns) ? config.excludedColumns : [];
354
+ const excludedRows = Array.isArray(config === null || config === void 0 ? void 0 : config.excludedRows) ? config.excludedRows : [];
355
+ const {
356
+ data,
357
+ nestedHeaders,
358
+ colHeaders
359
+ } = config;
360
+ const excludedHeaders = excludedRows.filter(rowIndex => rowIndex < 0);
361
+ if (Array.isArray(nestedHeaders)) {
362
+ dataWithHeaders.push(...getFilteredNestedHeaders(nestedHeaders, excludedHeaders, excludedColumns).map(listOfHeaders => {
363
+ return listOfHeaders.reduce((headers, header) => {
364
+ if (isObject(header)) {
365
+ headers.push(header.label, ...new Array(header.colspan - 1).fill(''));
366
+ } else {
367
+ headers.push(header);
368
+ }
369
+ return headers;
370
+ }, []);
371
+ }));
372
+ } else if (Array.isArray(colHeaders)) {
373
+ dataWithHeaders.push([...colHeaders.filter((columnHeaderData, columnIndex) => excludedColumns.includes(columnIndex) === false)]);
374
+ }
375
+ dataWithHeaders.push(...getFilteredCells(data, excludedRows.filter(rowIndex => rowIndex >= 0), excludedColumns.filter(columnIndex => columnIndex >= 0)));
376
+ return dataWithHeaders;
377
+ }
378
+
379
+ /**
380
+ * Encode text to HTML.
381
+ *
382
+ * @param {string} text Text to prepare.
383
+ * @returns {string}
384
+ */
385
+ function encodeHTMLEntities(text) {
386
+ return `${text}`.replace(/&/g, '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace(/(<br(\s*|\/)>(\r\n|\n)?|\r\n|\n)/g, '<br>\r\n').replace(/\x20{2,}/gi, substring => {
387
+ // The way how Excel serializes data with at least two spaces.
388
+ return `<span style="mso-spacerun: yes">${'&nbsp;'.repeat(substring.length - 1)} </span>`;
389
+ }).replace(/\t/gi, '&#9;');
390
+ }
391
+
392
+ /**
393
+ * Decode HTML to simple text.
394
+ *
395
+ * @param {string} html HTML for handling.
396
+ * @returns {string}
397
+ */
398
+ function decodeHTMLEntities(html) {
399
+ return html.replace(regEscapedChars, match => ESCAPED_HTML_CHARS[match])
400
+ // The way how Excel serializes data with at least two spaces.
401
+ .replace(/<span style="mso-spacerun: yes">(.+?)<\/span>/, '$1').replaceAll('&nbsp;', ' ');
402
+ }
403
+
404
+ /**
405
+ * Converts Handsontable's header coordinates into HTMLTableElement.tHead.
406
+ *
407
+ * @param {Core} hotInstance The Handsontable instance.
408
+ * @param {object} config Configuration for building HTMLTableElement.tHead.
409
+ * @param {Array<number>} config.rows List of row indexes which should be taken into account when creating
410
+ * the HTMLTableElement.tHead.
411
+ * @param {Array<number>} config.columns List of column indexes which should be taken into account when creating
412
+ * the HTMLTableElement.tHead.
413
+ * @returns {Array<string>} List of HTMLElements stored as strings.
414
+ */
415
+ function getHeadersHTMLByCoords(hotInstance, config) {
416
+ const {
417
+ rows,
418
+ columns
419
+ } = config;
420
+ const headers = rows.filter(rowIndex => rowIndex < 0);
421
+ const headersHTML = [];
422
+ if (headers.length === 0 || columns.length === 0) {
423
+ return [];
424
+ }
425
+ headers.forEach(rowIndex => {
426
+ const rowHTML = ['<tr>'];
427
+ for (let i = 0; i < columns.length; i += 1) {
428
+ const columnIndex = columns[i];
429
+ const headerCell = hotInstance.getCell(rowIndex, columnIndex);
430
+ const colspan = headerCell === null || headerCell === void 0 ? void 0 : headerCell.getAttribute('colspan');
431
+ let colspanAttribute = '';
432
+ if (colspan) {
433
+ const parsedColspan = parseInt(colspan, 10);
434
+ const colspanReduced = Math.min(parsedColspan, columns.length - i);
435
+ colspanAttribute = ` colspan=${colspanReduced}`;
436
+ i += colspanReduced - 1;
437
+ }
438
+ rowHTML.push(`<th${colspanAttribute}>${encodeHTMLEntities(parseEmptyValues(hotInstance.getColHeader(columnIndex, rowIndex)))}</th>`);
109
439
  }
110
- result.push('<tr>', ...columnsResult, '</tr>');
111
- if (row + 1 === inputLen) {
112
- result.push('</tbody>');
440
+ rowHTML.push('</tr>');
441
+ headersHTML.push(...rowHTML);
442
+ });
443
+ return ['<thead>', ...headersHTML, '</thead>'];
444
+ }
445
+
446
+ /**
447
+ * Converts Handsontable's coordinates into list of values for cells being headers.
448
+ *
449
+ * @param {Core} hotInstance The Handsontable instance.
450
+ * @param {object} config Configuration for building the cell value list.
451
+ * @param {Array<number>} config.rows List of row indexes which should be taken into account when creating the
452
+ * cell value list.
453
+ * @param {Array<number>} config.columns List of column indexes which should be taken into account when creating the
454
+ * cell value list.
455
+ * @returns {Array[]} List of displayed cell values.
456
+ */
457
+ function getHeadersDataByCoords(hotInstance, config) {
458
+ const headersData = [];
459
+ const {
460
+ columns,
461
+ rows
462
+ } = config;
463
+ const headers = rows.filter(rowIndex => rowIndex < 0);
464
+ headers.forEach(rowIndex => {
465
+ const rowData = [];
466
+ for (let i = 0; i < columns.length; i += 1) {
467
+ const columnIndex = columns[i];
468
+ const headerCell = hotInstance.getCell(rowIndex, columnIndex);
469
+ const colspan = headerCell === null || headerCell === void 0 ? void 0 : headerCell.getAttribute('colspan');
470
+ rowData.push(hotInstance.getColHeader(columnIndex, rowIndex));
471
+ if (colspan) {
472
+ const parsedColspan = parseInt(colspan, 10);
473
+ const colspanReduced = Math.min(parsedColspan, columns.length - i);
474
+ rowData.push(...new Array(colspanReduced - 1).fill(''));
475
+ i += colspanReduced - 1;
476
+ }
113
477
  }
478
+ headersData.push(rowData);
479
+ });
480
+ return headersData;
481
+ }
482
+
483
+ /**
484
+ * Converts Handsontable's header coordinates into HTMLTableElement.tBodies.
485
+ *
486
+ * @param {Core} hotInstance The Handsontable instance.
487
+ * @param {object} config Configuration for building HTMLTableElement.
488
+ * @param {Array<number>} config.rows List of row indexes which should be taken into account when creating the table.
489
+ * @param {Array<number>} config.columns List of column indexes which should be taken into account when creating the table.
490
+ * @returns {Array<string>} List of HTMLElements stored as strings.
491
+ */
492
+ function getBodyHTMLByCoords(hotInstance, config) {
493
+ const {
494
+ columns,
495
+ rows
496
+ } = config;
497
+ const bodyRows = rows.filter(rowIndex => rowIndex >= 0);
498
+ const cells = [];
499
+ if (bodyRows.length === 0 || columns.length === 0) {
500
+ return [];
114
501
  }
115
- result.push('</table>');
116
- return result.join('');
502
+ bodyRows.forEach((rowIndex, nthRow) => {
503
+ const rowHTML = ['<tr>'];
504
+ columns.forEach((columnIndex, nthColumn) => {
505
+ if (columnIndex < 0) {
506
+ rowHTML.push(`<th>${encodeHTMLEntities(parseEmptyValues(hotInstance.getRowHeader(rowIndex)))}</th>`);
507
+ return;
508
+ }
509
+ const cellValue = hotInstance.getCopyableData(rowIndex, columnIndex);
510
+ const cellValueParsed = encodeHTMLEntities(parseEmptyValues(cellValue));
511
+ const {
512
+ hidden,
513
+ rowspan,
514
+ colspan
515
+ } = hotInstance.getCellMeta(rowIndex, columnIndex);
516
+ if (!hidden) {
517
+ const attrs = [];
518
+ if (rowspan) {
519
+ const recalculatedRowSpan = Math.min(rowspan, bodyRows.slice(nthRow).length);
520
+ if (recalculatedRowSpan > 1) {
521
+ attrs.push(` rowspan="${recalculatedRowSpan}"`);
522
+ }
523
+ }
524
+ if (colspan) {
525
+ const recalculatedColumnSpan = Math.min(colspan, columns.slice(nthColumn).length);
526
+ if (recalculatedColumnSpan > 1) {
527
+ attrs.push(` colspan="${recalculatedColumnSpan}"`);
528
+ }
529
+ }
530
+ rowHTML.push(`<td${attrs.join('')}>${cellValueParsed}</td>`);
531
+ }
532
+ });
533
+ rowHTML.push('</tr>');
534
+ cells.push(...rowHTML);
535
+ });
536
+ return ['<tbody>', ...cells, '</tbody>'];
537
+ }
538
+
539
+ /**
540
+ * Converts Handsontable's coordinates into list of values for cells not being headers.
541
+ *
542
+ * @param {Core} hotInstance The Handsontable instance.
543
+ * @param {object} config Configuration for building the cell value list.
544
+ * @param {Array<number>} config.rows List of row indexes which should be taken into account when creating the
545
+ * cell value list.
546
+ * @param {Array<number>} config.columns List of column indexes which should be taken into account when creating the
547
+ * cell value list.
548
+ * @returns {Array[]} List of displayed cell values.
549
+ */
550
+ function getBodyDataByCoords(hotInstance, config) {
551
+ const cells = [];
552
+ const {
553
+ columns,
554
+ rows
555
+ } = config;
556
+ const bodyRows = rows.filter(rowIndex => rowIndex >= 0);
557
+ bodyRows.forEach(rowIndex => {
558
+ const rowData = [];
559
+ columns.forEach(columnIndex => {
560
+ const cellValue = hotInstance.getCopyableData(rowIndex, columnIndex);
561
+ const cellValueParsed = isEmpty(cellValue) ? '' : cellValue;
562
+ rowData.push(cellValueParsed);
563
+ });
564
+ cells.push(rowData);
565
+ });
566
+ return cells;
117
567
  }
118
568
 
119
569
  /**
@@ -161,18 +611,19 @@ export function htmlToGridSettings(element) {
161
611
  }
162
612
  return !isDataRow;
163
613
  });
614
+ const isAnyNested = thRows.find(tr => tr.querySelector('th[colspan]') !== null) !== undefined;
164
615
  thRowsLen = thRows.length;
165
616
  hasColHeaders = thRowsLen > 0;
166
- if (thRowsLen > 1) {
617
+ if (thRowsLen > 1 || isAnyNested) {
167
618
  settingsObj.nestedHeaders = Array.from(thRows).reduce((rows, row) => {
168
619
  const headersRow = Array.from(row.cells).reduce((headers, header, currentIndex) => {
169
620
  if (hasRowHeaders && currentIndex === 0) {
170
621
  return headers;
171
622
  }
172
623
  const {
173
- colSpan: colspan,
174
- innerHTML
624
+ colSpan: colspan
175
625
  } = header;
626
+ const innerHTML = decodeHTMLEntities(header.innerHTML);
176
627
  const nextHeader = colspan > 1 ? {
177
628
  label: innerHTML,
178
629
  colspan
@@ -188,7 +639,7 @@ export function htmlToGridSettings(element) {
188
639
  if (hasRowHeaders && index === 0) {
189
640
  return headers;
190
641
  }
191
- headers.push(header.innerHTML);
642
+ headers.push(decodeHTMLEntities(header.innerHTML));
192
643
  return headers;
193
644
  }, []);
194
645
  }
@@ -222,7 +673,7 @@ export function htmlToGridSettings(element) {
222
673
  rowSpan: rowspan,
223
674
  colSpan: colspan
224
675
  } = cell;
225
- const col = dataArr[row].findIndex(value => value === undefined);
676
+ const col = dataArr[row].findIndex(value => value === void 0);
226
677
  if (nodeName === 'TD') {
227
678
  if (rowspan > 1 || colspan > 1) {
228
679
  for (let rstart = row; rstart < row + rowspan; rstart++) {
@@ -249,7 +700,7 @@ export function htmlToGridSettings(element) {
249
700
  } else {
250
701
  cellValue = innerHTML.replace(/<br(\s*|\/)>[\r\n]?/gim, '\r\n');
251
702
  }
252
- dataArr[row][col] = cellValue.replace(regEscapedChars, match => ESCAPED_HTML_CHARS[match]);
703
+ dataArr[row][col] = decodeHTMLEntities(cellValue);
253
704
  } else {
254
705
  rowHeaders.push(innerHTML);
255
706
  }