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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge cells utilities
|
|
3
|
+
*/
|
|
4
|
+
import { normalizeRange, rangesOverlap } from './selection.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a proposed merge overlaps any existing merges (other than itself).
|
|
8
|
+
* @param {Object} proposed - { startRow, startCol, endRow, endCol }
|
|
9
|
+
* @param {Array} existingMerges
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function mergeHasConflict(proposed, existingMerges) {
|
|
13
|
+
const p = normalizeRange(proposed)
|
|
14
|
+
return existingMerges.some(m => rangesOverlap(p, normalizeRange(m)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find which merge (if any) covers cell (row, col).
|
|
19
|
+
* @param {number} row
|
|
20
|
+
* @param {number} col
|
|
21
|
+
* @param {Array} merges
|
|
22
|
+
* @returns {Object|null}
|
|
23
|
+
*/
|
|
24
|
+
export function findMergeAtCell(row, col, merges) {
|
|
25
|
+
return merges.find(m => {
|
|
26
|
+
const nm = normalizeRange(m)
|
|
27
|
+
return row >= nm.startRow && row <= nm.endRow && col >= nm.startCol && col <= nm.endCol
|
|
28
|
+
}) || null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check whether a cell is the top-left (anchor) of a merge
|
|
33
|
+
*/
|
|
34
|
+
export function isMergeAnchor(row, col, merges) {
|
|
35
|
+
return merges.some(m => {
|
|
36
|
+
const nm = normalizeRange(m)
|
|
37
|
+
return nm.startRow === row && nm.startCol === col
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add a merge. Returns null and a conflict flag if it overlaps an existing merge.
|
|
43
|
+
* @returns {{ merges: Array, conflict: boolean }}
|
|
44
|
+
*/
|
|
45
|
+
export function addMerge(proposed, existingMerges) {
|
|
46
|
+
const p = normalizeRange(proposed)
|
|
47
|
+
// Single cell → no-op
|
|
48
|
+
if (p.startRow === p.endRow && p.startCol === p.endCol) {
|
|
49
|
+
return { merges: existingMerges, conflict: false }
|
|
50
|
+
}
|
|
51
|
+
if (mergeHasConflict(p, existingMerges)) {
|
|
52
|
+
return { merges: existingMerges, conflict: true }
|
|
53
|
+
}
|
|
54
|
+
return { merges: [...existingMerges, p], conflict: false }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove a merge that covers cell (row, col).
|
|
59
|
+
* @returns {Array} updated merges
|
|
60
|
+
*/
|
|
61
|
+
export function removeMergeAt(row, col, existingMerges) {
|
|
62
|
+
return existingMerges.filter(m => {
|
|
63
|
+
const nm = normalizeRange(m)
|
|
64
|
+
return !(row >= nm.startRow && row <= nm.endRow && col >= nm.startCol && col <= nm.endCol)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the effective (row, col) to navigate to when arrow-keying into a merge.
|
|
70
|
+
* Returns the anchor cell of the merge if (row, col) is inside one.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveMergeAnchor(row, col, merges) {
|
|
73
|
+
const merge = findMergeAtCell(row, col, merges)
|
|
74
|
+
if (!merge) return { row, col }
|
|
75
|
+
const nm = normalizeRange(merge)
|
|
76
|
+
return { row: nm.startRow, col: nm.startCol }
|
|
77
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row/column grouping (outline) utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate that a group range is valid (start <= end, no negative indices)
|
|
7
|
+
*/
|
|
8
|
+
export function validateGroup(startIndex, endIndex) {
|
|
9
|
+
return Number.isInteger(startIndex) && Number.isInteger(endIndex) && startIndex <= endIndex && startIndex >= 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add a group to a groups array (type: 'row' | 'column')
|
|
14
|
+
* @param {'row'|'column'} type
|
|
15
|
+
* @param {number} startIndex
|
|
16
|
+
* @param {number} endIndex
|
|
17
|
+
* @param {Array} groups
|
|
18
|
+
* @returns {Array} new groups array
|
|
19
|
+
*/
|
|
20
|
+
export function addGroup(type, startIndex, endIndex, groups) {
|
|
21
|
+
if (!validateGroup(startIndex, endIndex)) return groups
|
|
22
|
+
// Prevent exact duplicate
|
|
23
|
+
const dup = groups.find(g => g.type === type && g.startIndex === startIndex && g.endIndex === endIndex)
|
|
24
|
+
if (dup) return groups
|
|
25
|
+
return [...groups, { type, startIndex, endIndex }]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Remove a group (exact match by type + startIndex + endIndex)
|
|
30
|
+
*/
|
|
31
|
+
export function removeGroup(type, startIndex, endIndex, groups) {
|
|
32
|
+
return groups.filter(g => !(g.type === type && g.startIndex === startIndex && g.endIndex === endIndex))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Toggle collapse state of a group in collapsedGroups
|
|
37
|
+
* @param {{ type, startIndex, endIndex }} group
|
|
38
|
+
* @param {Array} collapsedGroups
|
|
39
|
+
* @returns {Array} updated collapsedGroups
|
|
40
|
+
*/
|
|
41
|
+
export function toggleGroupCollapse(group, collapsedGroups) {
|
|
42
|
+
const exists = collapsedGroups.find(
|
|
43
|
+
g => g.type === group.type && g.startIndex === group.startIndex && g.endIndex === group.endIndex
|
|
44
|
+
)
|
|
45
|
+
if (exists) {
|
|
46
|
+
return collapsedGroups.filter(g => !(g.type === group.type && g.startIndex === group.startIndex && g.endIndex === group.endIndex))
|
|
47
|
+
}
|
|
48
|
+
return [...collapsedGroups, { type: group.type, startIndex: group.startIndex, endIndex: group.endIndex }]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a group is collapsed
|
|
53
|
+
*/
|
|
54
|
+
export function isGroupCollapsed(group, collapsedGroups) {
|
|
55
|
+
return collapsedGroups.some(
|
|
56
|
+
g => g.type === group.type && g.startIndex === group.startIndex && g.endIndex === group.endIndex
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get all row indices that are hidden due to collapsed groups
|
|
62
|
+
* @param {Array} groups
|
|
63
|
+
* @param {Array} collapsedGroups
|
|
64
|
+
* @returns {Set<number>}
|
|
65
|
+
*/
|
|
66
|
+
export function getCollapsedRowIndices(groups, collapsedGroups) {
|
|
67
|
+
const hidden = new Set()
|
|
68
|
+
for (const group of groups) {
|
|
69
|
+
if (group.type !== 'row') continue
|
|
70
|
+
if (isGroupCollapsed(group, collapsedGroups)) {
|
|
71
|
+
for (let i = group.startIndex; i <= group.endIndex; i++) {
|
|
72
|
+
hidden.add(i)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return hidden
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get all column indices hidden due to collapsed column groups
|
|
81
|
+
*/
|
|
82
|
+
export function getCollapsedColIndices(groups, collapsedGroups) {
|
|
83
|
+
const hidden = new Set()
|
|
84
|
+
for (const group of groups) {
|
|
85
|
+
if (group.type !== 'column') continue
|
|
86
|
+
if (isGroupCollapsed(group, collapsedGroups)) {
|
|
87
|
+
for (let i = group.startIndex; i <= group.endIndex; i++) {
|
|
88
|
+
hidden.add(i)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return hidden
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compute the nesting depth (outline level) for each index.
|
|
97
|
+
* Returns an array where element at position i = max nesting level for that index.
|
|
98
|
+
* Used for rendering "+/−" buttons at the right level offset.
|
|
99
|
+
*/
|
|
100
|
+
export function computeGroupDepths(type, groups, maxIndex) {
|
|
101
|
+
const depths = new Array(maxIndex + 1).fill(0)
|
|
102
|
+
for (const g of groups) {
|
|
103
|
+
if (g.type !== type) continue
|
|
104
|
+
for (let i = g.startIndex; i <= g.endIndex; i++) {
|
|
105
|
+
depths[i] = (depths[i] || 0) + 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return depths
|
|
109
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell selection utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a range so startRow <= endRow and startCol <= endCol
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeRange(range) {
|
|
9
|
+
return {
|
|
10
|
+
startRow: Math.min(range.startRow, range.endRow),
|
|
11
|
+
endRow: Math.max(range.startRow, range.endRow),
|
|
12
|
+
startCol: Math.min(range.startCol, range.endCol),
|
|
13
|
+
endCol: Math.max(range.startCol, range.endCol)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a cell (row, col) is inside a normalized range
|
|
19
|
+
*/
|
|
20
|
+
export function isCellInRange(row, col, range) {
|
|
21
|
+
const r = normalizeRange(range)
|
|
22
|
+
return row >= r.startRow && row <= r.endRow && col >= r.startCol && col <= r.endCol
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a cell is inside any of the given ranges
|
|
27
|
+
*/
|
|
28
|
+
export function isCellInAnyRange(row, col, ranges) {
|
|
29
|
+
return ranges.some(range => isCellInRange(row, col, range))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check whether two ranges overlap
|
|
34
|
+
*/
|
|
35
|
+
export function rangesOverlap(a, b) {
|
|
36
|
+
const ra = normalizeRange(a)
|
|
37
|
+
const rb = normalizeRange(b)
|
|
38
|
+
return !(ra.endRow < rb.startRow || ra.startRow > rb.endRow ||
|
|
39
|
+
ra.endCol < rb.startCol || ra.startCol > rb.endCol)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a range from activeCell to targetCell (for Shift+click / arrow+shift)
|
|
44
|
+
*/
|
|
45
|
+
export function buildRange(activeCell, targetCell) {
|
|
46
|
+
return {
|
|
47
|
+
startRow: activeCell.row,
|
|
48
|
+
startCol: activeCell.col,
|
|
49
|
+
endRow: targetCell.row,
|
|
50
|
+
endCol: targetCell.col
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get a single-cell range for a given cell
|
|
56
|
+
*/
|
|
57
|
+
export function singleCellRange(row, col) {
|
|
58
|
+
return { startRow: row, endRow: row, startCol: col, endCol: col }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the primary (active-cell-containing) range from a list of ranges
|
|
63
|
+
*/
|
|
64
|
+
export function getPrimaryRange(ranges, activeCell) {
|
|
65
|
+
if (!ranges || ranges.length === 0) return null
|
|
66
|
+
const found = ranges.find(r => isCellInRange(activeCell.row, activeCell.col, r))
|
|
67
|
+
return found ? normalizeRange(found) : normalizeRange(ranges[0])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Count cells in a range
|
|
72
|
+
*/
|
|
73
|
+
export function rangeSize(range) {
|
|
74
|
+
const r = normalizeRange(range)
|
|
75
|
+
return (r.endRow - r.startRow + 1) * (r.endCol - r.startCol + 1)
|
|
76
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual scrolling utilities
|
|
3
|
+
* Only DOM-visible rows are rendered to handle 200+ rows without lag.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate which rows should be rendered given scroll position and container height.
|
|
8
|
+
*
|
|
9
|
+
* @param {number} scrollTop - current vertical scroll offset in px
|
|
10
|
+
* @param {number} containerHeight - visible container height in px
|
|
11
|
+
* @param {number} totalRows - total number of data rows
|
|
12
|
+
* @param {number} rowHeight - default row height in px
|
|
13
|
+
* @param {number[]} rowHeights - per-row heights overrides (index → px), may be sparse
|
|
14
|
+
* @param {number} overscan - extra rows to render above/below (default 5)
|
|
15
|
+
* @returns {{ startIndex, endIndex, offsetTop, totalHeight }}
|
|
16
|
+
*/
|
|
17
|
+
export function calcVisibleRows(scrollTop, containerHeight, totalRows, rowHeight, rowHeights = [], overscan = 5) {
|
|
18
|
+
if (totalRows === 0) return { startIndex: 0, endIndex: -1, offsetTop: 0, totalHeight: 0 }
|
|
19
|
+
|
|
20
|
+
// Build cumulative heights
|
|
21
|
+
let cumulative = 0
|
|
22
|
+
let startIndex = 0
|
|
23
|
+
let endIndex = 0
|
|
24
|
+
let offsetTop = 0
|
|
25
|
+
let foundStart = false
|
|
26
|
+
|
|
27
|
+
const cumulativeHeights = new Array(totalRows + 1)
|
|
28
|
+
cumulativeHeights[0] = 0
|
|
29
|
+
for (let i = 0; i < totalRows; i++) {
|
|
30
|
+
const h = rowHeights[i] !== undefined ? rowHeights[i] : rowHeight
|
|
31
|
+
cumulativeHeights[i + 1] = cumulativeHeights[i] + h
|
|
32
|
+
}
|
|
33
|
+
const totalHeight = cumulativeHeights[totalRows]
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < totalRows; i++) {
|
|
36
|
+
const rowBottom = cumulativeHeights[i + 1]
|
|
37
|
+
const rowTop = cumulativeHeights[i]
|
|
38
|
+
|
|
39
|
+
if (!foundStart && rowBottom > scrollTop) {
|
|
40
|
+
startIndex = i
|
|
41
|
+
offsetTop = rowTop
|
|
42
|
+
foundStart = true
|
|
43
|
+
}
|
|
44
|
+
if (foundStart && rowTop < scrollTop + containerHeight) {
|
|
45
|
+
endIndex = i
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If scrollTop is past all content, show the last rows
|
|
50
|
+
if (!foundStart && totalRows > 0) {
|
|
51
|
+
startIndex = totalRows - 1
|
|
52
|
+
endIndex = totalRows - 1
|
|
53
|
+
offsetTop = cumulativeHeights[totalRows - 1]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Apply overscan
|
|
57
|
+
startIndex = Math.max(0, startIndex - overscan)
|
|
58
|
+
endIndex = Math.min(totalRows - 1, endIndex + overscan)
|
|
59
|
+
|
|
60
|
+
// Recalculate offsetTop with new startIndex
|
|
61
|
+
offsetTop = cumulativeHeights[startIndex]
|
|
62
|
+
|
|
63
|
+
return { startIndex, endIndex, offsetTop, totalHeight }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a cumulative-height lookup array from per-row heights.
|
|
68
|
+
* cumulativeHeights[i] = total px above row i.
|
|
69
|
+
* cumulativeHeights[totalRows] = total content height.
|
|
70
|
+
*
|
|
71
|
+
* Intended to be cached (e.g. as a Vue computed) and reused across scroll
|
|
72
|
+
* events so that findVisibleRange() can use O(log n) binary search.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} totalRows
|
|
75
|
+
* @param {number} defaultHeight
|
|
76
|
+
* @param {Object} rowHeightsMap - sparse { index: px } overrides
|
|
77
|
+
* @param {Set} hiddenSet - Set of hidden row indices (height = 0)
|
|
78
|
+
* @returns {number[]} length = totalRows + 1
|
|
79
|
+
*/
|
|
80
|
+
export function buildCumulativeHeights(totalRows, defaultHeight, rowHeightsMap = {}, hiddenSet = new Set()) {
|
|
81
|
+
const cum = new Array(totalRows + 1)
|
|
82
|
+
cum[0] = 0
|
|
83
|
+
for (let i = 0; i < totalRows; i++) {
|
|
84
|
+
const h = hiddenSet.has(i) ? 0 : (rowHeightsMap[i] !== undefined ? rowHeightsMap[i] : defaultHeight)
|
|
85
|
+
cum[i + 1] = cum[i] + h
|
|
86
|
+
}
|
|
87
|
+
return cum
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Find which rows are visible given a pre-built cumulativeHeights array.
|
|
92
|
+
* Uses binary search → O(log n) per frame (vs. O(n) in calcVisibleRows).
|
|
93
|
+
*
|
|
94
|
+
* @param {number} scrollTop
|
|
95
|
+
* @param {number} containerHeight
|
|
96
|
+
* @param {number} totalRows
|
|
97
|
+
* @param {number[]} cumulativeHeights - from buildCumulativeHeights()
|
|
98
|
+
* @param {number} overscan - extra rows above/below (default 5)
|
|
99
|
+
* @returns {{ startIndex, endIndex, offsetTop, totalHeight }}
|
|
100
|
+
*/
|
|
101
|
+
export function findVisibleRange(scrollTop, containerHeight, totalRows, cumulativeHeights, overscan = 5) {
|
|
102
|
+
if (totalRows === 0) return { startIndex: 0, endIndex: -1, offsetTop: 0, totalHeight: 0 }
|
|
103
|
+
|
|
104
|
+
const totalHeight = cumulativeHeights[totalRows]
|
|
105
|
+
const bottom = scrollTop + containerHeight
|
|
106
|
+
|
|
107
|
+
// Binary search: first row whose bottom edge > scrollTop
|
|
108
|
+
let lo = 0, hi = totalRows - 1
|
|
109
|
+
while (lo < hi) {
|
|
110
|
+
const mid = (lo + hi) >> 1
|
|
111
|
+
if (cumulativeHeights[mid + 1] <= scrollTop) lo = mid + 1
|
|
112
|
+
else hi = mid
|
|
113
|
+
}
|
|
114
|
+
let startIndex = lo
|
|
115
|
+
|
|
116
|
+
// Extend forward to cover the viewport bottom
|
|
117
|
+
let endIndex = startIndex
|
|
118
|
+
while (endIndex < totalRows - 1 && cumulativeHeights[endIndex + 1] < bottom) endIndex++
|
|
119
|
+
|
|
120
|
+
// Apply overscan
|
|
121
|
+
startIndex = Math.max(0, startIndex - overscan)
|
|
122
|
+
endIndex = Math.min(totalRows - 1, endIndex + overscan)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
startIndex,
|
|
126
|
+
endIndex,
|
|
127
|
+
offsetTop: cumulativeHeights[startIndex],
|
|
128
|
+
totalHeight
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the top offset of a row
|
|
134
|
+
*/
|
|
135
|
+
export function getRowTop(rowIndex, rowHeight, rowHeights = []) {
|
|
136
|
+
let top = 0
|
|
137
|
+
for (let i = 0; i < rowIndex; i++) {
|
|
138
|
+
top += rowHeights[i] !== undefined ? rowHeights[i] : rowHeight
|
|
139
|
+
}
|
|
140
|
+
return top
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the height of a specific row
|
|
145
|
+
*/
|
|
146
|
+
export function getRowHeight(rowIndex, rowHeight, rowHeights = []) {
|
|
147
|
+
return rowHeights[rowIndex] !== undefined ? rowHeights[rowIndex] : rowHeight
|
|
148
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Webpack-only version — imported via vue.config.js alias.
|
|
2
|
+
// worker-loader bundles each .worker.js file as a separate Web Worker entry.
|
|
3
|
+
import ExcelImportWorker from './excelImportWorker.worker.js'
|
|
4
|
+
import ExcelExportWorker from './excelExportWorker.worker.js'
|
|
5
|
+
|
|
6
|
+
export function createExcelImportWorker() { return new ExcelImportWorker() }
|
|
7
|
+
export function createExcelExportWorker() { return new ExcelExportWorker() }
|