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/README.md +97 -0
- package/dist/vue2-excel-grid.esm.js +5883 -0
- package/dist/vue2-excel-grid.umd.js +6 -0
- package/package.json +74 -0
- package/src/components/ExcelGrid.vue +3552 -0
- package/src/index.js +9 -0
- package/src/utils/clipboard.js +141 -0
- package/src/utils/excelExport.js +144 -0
- package/src/utils/excelExportWorker.worker.js +140 -0
- package/src/utils/excelImport.js +118 -0
- package/src/utils/excelImportCore.js +247 -0
- package/src/utils/excelImportWorker.worker.js +104 -0
- package/src/utils/fillHandle.js +119 -0
- package/src/utils/headerTree.js +108 -0
- package/src/utils/i18n.js +101 -0
- package/src/utils/merge.js +77 -0
- package/src/utils/outline.js +109 -0
- package/src/utils/selection.js +76 -0
- package/src/utils/virtualScroll.js +148 -0
- package/src/utils/workerFactory.js +4 -0
- package/src/utils/workerFactory.webpack.js +7 -0
package/src/index.js
ADDED
|
@@ -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
|
+
}
|