"use client";
import {
AlignCenterIcon,
AlignVerticalJustifyStartIcon,
AlignVerticalJustifyCenterIcon,
AlignVerticalJustifyEndIcon,
AlignLeftIcon,
AlignRightIcon,
BoldIcon,
ClipboardIcon,
CopyIcon,
EraserIcon,
GripHorizontalIcon,
GripVerticalIcon,
ItalicIcon,
MergeIcon,
SplitIcon,
PlusIcon,
PaletteIcon,
Redo2Icon,
ScissorsIcon,
StrikethroughIcon,
Trash2Icon,
Undo2Icon,
UnderlineIcon,
} from "lucide-react";
import Spreadsheet, {
type ColumnIndicatorProps,
type DataEditorProps,
EntireColumnsSelection,
EntireRowsSelection,
PointRange,
RangeSelection,
type Point,
type RowIndicatorProps,
type Selection,
type SpreadsheetRef,
type TableProps,
} from "react-spreadsheet";
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
type ClipboardEvent,
type KeyboardEvent,
} from "react";
import {
closestCenter,
DndContext,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type CollisionDetection,
type DragEndEvent,
} from "@dnd-kit/core";
import {
horizontalListSortingStrategy,
rectSortingStrategy,
sortableKeyboardCoordinates,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { flushSync } from "react-dom";
import { toast } from "sonner";
import {
ExtraSheetCellContext,
ExtraSheetCellView,
sheetRangeRect,
} from "@/components/admin/extra-sheet-cell";
import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog";
import { RichTextContent } from "@/components/rich-text-content";
import { SimpleTooltip } from "@/components/tooltip";
import { Button } from "@/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { ClearableInput as Input } from "@/components/admin/ui/clearable-input";
import {
InputGroup,
InputGroupAddon,
InputGroupTextarea,
} from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import {
addExtraSheetColumn,
addExtraSheetRow,
clearExtraSheetCellFormatting,
canMergeExtraSheetRange,
expandExtraSheetRange,
extraSheetAnchor,
extraSheetMergeAt,
extraSheetMergeMap,
extraSheetRangePoints,
mergeExtraSheetRange,
unmergeExtraSheetRange,
updateExtraSheetCell,
DEFAULT_EXTRA_SHEET_COLUMN_WIDTH,
MAX_EXTRA_SHEET_COLUMN_WIDTH,
MIN_EXTRA_SHEET_COLUMN_WIDTH,
deleteExtraSheetColumn,
deleteExtraSheetRow,
extraSheetRangeToTsv,
extraSheetToMatrix,
insertExtraSheetColumn,
insertExtraSheetRow,
matrixToExtraSheet,
moveExtraSheetColumn,
moveExtraSheetRow,
nextExtraSheetColumnLabel,
nextExtraSheetPoint,
parseExtraSheetTsv,
pasteExtraSheetText,
resizeExtraSheetColumn,
styleExtraSheetCells,
updateExtraSheetCells,
type ExtraSheetAlignment,
type ExtraSheetVerticalAlignment,
type ExtraSheet,
type ExtraSheetMatrix,
} from "@/lib/guides/extra-sheet";
import {
extraRichTextToPlainText,
normalizeExtraRichText,
type RichTextStylePatch,
} from "@/lib/rich-text";
import { cn } from "@/lib/utils";
interface SheetHistory {
items: ExtraSheet[];
index: number;
value: ExtraSheet;
}
const DEFAULT_CELL_BACKGROUND = "#16A34A";
const SHEET_HISTORY_LIMIT = 80;
const columnDragId = (id: string) => `column:${id}`;
const rowDragId = (id: string) => `row:${id}`;
const sameAxisCollision: CollisionDetection = (args) => {
const axis = String(args.active.id).split(":", 1)[0];
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter((container) =>
String(container.id).startsWith(`${axis}:`),
),
});
};
function movedIndex(current: number, from: number, to: number): number {
if (current === from) return to;
if (from < to && current > from && current <= to) return current - 1;
if (to < from && current >= to && current < from) return current + 1;
return current;
}
function spreadsheetColumnName(index: number): string {
let value = index + 1;
let label = "";
while (value > 0) {
value -= 1;
label = String.fromCharCode(65 + (value % 26)) + label;
value = Math.floor(value / 26);
}
return label;
}
function LiteralDataViewer({
cell,
}: {
cell?: {
value?: unknown;
backgroundColor?: string;
alignment?: ExtraSheetAlignment;
};
}) {
return (
);
}
function InlineDataEditor({
cell,
exitEditMode,
initialText,
onReady,
onCommit,
registerCommit,
onNavigate,
}: DataEditorProps<{ value: string }> & {
initialText?: string;
onReady?: () => void;
onCommit: (value: string) => void;
registerCommit: (commit: (() => void) | null) => void;
onNavigate: (key: "Enter" | "Tab", shift: boolean) => void;
}) {
const inputRef = useRef(null);
const originalText = useRef(
extraRichTextToPlainText(String(cell?.value ?? "")),
);
const [text, setText] = useState(
() => initialText ?? extraRichTextToPlainText(String(cell?.value ?? "")),
);
const committed = useRef(false);
const commit = useCallback(() => {
if (committed.current) return;
committed.current = true;
const nextValue = normalizeExtraRichText(text);
if (text !== originalText.current) {
onCommit(nextValue);
}
}, [onCommit, text]);
useLayoutEffect(() => {
registerCommit(commit);
return () => registerCommit(null);
}, [commit, registerCommit]);
useEffect(() => {
onReady?.();
const input = inputRef.current;
input?.focus();
input?.setSelectionRange(input.value.length, input.value.length);
}, [onReady]);
return (
);
}
function EditableColumnIndicator({
id,
column,
label,
selected,
onSelect,
onRename,
onResize,
width,
}: ColumnIndicatorProps & {
id: string;
onRename: (column: number) => void;
onResize: (column: number, width: number, commit: boolean) => void;
width: number;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id,
strategy: horizontalListSortingStrategy,
});
return (
{
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
event.preventDefault();
onResize(column, width + (event.key === "ArrowRight" ? 8 : -8), true);
}}
onPointerDown={(event) => {
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const startWidth = event.currentTarget.parentElement?.getBoundingClientRect().width
?? DEFAULT_EXTRA_SHEET_COLUMN_WIDTH;
const move = (moveEvent: PointerEvent) =>
onResize(column, startWidth + moveEvent.clientX - startX, false);
const up = (upEvent: PointerEvent) => {
onResize(column, startWidth + upEvent.clientX - startX, true);
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up, { once: true });
}}
/>
|
);
}
function SortableRowIndicator({
id,
row,
label,
selected,
onSelect,
}: RowIndicatorProps & { id: string }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id,
strategy: verticalListSortingStrategy,
});
return (
|
);
}
export function ExtraSheetEditor({
value,
createId,
onChange,
onRemove,
}: {
value: ExtraSheet;
createId: (prefix: string) => string;
onChange: (value: ExtraSheet) => void;
onRemove: () => void;
}) {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const sheetRootRef = useRef(null);
const spreadsheetRef = useRef(null);
const libraryActiveRef = useRef(null);
const initialEditTextRef = useRef(null);
const [active, setActive] = useState({ row: 0, column: 0 });
const [labelDialogOpen, setLabelDialogOpen] = useState(false);
const [renaming, setRenaming] = useState(false);
const [columnLabel, setColumnLabel] = useState("");
const [removeOpen, setRemoveOpen] = useState(false);
const [selectedCount, setSelectedCount] = useState(1);
const [selectionKind, setSelectionKind] = useState<"cells" | "rows" | "columns">("cells");
const [history, setHistory] = useState({
items: [value],
index: 0,
value,
});
const pendingInlineCommitRef = useRef<(() => void) | null>(null);
const colorTargetsRef = useRef<{ text?: Point[]; background?: Point[] }>({});
const [sheetSelection, setSheetSelection] = useState(
() => new RangeSelection(new PointRange(active, active)),
);
const [layoutVersion, setLayoutVersion] = useState(0);
const selectedPointsRef = useRef([active]);
const selectedRangeRef = useRef({ start: active, end: active });
const matrix = useMemo(() => extraSheetToMatrix(value), [value]);
const activeColumn = value.columns[active.column];
const activeRow = value.rows[active.row];
const activeValue = activeColumn && activeRow
? activeRow.cells[activeColumn.id] ?? ""
: "";
const activeAlignment = activeColumn && activeRow
? activeRow.alignments?.[activeColumn.id] ?? "left"
: "left";
const activeVerticalAlignment = activeColumn && activeRow
? activeRow.verticalAlignments?.[activeColumn.id] ?? "top" : "top";
const selectedRange = sheetSelection.toRange(matrix) ?? { start: active, end: active };
const canMerge = canMergeExtraSheetRange(value, selectedRange);
const canUnmerge = extraSheetRangePoints(selectedRange).some((point) => !!extraSheetMergeAt(value, point));
const mergeMap = useMemo(() => extraSheetMergeMap(value), [value]);
const cellContext = useMemo(() => ({ merges: mergeMap, layoutVersion }), [mergeMap, layoutVersion]);
const canUndo = history.index > 0;
const canRedo = history.index < history.items.length - 1;
const sortableIds = useMemo(
() => [
...value.columns.map((column) => columnDragId(column.id)),
...value.rows.map((row) => rowDragId(row.id)),
],
[value.columns, value.rows],
);
if (history.value !== value) {
setHistory(
history.items[history.index] === value
? { ...history, value }
: { items: [value], index: 0, value },
);
}
const commitSheet = useCallback(
(next: ExtraSheet) => {
if (JSON.stringify(next) === JSON.stringify(value)) return;
setHistory((current) => {
const items = [
...current.items.slice(0, current.index + 1),
next,
].slice(-SHEET_HISTORY_LIMIT);
return { items, index: items.length - 1, value: next };
});
onChange(next);
},
[onChange, value],
);
const travel = useCallback(
(direction: -1 | 1) => {
const index = history.index + direction;
const next = history.items[index];
if (!next) return;
setHistory((current) => ({ ...current, index, value: next }));
onChange(next);
const point = extraSheetAnchor(next, {
row: Math.min(active.row, next.rows.length - 1),
column: Math.min(active.column, next.columns.length - 1),
});
const range = expandExtraSheetRange(next, point, point);
setActive(point);
selectedPointsRef.current = extraSheetRangePoints(range);
selectedRangeRef.current = range;
setSheetSelection(new RangeSelection(new PointRange(range.start, range.end)));
setSelectedCount(selectedPointsRef.current.length);
const previous = libraryActiveRef.current;
if (!previous || previous.row !== point.row || previous.column !== point.column) {
spreadsheetRef.current?.activate(point);
}
},
[active, history, onChange],
);
const activateOnly = useCallback((point: Point) => {
const range = expandExtraSheetRange(value, point, point);
point = extraSheetAnchor(value, point);
setActive(point);
selectedPointsRef.current = extraSheetRangePoints(range);
selectedRangeRef.current = range;
setSheetSelection(new RangeSelection(new PointRange(range.start, range.end)));
setSelectedCount(selectedPointsRef.current.length);
setSelectionKind("cells");
const previous = libraryActiveRef.current;
if (!previous || previous.row !== point.row || previous.column !== point.column) {
spreadsheetRef.current?.activate(point);
}
}, [value]);
const moveRow = useCallback(
(from: number, to: number) => {
const next = moveExtraSheetRow(value, from, to);
if (next === value) return;
const point = { ...active, row: movedIndex(active.row, from, to) };
commitSheet(next);
activateOnly(point);
},
[active, activateOnly, commitSheet, value],
);
const moveColumn = useCallback(
(from: number, to: number) => {
const next = moveExtraSheetColumn(value, from, to);
if (next === value) return;
const point = { ...active, column: movedIndex(active.column, from, to) };
commitSheet(next);
activateOnly(point);
},
[active, activateOnly, commitSheet, value],
);
const handleDragEnd = useCallback(
({ active: dragged, over }: DragEndEvent) => {
if (!over || dragged.id === over.id) return;
const draggedId = String(dragged.id);
const overId = String(over.id);
if (draggedId.startsWith("row:") && overId.startsWith("row:")) {
const from = value.rows.findIndex(
(row) => rowDragId(row.id) === draggedId,
);
const to = value.rows.findIndex((row) => rowDragId(row.id) === overId);
if (from !== -1 && to !== -1) moveRow(from, to);
}
if (draggedId.startsWith("column:") && overId.startsWith("column:")) {
const from = value.columns.findIndex(
(column) => columnDragId(column.id) === draggedId,
);
const to = value.columns.findIndex(
(column) => columnDragId(column.id) === overId,
);
if (from !== -1 && to !== -1) moveColumn(from, to);
}
},
[moveColumn, moveRow, value.columns, value.rows],
);
const handleSheetKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
if (event.defaultPrevented || target.closest("input, textarea, [contenteditable='true']")) return;
const key = event.key.toLowerCase();
const commandKey = event.metaKey || event.ctrlKey;
const undo =
commandKey && !event.shiftKey && key === "z";
const redo =
(commandKey && event.shiftKey && key === "z") ||
(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) {
event.preventDefault();
travel(-1);
} else if (redo && canRedo) {
event.preventDefault();
travel(1);
} else if (format) {
event.preventDefault();
const activePattern = {
bold: /
commitSheet(styleExtraSheetCells(value, selectedPointsRef.current, patch));
const clearContents = () =>
commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { value: "" }));
const clearFormatting = () =>
commitSheet(clearExtraSheetCellFormatting(value, selectedPointsRef.current));
const selectedTsv = () => extraSheetRangeToTsv(
value,
selectedRangeRef.current.start,
selectedRangeRef.current.end,
);
const copySelection = async () => {
await navigator.clipboard.writeText(selectedTsv());
};
const cutSelection = async () => {
await copySelection();
clearContents();
};
const pasteText = (text: string) => {
if (!text) return;
const pasted = parseExtraSheetTsv(text);
if (selectedCount > 1 && pasted.length === 1 && pasted[0].length === 1) {
commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, {
value: normalizeExtraRichText(pasted[0][0]),
}));
return;
}
commitSheet(pasteExtraSheetText(value, active, text, createId));
};
const pasteFromClipboard = async () => {
pasteText(await navigator.clipboard.readText());
};
const handleCopy = (event: ClipboardEvent) => {
if ((event.target as HTMLElement).closest("input, textarea, [contenteditable='true']")) return;
event.preventDefault();
event.stopPropagation();
event.clipboardData.setData("text/plain", selectedTsv());
};
const handleCut = (event: ClipboardEvent) => {
if ((event.target as HTMLElement).closest("input, textarea, [contenteditable='true']")) return;
handleCopy(event);
clearContents();
};
const handlePaste = (event: ClipboardEvent) => {
if ((event.target as HTMLElement).closest("input, textarea, [contenteditable='true']")) return;
event.preventDefault();
event.stopPropagation();
pasteText(event.clipboardData.getData("text/plain"));
};
const normalizedLabel = columnLabel.trim().toLocaleLowerCase();
const labelInvalid =
!normalizedLabel ||
value.columns.some(
(column, index) =>
index !== (renaming ? active.column : -1) &&
column.label.toLocaleLowerCase() === normalizedLabel,
);
const openAddColumn = () => {
setRenaming(false);
setColumnLabel(nextExtraSheetColumnLabel(value.columns));
setLabelDialogOpen(true);
};
const openRenameColumn = useCallback(
(columnIndex = active.column) => {
const column = value.columns[columnIndex];
if (!column) return;
setActive((current) => ({ ...current, column: columnIndex }));
setRenaming(true);
setColumnLabel(column.label);
setLabelDialogOpen(true);
},
[active.column, value.columns],
);
const saveColumnLabel = () => {
if (labelInvalid) return;
if (renaming) {
commitSheet({
...value,
columns: value.columns.map((column, index) =>
index === active.column
? { ...column, label: columnLabel.trim() }
: column,
),
});
} else {
const column = value.columns.length;
commitSheet(
addExtraSheetColumn(
value,
columnLabel.trim(),
createId("extra-sheet-column"),
),
);
activateOnly({ ...active, column });
}
setLabelDialogOpen(false);
};
const insertRowAt = (index: number) => {
commitSheet(insertExtraSheetRow(value, index, createId("extra-sheet-row")));
activateOnly({ ...active, row: index });
};
const insertColumnAt = (index: number) => {
commitSheet(insertExtraSheetColumn(
value,
index,
nextExtraSheetColumnLabel(value.columns),
createId("extra-sheet-column"),
));
activateOnly({ ...active, column: index });
};
const removeActiveRow = () => {
if (value.rows.length === 1) return;
const next = deleteExtraSheetRow(value, active.row);
commitSheet(next);
activateOnly({ ...active, row: Math.min(active.row, next.rows.length - 1) });
};
const removeActiveColumn = () => {
if (value.columns.length === 1) return;
const next = deleteExtraSheetColumn(value, active.column);
commitSheet(next);
activateOnly({ ...active, column: Math.min(active.column, next.columns.length - 1) });
};
const resizeColumn = useCallback(
(column: number, width: number, commit: boolean) => {
const boundedWidth = Math.round(Math.max(
MIN_EXTRA_SHEET_COLUMN_WIDTH,
Math.min(MAX_EXTRA_SHEET_COLUMN_WIDTH, width),
));
if (!commit) {
const columnElement = sheetRootRef.current?.querySelector(
`col[data-sheet-column-index="${column}"]`,
);
if (columnElement) columnElement.style.width = `${boundedWidth}px`;
return;
}
commitSheet(resizeExtraSheetColumn(value, column, boundedWidth));
},
[commitSheet, value],
);
const columnIndicator = useCallback(
(props: ColumnIndicatorProps) => (
),
[openRenameColumn, resizeColumn, value.columns],
);
const rowIndicator = useCallback(
(props: RowIndicatorProps) => (
),
[value.rows],
);
const clearInitialEditText = useCallback(() => {
initialEditTextRef.current = null;
}, []);
const navigateInlineRef = useRef<(point: Point, key: "Enter" | "Tab", shift: boolean) => void>(() => {});
const commitInlineRef = useRef<(row: number, column: number, text: string) => void>(() => {});
useLayoutEffect(() => {
commitInlineRef.current = (row, column, text) => commitSheet(updateExtraSheetCell(value, row, column, text));
navigateInlineRef.current = (point, key, shift) => {
const direction = key === "Enter" ? { row: 1, column: 0 } : { row: 0, column: shift ? -1 : 1 };
const next = nextExtraSheetPoint(value, point, direction, key === "Tab");
if (next) activateOnly(next);
};
}, [value, commitSheet, activateOnly]);
const registerInlineCommit = useCallback((commit: (() => void) | null) => {
pendingInlineCommitRef.current = commit;
}, []);
const dataEditor = useCallback(
(props: DataEditorProps<{ value: string }>) => (
commitInlineRef.current(props.row, props.column, text)}
registerCommit={registerInlineCommit}
onNavigate={(key, shift) => navigateInlineRef.current({ row: props.row, column: props.column }, key, shift)}
/>
),
[clearInitialEditText, registerInlineCommit],
);
const sheetTable = useCallback(
({ children, columns, hideColumnIndicators }: TableProps) => (
{!hideColumnIndicators ? : null}
{Array.from({ length: columns }, (_, index) => (
))}
{children}
),
[value.columns],
);
useEffect(() => {
const table = sheetRootRef.current?.querySelector("table");
if (!table) return;
let frame = 0;
const observer = new ResizeObserver(() => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => setLayoutVersion((version) => version + 1));
});
observer.observe(table);
return () => { observer.disconnect(); cancelAnimationFrame(frame); };
}, [sheetTable]);
useLayoutEffect(() => {
const grid = sheetRootRef.current?.querySelector(".extra-sheet-grid");
const table = grid?.querySelector("table");
if (!grid || !table) return;
const merge = extraSheetMergeAt(value, active);
const rect = merge && sheetRangeRect(table, merge);
grid.classList.toggle("extra-sheet-has-active-merge", !!rect);
if (rect) {
grid.style.setProperty("--merged-active-width", `${rect.width}px`);
grid.style.setProperty("--merged-active-height", `${rect.height}px`);
}
}, [value, active, layoutVersion]);
const mergeSelection = () => {
try {
const next = mergeExtraSheetRange(value, selectedRangeRef.current);
if (next === value) return;
commitSheet(next);
activateOnly(selectedRangeRef.current.start);
} catch (error) {
toast.error(error instanceof Error ? error.message : "ไม่สามารถผสานเซลล์ได้");
}
};
const unmergeSelection = () => commitSheet(unmergeExtraSheetRange(value, selectedRangeRef.current));
const captureColorTargets = (kind: "text" | "background") => {
colorTargetsRef.current[kind] = [...selectedPointsRef.current];
};
const activeFormats = [
/ Boolean(format));
const activeTextColor = activeValue.match(/color:\s*(#[0-9A-Fa-f]{6})/i)?.[1]
?? "#111111";
return (
{
if (!(event.target as HTMLElement).closest(".Spreadsheet__data-editor")) {
flushSync(() => pendingInlineCommitRef.current?.());
}
}}
onCopyCapture={handleCopy}
onCutCapture={handleCut}
onPasteCapture={handlePaste}
>
ย้อนกลับ Ctrl/⌘ Z
>
}
>
{
const patch: RichTextStylePatch = {};
(["bold", "italic", "underline", "strike"] as const).forEach((format) => {
const wasActive = activeFormats.includes(format);
const isActive = formats.includes(format);
if (wasActive !== isActive) patch[format] = isActive;
});
if (Object.keys(patch).length) applyStyle(patch);
}}
>
ทำซ้ำ Ctrl/⌘ Shift Z
>
}
>
{
const alignment = alignments[0] as ExtraSheetAlignment | undefined;
if (alignment) {
commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { alignment }));
}
}}
>
{
const verticalAlignment = alignments[0] as ExtraSheetVerticalAlignment | undefined;
if (verticalAlignment) commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { verticalAlignment }));
}}
>
{selectedCount > 1
? `${selectedCount} cells`
: `${spreadsheetColumnName(active.column)}${active.row + 1}`}
{
if (event.currentTarget.value !== extraRichTextToPlainText(activeValue)) {
commitSheet(updateExtraSheetCells(value, [active], {
value: normalizeExtraRichText(event.currentTarget.value),
}));
}
}}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
event.currentTarget.blur();
}
}}
/>
column.label)}
ColumnIndicator={columnIndicator}
RowIndicator={rowIndicator}
Table={sheetTable}
DataViewer={LiteralDataViewer}
DataEditor={dataEditor}
onKeyDown={(event) => {
if (
(event.target as HTMLElement).closest(
".Spreadsheet__data-editor",
)
) {
return;
}
if (["Delete", "Backspace"].includes(event.key)) {
event.preventDefault();
commitSheet(updateExtraSheetCells(value, selectedPointsRef.current, { value: "" }));
return;
}
const directions: Record = {
ArrowUp: { row: -1, column: 0 }, ArrowDown: { row: 1, column: 0 },
ArrowLeft: { row: 0, column: -1 }, ArrowRight: { row: 0, column: 1 },
Tab: { row: 0, column: event.shiftKey ? -1 : 1 },
};
const direction = directions[event.key];
if (direction && (!event.shiftKey || event.key === "Tab") && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
const point = nextExtraSheetPoint(value, active, direction, event.key === "Tab");
if (point) activateOnly(point);
return;
}
const startsTyping =
event.key.length === 1 &&
!event.ctrlKey &&
!event.metaKey &&
!event.altKey;
if (event.key !== "F2" && !startsTyping) return;
event.preventDefault();
initialEditTextRef.current = startsTyping
? event.key
: null;
const root = event.currentTarget as HTMLElement;
root
.querySelector(".Spreadsheet__active-cell--view")
?.click();
}}
onBlur={() => { libraryActiveRef.current = null; }}
onActivate={(point) => {
libraryActiveRef.current = point;
const anchor = extraSheetAnchor(value, point);
if (anchor.row !== point.row || anchor.column !== point.column) {
spreadsheetRef.current?.activate(anchor);
return;
}
setActive(anchor);
}}
onSelect={(nextSelection: Selection) => {
const range = nextSelection.toRange(matrix);
if (range) {
const expanded = expandExtraSheetRange(value, range.start, range.end);
selectedPointsRef.current = extraSheetRangePoints(expanded);
selectedRangeRef.current = expanded;
setSelectedCount(selectedPointsRef.current.length);
const changed = expanded.start.row !== range.start.row || expanded.start.column !== range.start.column
|| expanded.end.row !== range.end.row || expanded.end.column !== range.end.column;
setSheetSelection(changed ? new RangeSelection(new PointRange(expanded.start, expanded.end)) : nextSelection);
setSelectionKind(
nextSelection instanceof EntireRowsSelection ? "rows"
: nextSelection instanceof EntireColumnsSelection ? "columns" : "cells",
);
}
}}
onChange={(next) =>
commitSheet(
matrixToExtraSheet(value, next as ExtraSheetMatrix, createId),
)
}
/>
void cutSelection()}>
ตัด Ctrl/⌘ X
void copySelection()}>
คัดลอก Ctrl/⌘ C
void pasteFromClipboard()}>
วาง Ctrl/⌘ V
{selectionKind === "rows" ? (
insertRowAt(active.row)}>เพิ่มแถวด้านบน
insertRowAt(active.row + 1)}>เพิ่มแถวด้านล่าง
ลบแถว
) : selectionKind === "columns" ? (
insertColumnAt(active.column)}>เพิ่มคอลัมน์ด้านซ้าย
insertColumnAt(active.column + 1)}>เพิ่มคอลัมน์ด้านขวา
openRenameColumn()}>เปลี่ยนชื่อคอลัมน์
ลบคอลัมน์
) : (
ผสานเซลล์
ยกเลิกผสาน
ล้างเนื้อหา
ล้างรูปแบบ
)}
{
onRemove();
setRemoveOpen(false);
}}
/>
);
}