vue2-excel-grid 1.0.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.
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import ExcelGrid from './components/ExcelGrid.vue'
2
+
3
+ export { ExcelGrid }
4
+
5
+ export default {
6
+ install(Vue) {
7
+ Vue.component('ExcelGrid', ExcelGrid)
8
+ }
9
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Clipboard utilities: TSV serialization/deserialization
3
+ */
4
+
5
+ /**
6
+ * Serialize a 2D array of values to TSV (tab-separated values) string
7
+ * @param {Array<Array>} matrix
8
+ * @returns {string}
9
+ */
10
+ export function matrixToTSV(matrix) {
11
+ return matrix
12
+ .map(row => row.map(cell => {
13
+ const s = cell == null ? '' : String(cell)
14
+ // Escape tabs and newlines
15
+ return s.replace(/\t/g, ' ').replace(/\r?\n/g, ' ')
16
+ }).join('\t'))
17
+ .join('\n')
18
+ }
19
+
20
+ /**
21
+ * Parse a TSV string into a 2D array
22
+ * @param {string} tsv
23
+ * @returns {Array<Array<string>>}
24
+ */
25
+ export function tsvToMatrix(tsv) {
26
+ if (!tsv) return [['']]
27
+ return tsv.split('\n').map(row => row.split('\t'))
28
+ }
29
+
30
+ /**
31
+ * Extract a 2D matrix from grid data for a given range.
32
+ *
33
+ * @param {Array} data - rows array of plain objects
34
+ * @param {Array} leafCols - flat leaf columns (with .key)
35
+ * @param {Object} range - { startRow, endRow, startCol, endCol }
36
+ * @param {boolean} rawOnly - always use raw value (for copy/export)
37
+ * @returns {Array<Array>}
38
+ */
39
+ export function extractMatrix(data, leafCols, range) {
40
+ const matrix = []
41
+ for (let r = range.startRow; r <= range.endRow; r++) {
42
+ const row = []
43
+ for (let c = range.startCol; c <= range.endCol; c++) {
44
+ const col = leafCols[c]
45
+ const val = col && data[r] ? data[r][col.key] : ''
46
+ row.push(val !== undefined ? val : '')
47
+ }
48
+ matrix.push(row)
49
+ }
50
+ return matrix
51
+ }
52
+
53
+ /**
54
+ * Transpose a 2D matrix (rows ↔ cols)
55
+ */
56
+ export function transposeMatrix(matrix) {
57
+ if (!matrix || matrix.length === 0) return []
58
+ const rows = matrix.length
59
+ const cols = matrix[0].length
60
+ return Array.from({ length: cols }, (_, c) =>
61
+ Array.from({ length: rows }, (_, r) => matrix[r][c])
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Tile a matrix to fill a larger target area — Excel-style paste broadcast.
67
+ *
68
+ * The target dimensions must be exact multiples of the source dimensions,
69
+ * otherwise the original matrix is returned unchanged (caller decides fallback).
70
+ *
71
+ * Examples:
72
+ * tileMatrix([[1]], 3, 3) → [[1,1,1],[1,1,1],[1,1,1]] (1×1 → 3×3)
73
+ * tileMatrix([[1,2]], 2, 4) → [[1,2,1,2],[1,2,1,2]] (1×2 → 2×4)
74
+ * tileMatrix([[1],[2]], 4, 2) → [[1,1],[2,2],[1,1],[2,2]] (2×1 → 4×2)
75
+ *
76
+ * @param {Array<Array>} matrix - source matrix
77
+ * @param {number} targetRows - desired row count
78
+ * @param {number} targetCols - desired col count
79
+ * @returns {Array<Array>}
80
+ */
81
+ export function tileMatrix(matrix, targetRows, targetCols) {
82
+ const srcRows = matrix.length
83
+ const srcCols = matrix[0] ? matrix[0].length : 0
84
+ if (!srcRows || !srcCols) return matrix
85
+ if (targetRows % srcRows !== 0 || targetCols % srcCols !== 0) return matrix
86
+
87
+ const result = []
88
+ for (let r = 0; r < targetRows; r++) {
89
+ const row = []
90
+ for (let c = 0; c < targetCols; c++) {
91
+ row.push(matrix[r % srcRows][c % srcCols])
92
+ }
93
+ result.push(row)
94
+ }
95
+ return result
96
+ }
97
+
98
+ /**
99
+ * Apply a 2D matrix of values into a data array starting at (targetRow, targetCol).
100
+ * Returns a new data array (immutable update).
101
+ *
102
+ * @param {Array} data
103
+ * @param {Array} leafCols
104
+ * @param {number} targetRow
105
+ * @param {number} targetCol
106
+ * @param {Array<Array>} matrix
107
+ * @param {string} mode - 'all' | 'values' | 'formats'
108
+ * @param {Function|null} isEditable - (row, col) => boolean
109
+ * @param {Function|null} isValid - (row, col, value) => boolean — false skips the cell (kept in `skipped`)
110
+ * @returns {{ newData, affectedRange, skipped }}
111
+ */
112
+ export function applyMatrix(data, leafCols, targetRow, targetCol, matrix, mode = 'all', isEditable = null, isValid = null) {
113
+ const newData = data.slice() // O(n) pointer copy — only affected rows get new objects
114
+ const endRow = Math.min(targetRow + matrix.length - 1, data.length - 1)
115
+ const endCol = Math.min(targetCol + (matrix[0] ? matrix[0].length - 1 : 0), leafCols.length - 1)
116
+ const skipped = []
117
+
118
+ for (let r = targetRow; r <= endRow; r++) {
119
+ const matRow = matrix[r - targetRow]
120
+ if (!matRow) continue
121
+ let rowCopy = null
122
+ for (let c = targetCol; c <= endCol; c++) {
123
+ const col = leafCols[c]
124
+ if (!col) continue
125
+ if (isEditable ? !isEditable(r, c) : col.editable === false) continue
126
+ if (mode === 'all' || mode === 'values') {
127
+ const val = matRow[c - targetCol]
128
+ const v = val !== undefined ? val : ''
129
+ if (isValid && !isValid(r, c, v)) { skipped.push({ row: r, col: c, value: v }); continue }
130
+ if (!rowCopy) { rowCopy = { ...(data[r] || {}) }; newData[r] = rowCopy }
131
+ rowCopy[col.key] = v
132
+ }
133
+ }
134
+ }
135
+
136
+ return {
137
+ newData,
138
+ affectedRange: { startRow: targetRow, endRow, startCol: targetCol, endCol },
139
+ skipped
140
+ }
141
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Excel export utility using SheetJS (xlsx)
3
+ */
4
+ import * as XLSX from 'xlsx'
5
+ import { flattenColumns, buildHeaderRows, columnTreeDepth, columnTotalWidth } from './headerTree.js'
6
+ import { normalizeRange } from './selection.js'
7
+
8
+ /**
9
+ * Export grid data to .xlsx and trigger browser download.
10
+ *
11
+ * @param {Object} opts
12
+ * @param {Array} opts.data - rows array of plain objects
13
+ * @param {Array} opts.columns - column config (may be nested)
14
+ * @param {Array} opts.leafCols - flat leaf columns
15
+ * @param {Array} opts.merges - merge descriptors { startRow, startCol, endRow, endCol }
16
+ * @param {Set} opts.hiddenColSet - Set of hidden flat col indices
17
+ * @param {Set} opts.hiddenRowSet - Set of hidden row indices
18
+ * @param {Object} opts.range - { startRow, endRow, startCol, endCol } for 'selection' scope
19
+ * @param {string} opts.filename
20
+ * @param {string} opts.sheetName
21
+ * @param {'all'|'selection'} opts.scope
22
+ * @param {boolean} opts.includeHidden
23
+ */
24
+ export function exportToExcel(opts) {
25
+ const {
26
+ data,
27
+ columns,
28
+ leafCols,
29
+ merges = [],
30
+ hiddenColSet = new Set(),
31
+ hiddenRowSet = new Set(),
32
+ range = null,
33
+ filename = 'export.xlsx',
34
+ sheetName = 'Sheet1',
35
+ scope = 'all',
36
+ includeHidden = false
37
+ } = opts
38
+
39
+ // Determine which leaf columns to export
40
+ const exportCols = leafCols.filter((col, idx) => {
41
+ if (!includeHidden && hiddenColSet.has(idx)) return false
42
+ if (scope === 'selection' && range) {
43
+ const r = normalizeRange(range)
44
+ if (idx < r.startCol || idx > r.endCol) return false
45
+ }
46
+ return true
47
+ })
48
+
49
+ // Determine which rows to export
50
+ let rowStart = 0
51
+ let rowEnd = data.length - 1
52
+ if (scope === 'selection' && range) {
53
+ const r = normalizeRange(range)
54
+ rowStart = r.startRow
55
+ rowEnd = r.endRow
56
+ }
57
+ const exportRows = []
58
+ for (let i = rowStart; i <= rowEnd; i++) {
59
+ if (!includeHidden && hiddenRowSet.has(i)) continue
60
+ exportRows.push({ rowIndex: i, data: data[i] })
61
+ }
62
+
63
+ // Build header rows for multi-level headers
64
+ const totalDepth = columnTreeDepth(columns)
65
+ const headerRows = buildHeaderRows(columns, totalDepth)
66
+
67
+ // Create workbook
68
+ const wb = XLSX.utils.book_new()
69
+ const wsData = []
70
+
71
+ // Header rows
72
+ for (let hi = 0; hi < headerRows.length; hi++) {
73
+ const hRow = []
74
+ for (const cell of headerRows[hi]) {
75
+ if (cell.isGroup) {
76
+ // Group header — find which export cols fall under this group
77
+ const groupLeaves = flattenColumns(cell.column.children || [])
78
+ const relevantLeaves = groupLeaves.filter(l => exportCols.some(ec => ec.key === l.key))
79
+ if (relevantLeaves.length === 0) continue
80
+ hRow.push(cell.column.title || '')
81
+ for (let i = 1; i < relevantLeaves.length; i++) hRow.push(null) // merged cells placeholders
82
+ } else {
83
+ // Leaf header
84
+ if (!exportCols.some(ec => ec.key === cell.column.key)) continue
85
+ if (cell.rowspan > 1) {
86
+ // Push placeholder for sub-rows
87
+ hRow.push(cell.column.title || '')
88
+ } else {
89
+ hRow.push(cell.column.title || '')
90
+ }
91
+ }
92
+ }
93
+ wsData.push(hRow)
94
+ }
95
+
96
+ // Data rows
97
+ for (const { data: rowData } of exportRows) {
98
+ const row = exportCols.map(col => {
99
+ const v = rowData ? rowData[col.key] : ''
100
+ return v !== undefined ? v : ''
101
+ })
102
+ wsData.push(row)
103
+ }
104
+
105
+ const ws = XLSX.utils.aoa_to_sheet(wsData)
106
+
107
+ // Apply column widths (pixel → character width approximation: 1 char ≈ 7px)
108
+ ws['!cols'] = exportCols.map(col => ({ wch: Math.round((col.width || 100) / 7) }))
109
+
110
+ // Apply merges (offset by header rows)
111
+ const sheetMerges = []
112
+ for (const m of merges) {
113
+ const nm = normalizeRange(m)
114
+ const r = scope === 'selection' && range ? normalizeRange(range) : { startRow: 0, startCol: 0 }
115
+ if (scope === 'selection') {
116
+ if (nm.startRow < r.startRow || nm.endRow > r.endRow) continue
117
+ if (nm.startCol < r.startCol || nm.endCol > r.endCol) continue
118
+ }
119
+ // Find export col positions
120
+ const startColExport = exportCols.findIndex(c => {
121
+ const li = leafCols.indexOf(c)
122
+ return li === nm.startCol
123
+ })
124
+ const endColExport = exportCols.findIndex(c => {
125
+ const li = leafCols.indexOf(c)
126
+ return li === nm.endCol
127
+ })
128
+ if (startColExport < 0 || endColExport < 0) continue
129
+
130
+ // Row offset: header rows + rows before startRow that were exported
131
+ const rowsBefore = exportRows.filter(er => er.rowIndex < nm.startRow).length
132
+ const rowsBeforeEnd = exportRows.filter(er => er.rowIndex <= nm.endRow).length
133
+ sheetMerges.push({
134
+ s: { r: totalDepth + rowsBefore, c: startColExport },
135
+ e: { r: totalDepth + rowsBeforeEnd - 1, c: endColExport }
136
+ })
137
+ }
138
+ if (sheetMerges.length) ws['!merges'] = sheetMerges
139
+
140
+ XLSX.utils.book_append_sheet(wb, ws, sheetName)
141
+
142
+ // Trigger download
143
+ XLSX.writeFile(wb, filename)
144
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Web Worker — Excel export.
3
+ * Builds the XLSX workbook off the main thread and returns a raw ArrayBuffer.
4
+ * The main thread receives it and triggers the browser download (DOM required).
5
+ *
6
+ * Protocol:
7
+ * IN postMessage({ data, columns, leafCols, merges,
8
+ * hiddenCols, hiddenRows, range,
9
+ * filename, sheetName, scope, includeHidden })
10
+ * OUT postMessage({ type: 'result', buffer: ArrayBuffer, filename }) [transferred]
11
+ * OUT postMessage({ type: 'error', message: '' })
12
+ */
13
+ import * as XLSX from 'xlsx'
14
+ import { flattenColumns, buildHeaderRows, columnTreeDepth } from './headerTree.js'
15
+
16
+ function normalizeRange(range) {
17
+ const r = range || {}
18
+ const sr = r.startRow == null ? 0 : r.startRow
19
+ const er = r.endRow == null ? 0 : r.endRow
20
+ const sc = r.startCol == null ? 0 : r.startCol
21
+ const ec = r.endCol == null ? 0 : r.endCol
22
+ return {
23
+ startRow: Math.min(sr, er),
24
+ endRow: Math.max(sr, er),
25
+ startCol: Math.min(sc, ec),
26
+ endCol: Math.max(sc, ec)
27
+ }
28
+ }
29
+
30
+ self.onmessage = function (e) {
31
+ const {
32
+ data = [],
33
+ columns = [],
34
+ leafCols = [],
35
+ merges = [],
36
+ hiddenCols = [],
37
+ hiddenRows = [],
38
+ range = null,
39
+ filename = 'export.xlsx',
40
+ sheetName = 'Sheet1',
41
+ scope = 'all',
42
+ includeHidden = false
43
+ } = e.data
44
+
45
+ try {
46
+ const hiddenColSet = new Set(hiddenCols)
47
+ const hiddenRowSet = new Set(hiddenRows)
48
+
49
+ // --- columns to export ---
50
+ const exportCols = leafCols.filter((col, idx) => {
51
+ if (!includeHidden && hiddenColSet.has(idx)) return false
52
+ if (scope === 'selection' && range) {
53
+ const r = normalizeRange(range)
54
+ if (idx < r.startCol || idx > r.endCol) return false
55
+ }
56
+ return true
57
+ })
58
+
59
+ // --- rows to export ---
60
+ let rowStart = 0
61
+ let rowEnd = data.length - 1
62
+ if (scope === 'selection' && range) {
63
+ const r = normalizeRange(range)
64
+ rowStart = r.startRow
65
+ rowEnd = r.endRow
66
+ }
67
+ const exportRows = []
68
+ for (let i = rowStart; i <= rowEnd; i++) {
69
+ if (!includeHidden && hiddenRowSet.has(i)) continue
70
+ exportRows.push({ rowIndex: i, data: data[i] })
71
+ }
72
+
73
+ // --- multi-level header rows ---
74
+ const totalDepth = columnTreeDepth(columns)
75
+ const headerRows = buildHeaderRows(columns, totalDepth)
76
+
77
+ const wsData = []
78
+
79
+ for (const hRow of headerRows) {
80
+ const row = []
81
+ for (const cell of hRow) {
82
+ if (cell.isGroup) {
83
+ const groupLeaves = flattenColumns(cell.column.children || [])
84
+ const relevant = groupLeaves.filter(l => exportCols.some(ec => ec.key === l.key))
85
+ if (relevant.length === 0) continue
86
+ row.push(cell.column.title || '')
87
+ for (let i = 1; i < relevant.length; i++) row.push(null)
88
+ } else {
89
+ if (!exportCols.some(ec => ec.key === cell.column.key)) continue
90
+ row.push(cell.column.title || '')
91
+ }
92
+ }
93
+ wsData.push(row)
94
+ }
95
+
96
+ for (const { data: rowData } of exportRows) {
97
+ wsData.push(exportCols.map(col => {
98
+ const v = rowData ? rowData[col.key] : ''
99
+ return v !== undefined ? v : ''
100
+ }))
101
+ }
102
+
103
+ const ws = XLSX.utils.aoa_to_sheet(wsData)
104
+ ws['!cols'] = exportCols.map(col => ({ wch: Math.round((col.width || 100) / 7) }))
105
+
106
+ // --- merge cells ---
107
+ const sheetMerges = []
108
+ for (const m of merges) {
109
+ const nm = normalizeRange(m)
110
+ const selR = scope === 'selection' && range ? normalizeRange(range) : { startRow: 0, startCol: 0 }
111
+ if (scope === 'selection') {
112
+ if (nm.startRow < selR.startRow || nm.endRow > selR.endRow) continue
113
+ if (nm.startCol < selR.startCol || nm.endCol > selR.endCol) continue
114
+ }
115
+ const startC = exportCols.findIndex(c => leafCols.indexOf(c) === nm.startCol)
116
+ const endC = exportCols.findIndex(c => leafCols.indexOf(c) === nm.endCol)
117
+ if (startC < 0 || endC < 0) continue
118
+
119
+ const rowsBefore = exportRows.filter(er => er.rowIndex < nm.startRow).length
120
+ const rowsBeforeEnd = exportRows.filter(er => er.rowIndex <= nm.endRow).length
121
+ sheetMerges.push({
122
+ s: { r: totalDepth + rowsBefore, c: startC },
123
+ e: { r: totalDepth + rowsBeforeEnd - 1, c: endC }
124
+ })
125
+ }
126
+ if (sheetMerges.length) ws['!merges'] = sheetMerges
127
+
128
+ const wb = XLSX.utils.book_new()
129
+ XLSX.utils.book_append_sheet(wb, ws, sheetName)
130
+
131
+ // XLSX.write returns Uint8Array with type:'array'; transfer its buffer.
132
+ const uint8 = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
133
+ // Slice to get an independent ArrayBuffer so we can transfer it safely.
134
+ const buffer = uint8.buffer.slice(uint8.byteOffset, uint8.byteOffset + uint8.byteLength)
135
+
136
+ self.postMessage({ type: 'result', buffer, filename }, [buffer])
137
+ } catch (err) {
138
+ self.postMessage({ type: 'error', message: err.message })
139
+ }
140
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Excel/CSV import utility using SheetJS (xlsx)
3
+ */
4
+ import * as XLSX from 'xlsx'
5
+ import { flatLeaves, detectImportBounds, buildNestedColumns } from './excelImportCore.js'
6
+
7
+ const CHUNK_SIZE = 2000
8
+
9
+ function yieldToUI() {
10
+ return new Promise(resolve => setTimeout(resolve, 0))
11
+ }
12
+
13
+ /**
14
+ * Parse a File object and return grid-compatible data.
15
+ *
16
+ * @param {File} file
17
+ * @param {Array} existingColumns
18
+ * @param {Object} options
19
+ * - sheetName {string} — target sheet (default: first sheet)
20
+ * - headerRow {number} — 0-based index of first header row (overrides auto-detect)
21
+ * - headerEndRow {number} — last header row for multi-row headers (overrides auto-detect)
22
+ * - dataStartRow {number} — 0-based index of first data row (overrides auto-detect)
23
+ * @param {Function} onProgress — (percent: 0-100, message: string) => void
24
+ * @returns {Promise<{ data, columns, merges }>}
25
+ */
26
+ export async function importFromFile(file, existingColumns, options = {}, onProgress) {
27
+ const report = (pct, msg) => onProgress && onProgress(pct, msg)
28
+
29
+ report(5, 'Fayl o\'qilmoqda...')
30
+ await yieldToUI()
31
+
32
+ const buffer = await file.arrayBuffer()
33
+
34
+ report(20, 'Excel tahlil qilinmoqda...')
35
+ await yieldToUI()
36
+
37
+ let workbook
38
+ try {
39
+ workbook = XLSX.read(buffer, { type: 'array', cellDates: true })
40
+ } catch (e) {
41
+ throw new Error('Excel fayl o\'qib bo\'lmadi: ' + e.message)
42
+ }
43
+
44
+ const { sheetName } = options
45
+ let sheet
46
+ if (sheetName) {
47
+ sheet = workbook.Sheets[sheetName]
48
+ if (!sheet) throw new Error(`Sheet "${sheetName}" topilmadi`)
49
+ } else {
50
+ const firstName = workbook.SheetNames[0]
51
+ if (!firstName) throw new Error('Workbook bo\'sh')
52
+ sheet = workbook.Sheets[firstName]
53
+ }
54
+
55
+ report(40, 'Ma\'lumotlar tayyorlanmoqda...')
56
+ await yieldToUI()
57
+
58
+ const aoa = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: false, defval: '' })
59
+ if (!aoa || aoa.length === 0) throw new Error('Sheet bo\'sh')
60
+
61
+ // `sheet['!merges']` row/col indices are sheet-absolute, but `aoa` is relative
62
+ // to the sheet's used range (e.g. starts at A2 instead of A1). Normalize merges
63
+ // to aoa-relative coordinates so they line up with header/data row indices below.
64
+ const usedRange = XLSX.utils.decode_range(sheet['!ref'] || 'A1:A1')
65
+ const rowOffset = usedRange.s.r
66
+ const colOffset = usedRange.s.c
67
+ const merges = (sheet['!merges'] || []).map(m => ({
68
+ s: { r: m.s.r - rowOffset, c: m.s.c - colOffset },
69
+ e: { r: m.e.r - rowOffset, c: m.e.c - colOffset }
70
+ }))
71
+
72
+ const detected = detectImportBounds(aoa, merges)
73
+ const headerRowIdx = options.headerRow != null ? options.headerRow : detected.headerRow
74
+ const headerEndRowIdx = options.headerEndRow != null ? options.headerEndRow : detected.headerEndRow
75
+ const dataStartRowIdx = options.dataStartRow != null ? options.dataStartRow : detected.dataStartRow
76
+
77
+ const existingLeaves = flatLeaves(existingColumns)
78
+ const usedSlugs = new Set()
79
+
80
+ const { columns: newColumns, colMap } = buildNestedColumns(
81
+ aoa, headerRowIdx, headerEndRowIdx, merges,
82
+ existingLeaves, usedSlugs
83
+ )
84
+
85
+ const totalDataRows = Math.max(aoa.length - dataStartRowIdx, 0)
86
+ report(55, `Qatorlar qayta ishlanmoqda (0 / ${totalDataRows})...`)
87
+ await yieldToUI()
88
+
89
+ const data = []
90
+ for (let i = dataStartRowIdx; i < aoa.length; i += CHUNK_SIZE) {
91
+ const end = Math.min(i + CHUNK_SIZE, aoa.length)
92
+ for (let r = i; r < end; r++) {
93
+ const rowArr = aoa[r]
94
+ const obj = {}
95
+ for (const { key, colIndex } of colMap) {
96
+ obj[key] = rowArr[colIndex] !== undefined ? rowArr[colIndex] : ''
97
+ }
98
+ data.push(obj)
99
+ }
100
+ const processed = Math.min(end - dataStartRowIdx, totalDataRows)
101
+ const pct = 55 + Math.round(40 * processed / Math.max(totalDataRows, 1))
102
+ report(pct, `Qatorlar: ${processed} / ${totalDataRows}`)
103
+ await yieldToUI()
104
+ }
105
+
106
+ const dataMerges = merges
107
+ .filter(m => m.s.r >= dataStartRowIdx)
108
+ .map(m => ({
109
+ startRow: m.s.r - dataStartRowIdx,
110
+ startCol: m.s.c,
111
+ endRow: m.e.r - dataStartRowIdx,
112
+ endCol: m.e.c
113
+ }))
114
+ .filter(m => m.startRow >= 0 && m.startCol >= 0)
115
+
116
+ report(100, 'Tayyor!')
117
+ return { data, columns: newColumns, merges: dataMerges }
118
+ }