From e5ae2c173e37807d8df1f261dcf5b3e6132667a4 Mon Sep 17 00:00:00 2001 From: liaibo Date: Sat, 29 Nov 2025 09:35:36 +0800 Subject: [PATCH] git save current version as 0.02 --- wolai-frontend/src/app/globals.css | 5 + .../src/app/tables/[tableId]/view/page.tsx | 4 +- .../online-table/FullScreenTableEditor.tsx | 76 ++++++++- .../online-table/HeadlessTableViewer.tsx | 157 ++++++++++++++++-- 4 files changed, 217 insertions(+), 25 deletions(-) diff --git a/wolai-frontend/src/app/globals.css b/wolai-frontend/src/app/globals.css index 21318910..cf77e4ca 100644 --- a/wolai-frontend/src/app/globals.css +++ b/wolai-frontend/src/app/globals.css @@ -407,4 +407,9 @@ .wolai-media__resize-handle.is-dragging { background: rgba(37, 99, 235, 0.85); } + /* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */ + .luckysheet-input-box, + #luckysheet-rich-text-editor { + z-index: 100 !important; + } } diff --git a/wolai-frontend/src/app/tables/[tableId]/view/page.tsx b/wolai-frontend/src/app/tables/[tableId]/view/page.tsx index 54786c47..e9919ed5 100644 --- a/wolai-frontend/src/app/tables/[tableId]/view/page.tsx +++ b/wolai-frontend/src/app/tables/[tableId]/view/page.tsx @@ -13,11 +13,11 @@ export default async function TableViewerPage({ params, searchParams }: TableVie const resolvedSearch = await searchParams; const tableId = resolvedParams.tableId; const embedMode = resolvedSearch?.embed === "1"; + const allowEditing = resolvedSearch?.readonly !== "1"; return (
- +
); } - diff --git a/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx b/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx index cab51730..bda2a640 100644 --- a/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx +++ b/wolai-frontend/src/components/online-table/FullScreenTableEditor.tsx @@ -41,6 +41,15 @@ const isPrintableKey = (event: KeyboardEvent) => { 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) => { if (!element) return false; if (element.closest(".luckysheet-wa-editor")) { @@ -209,11 +218,23 @@ const FullScreenTableEditor: React.FC = ({ tableId, }, [isLuckysheetReady]); const focusLuckysheetEditor = useCallback(() => { - requestAnimationFrame(() => { + 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); }); }, []); @@ -325,18 +346,53 @@ const FullScreenTableEditor: React.FC = ({ tableId, if (!isSingleCellSelection(range)) { return; } - setTimeout(() => { - const editor = document.getElementById("luckysheet-rich-text-editor"); - if (editor && document.activeElement === editor) { - return; - } - luckysheetInstance.enterEditMode(); - focusLuckysheetEditor(); - }, 0); + setTimeout(() => { + const editor = document.getElementById("luckysheet-rich-text-editor"); + if (editor && document.activeElement === editor && isInlineEditorVisible()) { + return; + } + luckysheetInstance.enterEditMode(); + focusLuckysheetEditor(); + }, 0); }, [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 = ["pointerup", "mouseup", "touchend"]; + events.forEach((eventName) => { + container.addEventListener(eventName, handlePointerUp, true); + }); + return () => { + events.forEach((eventName) => { + container.removeEventListener(eventName, handlePointerUp, true); + }); + }; + }, [isLuckysheetReady, tryEnterSingleClickEdit]); + // Luckysheet 初始化和清理 useEffect(() => { if (!isLuckysheetReady || !tableData || !containerRef.current || !window.luckysheet) { @@ -372,6 +428,8 @@ const FullScreenTableEditor: React.FC = ({ tableId, allowUpdate: false, gridKey, loadUrl, + pointEdit: true, + pointEditZoom: typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1, uploadImage: async (file: File) => { const formData = new FormData(); formData.append("image", file); diff --git a/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx b/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx index 29ff2be5..e6e035ca 100644 --- a/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx +++ b/wolai-frontend/src/components/online-table/HeadlessTableViewer.tsx @@ -15,14 +15,41 @@ import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoad import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; 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 { tableId: string; embed?: boolean; + editable?: boolean; } const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-"; -const HeadlessTableViewer: React.FC = ({ tableId, embed = false }) => { +const HeadlessTableViewer: React.FC = ({ tableId, embed = false, editable }) => { const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]); const containerRef = useRef(null); const isLuckysheetReady = useLuckysheetLoader(); @@ -62,13 +89,72 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe }; }, [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 () => { - if ( - !embed || - !table || - !window.luckysheet || - typeof window.luckysheet.getluckysheetfile !== "function" - ) { + if (!allowInlineEdit || !table || !window.luckysheet || typeof window.luckysheet.getluckysheetfile !== "function") { return; } setIsSaving(true); @@ -97,7 +183,7 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe } finally { setIsSaving(false); } - }, [embed, table, tableId]); + }, [allowInlineEdit, table, tableId]); const debouncedPersist = useDebouncedCallback(() => { void persistSnapshot(); @@ -132,21 +218,21 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe showtoolbar: false, showsheetbar: sheets.length > 1, showstatisticBar: false, - allowEdit: embed, + allowEdit: allowInlineEdit, allowUpdate: false, enableAddBackTop: false, enableAddRow: false, row: DEFAULT_TABLE_ROWS, column: DEFAULT_TABLE_COLUMNS, data: sheets, - pointEdit: embed, + pointEdit: allowInlineEdit, pointEditZoom: window.devicePixelRatio ?? 1, - pointEditUpdate: embed + pointEditUpdate: allowInlineEdit ? () => { debouncedPersist(); } : undefined, - hook: embed + hook: allowInlineEdit ? { updated: () => { debouncedPersist(); @@ -160,7 +246,50 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe 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 = ["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 overlayText = !isLuckysheetReady ? "正在加载 Luckysheet 资源..." : "正在加载表格数据..."; @@ -203,7 +332,7 @@ const HeadlessTableViewer: React.FC = ({ tableId, embe )} - {embed && (isSaving || saveError) && ( + {allowInlineEdit && (isSaving || saveError) && (
{isSaving && 同步中...} {saveError && {saveError}}