Skip to content

Commit f3e70bc

Browse files
committed
feat(createDataGrid): add row spanning computation
1 parent 320ae7c commit f3e70bc

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
// Utilities
4+
import { computed } from 'vue'
5+
6+
import { createRowSpanning } from './spanning'
7+
8+
describe('createRowSpanning', () => {
9+
it('returns empty map when no rowSpanning function', () => {
10+
const spans = createRowSpanning({
11+
items: computed(() => []),
12+
columns: ['a', 'b'],
13+
})
14+
expect(spans.value.size).toBe(0)
15+
})
16+
17+
it('computes span map for visible items', () => {
18+
const items = computed(() => [
19+
{ id: 1, category: 'A', name: 'X' },
20+
{ id: 2, category: 'A', name: 'Y' },
21+
{ id: 3, category: 'B', name: 'Z' },
22+
])
23+
24+
const spans = createRowSpanning({
25+
items,
26+
columns: ['category', 'name'],
27+
itemKey: 'id',
28+
rowSpanning: (item, column) => {
29+
if (column === 'category' && item.category === 'A') return 2
30+
return 1
31+
},
32+
})
33+
34+
expect(spans.value.get(1)?.get('category')).toEqual({ rowSpan: 2, hidden: false })
35+
expect(spans.value.get(2)?.get('category')).toEqual({ rowSpan: 1, hidden: true })
36+
expect(spans.value.get(3)?.get('category')).toEqual({ rowSpan: 1, hidden: false })
37+
expect(spans.value.get(1)?.get('name')).toEqual({ rowSpan: 1, hidden: false })
38+
})
39+
40+
it('does not span beyond visible items', () => {
41+
const items = computed(() => [
42+
{ id: 1, category: 'A' },
43+
{ id: 2, category: 'A' },
44+
])
45+
46+
const spans = createRowSpanning({
47+
items,
48+
columns: ['category'],
49+
itemKey: 'id',
50+
rowSpanning: (item, column) => {
51+
if (column === 'category' && item.category === 'A') return 5
52+
return 1
53+
},
54+
})
55+
56+
expect(spans.value.get(1)?.get('category')?.rowSpan).toBe(2)
57+
})
58+
})
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @module createDataGrid/spanning
3+
*
4+
* @remarks
5+
* Computes a row span map from visible items. For each cell, determines
6+
* rowSpan and whether it's hidden (covered by a span from a previous row).
7+
* Spans do not cross page boundaries.
8+
*/
9+
10+
// Utilities
11+
import { computed } from 'vue'
12+
13+
// Types
14+
import type { ID } from '#v0/types'
15+
import type { ComputedRef, Ref } from 'vue'
16+
17+
export interface SpanEntry {
18+
rowSpan: number
19+
hidden: boolean
20+
}
21+
22+
export interface RowSpanningOptions<T = Record<string, unknown>> {
23+
items: Ref<readonly T[]> | ComputedRef<readonly T[]>
24+
columns: readonly string[]
25+
itemKey?: string
26+
rowSpanning?: (item: T, column: string) => number
27+
}
28+
29+
export function createRowSpanning<T extends Record<string, unknown>> (
30+
options: RowSpanningOptions<T>,
31+
): ComputedRef<Map<ID, Map<string, SpanEntry>>> {
32+
const { items, columns, itemKey = 'id', rowSpanning } = options
33+
34+
return computed(() => {
35+
const result = new Map<ID, Map<string, SpanEntry>>()
36+
37+
if (!rowSpanning) return result
38+
39+
const list = items.value
40+
41+
// Track which cells are covered by a span from a previous row
42+
// covered[colIndex] = number of remaining rows to skip
43+
const covered = Array.from({ length: columns.length }).fill(0)
44+
45+
for (let row = 0; row < list.length; row++) {
46+
const item = list[row]
47+
const id = item[itemKey] as ID
48+
const cellMap = new Map<string, SpanEntry>()
49+
50+
for (const [col, column] of columns.entries()) {
51+
if (covered[col] > 0) {
52+
cellMap.set(column, { rowSpan: 1, hidden: true })
53+
covered[col]--
54+
} else {
55+
const span = Math.min(
56+
Math.max(1, rowSpanning(item, column)),
57+
list.length - row, // clamp to remaining rows
58+
)
59+
cellMap.set(column, { rowSpan: span, hidden: false })
60+
if (span > 1) {
61+
covered[col] = span - 1
62+
}
63+
}
64+
}
65+
66+
result.set(id, cellMap)
67+
}
68+
69+
return result
70+
})
71+
}

0 commit comments

Comments
 (0)