Files
mnote/wolai-frontend/src/components/editor/blocks/OnlineTableBlock.tsx
T

257 lines
8.2 KiB
TypeScript
Raw Normal View History

2026-04-13 19:21:42 +08:00
"use client";
import { BlockNoteEditor, Block } from "@blocknote/core";
import { createReactBlockSpec } from "@blocknote/react";
2026-01-26 13:19:48 +08:00
import React, { useCallback, useMemo, useState } from "react";
import type { CustomBlockSchema } from "../schema";
import CompactTablePreview from "@/components/online-table/CompactTablePreview";
import { useEditorBridgeStore } from "@/store/editor-bridge";
2026-04-13 19:21:42 +08:00
const DEFAULT_WIDTH = 960;
const DEFAULT_HEIGHT = 520;
const MIN_WIDTH = 420;
const MAX_WIDTH = 1400;
const MIN_HEIGHT = 320;
const MAX_HEIGHT = 900;
type ResizeHandle =
| "left"
| "right"
| "top"
| "bottom"
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right";
const handleMapping: Record<
ResizeHandle,
{ horizontal?: "left" | "right"; vertical?: "top" | "bottom" }
> = {
left: { horizontal: "left" },
right: { horizontal: "right" },
top: { vertical: "top" },
bottom: { vertical: "bottom" },
"top-left": { horizontal: "left", vertical: "top" },
"top-right": { horizontal: "right", vertical: "top" },
"bottom-left": { horizontal: "left", vertical: "bottom" },
"bottom-right": { horizontal: "right", vertical: "bottom" },
};
// 占位符组件:在紧凑模式下渲染表格块
const OnlineTableBlockComponent = ({
block,
editor,
2026-01-08 06:28:14 +08:00
}: any) => {
2026-04-13 19:21:42 +08:00
const { tableId } = block.props;
const openTableFullScreen = useEditorBridgeStore((state) => state.bridge?.openTableFullScreen);
2025-11-29 05:16:23 +08:00
const storedWidth = typeof block.props.width === "number" ? block.props.width : undefined;
const storedHeight = typeof block.props.height === "number" ? block.props.height : undefined;
const [activeHandle, setActiveHandle] = useState<ResizeHandle | null>(null);
const [draftSize, setDraftSize] = useState({
width: storedWidth ?? DEFAULT_WIDTH,
height: storedHeight ?? DEFAULT_HEIGHT,
});
2026-01-26 13:19:48 +08:00
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
const committedSize = useMemo(
() => ({
width: clamp(storedWidth ?? DEFAULT_WIDTH, MIN_WIDTH, MAX_WIDTH),
height: clamp(storedHeight ?? DEFAULT_HEIGHT, MIN_HEIGHT, MAX_HEIGHT),
}),
[storedHeight, storedWidth],
);
2025-11-29 05:16:23 +08:00
const commitSize = useCallback(
(next: { width: number; height: number }) => {
setDraftSize(next);
editor.updateBlock(block, {
2026-04-13 19:21:42 +08:00
props: {
...block.props,
width: next.width,
2025-11-29 05:16:23 +08:00
height: next.height,
},
});
},
2026-01-26 13:19:48 +08:00
[block, editor],
2025-11-29 05:16:23 +08:00
);
2026-04-13 19:21:42 +08:00
// 阶段三:实现双击/按钮进入全屏编辑
const handleFullScreen = () => {
if (openTableFullScreen) {
openTableFullScreen(tableId);
} else {
console.error("Editor bridge not ready or openTableFullScreen missing.");
}
};
const handleDelete = useCallback(() => {
editor.removeBlocks([block.id]);
2025-11-23 20:04:29 +08:00
}, [block.id, editor]);
2025-11-29 05:16:23 +08:00
const startResize = useCallback(
(handle: ResizeHandle) => (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const startY = event.clientY;
2026-01-26 13:19:48 +08:00
const startWidth = committedSize.width;
const startHeight = committedSize.height;
2025-11-29 05:16:23 +08:00
let nextWidth = startWidth;
let nextHeight = startHeight;
const axes = handleMapping[handle];
setActiveHandle(handle);
2026-01-26 13:19:48 +08:00
setDraftSize({ width: startWidth, height: startHeight });
2025-11-29 05:16:23 +08:00
document.body.style.userSelect = "none";
const cursor =
axes.horizontal && axes.vertical
? axes.horizontal === "left"
2026-04-13 19:21:42 +08:00
? axes.vertical === "top"
? "nwse-resize"
: "nesw-resize"
: axes.vertical === "top"
? "nesw-resize"
: "nwse-resize"
: axes.horizontal
? "ew-resize"
: "ns-resize";
document.body.style.cursor = cursor;
const handleMove = (moveEvent: MouseEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
if (axes.horizontal === "left") {
nextWidth = clamp(startWidth - deltaX, MIN_WIDTH, MAX_WIDTH);
} else if (axes.horizontal === "right") {
nextWidth = clamp(startWidth + deltaX, MIN_WIDTH, MAX_WIDTH);
} else {
nextWidth = startWidth;
}
if (axes.vertical === "top") {
nextHeight = clamp(startHeight - deltaY, MIN_HEIGHT, MAX_HEIGHT);
} else if (axes.vertical === "bottom") {
nextHeight = clamp(startHeight + deltaY, MIN_HEIGHT, MAX_HEIGHT);
} else {
nextHeight = startHeight;
}
setDraftSize({
width: nextWidth,
height: nextHeight,
});
};
const handleUp = () => {
window.removeEventListener("mousemove", handleMove);
window.removeEventListener("mouseup", handleUp);
document.body.style.userSelect = "";
document.body.style.cursor = "";
setActiveHandle(null);
commitSize({
width: Math.round(nextWidth),
height: Math.round(nextHeight),
});
};
2025-11-29 05:16:23 +08:00
window.addEventListener("mousemove", handleMove);
window.addEventListener("mouseup", handleUp);
},
2026-01-26 13:19:48 +08:00
[commitSize, committedSize.height, committedSize.width],
2025-11-29 05:16:23 +08:00
);
2026-01-26 13:19:48 +08:00
const size = useMemo(() => {
const src = activeHandle ? draftSize : committedSize;
return {
width: clamp(src.width, MIN_WIDTH, MAX_WIDTH),
height: clamp(src.height, MIN_HEIGHT, MAX_HEIGHT),
};
}, [activeHandle, committedSize, draftSize]);
2026-04-13 19:21:42 +08:00
const handleClass = (handle: ResizeHandle) =>
`wolai-table-resize-handle wolai-table-resize-handle--${handle} ${
activeHandle === handle ? "is-dragging" : ""
}`;
return (
<div className="w-full overflow-auto" contentEditable={false}>
<div
className="online-table-block group relative mx-auto"
style={{ width: size.width, minWidth: MIN_WIDTH }}
>
<CompactTablePreview
tableId={tableId}
onFullScreen={handleFullScreen}
onDelete={handleDelete}
height={size.height}
/>
<button
type="button"
aria-label="向左拖拽以调整宽度"
className={handleClass("left")}
onMouseDown={startResize("left")}
/>
<button
type="button"
aria-label="向右拖拽以调整宽度"
className={handleClass("right")}
onMouseDown={startResize("right")}
/>
<button
type="button"
aria-label="向上拖拽以调整高度"
className={handleClass("top")}
onMouseDown={startResize("top")}
/>
<button
type="button"
aria-label="向下拖拽以调整高度"
className={handleClass("bottom")}
onMouseDown={startResize("bottom")}
/>
<button
type="button"
aria-label="拖拽以调整宽高"
className={handleClass("top-left")}
onMouseDown={startResize("top-left")}
/>
<button
type="button"
aria-label="拖拽以调整宽高"
className={handleClass("top-right")}
onMouseDown={startResize("top-right")}
/>
<button
type="button"
aria-label="拖拽以调整宽高"
className={handleClass("bottom-left")}
onMouseDown={startResize("bottom-left")}
/>
<button
type="button"
aria-label="拖拽以调整宽高"
className={handleClass("bottom-right")}
onMouseDown={startResize("bottom-right")}
/>
</div>
</div>
);
};
// Block Spec 定义
export const onlineTableBlock = createReactBlockSpec(
{
type: "onlineTable",
propSchema: {
tableId: { default: "new-table-id" }, // 应该在创建时被覆盖
title: { default: "未命名表格" },
width: { default: DEFAULT_WIDTH },
height: { default: DEFAULT_HEIGHT },
},
content: "inline", // 允许内联内容,但通常表格块不会有太多内联内容
},
{
render: (props) => <OnlineTableBlockComponent {...props} />,
}
);