diff --git a/packages/core/package.json b/packages/core/package.json index 285934a2..46d79d81 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@fileverse-dev/fortune-core", - "version": "1.3.10", + "version": "1.3.11", "main": "lib/index.js", "module": "es/index.js", "typings": "lib/index.d.ts", diff --git a/packages/core/src/api/range.ts b/packages/core/src/api/range.ts index 619407ce..c196dac4 100644 --- a/packages/core/src/api/range.ts +++ b/packages/core/src/api/range.ts @@ -120,6 +120,16 @@ export function setCellValuesByRange( throw new Error("data size does not match range"); } + const sheet = getSheet(ctx, options); + const sheetId = sheet.id || ctx.currentSheetId; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + for (let i = 0; i < rowCount; i += 1) { for (let j = 0; j < columnCount; j += 1) { const row = range.row[0] + i; @@ -133,8 +143,21 @@ export function setCellValuesByRange( options, callAfterUpdate ); + if (ctx?.hooks?.updateCellYdoc && sheet.data) { + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r: row, c: column, v: sheet.data?.[row]?.[column] ?? null }, + key: `${row}_${column}`, + type: "update", + }); + } } } + + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } export function setCellFormatByRange( diff --git a/packages/core/src/api/sheet.ts b/packages/core/src/api/sheet.ts index 20ddfa6c..4ea28eab 100644 --- a/packages/core/src/api/sheet.ts +++ b/packages/core/src/api/sheet.ts @@ -6,6 +6,7 @@ import { CellMatrix, CellWithRowAndCol, Sheet } from "../types"; import { getSheetIndex } from "../utils"; import { api, + changeSheet, execfunction, getFlowdata, insertUpdateFunctionGroup, @@ -229,6 +230,35 @@ export function copySheet(ctx: Context, sheetId: string) { const sheetOrderList: Record = {}; sheetOrderList[newSheetId] = order; api.setSheetOrder(ctx, sheetOrderList); + + // Make the duplicated sheet active before doing any full-sheet external sync. + // Some integrations derive the active sheet from ctx.currentSheetId. + changeSheet(ctx, newSheetId, undefined, true, true); + + // Duplicating a sheet bypasses per-cell update flows; ensure external sync (e.g. Yjs) gets a full snapshot. + // Prefer updateAllCell for performance; otherwise emit celldata updates for the new sheet. + if (ctx?.hooks?.updateAllCell) { + ctx.hooks.updateAllCell(newSheetId); + } else if (ctx?.hooks?.updateCellYdoc) { + const changes: any[] = []; + const celldata = newSheet.celldata || dataToCelldata(newSheet.data); + if (Array.isArray(celldata)) { + celldata.forEach((d: any) => { + if (d == null) return; + const { r } = d; + const { c } = d; + if (!_.isNumber(r) || !_.isNumber(c)) return; + changes.push({ + sheetId: newSheetId, + path: ["celldata"], + key: `${r}_${c}`, + value: { r, c, v: d.v ?? null }, + type: d.v == null ? "delete" : "update", + }); + }); + } + if (changes.length > 0) ctx.hooks.updateCellYdoc(changes); + } } export function calculateSheetFromula(ctx: Context, id: string) { diff --git a/packages/core/src/events/keyboard.ts b/packages/core/src/events/keyboard.ts index 254355cd..bf1e5baa 100644 --- a/packages/core/src/events/keyboard.ts +++ b/packages/core/src/events/keyboard.ts @@ -650,27 +650,41 @@ export async function handleGlobalKeyDown( e.preventDefault(); return; } + let handledFlvShortcut = false; if ((e.ctrlKey || (e.metaKey && e.shiftKey)) && e.code === "KeyE") { textFormat(ctx, "center"); + handledFlvShortcut = true; } else if ((e.ctrlKey || (e.metaKey && e.shiftKey)) && e.code === "KeyL") { textFormat(ctx, "left"); + handledFlvShortcut = true; } else if ((e.ctrlKey || (e.metaKey && e.shiftKey)) && e.code === "KeyR") { textFormat(ctx, "right"); + handledFlvShortcut = true; } if ((e.metaKey || e.ctrlKey) && e.code === "KeyK") { handleLink(ctx, cellInput); } if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.code === "Semicolon") { fillDate(ctx); + handledFlvShortcut = true; } if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.code === "Semicolon") { fillTime(ctx); + handledFlvShortcut = true; } if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.code === "KeyR") { fillRightData(ctx); + handledFlvShortcut = true; } if ((e.metaKey || e.ctrlKey) && e.code === "KeyD") { fillDownData(ctx); + handledFlvShortcut = true; + } + if (handledFlvShortcut) { + jfrefreshgrid(ctx, null, undefined); + e.stopPropagation(); + e.preventDefault(); + return; } /* FLV */ diff --git a/packages/core/src/events/paste.ts b/packages/core/src/events/paste.ts index 4f95693c..060b4d76 100644 --- a/packages/core/src/events/paste.ts +++ b/packages/core/src/events/paste.ts @@ -373,19 +373,20 @@ const handleFormulaOnPaste = (ctx: Context, d: any) => { cell.f = f; cell.m = v.toString(); x[c] = cell; + + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { + r, + c, + v: d[r][c], + }, + key: `${r}_${c}`, + type: "update", + }); } d[r] = x; - changes.push({ - sheetId: ctx.currentSheetId, - path: ["celldata"], - value: { - r, - c, - v: d[r][c], - }, - key: `${r}_${c}`, - type: "update", - }); } } if (ctx?.hooks?.updateCellYdoc) { @@ -590,7 +591,9 @@ function pasteHandler(ctx: Context, data: any, borderInfo?: any) { // selectHightlightShow(); } jfrefreshgrid(ctx, null, undefined); - handleFormulaOnPaste(ctx, d); + if (data.includes("=")) { + handleFormulaOnPaste(ctx, d); + } // for (let r = 0; r < d.length; r += 1) { // const x = d[r]; // for (let c = 0; c < d[0].length; c += 1) { @@ -806,19 +809,17 @@ function pasteHandler(ctx: Context, data: any, borderInfo?: any) { x[c + curC] = cell; } - changes.push( - changes.push({ - sheetId: ctx.currentSheetId, - path: ["celldata"], - value: { - r, - c, - v: d[r][c], - }, - key: `${r}_${c}`, - type: "update", - }) - ); + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { + r: r + curR, + c: c + curC, + v: d[r + curR][c + curC], + }, + key: `${r + curR}_${c + curC}`, + type: "update", + }); } d[r + curR] = x; } @@ -840,7 +841,9 @@ function pasteHandler(ctx: Context, data: any, borderInfo?: any) { // selectHightlightShow(); // } jfrefreshgrid(ctx, null, undefined); - handleFormulaOnPaste(ctx, d); + if (data.includes("=")) { + handleFormulaOnPaste(ctx, d); + } // for (let r = 0; r < d.length; r += 1) { // const x = d[r]; @@ -956,6 +959,14 @@ function pasteHandlerOfCutPaste( expandRowsAndColumns(d, addr, addc); } + const changes: { + sheetId: string; + path: string[]; + value: any; + key: string; + type: "update"; + }[] = []; + const borderInfoCompute = getBorderInfoCompute(ctx, copySheetId); const c_dataVerification = _.cloneDeep( @@ -1016,6 +1027,13 @@ function pasteHandlerOfCutPaste( } d[i][j] = null; + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: i, c: j, v: null }, + key: `${i}_${j}`, + type: "update", + }); delete dataVerification[`${i}_${j}`]; @@ -1145,6 +1163,13 @@ function pasteHandlerOfCutPaste( } x[c] = _.cloneDeep(value); + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: h, c, v: d[h][c] }, + key: `${h}_${c}`, + type: "update", + }); if (value != null && copyHasMC && x[c]?.mc) { if (x[c]!.mc!.rs != null) { @@ -1175,6 +1200,10 @@ function pasteHandlerOfCutPaste( last.row = [minh, maxh]; last.column = [minc, maxc]; + if (changes.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(changes); + } + // 若有行高改变,重新计算行高改变 if (copyRowlChange) { // if (ctx.currentSheetId !== copySheetIndex) { @@ -1575,6 +1604,14 @@ function pasteHandlerOfCopyPaste( expandRowsAndColumns(d, addr, addc); } + const changes: { + sheetId: string; + path: string[]; + value: any; + key: string; + type: "update"; + }[] = []; + const borderInfoCompute = getBorderInfoCompute(ctx, copySheetIndex); const c_dataVerification = _.cloneDeep( @@ -1700,6 +1737,7 @@ function pasteHandlerOfCopyPaste( } } + let afterHookCalled = false; if (!_.isNil(value) && !_.isNil(value.f)) { let adjustedFormula = value.f; let isError = false; @@ -1783,6 +1821,7 @@ function pasteHandlerOfCopyPaste( m: funcV[1] instanceof Promise ? "[object Promise]" : funcV[1], }); + afterHookCalled = true; } } @@ -1825,6 +1864,16 @@ function pasteHandlerOfCopyPaste( }; } } + // If afterUpdateCell ran for this cell, it is expected to handle Yjs sync. + if (!(ctx?.hooks?.afterUpdateCell && afterHookCalled)) { + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: h, c, v: d[h][c] }, + key: `${h}_${c}`, + type: "update", + }); + } } d[h] = x; } @@ -1973,6 +2022,10 @@ function pasteHandlerOfCopyPaste( } } + if (changes.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(changes); + } + if (copyRowlChange || addr > 0 || addc > 0) { // cfg = rowlenByRange(d, minh, maxh, cfg); // const allParam = { diff --git a/packages/core/src/modules/cell.ts b/packages/core/src/modules/cell.ts index d74b2168..6e0480d3 100644 --- a/packages/core/src/modules/cell.ts +++ b/packages/core/src/modules/cell.ts @@ -158,6 +158,7 @@ export function setCellValue( // 若采用深拷贝,初始化时的单元格属性丢失 // let cell = $.extend(true, {}, d[r][c]); + // const oldValue = _.cloneDeep(d[r][c]); let cell = d[r][c]; let vupdate; @@ -219,6 +220,9 @@ export function setCellValue( } d[r][c] = cell; + // if (ctx?.hooks?.afterUpdateCell) { + // ctx.hooks.afterUpdateCell(r, c, oldValue, d[r][c] ?? null); + // } return; } @@ -464,16 +468,15 @@ export function setCellValue( // } d[r][c] = cell; + // if (ctx?.hooks?.afterUpdateCell) { + // ctx.hooks.afterUpdateCell(r, c, oldValue, d[r][c] ?? null); + // } // after cell data update if (ctx.luckysheet_selection_range) { ctx.luckysheet_selection_range = []; } - // if (ctx.hooks.afterUpdateCell) { - // const newCell = _.isPlainObject(cell) ? { ...cell } : cell; - // console.log("newCell ======== newCell", newCell, _.isPlainObject(cell)); - // ctx.hooks.afterUpdateCell?.(r, c, null, newCell); - // } + // Note: afterUpdateCell is invoked above (with old/new values). } export function getRealCellValue( @@ -1207,7 +1210,9 @@ export function updateCell( cancelNormalSelected(ctx); if (ctx.hooks.afterUpdateCell) { const newValue = _.cloneDeep(d[r][c]); - setTimeout(() => ctx.hooks.afterUpdateCell?.(r, c, null, newValue)); + setTimeout(() => + ctx.hooks.afterUpdateCell?.(r, c, oldValue, newValue) + ); } ctx.formulaCache.execFunctionGlobalData = null; return; @@ -1332,7 +1337,6 @@ export function updateCell( }; } */ - if (ctx.hooks.afterUpdateCell) { const newValue = _.cloneDeep(flowdata[r][c]); const { afterUpdateCell } = ctx.hooks; @@ -1977,6 +1981,7 @@ export function clearSelectedCellFormat(ctx: Context) { const activeSheetIndex = getSheetIndex(ctx, ctx.currentSheetId); if (activeSheetIndex == null) return; + const changeMap = new Map(); const activeSheetFile = ctx.luckysheetfile[activeSheetIndex]; const selectedRanges = ctx.luckysheet_select_save; if (!activeSheetFile || !selectedRanges) return; @@ -2000,15 +2005,29 @@ export function clearSelectedCellFormat(ctx: Context) { rowCells[columnIndex] = keepOnlyValueParts( rowCells[columnIndex] ) as Cell; + const v = (rowCells[columnIndex] as any) ?? null; + const key = `${rowIndex}_${columnIndex}`; + changeMap.set(key, { + sheetId: ctx.currentSheetId, + path: ["celldata"], + key, + value: { r: rowIndex, c: columnIndex, v }, + type: v == null ? "delete" : "update", + }); } } }); + + if (ctx?.hooks?.updateCellYdoc && changeMap.size > 0) { + ctx.hooks.updateCellYdoc(Array.from(changeMap.values())); + } } export function clearRowsCellsFormat(ctx: Context) { const activeSheetIndex = getSheetIndex(ctx, ctx.currentSheetId); if (activeSheetIndex == null) return; + const changeMap = new Map(); const activeSheetFile = ctx.luckysheetfile[activeSheetIndex]; const selectedRanges = ctx.luckysheet_select_save; if (!activeSheetFile || !selectedRanges) return; @@ -2028,15 +2047,29 @@ export function clearRowsCellsFormat(ctx: Context) { rowCells[columnIndex] = keepOnlyValueParts( rowCells[columnIndex] ) as Cell; + const v = (rowCells[columnIndex] as any) ?? null; + const key = `${rowIndex}_${columnIndex}`; + changeMap.set(key, { + sheetId: ctx.currentSheetId, + path: ["celldata"], + key, + value: { r: rowIndex, c: columnIndex, v }, + type: v == null ? "delete" : "update", + }); } } }); + + if (ctx?.hooks?.updateCellYdoc && changeMap.size > 0) { + ctx.hooks.updateCellYdoc(Array.from(changeMap.values())); + } } export function clearColumnsCellsFormat(ctx: Context) { const activeSheetIndex = getSheetIndex(ctx, ctx.currentSheetId); if (activeSheetIndex == null) return; + const changeMap = new Map(); const activeSheetFile = ctx.luckysheetfile[activeSheetIndex]; const selectedRanges = ctx.luckysheet_select_save; if (!activeSheetFile || !selectedRanges) return; @@ -2060,7 +2093,20 @@ export function clearColumnsCellsFormat(ctx: Context) { rowCells[columnIndex] = keepOnlyValueParts( rowCells[columnIndex] ) as Cell; + const v = (rowCells[columnIndex] as any) ?? null; + const key = `${rowIndex}_${columnIndex}`; + changeMap.set(key, { + sheetId: ctx.currentSheetId, + path: ["celldata"], + key, + value: { r: rowIndex, c: columnIndex, v }, + type: v == null ? "delete" : "update", + }); } } }); + + if (ctx?.hooks?.updateCellYdoc && changeMap.size > 0) { + ctx.hooks.updateCellYdoc(Array.from(changeMap.values())); + } } diff --git a/packages/core/src/modules/comment.ts b/packages/core/src/modules/comment.ts index 906f3197..d632eed2 100644 --- a/packages/core/src/modules/comment.ts +++ b/packages/core/src/modules/comment.ts @@ -257,6 +257,18 @@ export function removeEditingComment(ctx: Context, globalCache: GlobalCache) { ctx.commentBoxes = _.filter(ctx.commentBoxes, (v) => v.rc !== `${r}_${c}`); } + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } + if (ctx.hooks.afterUpdateComment) { setTimeout(() => { ctx.hooks.afterUpdateComment?.(r, c, oldValue, value); @@ -302,6 +314,18 @@ export function newComment( autoFocus: true, }; + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: flowdata[r][c] ?? null }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } + if (ctx.hooks.afterInsertComment) { setTimeout(() => { ctx.hooks.afterInsertComment?.(r, c); @@ -361,6 +385,18 @@ export function deleteComment( if (!cell) return; cell.ps = undefined; + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } + if (ctx.hooks.afterDeleteComment) { setTimeout(() => { ctx.hooks.afterDeleteComment?.(r, c); @@ -399,6 +435,19 @@ export function showHideComment( } else { comment.isShow = true; } + + const cell = flowdata?.[r]?.[c]; + if (cell && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } } export function showHideAllComments(ctx: Context) { @@ -423,6 +472,13 @@ export function showHideAllComments(ctx: Context) { const rcs = []; if (allComments.length > 0) { + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; if (isAllShow) { // 全部显示,操作为隐藏所有批注 for (let i = 0; i < allComments.length; i += 1) { @@ -432,6 +488,13 @@ export function showHideAllComments(ctx: Context) { if (comment?.isShow) { comment.isShow = false; rcs.push(`${r}_${c}`); + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: flowdata[r][c] ?? null }, + key: `${r}_${c}`, + type: "update", + }); } } ctx.commentBoxes = []; @@ -443,9 +506,19 @@ export function showHideAllComments(ctx: Context) { if (comment && !comment.isShow) { comment.isShow = true; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: flowdata[r][c] ?? null }, + key: `${r}_${c}`, + type: "update", + }); } } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } } @@ -686,6 +759,18 @@ export function onCommentBoxResizeEnd(ctx: Context, globalCache: GlobalCache) { cell.ps.width = width / ctx.zoomRatio; cell.ps.height = height / ctx.zoomRatio; setEditingComment(ctx, flowdata, r, c); + + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } } } } @@ -750,6 +835,18 @@ export function onCommentBoxMoveEnd(ctx: Context, globalCache: GlobalCache) { cell.ps.left = left / ctx.zoomRatio; cell.ps.top = top / ctx.zoomRatio; setEditingComment(ctx, flowdata, r, c); + + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } } } } diff --git a/packages/core/src/modules/dataVerification.ts b/packages/core/src/modules/dataVerification.ts index d0cd4042..8792201c 100644 --- a/packages/core/src/modules/dataVerification.ts +++ b/packages/core/src/modules/dataVerification.ts @@ -351,6 +351,26 @@ export function checkboxChange(ctx: Context, r: number, c: number) { } const d = getFlowdata(ctx); setCellValue(ctx, r, c, d, value); + + // Sync the data validation state itself (e.g. checkbox checked flag) to Yjs. + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d?.[r]?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }, + { + sheetId: ctx.currentSheetId, + path: ["dataVerification"], + key: `${r}_${c}`, + value: JSON.parse(JSON.stringify(item)), + type: "update", + }, + ]); + } } // 数据无效时的提示信息 @@ -880,6 +900,21 @@ export function setDropdownValue(ctx: Context, value: string, arr: any) { v: value, pillColor: selectedColor, }); + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { + r: rowIndex, + c: colIndex, + v: d?.[rowIndex]?.[colIndex] ?? null, + }, + key: `${rowIndex}_${colIndex}`, + type: "update", + }, + ]); + } const currentRowHeight = getRowHeight(ctx, [rowIndex])[rowIndex]; // eslint-disable-next-line no-unsafe-optional-chaining const newHeight = 22 * (value.split(",").length || valueData?.length) || 22; diff --git a/packages/core/src/modules/dropCell.ts b/packages/core/src/modules/dropCell.ts index 30213eb3..10803f45 100644 --- a/packages/core/src/modules/dropCell.ts +++ b/packages/core/src/modules/dropCell.ts @@ -2337,6 +2337,14 @@ export function updateDropCell(ctx: Context) { const apply_str_c = applyRange.column[0]; const apply_end_c = applyRange.column[1]; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + if (direction === "down" || direction === "up") { const asLen = apply_end_r - apply_str_r + 1; @@ -2350,6 +2358,7 @@ export function updateDropCell(ctx: Context) { for (let j = apply_str_r; j <= apply_end_r; j += 1) { if (hiddenRows.has(`${j}`)) continue; const cell = applyData[j - apply_str_r]; + let afterHookCalled = false; if (cell?.f != null) { const f = `=${formula.functionCopy( @@ -2371,6 +2380,7 @@ export function updateDropCell(ctx: Context) { v: v[1] instanceof Promise ? v[1] : cell.v, m: v[1] instanceof Promise ? "[object Promise]" : v[1], }); + afterHookCalled = true; } if (cell.spl != null) { @@ -2425,6 +2435,16 @@ export function updateDropCell(ctx: Context) { } d[j][i] = cell || null; + // If afterUpdateCell is used for this cell, it is expected to handle Yjs sync. + if (!afterHookCalled) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: j, c: i, v: d[j][i] }, + key: `${j}_${i}`, + type: "update", + }); + } // 边框 const bd_r = copy_str_r + ((j - apply_str_r) % csLen); @@ -2471,6 +2491,7 @@ export function updateDropCell(ctx: Context) { for (let j = apply_end_r; j >= apply_str_r; j -= 1) { if (hiddenRows.has(`${j}`)) continue; const cell = applyData[apply_end_r - j]; + let afterHookCalled = false; if (cell?.f != null) { const f = `=${formula.functionCopy( @@ -2492,6 +2513,7 @@ export function updateDropCell(ctx: Context) { v: v[1] instanceof Promise ? v[1] : cell.v, m: v[1] instanceof Promise ? "[object Promise]" : v[1], }); + afterHookCalled = true; } if (cell.spl != null) { @@ -2534,6 +2556,16 @@ export function updateDropCell(ctx: Context) { } d[j][i] = cell || null; + // If afterUpdateCell is used for this cell, it is expected to handle Yjs sync. + if (!afterHookCalled) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: j, c: i, v: d[j][i] }, + key: `${j}_${i}`, + type: "update", + }); + } // 边框 const bd_r = copy_end_r - ((apply_end_r - j) % csLen); @@ -2589,6 +2621,7 @@ export function updateDropCell(ctx: Context) { for (let j = apply_str_c; j <= apply_end_c; j += 1) { if (hiddenCols.has(`${j}`)) continue; const cell = applyData[j - apply_str_c]; + let afterHookCalled = false; if (cell?.f != null) { const f = `=${formula.functionCopy( @@ -2610,6 +2643,7 @@ export function updateDropCell(ctx: Context) { v: v[1] instanceof Promise ? v[1] : cell.v, m: v[1] instanceof Promise ? "[object Promise]" : v[1], }); + afterHookCalled = true; } if (cell.spl != null) { @@ -2652,6 +2686,16 @@ export function updateDropCell(ctx: Context) { } d[i][j] = cell || null; + // If afterUpdateCell is used for this cell, it is expected to handle Yjs sync. + if (!afterHookCalled) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: i, c: j, v: d[i][j] }, + key: `${i}_${j}`, + type: "update", + }); + } // 边框 const bd_r = i; @@ -2697,6 +2741,7 @@ export function updateDropCell(ctx: Context) { for (let j = apply_end_c; j >= apply_str_c; j -= 1) { if (hiddenCols.has(`${j}`)) continue; const cell = applyData[apply_end_c - j]; + let afterHookCalled = false; if (cell?.f != null) { const f = `=${formula.functionCopy( @@ -2718,6 +2763,7 @@ export function updateDropCell(ctx: Context) { v: v[1] instanceof Promise ? v[1] : cell.v, m: v[1] instanceof Promise ? "[object Promise]" : v[1], }); + afterHookCalled = true; } if (cell.spl != null) { @@ -2760,6 +2806,16 @@ export function updateDropCell(ctx: Context) { } d[i][j] = cell || null; + // If afterUpdateCell is used for this cell, it is expected to handle Yjs sync. + if (!afterHookCalled) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: i, c: j, v: d[i][j] }, + key: `${i}_${j}`, + type: "update", + }); + } // 边框 const bd_r = i; @@ -2804,6 +2860,10 @@ export function updateDropCell(ctx: Context) { } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } + // 条件格式 const cdformat = file.luckysheet_conditionformat_save; if (cdformat != null && cdformat.length > 0) { diff --git a/packages/core/src/modules/formula.ts b/packages/core/src/modules/formula.ts index f1879efe..31bc206b 100644 --- a/packages/core/src/modules/formula.ts +++ b/packages/core/src/modules/formula.ts @@ -1385,6 +1385,21 @@ export function groupValuesRefresh(ctx: Context) { updateValue.v = item.v; updateValue.f = item.f; setCellValue(ctx, item.r, item.c, data, updateValue); + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: item.id, + path: ["celldata"], + value: { + r: item.r, + c: item.c, + v: data?.[item.r]?.[item.c] ?? null, + }, + key: `${item.r}_${item.c}`, + type: "update", + }, + ]); + } // server.saveParam("v", item.id, data[item.r][item.c], { // "r": item.r, // "c": item.c diff --git a/packages/core/src/modules/hyperlink.ts b/packages/core/src/modules/hyperlink.ts index b8e60716..59415d3b 100644 --- a/packages/core/src/modules/hyperlink.ts +++ b/packages/core/src/modules/hyperlink.ts @@ -72,6 +72,25 @@ export function saveHyperlink( cell.m = linkText || linkAddress; cell.hl = { r, c, id: ctx.currentSheetId }; flowdata[r][c] = cell; + + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["hyperlink"], + key: `${r}_${c}`, + value: { linkType, linkAddress }, + type: "update", + }, + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } } const offsets = ctx.linkCard?.selectionOffsets; if (offsets) { @@ -126,6 +145,7 @@ export function removeHyperlink(ctx: Context, r: number, c: number) { if (!allowEdit) return; const sheetIndex = getSheetIndex(ctx, ctx.currentSheetId); const flowdata = getFlowdata(ctx); + let updatedCell: any = null; if (flowdata != null && sheetIndex != null) { const hyperlink = _.omit( ctx.luckysheetfile[sheetIndex].hyperlink, @@ -137,9 +157,32 @@ export function removeHyperlink(ctx: Context, r: number, c: number) { delete flowdata[r][c]?.hl; delete flowdata[r][c]?.un; delete flowdata[r][c]?.fc; + updatedCell = flowdata[r][c]; } } ctx.linkCard = undefined; + + if (ctx?.hooks?.updateCellYdoc) { + const changes: any[] = [ + { + sheetId: ctx.currentSheetId, + path: ["hyperlink"], + key: `${r}_${c}`, + value: null, + type: "delete", + }, + ]; + if (updatedCell != null) { + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: updatedCell }, + key: `${r}_${c}`, + type: "update", + }); + } + ctx.hooks.updateCellYdoc(changes); + } } export function showLinkCard( diff --git a/packages/core/src/modules/merge.ts b/packages/core/src/modules/merge.ts index a6030026..dbc8ec7f 100644 --- a/packages/core/src/modules/merge.ts +++ b/packages/core/src/modules/merge.ts @@ -26,6 +26,14 @@ export function mergeCells( const d = sheet.data!; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + // if (!checkProtectionNotEnable(ctx.currentSheetId)) { // return; // } @@ -67,6 +75,13 @@ export function mergeCells( delete cell_clone.spl; d[r][c] = cell_clone; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } } @@ -134,6 +149,13 @@ export function mergeCells( delete cell_clone.spl; d[r][c] = cell_clone; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } } @@ -176,6 +198,13 @@ export function mergeCells( } d[r][c] = { mc: { r: r1, c: c1 } }; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } @@ -183,6 +212,13 @@ export function mergeCells( const a = d[r1][c1]; if (!a) return; a.mc = { r: r1, c: c1, rs: r2 - r1 + 1, cs: c2 - c1 + 1 }; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r: r1, c: c1, v: d[r1][c1] }, + key: `${r1}_${c1}`, + type: "update", + }); cfg.merge[`${r1}_${c1}`] = { r: r1, @@ -213,12 +249,26 @@ export function mergeCells( } d[r][c] = { mc: { r: r1, c } }; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } d[r1][c] = fv; const a = d[r1][c]; if (!a) return; a.mc = { r: r1, c, rs: r2 - r1 + 1, cs: 1 }; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r: r1, c, v: d[r1][c] }, + key: `${r1}_${c}`, + type: "update", + }); cfg.merge[`${r1}_${c}`] = { r: r1, @@ -250,12 +300,26 @@ export function mergeCells( } d[r][c] = { mc: { r, c: c1 } }; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } d[r][c1] = fv; const a = d[r][c1]; if (!a) return; a.mc = { r, c: c1, rs: 1, cs: c2 - c1 + 1 }; + cellChanges.push({ + sheetId, + path: ["celldata"], + value: { r, c: c1, v: d[r][c1] }, + key: `${r}_${c1}`, + type: "update", + }); cfg.merge[`${r}_${c1}`] = { r, @@ -267,6 +331,9 @@ export function mergeCells( } } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } sheet.config = cfg; if (sheet.id === ctx.currentSheetId) { ctx.config = cfg; diff --git a/packages/core/src/modules/moveCells.ts b/packages/core/src/modules/moveCells.ts index 456008e8..f51db432 100644 --- a/packages/core/src/modules/moveCells.ts +++ b/packages/core/src/modules/moveCells.ts @@ -321,6 +321,14 @@ export function onCellsMoveEnd( const borderInfoCompute = getBorderInfoCompute(ctx, ctx.currentSheetId); + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + const hyperLinkList: Record< string, { @@ -347,6 +355,13 @@ export function onCellsMoveEnd( } d[r][c] = null; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: null }, + key: `${r}_${c}`, + type: "update", + }); if (ctx.luckysheetfile[index].hyperlink?.[`${r}_${c}`]) { hyperLinkList[`${r}_${c}`] = ctx.luckysheetfile[index].hyperlink?.[`${r}_${c}`]!; @@ -481,6 +496,13 @@ export function onCellsMoveEnd( } } d[r + row_s][c + col_s] = value; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: r + row_s, c: c + col_s, v: d[r + row_s][c + col_s] }, + key: `${r + row_s}_${c + col_s}`, + type: "update", + }); if (hyperLinkList?.[`${r + last.row[0]}_${c + last.column[0]}`]) { ctx.luckysheetfile[index].hyperlink![`${r + row_s}_${c + col_s}`] = hyperLinkList?.[`${r + last.row[0]}_${c + last.column[0]}`] as { @@ -550,6 +572,10 @@ export function onCellsMoveEnd( ctx.luckysheetfile[sheetIndex].config = _.assign({}, cfg); } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } + // const allParam = { // cfg, // RowlChange, diff --git a/packages/core/src/modules/rowcol.ts b/packages/core/src/modules/rowcol.ts index 3a7db5ef..5f4d4531 100644 --- a/packages/core/src/modules/rowcol.ts +++ b/packages/core/src/modules/rowcol.ts @@ -27,6 +27,71 @@ const refreshLocalMergeData = (merge_new: Record, file: Sheet) => { } }); }; +const getMergeBounds = (mergeMap: Record | null | undefined) => { + if (!mergeMap) return null; + let minR = Infinity; + let minC = Infinity; + let maxR = -Infinity; + let maxC = -Infinity; + + Object.values(mergeMap).forEach((mc: any) => { + if (!mc) return; + const r = Number(mc.r); + const c = Number(mc.c); + const rs = Number(mc.rs ?? 1); + const cs = Number(mc.cs ?? 1); + if (!Number.isFinite(r) || !Number.isFinite(c)) return; + + minR = Math.min(minR, r); + minC = Math.min(minC, c); + maxR = Math.max(maxR, r + Math.max(1, rs) - 1); + maxC = Math.max(maxC, c + Math.max(1, cs) - 1); + }); + + if (minR === Infinity) return null; + return { minR, minC, maxR, maxC }; +}; + +const emitCellRangeToYdoc = ( + ctx: Context, + sheetId: string, + d: any[][], + r1: number, + r2: number, + c1: number, + c2: number +) => { + if (!ctx?.hooks?.updateCellYdoc) return; + if (!d || !Array.isArray(d) || d.length === 0) return; + const rowEnd = Math.min(r2, d.length - 1); + const colEnd = Math.min(c2, (d[0]?.length ?? 0) - 1); + const rowStart = Math.max(0, r1); + const colStart = Math.max(0, c1); + if (rowStart > rowEnd || colStart > colEnd) return; + + const changes: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + + for (let r = rowStart; r <= rowEnd; r += 1) { + const row = d[r] || []; + for (let c = colStart; c <= colEnd; c += 1) { + changes.push({ + sheetId, + path: ["celldata"], + value: { r, c, v: row?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }); + } + } + + if (changes.length > 0) ctx.hooks.updateCellYdoc(changes); +}; /** * 增加行列 @@ -1153,6 +1218,38 @@ export function insertRowCol( refreshLocalMergeData(merge_new, file); + // Yjs: row/col insertion shifts many cells; emit the disturbed range. + const mergeBounds = getMergeBounds(cfg.merge); + if (type === "row") { + const baseStart = direction === "lefttop" ? index : index + 1; + const startR = mergeBounds + ? Math.min(baseStart, mergeBounds.minR) + : baseStart; + emitCellRangeToYdoc( + ctx, + id, + d as any[][], + startR, + d.length - 1, + 0, + (d[0]?.length ?? 1) - 1 + ); + } else { + const baseStart = direction === "lefttop" ? index : index + 1; + const startC = mergeBounds + ? Math.min(baseStart, mergeBounds.minC) + : baseStart; + emitCellRangeToYdoc( + ctx, + id, + d as any[][], + 0, + d.length - 1, + startC, + (d[0]?.length ?? 1) - 1 + ); + } + // if (type === "row") { // const scrollLeft = $("#luckysheet-cell-main").scrollLeft(); // const scrollTop = $("#luckysheet-cell-main").scrollTop(); @@ -2063,6 +2160,32 @@ export function deleteRowCol( refreshLocalMergeData(merge_new, file); + // Yjs: row/col deletion shifts many cells; emit the disturbed range. + const mergeBounds = getMergeBounds(cfg.merge); + if (type === "row") { + const startR = mergeBounds ? Math.min(start, mergeBounds.minR) : start; + emitCellRangeToYdoc( + ctx, + id, + d as any[][], + startR, + d.length - 1, + 0, + (d[0]?.length ?? 1) - 1 + ); + } else { + const startC = mergeBounds ? Math.min(start, mergeBounds.minC) : start; + emitCellRangeToYdoc( + ctx, + id, + d as any[][], + 0, + d.length - 1, + startC, + (d[0]?.length ?? 1) - 1 + ); + } + if (file.id === ctx.currentSheetId) { ctx.config = cfg; // jfrefreshgrid_adRC( diff --git a/packages/core/src/modules/searchReplace.ts b/packages/core/src/modules/searchReplace.ts index 6eb5a080..afea65fb 100644 --- a/packages/core/src/modules/searchReplace.ts +++ b/packages/core/src/modules/searchReplace.ts @@ -467,6 +467,17 @@ export function replace( // } setCellValue(ctx, r, c, d, v); + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d?.[r]?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } } else { let reg; if (checkModes.caseCheck) { @@ -485,6 +496,17 @@ export function replace( const v = valueShowEs(r, c, d).toString().replace(reg, replaceText); setCellValue(ctx, r, c, d, v); + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d?.[r]?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } } ctx.luckysheet_select_save = normalizeSelection(ctx, [ @@ -550,6 +572,13 @@ export function replaceAll( } const d = flowdata; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; let replaceCount = 0; if (checkModes.wordCheck) { for (let i = 0; i < searchIndexArr.length; i += 1) { @@ -563,6 +592,15 @@ export function replaceAll( const v = replaceText; setCellValue(ctx, r, c, d, v); + if (ctx?.hooks?.updateCellYdoc) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d?.[r]?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }); + } range.push({ row: [r, r], column: [c, c] }); replaceCount += 1; @@ -586,12 +624,25 @@ export function replaceAll( const v = valueShowEs(r, c, d).toString().replace(reg, replaceText); setCellValue(ctx, r, c, d, v); + if (ctx?.hooks?.updateCellYdoc) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d?.[r]?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }); + } range.push({ row: [r, r], column: [c, c] }); replaceCount += 1; } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } + // jfrefreshgrid(d, range); ctx.luckysheet_select_save = normalizeSelection(ctx, range); diff --git a/packages/core/src/modules/selection.ts b/packages/core/src/modules/selection.ts index 0d1572fc..e409d8cd 100644 --- a/packages/core/src/modules/selection.ts +++ b/packages/core/src/modules/selection.ts @@ -315,6 +315,13 @@ export function pasteHandlerOfPaintModel( // let d = editor.deepCopyFlowData(ctx.flowdata);//取数据 const flowdata = getFlowdata(ctx); // 取数据 if (flowdata == null) return; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; const cellMaxLength = flowdata[0].length; const rowMaxLength = flowdata.length; @@ -494,12 +501,25 @@ export function pasteHandlerOfPaintModel( } } } + + // Persist every touched cell to Yjs, including "empty" cells. + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: h, c, v: x[c] ?? null }, + key: `${h}_${c}`, + type: "update", + }); } flowdata[h] = x; } } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } + const currFile = ctx.luckysheetfile[getSheetIndex(ctx, ctx.currentSheetId)!]; currFile.config = cfg; // Only overwrite dataVerification when we actually merged in pasted validations; @@ -2172,17 +2192,25 @@ export function deleteSelectedCellText(ctx: Context): string { if (ctx?.hooks?.afterUpdateCell) { ctx.hooks.afterUpdateCell(r, c, null, d[r][c]); } - changes.push({ - sheetId: ctx.currentSheetId, - path: ["celldata"], - value: { - r, - c, - v: d[r][c], - }, - }); + // If afterUpdateCell is provided, it is expected to handle syncing external state (e.g. Yjs). + if (!ctx?.hooks?.afterUpdateCell) { + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { + r, + c, + v: d[r][c], + }, + key: `${r}_${c}`, + type: "update", + }); + } } } + if (changes.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(changes); + } } // jfrefreshgrid(d, ctx.luckysheet_select_save); @@ -2221,6 +2249,14 @@ export function deleteSelectedCellFormat(ctx: Context): string { return "partMC"; } + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + for (let s = 0; s < selection.length; s += 1) { const r1 = selection[s].row[0]; const r2 = selection[s].row[1]; @@ -2240,10 +2276,21 @@ export function deleteSelectedCellFormat(ctx: Context): string { delete cell.bg; delete cell.tb; } + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } } } + + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } return "success"; } @@ -2276,6 +2323,14 @@ export function fillRightData(ctx: Context): string { return "partMC"; } + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + for (let s = 0; s < selection.length; s += 1) { const r1 = selection[s].row[0]; const r2 = selection[s].row[1]; @@ -2296,19 +2351,19 @@ export function fillRightData(ctx: Context): string { const srcCol = c1 - 1; const prev = d[r1][c1 - 1]; d[r1][c1] = prev != null ? { ...prev } : {}; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: r1, c: c1, v: d[r1][c1] }, + key: `${r1}_${c1}`, + type: "update", + }); if (file != null) { const srcKey = `${srcRow}_${srcCol}`; const tgtKey = `${r1}_${c1}`; if (dataVerification != null) { const dv = dataVerification[srcKey]; if (dv != null) { - console.log( - "[fillRightData] dataVerification copy from", - { row: srcRow, col: srcCol }, - "→", - { row: r1, col: c1 }, - dv - ); file.dataVerification = { ...(file.dataVerification || {}), [tgtKey]: _.cloneDeep(dv), @@ -2350,6 +2405,13 @@ export function fillRightData(ctx: Context): string { for (let c = c1 + 1; c <= c2; c += 1) { if (d[r]) { d[r][c] = sourceCell != null ? { ...sourceCell } : d[r][c] ?? {}; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } if (file != null) { const srcKey = `${r}_${c1}`; @@ -2357,13 +2419,6 @@ export function fillRightData(ctx: Context): string { if (dataVerification != null) { const dv = dataVerification[srcKey]; if (dv != null) { - console.log( - "[fillRightData] dataVerification copy from", - { row: r, col: c1 }, - "→", - { row: r, col: c }, - dv - ); file.dataVerification = { ...(file.dataVerification || {}), [tgtKey]: _.cloneDeep(dv), @@ -2402,6 +2457,9 @@ export function fillRightData(ctx: Context): string { } } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } return "success"; } @@ -2434,6 +2492,14 @@ export function fillDownData(ctx: Context): string { return "partMC"; } + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + for (let s = 0; s < selection.length; s += 1) { const r1 = selection[s].row[0]; const r2 = selection[s].row[1]; @@ -2454,19 +2520,19 @@ export function fillDownData(ctx: Context): string { const prev = d[r1 - 1][c1]; if (!d[r1]) d[r1] = []; d[r1][c1] = prev != null ? { ...prev } : {}; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: r1, c: c1, v: d[r1][c1] }, + key: `${r1}_${c1}`, + type: "update", + }); if (file != null) { const srcKey = `${srcRow}_${srcCol}`; const tgtKey = `${r1}_${c1}`; if (dataVerification != null) { const dv = dataVerification[srcKey]; if (dv != null) { - console.log( - "[fillDownData] dataVerification copy from", - { row: srcRow, col: srcCol }, - "→", - { row: r1, col: c1 }, - dv - ); file.dataVerification = { ...(file.dataVerification || {}), [tgtKey]: _.cloneDeep(dv), @@ -2508,19 +2574,19 @@ export function fillDownData(ctx: Context): string { for (let r = r1 + 1; r <= r2; r += 1) { if (!d[r]) d[r] = []; d[r][c] = sourceCell != null ? { ...sourceCell } : d[r][c] ?? {}; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); if (file != null) { const srcKey = `${r1}_${c}`; const tgtKey = `${r}_${c}`; if (dataVerification != null) { const dv = dataVerification[srcKey]; if (dv != null) { - console.log( - "[fillDownData] dataVerification copy from", - { row: r1, col: c }, - "→", - { row: r, col: c }, - dv - ); file.dataVerification = { ...(file.dataVerification || {}), [tgtKey]: _.cloneDeep(dv), @@ -2559,6 +2625,9 @@ export function fillDownData(ctx: Context): string { } } } + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } return "success"; } @@ -2577,6 +2646,14 @@ export function textFormat( const d = getFlowdata(ctx); if (!d) return "dataNullError"; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + let has_PartMC = false; for (let s = 0; s < selection.length; s += 1) { @@ -2614,10 +2691,21 @@ export function textFormat( cell.tb = "1"; cell.ht = 2; } + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } } } + + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } return "success"; } @@ -2633,6 +2721,14 @@ export function fillDate(ctx: Context): string { const d = getFlowdata(ctx); if (!d) return "dataNullError"; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + let has_PartMC = false; for (let s = 0; s < selection.length; s += 1) { @@ -2667,9 +2763,20 @@ export function fillDate(ctx: Context): string { "0" )}/${today.getFullYear()}`; d[r][c] = { v: formattedDate }; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } } + + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } return "success"; } @@ -2685,6 +2792,14 @@ export function fillTime(ctx: Context): string { const d = getFlowdata(ctx); if (!d) return "dataNullError"; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + let has_PartMC = false; for (let s = 0; s < selection.length; s += 1) { @@ -2718,9 +2833,20 @@ export function fillTime(ctx: Context): string { now.getSeconds() ).padStart(2, "0")}`; d[r][c] = { v: formattedTime }; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d[r][c] }, + key: `${r}_${c}`, + type: "update", + }); } } } + + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } } return "success"; } diff --git a/packages/core/src/modules/sort.ts b/packages/core/src/modules/sort.ts index ae1d8281..ea9b69ab 100644 --- a/packages/core/src/modules/sort.ts +++ b/packages/core/src/modules/sort.ts @@ -116,6 +116,30 @@ export function sortDataRange( // }; // } jfrefreshgrid(ctx, sheetData, [{ row: [str, edr], column: [stc, edc] }]); + + // Sync sorted values to Yjs (sorting rewrites many cells without setCellValue()). + if (ctx?.hooks?.updateCellYdoc) { + const changes: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + for (let r = str; r <= edr; r += 1) { + const row = sheetData[r] || []; + for (let c = stc; c <= edc; c += 1) { + changes.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: row?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }); + } + } + if (changes.length > 0) ctx.hooks.updateCellYdoc(changes); + } } export function sortSelection( @@ -221,6 +245,13 @@ function createRowsOrColumnsForSpilledValues( ) { const flowdata = getFlowdata(ctx); if (!flowdata) return; + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; // update luckysheetfile metadata if needed try { @@ -240,20 +271,37 @@ function createRowsOrColumnsForSpilledValues( console.error("Failed to update sheet metadata for spill operation", error); } + const requiredRowCount = startRow + spillRows; + const requiredColCount = startColumn + spillCols; + // make sure the matrix has at least `startRow + spillRows` rows. - while (flowdata.length < startRow + spillRows) { + while (flowdata.length < requiredRowCount) { flowdata.push([]); // push empty row } // For each row that will be touched by the spill: - for (let rowIndex = startRow; rowIndex < startRow + spillRows; rowIndex++) { + for (let rowIndex = startRow; rowIndex < requiredRowCount; rowIndex++) { if (!Array.isArray(flowdata[rowIndex])) { flowdata[rowIndex] = []; } // Ensure the row has at least `startColumn + spillCols` columns. - while (flowdata[rowIndex].length < startColumn + spillCols) { + const prevLen = flowdata[rowIndex].length; + while (flowdata[rowIndex].length < requiredColCount) { flowdata[rowIndex].push(null); } + for (let c = Math.max(prevLen, startColumn); c < requiredColCount; c += 1) { + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: rowIndex, c, v: flowdata[rowIndex][c] ?? null }, + key: `${rowIndex}_${c}`, + type: "update", + }); + } + } + + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); } } @@ -321,5 +369,30 @@ export function spillSortResult( } } + if (ctx?.hooks?.updateCellYdoc) { + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + for (let r = 0; r < rowCount; r += 1) { + const rr = startRow + r; + const row = sheetData?.[rr] || []; + for (let c = 0; c < colCount; c += 1) { + const cc = startCol + c; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: rr, c: cc, v: row?.[cc] ?? null }, + key: `${rr}_${cc}`, + type: "update", + }); + } + } + if (cellChanges.length > 0) ctx.hooks.updateCellYdoc(cellChanges); + } + return true; } diff --git a/packages/core/src/modules/splitColumn.ts b/packages/core/src/modules/splitColumn.ts index b1f4fe52..527f04c9 100644 --- a/packages/core/src/modules/splitColumn.ts +++ b/packages/core/src/modules/splitColumn.ts @@ -25,12 +25,33 @@ export function updateMoreCell( ) { if (ctx.allowEdit === false) return; const flowdata = getFlowdata(ctx); + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; dataMatrix.forEach((datas, i) => { datas.forEach((data, j) => { const v = dataMatrix[i][j]; setCellValue(ctx, r + i, c + j, flowdata, v); + if (ctx?.hooks?.updateCellYdoc) { + const rr = r + i; + const cc = c + j; + cellChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: rr, c: cc, v: flowdata?.[rr]?.[cc] ?? null }, + key: `${rr}_${cc}`, + type: "update", + }); + } }); }); + if (cellChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(cellChanges); + } // jfrefreshgrid(d, range); // selectHightlightShow(); } diff --git a/packages/core/src/modules/toolbar.ts b/packages/core/src/modules/toolbar.ts index 979dd1c9..4b4e4d56 100644 --- a/packages/core/src/modules/toolbar.ts +++ b/packages/core/src/modules/toolbar.ts @@ -619,6 +619,17 @@ function backFormulaInput( const v = execfunction(ctx, f, r, c); const value = { v: v[1], f: v[2] }; setCellValue(ctx, r, c, d, value); + if (ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc([ + { + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r, c, v: d?.[r]?.[c] ?? null }, + key: `${r}_${c}`, + type: "update", + }, + ]); + } ctx.formulaCache.execFunctionExist ||= []; ctx.formulaCache.execFunctionExist.push({ r, @@ -1411,6 +1422,8 @@ export function handleClearFormat(ctx: Context) { if (ctx.allowEdit === false) return; const flowdata = getFlowdata(ctx); if (!flowdata) return; + const ydocChanges: any[] = []; + let borderInfoChanged = false; ctx.luckysheet_select_save?.every((selection) => { const [rowSt, rowEd] = selection.row; const [colSt, colEd] = selection.column; @@ -1422,6 +1435,13 @@ export function handleClearFormat(ctx: Context) { const cell = flowdata[r][c]; if (!cell) continue; flowdata[r][c] = _.pick(cell, "v", "m", "mc", "f", "ct"); + ydocChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + key: `${r}_${c}`, + value: { r, c, v: flowdata[r][c] }, + type: "update", + }); } } // 清空表格样式时,清除边框样式 @@ -1481,9 +1501,29 @@ export function handleClearFormat(ctx: Context) { } ctx.luckysheetfile[index].config!.borderInfo = source_borderInfo; + borderInfoChanged = true; } return true; }); + + if (ctx?.hooks?.updateCellYdoc) { + if (borderInfoChanged) { + const index = getSheetIndex(ctx, ctx.currentSheetId); + const borderInfo = + index == null + ? ctx.config?.borderInfo ?? [] + : ctx.luckysheetfile[index]?.config?.borderInfo ?? []; + ydocChanges.push({ + sheetId: ctx.currentSheetId, + path: ["config", "borderInfo"], + value: borderInfo, + type: "update", + }); + } + if (ydocChanges.length > 0) { + ctx.hooks.updateCellYdoc(ydocChanges); + } + } } export function handleTextColor( diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index aa3838b8..f04d4996 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -18,9 +18,12 @@ export type Hooks = { iframeListChange?: () => void; conditionRulesChange?: () => void; conditionFormatChange?: () => void; + filterSelectChange?: () => void; + filterChange?: () => void; cellDataChange?: () => void; hyperlinkChange?: () => void; updateCellYdoc?: (changes: SheetChangePath[]) => void; + updateAllCell?: (sheetId: string) => void; beforeUpdateCell?: (r: number, c: number, value: any) => boolean; afterUpdateCell?: ( row: number, @@ -142,6 +145,8 @@ export type Hooks = { afterIframesChange?: () => void; afterFrozenChange?: () => void; afterOrderChanges?: () => void; + afterColorChanges?: () => void; + afterHideChanges?: () => void; afterConfigChanges?: () => void; afterColRowChanges?: () => void; afterShowGridLinesChange?: () => void; diff --git a/packages/react/package.json b/packages/react/package.json index 76d9cf0a..fec1b087 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@fileverse-dev/fortune-react", - "version": "1.3.10", + "version": "1.3.11", "main": "lib/index.js", "types": "lib/index.d.ts", "module": "es/index.js", @@ -16,7 +16,7 @@ "tsc": "tsc" }, "dependencies": { - "@fileverse-dev/fortune-core": "1.3.10", + "@fileverse-dev/fortune-core": "1.3.11", "@fileverse/ui": "5.0.0", "@tippyjs/react": "^4.2.6", "@types/regenerator-runtime": "^0.13.6", diff --git a/packages/react/src/components/SheetOverlay/drag_and_drop/column-helpers.tsx b/packages/react/src/components/SheetOverlay/drag_and_drop/column-helpers.tsx index 356af520..6dbb09a6 100644 --- a/packages/react/src/components/SheetOverlay/drag_and_drop/column-helpers.tsx +++ b/packages/react/src/components/SheetOverlay/drag_and_drop/column-helpers.tsx @@ -480,6 +480,34 @@ export const useColumnDragAndDrop = ( sourceIndex, targetIndex ); + // Notify Yjs for every cell in the disturbed range (moved column + all columns in between) + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + const affectedColStart = Math.min(sourceIndex, targetIndex); + const affectedColEnd = + Math.max(sourceIndex, targetIndex) + selectedSourceCol.length - 1; + const numRows = rows.length; + for (let r = 0; r < numRows; r += 1) { + const row = rows[r]; + for (let c = affectedColStart; c <= affectedColEnd; c += 1) { + const cell = row?.[c]; + cellChanges.push({ + sheetId: draft.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell ?? null }, + key: `${r}_${c}`, + type: "update", + }); + } + } + if (cellChanges.length > 0 && draft.hooks?.updateCellYdoc) { + draft.hooks.updateCellYdoc(cellChanges); + } const rowLen = d?.length || 0; api.setSelection( draft, diff --git a/packages/react/src/components/SheetOverlay/drag_and_drop/row-helpers.tsx b/packages/react/src/components/SheetOverlay/drag_and_drop/row-helpers.tsx index 0aa32bfd..642ce3f2 100644 --- a/packages/react/src/components/SheetOverlay/drag_and_drop/row-helpers.tsx +++ b/packages/react/src/components/SheetOverlay/drag_and_drop/row-helpers.tsx @@ -454,6 +454,34 @@ export const useRowDragAndDrop = ( "row", context.currentSheetId ); + // Notify Yjs for every cell in the disturbed range (moved row + all rows in between) + const cellChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + const affectedRowStart = Math.min(sourceIndex, targetIndex); + const affectedRowEnd = + Math.max(sourceIndex, targetIndex) + selectedSourceRow.length - 1; + const numCols = d?.[0]?.length ?? 0; + for (let r = affectedRowStart; r <= affectedRowEnd; r += 1) { + const row = rows[r]; + for (let c = 0; c < numCols; c += 1) { + const cell = row?.[c]; + cellChanges.push({ + sheetId: draft.currentSheetId, + path: ["celldata"], + value: { r, c, v: cell ?? null }, + key: `${r}_${c}`, + type: "update", + }); + } + } + if (cellChanges.length > 0 && draft.hooks?.updateCellYdoc) { + draft.hooks.updateCellYdoc(cellChanges); + } const colLen = d?.[0]?.length || 0; api.setSelection( draft, diff --git a/packages/react/src/components/Workbook/index.tsx b/packages/react/src/components/Workbook/index.tsx index 04903847..fcd85cf1 100644 --- a/packages/react/src/components/Workbook/index.tsx +++ b/packages/react/src/components/Workbook/index.tsx @@ -198,6 +198,132 @@ const Workbook = React.forwardRef( [onOp] ); + const emitYjsFromPatches = useCallback( + (ctxBefore: Context, ctxAfter: Context, patches: Patch[]) => { + const { updateCellYdoc, updateAllCell } = ctxBefore.hooks ?? {}; + if (!updateCellYdoc) return; + + const mapFields = new Set([ + "celldata", + "calcChain", + "dataBlockCalcFunction", + "liveQueryList", + "dataVerification", + "hyperlink", + "conditionRules", + ]); + + // De-dupe: last patch wins for the same (sheetId + path[0] + key). + const changeMap = new Map(); + + const upsert = (change: any) => { + const k = `${change.sheetId}:${change.path?.[0] ?? ""}:${ + change.key ?? "" + }`; + changeMap.set(k, change); + }; + + const upsertCell = (sheetId: string, r: number, c: number) => { + const cell = + (getFlowdata(ctxAfter, sheetId) as any)?.[r]?.[c] ?? null; + const key = `${r}_${c}`; + upsert({ + sheetId, + path: ["celldata"], + key, + value: { r, c, v: cell }, + type: cell == null ? "delete" : "update", + }); + }; + + // Best-effort: translate patches that touch sheet.data or any "map-like" objects keyed by "r_c". + patches.forEach((p) => { + const path = p.path as any[]; + if (path?.[0] !== "luckysheetfile") return; + const sheetIndex = path[1]; + if (!_.isNumber(sheetIndex)) return; + + const sheetBefore = ctxBefore.luckysheetfile?.[sheetIndex]; + const sheetAfter = ctxAfter.luckysheetfile?.[sheetIndex]; + const sheetId = (sheetAfter?.id || sheetBefore?.id) as + | string + | undefined; + if (!sheetId) return; + + const root = path[2]; + + if (root === "data") { + // Any patch under ["data", r, c, ...] -> update whole cell in Yjs. + if (_.isNumber(path[3]) && _.isNumber(path[4])) { + upsertCell(sheetId, path[3], path[4]); + return; + } + + // Row replacement ["data", r] + if (_.isNumber(path[3]) && path.length === 4) { + const r = path[3] as number; + const beforeRow = (sheetBefore as any)?.data?.[r] ?? []; + const afterRow = (sheetAfter as any)?.data?.[r] ?? []; + const max = Math.max(beforeRow.length ?? 0, afterRow.length ?? 0); + for (let c = 0; c < max; c += 1) { + if (!_.isEqual(beforeRow[c] ?? null, afterRow[c] ?? null)) { + upsertCell(sheetId, r, c); + } + } + return; + } + + // Whole-matrix replacement ["data"] (rare). If huge, fall back to updateAllCell when available. + if (path.length === 3) { + const dataAfter = (sheetAfter as any)?.data as + | any[][] + | undefined; + const rows = dataAfter?.length ?? 0; + const cols = rows > 0 ? dataAfter?.[0]?.length ?? 0 : 0; + const size = rows * cols; + if (size > 50000 && updateAllCell) { + updateAllCell(sheetId); + return; + } + for (let r = 0; r < rows; r += 1) { + const beforeRow = (sheetBefore as any)?.data?.[r] ?? []; + const afterRow = (sheetAfter as any)?.data?.[r] ?? []; + const max = Math.max( + beforeRow.length ?? 0, + afterRow.length ?? 0 + ); + for (let c = 0; c < max; c += 1) { + if (!_.isEqual(beforeRow[c] ?? null, afterRow[c] ?? null)) { + upsertCell(sheetId, r, c); + } + } + } + } + return; + } + + // Map-like objects on sheet keyed by "r_c": ["hyperlink", "0_1"], etc. + if (typeof root === "string" && mapFields.has(root)) { + const key = path[3]; + if (typeof key === "string") { + upsert({ + sheetId, + path: [root], + key, + value: p.value, + type: + p.op === "remove" || p.value == null ? "delete" : "update", + }); + } + } + }); + + const changes = Array.from(changeMap.values()); + if (changes.length > 0) updateCellYdoc(changes); + }, + [] + ); + function reduceUndoList(ctx: Context, ctxBefore: Context) { const sheetsId = ctx.luckysheetfile.map((sheet) => sheet.id); const sheetDeletedByMe = globalCache.current.undoList @@ -380,6 +506,7 @@ const Workbook = React.forwardRef( delete inversedOptions!.addSheet!.value!.data; } emitOp(newContext, history.inversePatches, inversedOptions, true); + emitYjsFromPatches(ctx_, newContext, history.inversePatches); // Sync ctx.config from current sheet after applying inverse patches. // This ensures components watching context.config (e.g. Sheet.tsx which // recalculates visibledatacolumn) react correctly to config changes. @@ -422,6 +549,7 @@ const Workbook = React.forwardRef( globalCache.current.undoList.push(history); emitOp(newContext, history.patches, history.options); + emitYjsFromPatches(ctx_, newContext, history.patches); // Sync ctx.config from current sheet after applying patches. const sheetIdxAfterRedo = getSheetIndex( newContext, @@ -465,19 +593,14 @@ const Workbook = React.forwardRef( setContext((ctx: any) => { const gridData = getFlowdata(ctx); const cellData = api.dataToCelldata(gridData as any); - let denominatedUsed = false; - // eslint-disable-next-line no-restricted-syntax - for (const cell of cellData) { + const denominatedUsed = (cellData ?? []).some((cell: any) => { const value = cell?.v?.m?.toString(); - if ( + return ( value?.includes("BTC") || value?.includes("ETH") || value?.includes("SOL") - ) { - denominatedUsed = true; - break; - } - } + ); + }); const denoWarn = document.getElementById("denomination-warning"); const scrollBar = document.getElementsByClassName( "luckysheet-scrollbar-x" @@ -577,6 +700,30 @@ const Workbook = React.forwardRef( } }, [currentSheet?.order]); + const sheetColorSig = useMemo(() => { + return (context?.luckysheetfile ?? []) + .map((s) => `${s.id}:${s.color ?? ""}`) + .join("|"); + }, [context?.luckysheetfile]); + + useEffect(() => { + if (context?.hooks?.afterColorChanges) { + context.hooks.afterColorChanges(); + } + }, [sheetColorSig]); + + const sheetHideSig = useMemo(() => { + return (context?.luckysheetfile ?? []) + .map((s) => `${s.id}:${s.hide ?? 0}`) + .join("|"); + }, [context?.luckysheetfile]); + + useEffect(() => { + if (context?.hooks?.afterHideChanges) { + context.hooks.afterHideChanges(); + } + }, [sheetHideSig]); + useEffect(() => { if (context?.hooks?.afterConfigChanges) { context.hooks.afterConfigChanges(); @@ -643,6 +790,18 @@ const Workbook = React.forwardRef( } }, [currentSheet?.luckysheet_conditionformat_save]); + useEffect(() => { + if (context?.hooks?.filterSelectChange) { + context.hooks.filterSelectChange(); + } + }, [currentSheet?.filter_select]); + + useEffect(() => { + if (context?.hooks?.filterChange) { + context.hooks.filterChange(); + } + }, [currentSheet?.filter]); + useEffect(() => { if (context?.hooks?.hyperlinkChange) { context.hooks.hyperlinkChange(); diff --git a/packages/react/src/utils/convertCellsToCrypto.ts b/packages/react/src/utils/convertCellsToCrypto.ts index fa24bdfa..f149c960 100644 --- a/packages/react/src/utils/convertCellsToCrypto.ts +++ b/packages/react/src/utils/convertCellsToCrypto.ts @@ -222,6 +222,14 @@ export async function convertCellsToCrypto({ const d = getFlowdata(ctx); if (!d || !Array.isArray(d)) return; + const ydocChanges: { + sheetId: string; + path: string[]; + key?: string; + value: any; + type?: "update" | "delete"; + }[] = []; + cellUpdates.forEach( ({ row, @@ -251,8 +259,20 @@ export async function convertCellsToCrypto({ cellCp.baseCurrencyPrice = baseCurrencyPrice; d[row][col] = cellCp as CryptoCell; + + ydocChanges.push({ + sheetId: ctx.currentSheetId, + path: ["celldata"], + value: { r: row, c: col, v: d[row]?.[col] ?? null }, + key: `${row}_${col}`, + type: "update", + }); } ); + + if (ydocChanges.length > 0 && ctx?.hooks?.updateCellYdoc) { + ctx.hooks.updateCellYdoc(ydocChanges); + } }); setContext((ctx: any) => { api.calculateSheetFromula(ctx, ctx.currentSheetId); diff --git a/stories/Features.stories.tsx b/stories/Features.stories.tsx index 70bc2845..7ad4f633 100644 --- a/stories/Features.stories.tsx +++ b/stories/Features.stories.tsx @@ -13,6 +13,27 @@ export default { component: Workbook, } as Meta; +const debugHooks = { + updateCellYdoc: (changes: any) => { + // eslint-disable-next-line no-console + console.log("[Features.stories] updateCellYdoc called", changes); + }, + afterUpdateCell: (r: number, c: number, value: any) => { + // eslint-disable-next-line no-console + console.log( + `[Features.stories] afterUpdateCell called: r=${r}, c=${c}, value=${value}` + ); + }, + afterHideChanges: () => { + // eslint-disable-next-line no-console + console.log("[Features.stories] afterHideChanges called"); + }, + updateAllCell: () => { + // eslint-disable-next-line no-console + console.log("[Features.stories] updateAllCell called"); + }, +}; + const Template: StoryFn = ({ // eslint-disable-next-line react/prop-types data: data0, @@ -179,6 +200,7 @@ const Template: StoryFn = ({ "|", "clear-format", ]} + hooks={debugHooks} /> ); @@ -230,7 +252,7 @@ export const MultiInstance: StoryFn = () => { boxSizing: "border-box", }} > - +
= () => { boxSizing: "border-box", }} > - +
);