j
CI / Verify (push) Successful in 1m30s
CI / Build immutable images and deploy (push) Successful in 2m5s

This commit is contained in:
2026-09-26 01:54:50 +07:00 Unverified
parent 079468f259
commit db9b5ecdc8
6 changed files with 184 additions and 77 deletions
+33 -14
View File
@@ -161,7 +161,6 @@
.extra-sheet-grid .Spreadsheet__table { .extra-sheet-grid .Spreadsheet__table {
width: max-content; width: max-content;
min-width: 100%;
} }
.extra-sheet-grid .Spreadsheet__cell, .extra-sheet-grid .Spreadsheet__cell,
@@ -210,6 +209,19 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.extra-sheet-grid .Spreadsheet__active-cell--edit {
overflow: hidden;
border: 0;
box-shadow: none;
}
.extra-sheet-grid .Spreadsheet__active-cell--edit:focus,
.extra-sheet-grid .Spreadsheet__active-cell--edit:focus-visible,
.extra-sheet-grid .Spreadsheet__data-editor textarea:focus,
.extra-sheet-grid .Spreadsheet__data-editor textarea:focus-visible {
outline: 0;
}
.extra-sheet-text { .extra-sheet-text {
--tw-prose-body: #000; --tw-prose-body: #000;
--tw-prose-headings: #000; --tw-prose-headings: #000;
@@ -224,23 +236,30 @@
} }
.extra-sheet-text--public { .extra-sheet-text--public {
--tw-prose-body: #fff; --tw-prose-body: currentColor;
--tw-prose-headings: #fff; --tw-prose-headings: currentColor;
--tw-prose-lead: #fff; --tw-prose-lead: currentColor;
--tw-prose-links: #fff; --tw-prose-links: currentColor;
--tw-prose-bold: #fff; --tw-prose-bold: currentColor;
--tw-prose-counters: #fff; --tw-prose-counters: currentColor;
--tw-prose-bullets: #fff; --tw-prose-bullets: currentColor;
--tw-prose-quotes: #fff; --tw-prose-quotes: currentColor;
--tw-prose-code: #fff; --tw-prose-code: currentColor;
color: #fff; color: inherit;
} }
.extra-sheet-grid .Spreadsheet__data-editor textarea { .extra-sheet-grid .Spreadsheet__data-editor textarea {
box-sizing: border-box; box-sizing: border-box;
min-height: 100%; width: 100%;
min-width: 100%; height: 100%;
resize: both; margin: 0;
border: 0;
outline: 0;
background: transparent;
color: #000;
font: inherit;
padding: 0.25rem;
resize: none;
white-space: pre-wrap; white-space: pre-wrap;
} }
+44 -21
View File
@@ -95,6 +95,8 @@ import {
addExtraSheetRow, addExtraSheetRow,
clearExtraSheetCellFormatting, clearExtraSheetCellFormatting,
DEFAULT_EXTRA_SHEET_COLUMN_WIDTH, DEFAULT_EXTRA_SHEET_COLUMN_WIDTH,
MAX_EXTRA_SHEET_COLUMN_WIDTH,
MIN_EXTRA_SHEET_COLUMN_WIDTH,
deleteExtraSheetColumn, deleteExtraSheetColumn,
deleteExtraSheetRow, deleteExtraSheetRow,
extraSheetRangeToTsv, extraSheetRangeToTsv,
@@ -408,6 +410,7 @@ export function ExtraSheetEditor({
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}), }),
); );
const sheetRootRef = useRef<HTMLDivElement>(null);
const spreadsheetRef = useRef<SpreadsheetRef>(null); const spreadsheetRef = useRef<SpreadsheetRef>(null);
const initialEditTextRef = useRef<string | null>(null); const initialEditTextRef = useRef<string | null>(null);
const [active, setActive] = useState<Point>({ row: 0, column: 0 }); const [active, setActive] = useState<Point>({ row: 0, column: 0 });
@@ -417,7 +420,6 @@ export function ExtraSheetEditor({
const [removeOpen, setRemoveOpen] = useState(false); const [removeOpen, setRemoveOpen] = useState(false);
const [selectedCount, setSelectedCount] = useState(1); const [selectedCount, setSelectedCount] = useState(1);
const [selectionKind, setSelectionKind] = useState<"cells" | "rows" | "columns">("cells"); const [selectionKind, setSelectionKind] = useState<"cells" | "rows" | "columns">("cells");
const [draftWidths, setDraftWidths] = useState<Record<number, number>>({});
const [history, setHistory] = useState<SheetHistory>({ const [history, setHistory] = useState<SheetHistory>({
items: [value], items: [value],
index: 0, index: 0,
@@ -548,17 +550,34 @@ export function ExtraSheetEditor({
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target.closest("input, textarea, [contenteditable='true']")) return; if (target.closest("input, textarea, [contenteditable='true']")) return;
const key = event.key.toLowerCase(); const key = event.key.toLowerCase();
const commandKey = event.metaKey || event.ctrlKey;
const undo = const undo =
(event.metaKey || event.ctrlKey) && !event.shiftKey && key === "z"; commandKey && !event.shiftKey && key === "z";
const redo = const redo =
((event.metaKey || event.ctrlKey) && event.shiftKey && key === "z") || (commandKey && event.shiftKey && key === "z") ||
(event.ctrlKey && !event.shiftKey && key === "y"); (event.ctrlKey && !event.shiftKey && key === "y");
const format = commandKey && !event.altKey && !event.shiftKey
? ({ b: "bold", i: "italic", u: "underline" } as const)[key as "b" | "i" | "u"]
: event.altKey && event.shiftKey && key === "5"
? "strike"
: undefined;
if (undo && canUndo) { if (undo && canUndo) {
event.preventDefault(); event.preventDefault();
travel(-1); travel(-1);
} else if (redo && canRedo) { } else if (redo && canRedo) {
event.preventDefault(); event.preventDefault();
travel(1); travel(1);
} else if (format) {
event.preventDefault();
const activePattern = {
bold: /<strong\b/i,
italic: /<em\b/i,
underline: /<u\b/i,
strike: /<s\b/i,
}[format];
commitSheet(styleExtraSheetCells(value, selectedPointsRef.current, {
[format]: !activePattern.test(activeValue),
}));
} else if (event.key === "Delete" || event.key === "Backspace") { } else if (event.key === "Delete" || event.key === "Backspace") {
event.preventDefault(); event.preventDefault();
commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { value: "" })); commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { value: "" }));
@@ -689,14 +708,18 @@ export function ExtraSheetEditor({
}; };
const resizeColumn = useCallback( const resizeColumn = useCallback(
(column: number, width: number, commit: boolean) => { (column: number, width: number, commit: boolean) => {
setDraftWidths((current) => ({ ...current, [column]: width })); const boundedWidth = Math.round(Math.max(
if (!commit) return; MIN_EXTRA_SHEET_COLUMN_WIDTH,
setDraftWidths((current) => { Math.min(MAX_EXTRA_SHEET_COLUMN_WIDTH, width),
const next = { ...current }; ));
delete next[column]; if (!commit) {
return next; const columnElement = sheetRootRef.current?.querySelector<HTMLTableColElement>(
}); `col[data-sheet-column-index="${column}"]`,
commitSheet(resizeExtraSheetColumn(value, column, width)); );
if (columnElement) columnElement.style.width = `${boundedWidth}px`;
return;
}
commitSheet(resizeExtraSheetColumn(value, column, boundedWidth));
}, },
[commitSheet, value], [commitSheet, value],
); );
@@ -707,12 +730,11 @@ export function ExtraSheetEditor({
id={columnDragId(value.columns[props.column].id)} id={columnDragId(value.columns[props.column].id)}
onRename={openRenameColumn} onRename={openRenameColumn}
onResize={resizeColumn} onResize={resizeColumn}
width={draftWidths[props.column] width={value.columns[props.column].width
?? value.columns[props.column].width
?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH} ?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH}
/> />
), ),
[draftWidths, openRenameColumn, resizeColumn, value.columns], [openRenameColumn, resizeColumn, value.columns],
); );
const rowIndicator = useCallback( const rowIndicator = useCallback(
(props: RowIndicatorProps) => ( (props: RowIndicatorProps) => (
@@ -744,9 +766,9 @@ export function ExtraSheetEditor({
{Array.from({ length: columns }, (_, index) => ( {Array.from({ length: columns }, (_, index) => (
<col <col
key={value.columns[index]?.id ?? index} key={value.columns[index]?.id ?? index}
data-sheet-column-index={index}
style={{ style={{
width: draftWidths[index] width: value.columns[index]?.width
?? value.columns[index]?.width
?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH, ?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH,
}} }}
/> />
@@ -755,7 +777,7 @@ export function ExtraSheetEditor({
<tbody>{children}</tbody> <tbody>{children}</tbody>
</table> </table>
), ),
[draftWidths, value.columns], [value.columns],
); );
const activeFormats = [ const activeFormats = [
/<strong\b/i.test(activeValue) ? "bold" : null, /<strong\b/i.test(activeValue) ? "bold" : null,
@@ -768,6 +790,7 @@ export function ExtraSheetEditor({
return ( return (
<div <div
ref={sheetRootRef}
className="flex min-w-0 flex-col gap-3" className="flex min-w-0 flex-col gap-3"
aria-label="ตารางข้อมูลเสริม" aria-label="ตารางข้อมูลเสริม"
onKeyDown={handleSheetKeyDown} onKeyDown={handleSheetKeyDown}
@@ -811,16 +834,16 @@ export function ExtraSheetEditor({
if (Object.keys(patch).length) applyStyle(patch); if (Object.keys(patch).length) applyStyle(patch);
}} }}
> >
<ToggleGroupItem value="bold" aria-label="ตัวหนา"> <ToggleGroupItem value="bold" aria-label="ตัวหนา" title="ตัวหนา (Ctrl/⌘ B)">
<BoldIcon /> <BoldIcon />
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="ตัวเอียง"> <ToggleGroupItem value="italic" aria-label="ตัวเอียง" title="ตัวเอียง (Ctrl/⌘ I)">
<ItalicIcon /> <ItalicIcon />
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="ขีดเส้นใต้"> <ToggleGroupItem value="underline" aria-label="ขีดเส้นใต้" title="ขีดเส้นใต้ (Ctrl/⌘ U)">
<UnderlineIcon /> <UnderlineIcon />
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem value="strike" aria-label="ขีดฆ่า"> <ToggleGroupItem value="strike" aria-label="ขีดฆ่า" title="ขีดฆ่า (Alt/⌥ Shift 5)">
<StrikethroughIcon /> <StrikethroughIcon />
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
+1 -1
View File
@@ -4829,7 +4829,7 @@ function ExtraEditor({ guide, extraSectionId, initial, characters, weapons, arti
} }
> >
<PlusIcon data-icon="inline-start" /> <PlusIcon data-icon="inline-start" />
เพิ่มตาราง 2 × 2 เพิ่มตาราง 4 × 6
</Button> </Button>
)} )}
</Field> </Field>
+16 -10
View File
@@ -56,7 +56,10 @@ import {
} from "@/lib/guides/talents"; } from "@/lib/guides/talents";
import { parseYouTubeUrl } from "@/lib/guides/youtube"; import { parseYouTubeUrl } from "@/lib/guides/youtube";
import { normalizeExtraRichText } from "@/lib/rich-text"; import { normalizeExtraRichText } from "@/lib/rich-text";
import { DEFAULT_EXTRA_SHEET_COLUMN_WIDTH } from "@/lib/guides/extra-sheet"; import {
DEFAULT_EXTRA_SHEET_COLUMN_WIDTH,
extraSheetUsedRange,
} from "@/lib/guides/extra-sheet";
import { publicGuideSections } from "@/lib/guides/public-sections"; import { publicGuideSections } from "@/lib/guides/public-sections";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -1295,12 +1298,14 @@ function ExtraSectionContent({
{action} {action}
</header> </header>
<div className={cn("relative z-10 grid gap-5 md:grid-cols-2", exporting && "grid-cols-2")}> <div className={cn("relative z-10 grid gap-5 md:grid-cols-2", exporting && "grid-cols-2")}>
{extra.rows.map((row) => ( {extra.rows.map((row) => {
const displaySheet = row.sheet ? extraSheetUsedRange(row.sheet) : null;
return (
<Card <Card
key={row.id} key={row.id}
className={cn( className={cn(
"overflow-hidden py-0", "overflow-hidden py-0",
row.sheet && (exporting ? "col-span-2" : "md:col-span-2"), displaySheet && (exporting ? "col-span-2" : "md:col-span-2"),
)} )}
style={row.backgroundColor ? { backgroundColor: row.backgroundColor } : undefined} style={row.backgroundColor ? { backgroundColor: row.backgroundColor } : undefined}
> >
@@ -1355,7 +1360,7 @@ function ExtraSectionContent({
</CardDescription> </CardDescription>
{row.percent ? <Badge>{metric(row.percent, exporting ? "en-US" : undefined)}</Badge> : null} {row.percent ? <Badge>{metric(row.percent, exporting ? "en-US" : undefined)}</Badge> : null}
</CardHeader> </CardHeader>
{row.sheet ? ( {displaySheet ? (
<CardContent <CardContent
className={cn( className={cn(
"pb-5", "pb-5",
@@ -1365,14 +1370,14 @@ function ExtraSectionContent({
<Table <Table
className={cn(exporting && "table-fixed")} className={cn(exporting && "table-fixed")}
style={{ style={{
minWidth: `${row.sheet.columns.reduce( minWidth: `${displaySheet.columns.reduce(
(total, column) => total + (column.width ?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH), (total, column) => total + (column.width ?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH),
0, 0,
)}px`, )}px`,
}} }}
> >
<colgroup> <colgroup>
{row.sheet.columns.map((column) => ( {displaySheet.columns.map((column) => (
<col <col
key={column.id} key={column.id}
style={{ width: column.width ?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH }} style={{ width: column.width ?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH }}
@@ -1381,7 +1386,7 @@ function ExtraSectionContent({
</colgroup> </colgroup>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{row.sheet.columns.map((column) => ( {displaySheet.columns.map((column) => (
<TableHead key={column.id} className="border-l first:border-l-0 whitespace-normal"> <TableHead key={column.id} className="border-l first:border-l-0 whitespace-normal">
{column.label} {column.label}
</TableHead> </TableHead>
@@ -1389,9 +1394,9 @@ function ExtraSectionContent({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{row.sheet.rows.map((sheetRow) => ( {displaySheet.rows.map((sheetRow) => (
<TableRow key={sheetRow.id}> <TableRow key={sheetRow.id}>
{row.sheet!.columns.map((column) => ( {displaySheet.columns.map((column) => (
<TableCell <TableCell
key={column.id} key={column.id}
className="border-l first:border-l-0 whitespace-normal" className="border-l first:border-l-0 whitespace-normal"
@@ -1413,7 +1418,8 @@ function ExtraSectionContent({
</CardContent> </CardContent>
) : null} ) : null}
</Card> </Card>
))} );
})}
</div> </div>
</section> </section>
); );
+53 -28
View File
@@ -10,6 +10,7 @@ import {
extraSheetSchema, extraSheetSchema,
extraSheetRangeToTsv, extraSheetRangeToTsv,
extraSheetToMatrix, extraSheetToMatrix,
extraSheetUsedRange,
matrixToExtraSheet, matrixToExtraSheet,
moveExtraSheetColumn, moveExtraSheetColumn,
moveExtraSheetRow, moveExtraSheetRow,
@@ -29,14 +30,17 @@ function idFactory() {
} }
describe("Extra sheet", () => { describe("Extra sheet", () => {
it("creates a valid two-by-two sheet", () => { it("creates a valid four-by-six sheet", () => {
const sheet = createExtraSheet(idFactory()); const sheet = createExtraSheet(idFactory());
expect(extraSheetSchema.safeParse(sheet).success).toBe(true); expect(extraSheetSchema.safeParse(sheet).success).toBe(true);
expect(sheet.columns.map((column) => column.label)).toEqual(["คอลัมน์ 1", "คอลัมน์ 2"]); expect(sheet.columns.map((column) => column.label)).toEqual(
expect(extraSheetToMatrix(sheet)).toEqual([ Array.from({ length: 6 }, (_, index) => `คอลัมน์ ${index + 1}`),
[{ value: "", backgroundColor: undefined }, { value: "", backgroundColor: undefined }], );
[{ value: "", backgroundColor: undefined }, { value: "", backgroundColor: undefined }], expect(extraSheetToMatrix(sheet)).toEqual(
]); Array.from({ length: 4 }, () =>
Array.from({ length: 6 }, () => ({ value: "", backgroundColor: undefined })),
),
);
}); });
it("preserves cell identity while rows and columns move", () => { it("preserves cell identity while rows and columns move", () => {
@@ -49,10 +53,11 @@ describe("Extra sheet", () => {
sheet.rows[1].cells[firstColumnId] = "C"; sheet.rows[1].cells[firstColumnId] = "C";
const moved = moveExtraSheetRow(moveExtraSheetColumn(sheet, 0, 1), 0, 1); const moved = moveExtraSheetRow(moveExtraSheetColumn(sheet, 0, 1), 0, 1);
expect(extraSheetToMatrix(moved)).toEqual([ const matrix = extraSheetToMatrix(moved);
[{ value: "", backgroundColor: undefined }, { value: "C", backgroundColor: undefined }], expect(matrix).toHaveLength(4);
[{ value: "B", backgroundColor: undefined }, { value: "A", backgroundColor: undefined }], expect(matrix[0]).toHaveLength(6);
]); expect(matrix[0].slice(0, 2).map((cell) => cell?.value)).toEqual(["", "C"]);
expect(matrix[1].slice(0, 2).map((cell) => cell?.value)).toEqual(["B", "A"]);
}); });
it("adds and removes rows and columns without orphaned cells", () => { it("adds and removes rows and columns without orphaned cells", () => {
@@ -63,14 +68,14 @@ describe("Extra sheet", () => {
addExtraSheetColumn(initial, "Notes", columnId), addExtraSheetColumn(initial, "Notes", columnId),
createId("row"), createId("row"),
); );
expect(expanded.rows).toHaveLength(3); expect(expanded.rows).toHaveLength(5);
expect(expanded.rows.every((row) => row.cells[columnId] === "")).toBe(true); expect(expanded.rows.every((row) => row.cells[columnId] === "")).toBe(true);
const reduced = deleteExtraSheetColumn(expanded, 2); const reduced = deleteExtraSheetColumn(expanded, expanded.columns.length - 1);
expect(reduced.rows.every((row) => !(columnId in row.cells))).toBe(true); expect(reduced.rows.every((row) => !(columnId in row.cells))).toBe(true);
const secondRowId = reduced.rows[1].id; const secondRowId = reduced.rows[1].id;
const withoutFirstRow = deleteExtraSheetRow(reduced, 0); const withoutFirstRow = deleteExtraSheetRow(reduced, 0);
expect(withoutFirstRow.rows).toHaveLength(2); expect(withoutFirstRow.rows).toHaveLength(4);
expect(withoutFirstRow.rows[0].id).toBe(secondRowId); expect(withoutFirstRow.rows[0].id).toBe(secondRowId);
}); });
@@ -78,14 +83,16 @@ describe("Extra sheet", () => {
const createId = idFactory(); const createId = idFactory();
const initial = createExtraSheet(createId); const initial = createExtraSheet(createId);
const expanded = matrixToExtraSheet(initial, [ const expanded = matrixToExtraSheet(initial, [
[{ value: "1" }, { value: "2" }, { value: "3" }], [],
[{ value: "4" }, { value: "5" }, { value: "6" }], [],
[{ value: "7" }, { value: "8" }, { value: "=1+1" }], [],
[],
Array.from({ length: 9 }, (_, index) => ({ value: index === 8 ? "=1+1" : "" })),
], createId); ], createId);
expect(expanded.columns).toHaveLength(3); expect(expanded.columns).toHaveLength(9);
expect(expanded.rows).toHaveLength(3); expect(expanded.rows).toHaveLength(5);
expect(extraSheetToMatrix(expanded)[2][2]?.value).toBe("=1+1"); expect(extraSheetToMatrix(expanded)[4][8]?.value).toBe("=1+1");
expect(extraSheetSchema.safeParse(expanded).success).toBe(true); expect(extraSheetSchema.safeParse(expanded).success).toBe(true);
}); });
@@ -101,6 +108,23 @@ describe("Extra sheet", () => {
} }
}); });
it("returns only the used display range", () => {
const sheet = createExtraSheet(idFactory());
expect(extraSheetUsedRange(sheet)).toBeNull();
sheet.rows[1].cells[sheet.columns[2].id] = "<p>Start</p>";
sheet.rows[3].backgrounds[sheet.columns[4].id] = "#123456";
const used = extraSheetUsedRange(sheet);
expect(used?.columns.map((column) => column.label)).toEqual([
"คอลัมน์ 3",
"คอลัมน์ 4",
"คอลัมน์ 5",
]);
expect(used?.rows).toHaveLength(3);
expect(used && extraSheetSchema.safeParse(used).success).toBe(true);
});
it("updates and sanitizes rich-text cells while preserving legacy text", () => { it("updates and sanitizes rich-text cells while preserving legacy text", () => {
const initial = createExtraSheet(idFactory()); const initial = createExtraSheet(idFactory());
const updated = updateExtraSheetCell(initial, 0, 0, "legacy\ntext"); const updated = updateExtraSheetCell(initial, 0, 0, "legacy\ntext");
@@ -121,10 +145,11 @@ describe("Extra sheet", () => {
{ row: 1, column: 1 }, { row: 1, column: 1 },
], { value: "<p>Shared</p>", backgroundColor: "#16A34A" }); ], { value: "<p>Shared</p>", backgroundColor: "#16A34A" });
expect(extraSheetToMatrix(updated)).toEqual([ const matrix = extraSheetToMatrix(updated);
[{ value: "<p>Shared</p>", backgroundColor: "#16A34A" }, { value: "", backgroundColor: undefined }], expect(matrix[0][0]).toMatchObject({ value: "<p>Shared</p>", backgroundColor: "#16A34A" });
[{ value: "", backgroundColor: undefined }, { value: "<p>Shared</p>", backgroundColor: "#16A34A" }], expect(matrix[1][1]).toMatchObject({ value: "<p>Shared</p>", backgroundColor: "#16A34A" });
]); expect(matrix[0][1]).toMatchObject({ value: "", backgroundColor: undefined });
expect(matrix[1][0]).toMatchObject({ value: "", backgroundColor: undefined });
expect(extraSheetSchema.safeParse(updated).success).toBe(true); expect(extraSheetSchema.safeParse(updated).success).toBe(true);
expect(updateExtraSheetCells(updated, [{ row: 0, column: 0 }], { backgroundColor: null }) expect(updateExtraSheetCells(updated, [{ row: 0, column: 0 }], { backgroundColor: null })
.rows[0].backgrounds).toEqual({}); .rows[0].backgrounds).toEqual({});
@@ -204,10 +229,10 @@ describe("Extra sheet", () => {
expect(copied).toBe('"Alpha\nBeta"'); expect(copied).toBe('"Alpha\nBeta"');
expect(parseExtraSheetTsv('"Alpha\nBeta"\tGamma')).toEqual([["Alpha\nBeta", "Gamma"]]); expect(parseExtraSheetTsv('"Alpha\nBeta"\tGamma')).toEqual([["Alpha\nBeta", "Gamma"]]);
const pasted = pasteExtraSheetText(sheet, { row: 2, column: 2 }, '"Alpha\nBeta"\tGamma', createId); const pasted = pasteExtraSheetText(sheet, { row: 2, column: 7 }, '"Alpha\nBeta"\tGamma', createId);
expect(pasted.rows).toHaveLength(3); expect(pasted.rows).toHaveLength(4);
expect(pasted.columns).toHaveLength(4); expect(pasted.columns).toHaveLength(9);
expect(pasted.rows[2].cells[pasted.columns[2].id]).toBe("<p>Alpha<br>Beta</p>"); expect(pasted.rows[2].cells[pasted.columns[7].id]).toBe("<p>Alpha<br>Beta</p>");
expect(pasted.rows[2].cells[pasted.columns[3].id]).toBe("<p>Gamma</p>"); expect(pasted.rows[2].cells[pasted.columns[8].id]).toBe("<p>Gamma</p>");
}); });
}); });
+37 -3
View File
@@ -68,6 +68,40 @@ export type ExtraSheetCell = {
export type ExtraSheetMatrix = Array<Array<ExtraSheetCell | undefined>>; export type ExtraSheetMatrix = Array<Array<ExtraSheetCell | undefined>>;
type SheetPoint = { row: number; column: number }; type SheetPoint = { row: number; column: number };
export function extraSheetUsedRange(sheet: ExtraSheet): ExtraSheet | null {
const usedPoints = sheet.rows.flatMap((row, rowIndex) =>
sheet.columns.flatMap((column, columnIndex) => {
const used = extraRichTextToPlainText(row.cells[column.id] ?? "").trim()
|| row.backgrounds?.[column.id]
|| row.alignments?.[column.id];
return used ? [{ row: rowIndex, column: columnIndex }] : [];
}),
);
if (!usedPoints.length) return null;
const rowIndexes = usedPoints.map((point) => point.row);
const columnIndexes = usedPoints.map((point) => point.column);
const firstRow = Math.min(...rowIndexes);
const lastRow = Math.max(...rowIndexes);
const firstColumn = Math.min(...columnIndexes);
const lastColumn = Math.max(...columnIndexes);
const columns = sheet.columns.slice(firstColumn, lastColumn + 1);
return {
columns,
rows: sheet.rows.slice(firstRow, lastRow + 1).map((row) => ({
...row,
cells: Object.fromEntries(columns.map((column) => [column.id, row.cells[column.id] ?? ""])),
backgrounds: Object.fromEntries(columns.flatMap((column) =>
row.backgrounds?.[column.id] ? [[column.id, row.backgrounds[column.id]]] : [],
)),
alignments: Object.fromEntries(columns.flatMap((column) =>
row.alignments?.[column.id] ? [[column.id, row.alignments[column.id]]] : [],
)),
})),
};
}
export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): string { export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): string {
const labels = new Set(columns.map((column) => column.label.toLocaleLowerCase())); const labels = new Set(columns.map((column) => column.label.toLocaleLowerCase()));
let index = columns.length + 1; let index = columns.length + 1;
@@ -76,14 +110,14 @@ export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): strin
} }
export function createExtraSheet(createId: (prefix: string) => string): ExtraSheet { export function createExtraSheet(createId: (prefix: string) => string): ExtraSheet {
const columns = [1, 2].map((index) => ({ const columns = Array.from({ length: 6 }, (_, index) => ({
id: createId("extra-sheet-column"), id: createId("extra-sheet-column"),
label: `คอลัมน์ ${index}`, label: `คอลัมน์ ${index + 1}`,
width: DEFAULT_EXTRA_SHEET_COLUMN_WIDTH, width: DEFAULT_EXTRA_SHEET_COLUMN_WIDTH,
})); }));
return { return {
columns, columns,
rows: [1, 2].map(() => ({ rows: Array.from({ length: 4 }, () => ({
id: createId("extra-sheet-row"), id: createId("extra-sheet-row"),
cells: Object.fromEntries(columns.map((column) => [column.id, ""])), cells: Object.fromEntries(columns.map((column) => [column.id, ""])),
backgrounds: {}, backgrounds: {},