mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
fix(desktop): keep spreadsheet numbers whole in the workspace preview
Columns without a stored width fell back to a fixed 72px, and stored widths were chosen for Excel's font, which is narrower than the preview's. Values such as 12,000.00 were clipped to "12,000...". A column the file gives no width now fits its widest cell, and a number column never ends up narrower than its numbers. Text in a column whose width the file sets is still clipped, as Excel clips it, and merged headings that spill over their span do not widen a column.
This commit is contained in:
@@ -210,6 +210,60 @@ describe('the shape of a sheet', () => {
|
||||
expect(grid.columnWidths[3]).toBe(72) // not said
|
||||
})
|
||||
|
||||
it('widens a column the file gives no width to fit its numbers, so a value is never cut to "12,000…"', async () => {
|
||||
const grid = await firstGrid(workbookOf([{ name: 'A', rows: [['项目', '预算(元)'], ['服务器', '1,234,567,890.00'], ['差旅', '8,000.00']] }]))
|
||||
|
||||
// 16 digits/separators at ~6.6px, plus the cell's padding.
|
||||
expect(grid.columnWidths[1]).toBeGreaterThanOrEqual(120)
|
||||
// A short CJK label still gets the default floor, not a sliver.
|
||||
expect(grid.columnWidths[0]).toBeGreaterThanOrEqual(72)
|
||||
})
|
||||
|
||||
it('leaves a short column at the default width', async () => {
|
||||
const grid = await firstGrid(workbookOf([{ name: 'A', rows: [['a', 'b'], ['c', 'd']] }]))
|
||||
|
||||
expect(grid.columnWidths).toEqual([72, 72])
|
||||
})
|
||||
|
||||
it('caps how far a long text can widen a column', async () => {
|
||||
const grid = await firstGrid(workbookOf([{ name: 'A', rows: [['x'.repeat(500)]] }]))
|
||||
|
||||
expect(grid.columnWidths[0]).toBe(360)
|
||||
})
|
||||
|
||||
it('does not widen a column for a merged heading that spills over its neighbours', async () => {
|
||||
const grid = await firstGrid(workbookOf([{
|
||||
name: 'A',
|
||||
rows: [['Quarterly budget summary for the whole group', '', ''], ['a', 'b', 'c']],
|
||||
merges: [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }],
|
||||
}]))
|
||||
|
||||
expect(grid.columnWidths).toEqual([72, 72, 72])
|
||||
})
|
||||
|
||||
it('widens a stated column for a number that would otherwise be cut, as Excel would show ####', async () => {
|
||||
// 12 characters is what the file asked for; the preview font needs more for this figure.
|
||||
const grid = await firstGrid(workbookOf([{
|
||||
name: 'A',
|
||||
rows: [['预算', 1234567890.12]],
|
||||
columns: [{ wpx: 40 }, { wpx: 40 }],
|
||||
}]))
|
||||
|
||||
expect(grid.columnWidths[1]).toBeGreaterThan(40)
|
||||
// The text column keeps the width the file set: clipping text is what Excel does too.
|
||||
expect(grid.columnWidths[0]).toBe(40)
|
||||
})
|
||||
|
||||
it('never widens a column the file sets, nor a hidden one', async () => {
|
||||
const grid = await firstGrid(workbookOf([{
|
||||
name: 'A',
|
||||
rows: [['x'.repeat(100), 'y'.repeat(100)]],
|
||||
columns: [{ wpx: 90 }, { hidden: true }],
|
||||
}]))
|
||||
|
||||
expect(grid.columnWidths).toEqual([90, 0])
|
||||
})
|
||||
|
||||
it('keeps column widths within what can be drawn', async () => {
|
||||
const grid = await firstGrid(workbookOf([{ name: 'A', rows: [['a', 'b']], columns: [{ wpx: 2 }, { wpx: 9000 }] }]))
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@ export type SpreadsheetEngineOptions = {
|
||||
const DEFAULT_COLUMN_WIDTH = 72
|
||||
const MIN_COLUMN_WIDTH = 28
|
||||
const MAX_COLUMN_WIDTH = 480
|
||||
/** A column grows to fit its text or numbers, up to this. */
|
||||
const MAX_AUTO_COLUMN_WIDTH = 360
|
||||
const CELL_PADDING = 18
|
||||
const MIN_ROW_HEIGHT = 16
|
||||
const MAX_ROW_HEIGHT = 400
|
||||
|
||||
@@ -120,6 +123,31 @@ function cellAlign(cell: SheetJs.CellObject): CellAlign {
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/** Rendered width of cell text at the grid's 12px size; a CJK character is about twice a Latin one. */
|
||||
function textWidth(text: string): number {
|
||||
let width = 0
|
||||
for (const char of text) width += (char.codePointAt(0) ?? 0) >= 0x2e80 ? 12 : 6.6
|
||||
return width
|
||||
}
|
||||
|
||||
/**
|
||||
* How wide a column must be to show its cells whole. Excel would draw a number that does
|
||||
* not fit as `####`; a preview that cuts it to `12,000…` hides the value just the same, and
|
||||
* the preview's font is wider than the one the width was chosen for. `numbersOnly` is for a
|
||||
* column whose width the file states: its text may still be clipped, as Excel clips it, but
|
||||
* a number may not. A merged cell's text spills over its span, so it takes no part.
|
||||
*/
|
||||
function fitColumnWidth(cells: Grid['cells'], column: number, spilling: ReadonlySet<string>, numbersOnly: boolean): number {
|
||||
let widest = 0
|
||||
for (let row = 0; row < cells.length; row += 1) {
|
||||
const cell = cells[row]?.[column]
|
||||
if (!cell || spilling.has(`${row}:${column}`)) continue
|
||||
if (numbersOnly && cell.align !== 'right') continue
|
||||
widest = Math.max(widest, textWidth(cell.text))
|
||||
}
|
||||
return widest === 0 ? 0 : Math.min(Math.ceil(widest + CELL_PADDING), MAX_AUTO_COLUMN_WIDTH)
|
||||
}
|
||||
|
||||
/** Turn a dense SheetJS worksheet into the grid the viewer draws, and let SheetJS's objects go. */
|
||||
function toGrid(sheet: SheetJs.WorkSheet, limits: SpreadsheetLimits): Grid {
|
||||
const data = ((sheet as SheetJs.DenseSheet)['!data'] ?? []) as Array<Array<SheetJs.CellObject | undefined> | undefined>
|
||||
@@ -153,11 +181,19 @@ function toGrid(sheet: SheetJs.WorkSheet, limits: SpreadsheetLimits): Grid {
|
||||
cells.push(line)
|
||||
}
|
||||
|
||||
const spilling = new Set<string>()
|
||||
for (const range of sheet['!merges'] ?? []) {
|
||||
if (range.e.c > range.s.c) spilling.add(`${range.s.r}:${range.s.c}`)
|
||||
}
|
||||
|
||||
const columnInfo = sheet['!cols'] ?? []
|
||||
const columnWidths = Array.from({ length: columns }, (_, column) => {
|
||||
const info = columnInfo[column]
|
||||
if (info?.hidden) return 0
|
||||
const width = info?.wpx ?? (info?.wch ? info.wch * 7 + 5 : DEFAULT_COLUMN_WIDTH)
|
||||
const stated = info?.wpx ?? (info?.wch ? info.wch * 7 + 5 : undefined)
|
||||
const width = stated === undefined
|
||||
? Math.max(DEFAULT_COLUMN_WIDTH, fitColumnWidth(cells, column, spilling, false))
|
||||
: Math.max(stated, fitColumnWidth(cells, column, spilling, true))
|
||||
return clamp(Math.round(width), MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user