|
| 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