sheets
Multi-sheet support, sheet ordering, and sheet operations.
Sheet Definition
Each sheet is a self-contained Y.Map entry in the root sheets map, keyed by a SheetId (12-char base64url). The sheet’s name is stored as a Y.Text instance, enabling collaborative editing of the sheet name itself.
// Get sheet name
const sheet = sheets.get(sheetId) as Y.Map<unknown>
const name = sheet.get('name') as Y.Text
console.log(name.toString()) // "Sheet 1"
// Rename a sheet (collaborative)
name.delete(0, name.length)
name.insert(0, 'Revenue Data')Why Y.Text for sheet names?
Using Y.Text instead of a plain string allows two users to concurrently edit a sheet name (e.g. both typing in the rename field). While rare, this prevents the last-writer-wins conflict that a plain string would cause.
Sheet Ordering
Sheet tab order is maintained by the root sheetOrder array:
const sheetOrder = yDoc.getArray('sheetOrder') // Y.Array<SheetId>
The position of a SheetId in this array determines its tab position. The first entry is the leftmost tab, etc.
// Get the first sheet
const firstSheetId = sheetOrder.get(0) as string
const firstSheet = sheets.get(firstSheetId) as Y.Map<unknown>
// Iterate over all sheets in tab order
for (let i = 0; i < sheetOrder.length; i++) {
const sheetId = sheetOrder.get(i) as string
const sheet = sheets.get(sheetId) as Y.Map<unknown>
const name = (sheet.get('name') as Y.Text).toString()
console.log(`Tab ${i + 1}: ${name}`)
}Sheet Independence
Each sheet is fully self-contained. All of the following are scoped to individual sheets and do not reference other sheets:
- Row and column ordering (
rowOrder,colOrder) - Cell data (
rows) - Merged cells (
merges) - Borders, hyperlinks, validations, conditional formats
- Hidden rows/columns, sizing, frozen panes
This design means:
- Deleting a sheet cleanly removes all associated data
- Sheets can be synced independently (only observe active sheets)
- No orphaned references when sheets are removed
Sheet Operations
Adding a Sheet
yDoc.transact(() => {
const sheetId = generateSheetId()
// Create sheet structure
const sheet = new Y.Map()
sheets.set(sheetId, sheet)
// Set name
const name = new Y.Text()
name.insert(0, 'Sheet 2')
sheet.set('name', name)
// Initialize columns and rows
const colOrder = new Y.Array()
for (let i = 0; i < 26; i++) {
colOrder.push([generateColId()])
}
sheet.set('colOrder', colOrder)
const rowOrder = new Y.Array()
for (let i = 0; i < 100; i++) {
rowOrder.push([generateRowId()])
}
sheet.set('rowOrder', rowOrder)
// Initialize empty sub-type maps
sheet.set('rows', new Y.Map())
sheet.set('merges', new Y.Map())
sheet.set('borders', new Y.Map())
sheet.set('hyperlinks', new Y.Map())
sheet.set('validations', new Y.Map())
sheet.set('conditionalFormats', new Y.Array())
sheet.set('hiddenRows', new Y.Map())
sheet.set('hiddenCols', new Y.Map())
sheet.set('rowHeights', new Y.Map())
sheet.set('colWidths', new Y.Map())
sheet.set('frozen', new Y.Map())
// Add to tab order (at the end)
sheetOrder.push([sheetId])
})Removing a Sheet
yDoc.transact(() => {
// Remove from tab order
const index = sheetOrder.toArray().indexOf(sheetId)
if (index !== -1) {
sheetOrder.delete(index, 1)
}
// Remove sheet data
sheets.delete(sheetId)
})Last sheet protection
The application layer should prevent deleting the last sheet. A Calcillo document must always have at least one sheet.
Reordering Sheets
yDoc.transact(() => {
// Move sheet from position 2 to position 0 (make it first tab)
const sheetId = sheetOrder.get(2) as string
sheetOrder.delete(2, 1)
sheetOrder.insert(0, [sheetId])
})Duplicating a Sheet
Sheet duplication involves deep-copying all sub-type data into a new sheet with fresh IDs. The new sheet gets a new SheetId, but internal row and column IDs must also be regenerated to avoid ID collisions. Cell content and formatting are copied, but formulas referencing the original sheet’s cells are not automatically updated.
Active Sheet
The currently active (visible) sheet is not stored in the CRDT. Each user can view a different sheet independently. The active sheet is communicated through Yjs awareness:
awareness.setLocalStateField('activeSheet', sheetId)This ensures that one user switching tabs does not affect other users’ views.