canvas-datagrid 0.25.3 → 0.26.0

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.
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const isSupportedHtml = function (pasteValue) {
4
+ // We need to match new lines in the HTML, .* won't match new line characters.
5
+ // `s` regex modifier can't be used with `ecmaVersion === 2017`.
6
+ // As a workaround using [\s\S]*. Fix when we upgrade `ecmaVersion`.
7
+ const genericDiv = /(?:^(<meta[^>]*>)?[\s\S]*<div[^>]*>)/;
8
+ const genericSpan = /(?:^(<meta[^>]*>)?[\s\S]*<span[^>]*>)/;
9
+ const genericTable = /(?:^(<meta[^>]*>)?[\s\S]*<table[^>]*>)/; // Matches Google Sheets format clipboard data format too.
10
+ const excelTable = /(?:<!--StartFragment-->[\s\S]*<tr[^>]*>)/;
11
+ const excelTableRow = /(?:<!--StartFragment-->[\s\S]*<td[^>]*>)/;
12
+
13
+ return [
14
+ genericDiv,
15
+ genericTable,
16
+ genericSpan,
17
+ excelTable,
18
+ excelTableRow,
19
+ ].some((expression) => expression.test(pasteValue));
20
+ };
21
+
22
+ // Explanation of nodeType here: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
23
+ const IGNORE_NODETYPES = [8, 3]; // '#text' and '#comment'
24
+
25
+ const isHtmlTable = function (pasteValue) {
26
+ return /(?:<table[^>]*>)|(?:<tr[^]*>)/.test(pasteValue);
27
+ };
28
+
29
+ const sanitizeElementData = function (element) {
30
+ // It is not entirely clear if this check on nodeType is required.
31
+ let elementData = element.nodeType === 1 ? element.innerText : element.data;
32
+
33
+ return String(elementData).replace(/\s+/g, ' ').trim();
34
+ };
35
+
36
+ const parseHtmlText = function (data) {
37
+ const doc = new DOMParser().parseFromString(data, 'text/html');
38
+ const element = doc.querySelector('div') || doc.querySelector('span');
39
+ const elementData = sanitizeElementData(element);
40
+
41
+ return elementData
42
+ .split('\n')
43
+ .map((item) => item.split('\t').map((value) => ({ value: [{ value }] })));
44
+ };
45
+
46
+ const parseHtmlTable = function (data) {
47
+ const doc = new DOMParser().parseFromString(data, 'text/html');
48
+ const trs = doc.querySelectorAll('table tr');
49
+ const rows = [];
50
+
51
+ for (const tr of trs) {
52
+ const row = [];
53
+
54
+ for (const childNode of tr.childNodes) {
55
+ if (IGNORE_NODETYPES.includes(childNode.nodeType)) continue;
56
+
57
+ const col = { value: [] };
58
+ const value = sanitizeElementData(childNode);
59
+
60
+ if (value) col.value.push({ value });
61
+
62
+ row.push(col);
63
+ }
64
+
65
+ rows.push(row);
66
+ }
67
+
68
+ return rows;
69
+ };
70
+
71
+ const parseText = function (data) {
72
+ return data
73
+ .split('\n')
74
+ .map((item) => item.split('\t').map((value) => ({ value: [{ value }] })));
75
+ };
76
+
77
+ const parseData = function (data, mimeType) {
78
+ if (mimeType === 'text/html' && isHtmlTable(data)) {
79
+ return parseHtmlTable(data);
80
+ } else if (mimeType === 'text/html') {
81
+ return parseHtmlText(data);
82
+ }
83
+
84
+ // Default data format is string, so split on new line,
85
+ // and then enclose in an array (a row with one cell):
86
+ return parseText(data);
87
+ };
88
+
89
+ const htmlSafe = function (value) {
90
+ if (typeof value !== 'string') return value;
91
+
92
+ return value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
93
+ };
94
+
95
+ const createTextString = function (selectedData, isNeat) {
96
+ // Selected like [[0, 1], [0, 1]] of [[0, 3]] is neat; Selected like [[0, 1], [1, 2]] is untidy.
97
+ // If not isNeat we just return a simple string of concatenated values.
98
+ if (!isNeat)
99
+ return selectedData.map((row) => Object.values(row).join('')).join('');
100
+
101
+ // If isNeat, we can create tab separated mutti-line text.
102
+ return selectedData.map((row) => Object.values(row).join('\t')).join('\n');
103
+ };
104
+
105
+ const createHTMLString = function (selectedData, isNeat) {
106
+ if (!isNeat) return createTextString(selectedData, isNeat);
107
+
108
+ // If isNeat, we can create a HTML table with the selected data.
109
+ let htmlString = '<table>';
110
+ htmlString += selectedData
111
+ .map(
112
+ (row) =>
113
+ '<tr>' +
114
+ Object.values(row)
115
+ .map((value) => ['<td>', htmlSafe(value), '</td>'].join(''))
116
+ .join('') +
117
+ '</tr>',
118
+ )
119
+ .join('');
120
+ htmlString += '</table>';
121
+
122
+ return htmlString;
123
+ };
124
+
125
+ /**
126
+ * deduce the type of values in the column 'colNum' of data represented by 'rows'
127
+ * if the column is not empty and all values in the column are numbers, then the type is 'number',
128
+ * otherwise the type is 'string'.
129
+ * Purpose: check the type of pasted data when a new column is created dynamically
130
+ * @param {Array} rows - array of rows, e.g. [ [{"value": [{"value": "a"}]},{"value": [{"value": "b"}]}], [{"value": [{"value": "1"}]},{"value": [{"value": "2"}]}] ]
131
+ * @param {number} colNum - Column index to check
132
+ * @returns {("string" | "number")} deduced type
133
+ */
134
+ const deduceColTypeFromData = function (rows, colNum) {
135
+ const len = rows.length;
136
+ // the first row can represent column headings.
137
+ // if one row is pasted, check it, otherwise start from the second row.
138
+ const startRow = len === 1 ? 0 : 1;
139
+ let isColEmpty = true;
140
+ for (let i = startRow; i < len; i += 1) {
141
+ const cellData = rows[i][colNum];
142
+ if (!cellData || cellData.value.length === 0) {
143
+ continue;
144
+ }
145
+ isColEmpty = false;
146
+ const val = cellData.value[0].value;
147
+ if (!Number.isFinite(Number(val))) {
148
+ return 'string';
149
+ }
150
+ }
151
+ return isColEmpty ? 'string' : 'number';
152
+ };
153
+
154
+ export {
155
+ createTextString,
156
+ createHTMLString,
157
+ isSupportedHtml,
158
+ htmlSafe,
159
+ parseData,
160
+ parseHtmlTable,
161
+ parseHtmlText,
162
+ parseText,
163
+ sanitizeElementData,
164
+ deduceColTypeFromData,
165
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Merge a new hidden row range into existed ranges array
5
+ * @param {any[]} hiddenRowRanges tuples: Array<[bgeinRowIndex, endRowIndex]>
6
+ * @param {number[]} newRange tuple: [beginRowIndex, endRowIndex]
7
+ * @returns {boolean}
8
+ */
9
+ const mergeHiddenRowRanges = function (hiddenRowRanges, newRange) {
10
+ const [beginRowIndex, endRowIndex] = newRange;
11
+ if (endRowIndex < beginRowIndex) return false;
12
+ let inserted = false;
13
+ for (let i = 0; i < hiddenRowRanges.length; i++) {
14
+ const range = hiddenRowRanges[i];
15
+ if (beginRowIndex > range[1] + 1) continue;
16
+ if (beginRowIndex <= range[0] && endRowIndex >= range[0]) {
17
+ hiddenRowRanges[i] = [beginRowIndex, Math.max(endRowIndex, range[1])];
18
+ inserted = true;
19
+ break;
20
+ }
21
+ if (beginRowIndex >= range[0]) {
22
+ hiddenRowRanges[i] = [range[0], Math.max(endRowIndex, range[1])];
23
+ inserted = true;
24
+ break;
25
+ }
26
+ }
27
+ if (!inserted) hiddenRowRanges.push([beginRowIndex, endRowIndex]);
28
+ // merge intersections after sorting ranges
29
+ hiddenRowRanges.sort((a, b) => a[0] - b[0]);
30
+ for (let i = 0; i < hiddenRowRanges.length - 1; i++) {
31
+ const range = hiddenRowRanges[i];
32
+ const nextRange = hiddenRowRanges[i + 1];
33
+ if (nextRange[0] <= range[1] + 1) {
34
+ hiddenRowRanges[i] = [range[0], Math.max(range[1], nextRange[1])];
35
+ hiddenRowRanges.splice(i + 1, 1);
36
+ i--;
37
+ }
38
+ }
39
+ return true;
40
+ };
41
+
42
+ export { mergeHiddenRowRanges };