git save current version as 0.02
This commit is contained in:
@@ -407,4 +407,9 @@
|
|||||||
.wolai-media__resize-handle.is-dragging {
|
.wolai-media__resize-handle.is-dragging {
|
||||||
background: rgba(37, 99, 235, 0.85);
|
background: rgba(37, 99, 235, 0.85);
|
||||||
}
|
}
|
||||||
|
/* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */
|
||||||
|
.luckysheet-input-box,
|
||||||
|
#luckysheet-rich-text-editor {
|
||||||
|
z-index: 100 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ export default async function TableViewerPage({ params, searchParams }: TableVie
|
|||||||
const resolvedSearch = await searchParams;
|
const resolvedSearch = await searchParams;
|
||||||
const tableId = resolvedParams.tableId;
|
const tableId = resolvedParams.tableId;
|
||||||
const embedMode = resolvedSearch?.embed === "1";
|
const embedMode = resolvedSearch?.embed === "1";
|
||||||
|
const allowEditing = resolvedSearch?.readonly !== "1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={embedMode ? "min-h-screen bg-transparent" : "min-h-screen bg-white"}>
|
<div className={embedMode ? "min-h-screen bg-transparent" : "min-h-screen bg-white"}>
|
||||||
<HeadlessTableViewer tableId={tableId} embed={embedMode} />
|
<HeadlessTableViewer tableId={tableId} embed={embedMode} editable={allowEditing} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ const isPrintableKey = (event: KeyboardEvent) => {
|
|||||||
return event.key === "Process" || event.key === "Unidentified";
|
return event.key === "Process" || event.key === "Unidentified";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isInlineEditorVisible = () => {
|
||||||
|
const inputBox = document.getElementById("luckysheet-input-box");
|
||||||
|
if (!inputBox) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const style = window.getComputedStyle(inputBox);
|
||||||
|
return style.top !== "-10000px" && style.display !== "none";
|
||||||
|
};
|
||||||
|
|
||||||
const isElementInsideEditorToolbar = (element: HTMLElement | null) => {
|
const isElementInsideEditorToolbar = (element: HTMLElement | null) => {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
if (element.closest(".luckysheet-wa-editor")) {
|
if (element.closest(".luckysheet-wa-editor")) {
|
||||||
@@ -209,11 +218,23 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
}, [isLuckysheetReady]);
|
}, [isLuckysheetReady]);
|
||||||
|
|
||||||
const focusLuckysheetEditor = useCallback(() => {
|
const focusLuckysheetEditor = useCallback(() => {
|
||||||
requestAnimationFrame(() => {
|
const applyFocus = () => {
|
||||||
const editor = document.getElementById("luckysheet-rich-text-editor");
|
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||||
if (editor && typeof editor.focus === "function") {
|
if (editor && typeof editor.focus === "function") {
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && editor.childNodes.length > 0) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(editor);
|
||||||
|
range.collapse(false);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
applyFocus();
|
||||||
|
setTimeout(applyFocus, 0);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -325,18 +346,53 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
if (!isSingleCellSelection(range)) {
|
if (!isSingleCellSelection(range)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const editor = document.getElementById("luckysheet-rich-text-editor");
|
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||||
if (editor && document.activeElement === editor) {
|
if (editor && document.activeElement === editor && isInlineEditorVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
luckysheetInstance.enterEditMode();
|
luckysheetInstance.enterEditMode();
|
||||||
focusLuckysheetEditor();
|
focusLuckysheetEditor();
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
[focusLuckysheetEditor, isSingleCellSelection],
|
[focusLuckysheetEditor, isSingleCellSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLuckysheetReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const container = document.getElementById(LUCKY_SHEET_CONTAINER_ID);
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handlePointerUp = (event: PointerEvent | MouseEvent | TouchEvent) => {
|
||||||
|
const target = event.target instanceof Node ? event.target : null;
|
||||||
|
if (target && !container.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const selection = window.luckysheet?.getluckysheet_select_save?.();
|
||||||
|
if (!selection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = Array.isArray(selection)
|
||||||
|
? (selection as LuckysheetSelection[])
|
||||||
|
: [selection as LuckysheetSelection];
|
||||||
|
tryEnterSingleClickEdit(normalized);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const events: Array<keyof DocumentEventMap> = ["pointerup", "mouseup", "touchend"];
|
||||||
|
events.forEach((eventName) => {
|
||||||
|
container.addEventListener(eventName, handlePointerUp, true);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
events.forEach((eventName) => {
|
||||||
|
container.removeEventListener(eventName, handlePointerUp, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [isLuckysheetReady, tryEnterSingleClickEdit]);
|
||||||
|
|
||||||
// Luckysheet 初始化和清理
|
// Luckysheet 初始化和清理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLuckysheetReady || !tableData || !containerRef.current || !window.luckysheet) {
|
if (!isLuckysheetReady || !tableData || !containerRef.current || !window.luckysheet) {
|
||||||
@@ -372,6 +428,8 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
|
|||||||
allowUpdate: false,
|
allowUpdate: false,
|
||||||
gridKey,
|
gridKey,
|
||||||
loadUrl,
|
loadUrl,
|
||||||
|
pointEdit: true,
|
||||||
|
pointEditZoom: typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1,
|
||||||
uploadImage: async (file: File) => {
|
uploadImage: async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("image", file);
|
formData.append("image", file);
|
||||||
|
|||||||
@@ -15,14 +15,41 @@ import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoad
|
|||||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||||
import { extractRowsForPreview } from "@/components/online-table/utils";
|
import { extractRowsForPreview } from "@/components/online-table/utils";
|
||||||
|
|
||||||
|
type LuckysheetSelection =
|
||||||
|
| {
|
||||||
|
row?: [number, number];
|
||||||
|
column?: [number, number];
|
||||||
|
row_focus?: number;
|
||||||
|
column_focus?: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const isPrintableKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.defaultPrevented) return false;
|
||||||
|
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
||||||
|
if (event.key === "Enter" || event.key === "Tab" || event.key === "Escape") return false;
|
||||||
|
if (event.key.length === 1) return true;
|
||||||
|
return event.key === "Process" || event.key === "Unidentified";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInlineEditorVisible = () => {
|
||||||
|
const inputBox = document.getElementById("luckysheet-input-box");
|
||||||
|
if (!inputBox) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const style = window.getComputedStyle(inputBox);
|
||||||
|
return style.top !== "-10000px" && style.display !== "none";
|
||||||
|
};
|
||||||
|
|
||||||
interface HeadlessTableViewerProps {
|
interface HeadlessTableViewerProps {
|
||||||
tableId: string;
|
tableId: string;
|
||||||
embed?: boolean;
|
embed?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
|
const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
|
||||||
|
|
||||||
const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embed = false }) => {
|
const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embed = false, editable }) => {
|
||||||
const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]);
|
const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isLuckysheetReady = useLuckysheetLoader();
|
const isLuckysheetReady = useLuckysheetLoader();
|
||||||
@@ -62,13 +89,72 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
};
|
};
|
||||||
}, [tableId, reloadVersion]);
|
}, [tableId, reloadVersion]);
|
||||||
|
|
||||||
|
const allowInlineEdit = editable ?? embed;
|
||||||
|
const focusLuckysheetEditor = useCallback(() => {
|
||||||
|
const applyFocus = () => {
|
||||||
|
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||||
|
if (editor && typeof editor.focus === "function") {
|
||||||
|
editor.focus();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && editor.childNodes.length > 0) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(editor);
|
||||||
|
range.collapse(false);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
applyFocus();
|
||||||
|
setTimeout(applyFocus, 0);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isSingleCellSelection = useCallback((range: LuckysheetSelection[] | undefined) => {
|
||||||
|
if (!Array.isArray(range) || range.length !== 1) return false;
|
||||||
|
const target = range[0];
|
||||||
|
if (!target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rowRange = target.row ?? (typeof target.row_focus === "number" ? [target.row_focus, target.row_focus] : undefined);
|
||||||
|
const columnRange = target.column ?? (typeof target.column_focus === "number" ? [target.column_focus, target.column_focus] : undefined);
|
||||||
|
if (!rowRange || !columnRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return rowRange[0] === rowRange[1] && columnRange[0] === columnRange[1];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tryEnterInlineEdit = useCallback(() => {
|
||||||
|
if (!allowInlineEdit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const luckysheetInstance = window.luckysheet;
|
||||||
|
if (!luckysheetInstance || typeof luckysheetInstance.enterEditMode !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const selection = luckysheetInstance.getluckysheet_select_save?.();
|
||||||
|
const normalized = Array.isArray(selection)
|
||||||
|
? (selection as LuckysheetSelection[])
|
||||||
|
: selection
|
||||||
|
? [selection as LuckysheetSelection]
|
||||||
|
: undefined;
|
||||||
|
if (!isSingleCellSelection(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
const editor = document.getElementById("luckysheet-rich-text-editor");
|
||||||
|
if (editor && document.activeElement === editor && isInlineEditorVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
luckysheetInstance.enterEditMode();
|
||||||
|
focusLuckysheetEditor();
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}, [allowInlineEdit, focusLuckysheetEditor, isSingleCellSelection]);
|
||||||
|
|
||||||
const persistSnapshot = useCallback(async () => {
|
const persistSnapshot = useCallback(async () => {
|
||||||
if (
|
if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") {
|
||||||
!embed ||
|
|
||||||
!table ||
|
|
||||||
!window.luckysheet ||
|
|
||||||
typeof window.luckysheet.getluckysheetfile !== "function"
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
@@ -97,7 +183,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [embed, table, tableId]);
|
}, [allowInlineEdit, table, tableId]);
|
||||||
|
|
||||||
const debouncedPersist = useDebouncedCallback(() => {
|
const debouncedPersist = useDebouncedCallback(() => {
|
||||||
void persistSnapshot();
|
void persistSnapshot();
|
||||||
@@ -132,21 +218,21 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
showtoolbar: false,
|
showtoolbar: false,
|
||||||
showsheetbar: sheets.length > 1,
|
showsheetbar: sheets.length > 1,
|
||||||
showstatisticBar: false,
|
showstatisticBar: false,
|
||||||
allowEdit: embed,
|
allowEdit: allowInlineEdit,
|
||||||
allowUpdate: false,
|
allowUpdate: false,
|
||||||
enableAddBackTop: false,
|
enableAddBackTop: false,
|
||||||
enableAddRow: false,
|
enableAddRow: false,
|
||||||
row: DEFAULT_TABLE_ROWS,
|
row: DEFAULT_TABLE_ROWS,
|
||||||
column: DEFAULT_TABLE_COLUMNS,
|
column: DEFAULT_TABLE_COLUMNS,
|
||||||
data: sheets,
|
data: sheets,
|
||||||
pointEdit: embed,
|
pointEdit: allowInlineEdit,
|
||||||
pointEditZoom: window.devicePixelRatio ?? 1,
|
pointEditZoom: window.devicePixelRatio ?? 1,
|
||||||
pointEditUpdate: embed
|
pointEditUpdate: allowInlineEdit
|
||||||
? () => {
|
? () => {
|
||||||
debouncedPersist();
|
debouncedPersist();
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
hook: embed
|
hook: allowInlineEdit
|
||||||
? {
|
? {
|
||||||
updated: () => {
|
updated: () => {
|
||||||
debouncedPersist();
|
debouncedPersist();
|
||||||
@@ -160,7 +246,50 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
window.luckysheet.destroy(containerId);
|
window.luckysheet.destroy(containerId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [containerId, isLuckysheetReady, table, tableId]);
|
}, [allowInlineEdit, containerId, debouncedPersist, isLuckysheetReady, table, tableId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allowInlineEdit || !isLuckysheetReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handlePointerUp = (event: PointerEvent | MouseEvent | TouchEvent) => {
|
||||||
|
const target = event.target instanceof Node ? event.target : null;
|
||||||
|
if (target && !container.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tryEnterInlineEdit();
|
||||||
|
};
|
||||||
|
const events: Array<keyof DocumentEventMap> = ["pointerup", "mouseup", "touchend"];
|
||||||
|
events.forEach((eventName) => container.addEventListener(eventName, handlePointerUp, true));
|
||||||
|
return () => {
|
||||||
|
events.forEach((eventName) => container.removeEventListener(eventName, handlePointerUp, true));
|
||||||
|
};
|
||||||
|
}, [allowInlineEdit, containerId, isLuckysheetReady, tryEnterInlineEdit]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allowInlineEdit || !isLuckysheetReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (!isPrintableKey(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tryEnterInlineEdit();
|
||||||
|
};
|
||||||
|
const handleCompositionStart = () => {
|
||||||
|
tryEnterInlineEdit();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeydown, true);
|
||||||
|
window.addEventListener("compositionstart", handleCompositionStart, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeydown, true);
|
||||||
|
window.removeEventListener("compositionstart", handleCompositionStart, true);
|
||||||
|
};
|
||||||
|
}, [allowInlineEdit, isLuckysheetReady, tryEnterInlineEdit]);
|
||||||
|
|
||||||
const overlayVisible = isLoading || !isLuckysheetReady;
|
const overlayVisible = isLoading || !isLuckysheetReady;
|
||||||
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
const overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据...";
|
||||||
@@ -203,7 +332,7 @@ const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embe
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{embed && (isSaving || saveError) && (
|
{allowInlineEdit && (isSaving || saveError) && (
|
||||||
<div className="pointer-events-none absolute bottom-2 right-3 flex flex-col items-end text-xs">
|
<div className="pointer-events-none absolute bottom-2 right-3 flex flex-col items-end text-xs">
|
||||||
{isSaving && <span className="rounded-md bg-white/80 px-2 py-0.5 text-gray-500 shadow">同步中...</span>}
|
{isSaving && <span className="rounded-md bg-white/80 px-2 py-0.5 text-gray-500 shadow">同步中...</span>}
|
||||||
{saveError && <span className="mt-1 rounded-md bg-white/80 px-2 py-0.5 text-red-500 shadow">{saveError}</span>}
|
{saveError && <span className="mt-1 rounded-md bg-white/80 px-2 py-0.5 text-red-500 shadow">{saveError}</span>}
|
||||||
|
|||||||
Reference in New Issue
Block a user