grid structure
Row and column ordering, sizing, visibility, and merged cells.
Row and Column Ordering
Each sheet maintains two Y.Array structures for row and column ordering:
| Sub-type | Yjs Type | Description |
|---|---|---|
rowOrder |
Y.Array<RowId> |
Ordered list of row IDs (9-char base64url) |
colOrder |
Y.Array<ColId> |
Ordered list of column IDs (5-char base64url) |
The position of an ID in its array determines the display position. Row rowOrder[0] displays as row 1, rowOrder[1] as row 2, etc. Column colOrder[0] displays as column A, colOrder[1] as column B, etc.
Row and Column Operations
// Insert a new row at position 5
const newRowId = generateRowId()
rowOrder.insert(5, [newRowId])
// Delete row at position 3
rowOrder.delete(3, 1)
// Also clean up cell data:
rows.delete(rowOrder.get(3))
// Move a column from position 2 to position 5
const colId = colOrder.get(2)
colOrder.delete(2, 1)
colOrder.insert(5, [colId])ID-based stability
Because rows and columns are identified by random IDs rather than sequential indices, concurrent insertions and deletions merge cleanly. Two users inserting rows at different positions will both succeed without conflict, and the resulting order is deterministic.
Row and Column Sizing
Custom dimensions override the application defaults:
| Sub-type | Yjs Type | Description |
|---|---|---|
rowHeights |
Y.Map<number> |
rowId → height in pixels |
colWidths |
Y.Map<number> |
colId → width in pixels |
Rows and columns not present in these maps use application default sizes. Only non-default sizes are stored.
// Set column width to 200px
colWidths.set(colId, 200)
// Get row height (with default fallback)
const height = rowHeights.get(rowId) ?? DEFAULT_ROW_HEIGHTHidden Rows and Columns
| Sub-type | Yjs Type | Description |
|---|---|---|
hiddenRows |
Y.Map<boolean> |
rowId → true for hidden rows |
hiddenCols |
Y.Map<boolean> |
colId → true for hidden columns |
Hidden rows and columns remain in the rowOrder/colOrder arrays (preserving their position) but are not rendered. Only rows/columns that are actually hidden have entries in these maps.
// Hide a row
hiddenRows.set(rowId, true)
// Unhide a row
hiddenRows.delete(rowId)
// Check if a column is hidden
const isHidden = hiddenCols.get(colId) === trueMerged Cells
Merged cells are stored in the per-sheet merges map:
| Sub-type | Yjs Type | Key Format |
|---|---|---|
merges |
Y.Map<MergeInfo> |
"${startRowId}_${startColId}" |
MergeInfo Structure
interface MergeInfo {
startRow: RowId // Top-left row ID
endRow: RowId // Bottom-right row ID
startCol: ColId // Top-left column ID
endCol: ColId // Bottom-right column ID
}The merge key uses the top-left cell’s composite key. The value stores all four corner IDs, which define the rectangular merge region.
Merge Operations
// Create a merge spanning rows 2-4, columns B-D
merges.set(`${rowId2}_${colIdB}`, {
startRow: rowId2,
endRow: rowId4,
startCol: colIdB,
endCol: colIdD
})
// Remove a merge
merges.delete(`${rowId2}_${colIdB}`)Concurrent deletion and merges
When a row or column that participates in a merge is deleted concurrently, the merge must be validated. If the start row/column of a merge no longer exists in rowOrder/colOrder, the merge entry becomes orphaned and should be cleaned up. The application layer handles this validation when processing CRDT updates.
Cell Content in Merged Regions
Only the top-left cell of a merged region holds content. Other cells in the region should be empty. When a merge is created, content from non-top-left cells is discarded. When a merge is removed, only the top-left cell retains its content.
Cell Access Patterns
Common patterns for working with the grid:
// Iterate over all cells in a row
const rowMap = rows.get(rowId) as Y.Map<Cell>
if (rowMap) {
for (const [colId, cell] of rowMap.entries()) {
// Process cell
}
}
// Get the display position of a cell
const rowIndex = rowOrder.toArray().indexOf(rowId) // 0-based
const colIndex = colOrder.toArray().indexOf(colId) // 0-based
const cellRef = `${indexToColumnLetter(colIndex)}${rowIndex + 1}` // e.g. "B3"
// Find the ID for a display reference like "C5"
const colId = colOrder.get(2) // C = index 2
const rowId = rowOrder.get(4) // 5 = index 4 (0-based)