j
This commit is contained in:
+33
-14
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user