Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
220 lines
5.5 KiB
TypeScript
220 lines
5.5 KiB
TypeScript
import type { SpreadsheetColumnConfig } from '../types';
|
|
import {
|
|
COLUMN_LETTER_HEIGHT,
|
|
DEFAULT_COL_WIDTH,
|
|
DEFAULT_ROW_HEIGHT,
|
|
MIN_COL_WIDTH,
|
|
ROW_NUMBER_WIDTH,
|
|
} from '../types';
|
|
import { getDataColX } from '../canvas/columnLayout';
|
|
|
|
const CELL_PADDING = 16;
|
|
const SAMPLE_ROWS = 50;
|
|
|
|
function measureTextWidth(
|
|
ctx: CanvasRenderingContext2D,
|
|
text: string,
|
|
): number {
|
|
return ctx.measureText(text).width + CELL_PADDING;
|
|
}
|
|
|
|
function formatCellDisplay(value: unknown, cellType: import('../types').CellType): string { if (value === null || value === undefined) return '';
|
|
if (cellType === 'number') {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? String(n) : String(value);
|
|
}
|
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
return String(value);
|
|
}
|
|
|
|
export function measureColumnWidths(
|
|
canvas: HTMLCanvasElement,
|
|
headers: Array<{ title: string; col: number }>,
|
|
columns: SpreadsheetColumnConfig[],
|
|
rows: Record<string, unknown>[],
|
|
defaultWidth: number,
|
|
resolveDisplayValue?: (
|
|
dataRowIndex: number,
|
|
colCfg: SpreadsheetColumnConfig,
|
|
row: Record<string, unknown>,
|
|
) => unknown,
|
|
formatWithSuffix?: (
|
|
value: unknown,
|
|
colCfg: SpreadsheetColumnConfig,
|
|
) => string,
|
|
): number[] { const ctx = canvas.getContext('2d');
|
|
if (!ctx) return headers.map(() => defaultWidth);
|
|
|
|
ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
|
|
|
const maxCol = Math.max(...headers.map((h) => h.col), 0);
|
|
const widths = new Array<number>(maxCol + 1).fill(defaultWidth);
|
|
|
|
for (const header of headers) {
|
|
widths[header.col] = Math.max(
|
|
widths[header.col] ?? defaultWidth,
|
|
measureTextWidth(ctx, header.title),
|
|
MIN_COL_WIDTH,
|
|
);
|
|
}
|
|
|
|
const sample = rows.slice(0, SAMPLE_ROWS);
|
|
for (const colCfg of columns) {
|
|
let maxW = widths[colCfg.col] ?? defaultWidth;
|
|
sample.forEach((row, dataRowIdx) => {
|
|
const raw = resolveDisplayValue
|
|
? resolveDisplayValue(dataRowIdx, colCfg, row)
|
|
: row[colCfg.fieldId];
|
|
const text = formatWithSuffix
|
|
? formatWithSuffix(raw, colCfg)
|
|
: formatCellDisplay(raw, colCfg.cellType === 'formula' ? 'number' : colCfg.cellType);
|
|
maxW = Math.max(maxW, measureTextWidth(ctx, text));
|
|
});
|
|
widths[colCfg.col] = Math.max(maxW, MIN_COL_WIDTH);
|
|
}
|
|
return widths;
|
|
}
|
|
|
|
export function getRowHeight(
|
|
cellType: import('../types').CellType, configured: number | 'auto' | undefined,
|
|
): number {
|
|
if (typeof configured === 'number') return configured;
|
|
switch (cellType) {
|
|
case 'image':
|
|
case 'video':
|
|
return 48;
|
|
default:
|
|
return DEFAULT_ROW_HEIGHT;
|
|
}
|
|
}
|
|
|
|
export function getTotalWidth(colWidths: number[]): number {
|
|
return colWidths.reduce((sum, w) => sum + w, 0);
|
|
}
|
|
|
|
export function getRowTop(rowIndex: number, rowHeight: number): number {
|
|
return rowIndex * rowHeight;
|
|
}
|
|
|
|
export function getColLeft(colIndex: number, colWidths: number[]): number {
|
|
let left = 0;
|
|
for (let i = 0; i < colIndex; i += 1) {
|
|
left += colWidths[i] ?? DEFAULT_COL_WIDTH;
|
|
}
|
|
return left;
|
|
}
|
|
|
|
function getDataColScreenX(
|
|
col: number,
|
|
colWidths: number[],
|
|
scrollLeft: number,
|
|
fixedCols: number[],
|
|
fixedWidth: number,
|
|
): number {
|
|
return getDataColX(col, colWidths, scrollLeft, fixedCols, fixedWidth);
|
|
}
|
|
|
|
export function hitTestCell(
|
|
x: number,
|
|
y: number,
|
|
scrollTop: number,
|
|
scrollLeft: number,
|
|
colWidths: number[],
|
|
rowHeight: number,
|
|
rowCount: number,
|
|
colCount: number,
|
|
fixedCols: number[] = [],
|
|
fixedWidth = ROW_NUMBER_WIDTH,
|
|
): { row: number; col: number } | null {
|
|
const bodyY = y + scrollTop;
|
|
if (bodyY < 0) return null;
|
|
|
|
const row = Math.floor(bodyY / rowHeight);
|
|
if (row < 0 || row >= rowCount) return null;
|
|
|
|
for (let col = 0; col < colCount; col += 1) {
|
|
const cellX = getDataColScreenX(
|
|
col,
|
|
colWidths,
|
|
scrollLeft,
|
|
fixedCols,
|
|
fixedWidth,
|
|
);
|
|
const w = colWidths[col] ?? DEFAULT_COL_WIDTH;
|
|
if (x >= cellX && x < cellX + w) {
|
|
return { row, col };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function hitTestRowNumber(
|
|
x: number,
|
|
y: number,
|
|
scrollTop: number,
|
|
rowHeight: number,
|
|
rowCount: number,
|
|
): number | null {
|
|
if (x < 0 || x >= ROW_NUMBER_WIDTH) return null;
|
|
const bodyY = y + scrollTop;
|
|
if (bodyY < 0) return null;
|
|
const row = Math.floor(bodyY / rowHeight);
|
|
if (row < 0 || row >= rowCount) return null;
|
|
return row;
|
|
}
|
|
|
|
export type HeaderHitArea = 'corner' | 'columnLetter' | 'columnTitle';
|
|
|
|
export function hitTestHeaderArea(
|
|
x: number,
|
|
y: number,
|
|
scrollLeft: number,
|
|
colWidths: number[],
|
|
colCount: number,
|
|
fixedCols: number[] = [],
|
|
fixedWidth = ROW_NUMBER_WIDTH,
|
|
): { area: HeaderHitArea; col?: number } | null {
|
|
if (x >= 0 && x < ROW_NUMBER_WIDTH) {
|
|
return { area: 'corner' };
|
|
}
|
|
const col = hitTestHeaderCol(
|
|
x,
|
|
scrollLeft,
|
|
colWidths,
|
|
colCount,
|
|
fixedCols,
|
|
fixedWidth,
|
|
);
|
|
if (col === null) return null;
|
|
if (y < COLUMN_LETTER_HEIGHT) {
|
|
return { area: 'columnLetter', col };
|
|
}
|
|
return { area: 'columnTitle', col };
|
|
}
|
|
|
|
export function hitTestHeaderCol(
|
|
x: number,
|
|
scrollLeft: number,
|
|
colWidths: number[],
|
|
colCount: number,
|
|
fixedCols: number[] = [],
|
|
fixedWidth = ROW_NUMBER_WIDTH,
|
|
): number | null {
|
|
for (let col = 0; col < colCount; col += 1) {
|
|
const cellX = getDataColScreenX(
|
|
col,
|
|
colWidths,
|
|
scrollLeft,
|
|
fixedCols,
|
|
fixedWidth,
|
|
);
|
|
const w = colWidths[col] ?? DEFAULT_COL_WIDTH;
|
|
if (x >= cellX && x < cellX + w) return col;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export { getDataColScreenX };
|
|
|
|
export { formatCellDisplay, CELL_PADDING };
|