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.
@@ -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,4 @@
1
+ // Rollup/library build fallback — no workers available in this context.
2
+ // Webpack builds override this file via alias in vue.config.js.
3
+ export function createExcelImportWorker() { return null }
4
+ export function createExcelExportWorker() { return null }
@@ -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() }