diff --git a/packages/core/src/api/sheet.ts b/packages/core/src/api/sheet.ts index b61e0959..0e779c31 100644 --- a/packages/core/src/api/sheet.ts +++ b/packages/core/src/api/sheet.ts @@ -4,7 +4,7 @@ import { dataToCelldata, getSheet } from "./common"; import { Context } from "../context"; import { CellMatrix, CellWithRowAndCol, Sheet } from "../types"; import { getSheetIndex } from "../utils"; -import { api, locale } from ".."; +import { api, execfunction, insertUpdateFunctionGroup, locale } from ".."; export function getAllSheets(ctx: Context) { return ctx.luckysheetfile; @@ -29,7 +29,6 @@ export function initSheetData( lastRowNum = Math.max(lastRowNum, draftCtx.defaultrowNum); lastColNum = Math.max(lastColNum, draftCtx.defaultcolumnNum); } - delete draftCtx.luckysheetfile[index]?.celldata; if (lastRowNum && lastColNum) { const expandedData: Sheet["data"] = _.times(lastRowNum, () => _.times(lastColNum, () => null) @@ -39,9 +38,11 @@ export function initSheetData( }); if (draftCtx.luckysheetfile[index] == null) { newData.data = expandedData; + delete newData.celldata; draftCtx.luckysheetfile.push(newData); } else { draftCtx.luckysheetfile[index].data = expandedData; + delete draftCtx.luckysheetfile[index].celldata; } return expandedData; } @@ -135,3 +136,24 @@ export function copySheet(ctx: Context, sheetId: string) { ] = order; api.setSheetOrder(ctx, sheetOrderList); } + +export function calculateSheetFromula(ctx: Context, id: string) { + const index = getSheetIndex(ctx, id) as number; + if (!ctx.luckysheetfile[index].data) return; + for (let r = 0; r < ctx.luckysheetfile[index].data!.length; r += 1) { + for (let c = 0; c < ctx.luckysheetfile[index].data![r].length; c += 1) { + if (!ctx.luckysheetfile[index].data![r][c]?.f) { + continue; + } + const result = execfunction( + ctx, + ctx.luckysheetfile[index].data![r][c]?.f!, + r, + c, + id + ); + api.setCellValue(ctx, r, c, result[1], null); + insertUpdateFunctionGroup(ctx, r, c, id); + } + } +} diff --git a/packages/core/src/events/mouse.ts b/packages/core/src/events/mouse.ts index 143b9795..cff5bbd6 100644 --- a/packages/core/src/events/mouse.ts +++ b/packages/core/src/events/mouse.ts @@ -279,24 +279,16 @@ export function handleCellAreaMouseDown( // $("#luckysheet-dataVerification-showHintBox").hide(); // 如果右键在选区内, 停止mousedown处理 - let isInSelection = false; - _.forEach(ctx.luckysheet_select_save, (obj_s) => { - if ( + const isInSelection = _.some( + ctx.luckysheet_select_save, + (obj_s) => obj_s.row != null && row_index >= obj_s.row[0] && row_index <= obj_s.row[1] && col_index >= obj_s.column[0] && col_index <= obj_s.column[1] - ) { - isInSelection = true; - return false; - } - return true; - }); - - if (isInSelection) { - return; - } + ); + if (isInSelection) return; } // //单元格数据下钻 @@ -1436,24 +1428,34 @@ export function handleContextMenu( const col_pre = col_location[0]; const col_index = col_location[2]; // 如果右键点击在选区内则不做选区处理 - let isInSelection = false; - ctx.luckysheet_select_save?.some((obj_s) => { - if ( + const isInSelection = _.some( + ctx.luckysheet_select_save, + (obj_s) => obj_s.row != null && row_index >= obj_s.row[0] && row_index <= obj_s.row[1] && col_index >= obj_s.column[0] && col_index <= obj_s.column[1] - ) { - isInSelection = true; - return false; - } - return true; - }); - - if (isInSelection) { + ); + if (!isInSelection && (e.metaKey || e.ctrlKey)) { + // 选区添加 + ctx.luckysheet_select_save?.push({ + left: col_pre, + width: col - col_pre - 1, + top: row_pre, + height: row - row_pre - 1, + left_move: col_pre, + width_move: col - col_pre - 1, + top_move: row_pre, + height_move: row - row_pre - 1, + row: [row_index, row_index], + column: [col_index, col_index], + row_focus: row_index, + column_focus: col_index, + }); return; } + if (isInSelection) return; const row_index_ed = row_index; const col_index_ed = col_index; ctx.luckysheet_select_save = [ @@ -1480,22 +1482,15 @@ export function handleContextMenu( const row_pre = row_location[0]; const row_index = row_location[2]; // 如果右键点击在选区内则不做选区处理 - let isInSelection = false; - ctx.luckysheet_select_save?.some((obj_s) => { - if ( + const isInSelection = _.some( + ctx.luckysheet_select_save, + (obj_s) => obj_s.row != null && row_index >= obj_s.row[0] && row_index <= obj_s.row[1] - ) { - isInSelection = true; - return false; - } - return true; - }); + ); - if (isInSelection) { - return; - } + if (isInSelection) return; const col_index = ctx.visibledatacolumn.length - 1; const col = ctx.visibledatacolumn[col_index]; const col_pre = 0; @@ -1532,22 +1527,15 @@ export function handleContextMenu( const col_pre = col_location[0]; const col_index = col_location[2]; // 如果右键点击在选区内则不做选区处理 - let isInSelection = false; - ctx.luckysheet_select_save?.some((obj_s) => { - if ( + const isInSelection = _.some( + ctx.luckysheet_select_save, + (obj_s) => obj_s.row != null && col_index >= obj_s.column[0] && col_index <= obj_s.column[1] - ) { - isInSelection = true; - return false; - } - return true; - }); + ); - if (isInSelection) { - return; - } + if (isInSelection) return; const left = col_pre; const width = col - col_pre - 1; const columnseleted = [col_index, col_index]; @@ -4355,25 +4343,17 @@ export function handleRowHeaderMouseDown( // mousedown是右键 if (e.button === 2) { // 如果右键在选区内, 停止mousedown处理 - let isInSelection = false; - const flowdata = getFlowdata(ctx); - _.forEach(ctx.luckysheet_select_save, (obj_s) => { - if ( + const isInSelection = _.some( + ctx.luckysheet_select_save, + (obj_s) => obj_s.row != null && row_index >= obj_s.row[0] && row_index <= obj_s.row[1] && obj_s.column[0] === 0 && obj_s.column[1] === (flowdata?.[0]?.length ?? 0) - 1 - ) { - isInSelection = true; - return false; - } - return true; - }); - if (isInSelection) { - return; - } + ); + if (isInSelection) return; } let top = row_pre; @@ -4687,7 +4667,7 @@ export function handleRowHeaderMouseDown( ctx.luckysheet_select_save![ctx.luckysheet_select_save!.length - 1] = last; - } else if (e.ctrlKey) { + } else if (e.ctrlKey || e.metaKey) { ctx.luckysheet_select_save?.push({ left: colLocationByIndex(0, ctx.visibledatacolumn)[0], width: @@ -4781,27 +4761,17 @@ export function handleColumnHeaderMouseDown( // mousedown是右键 if (e.button === 2) { - let isInSelection = false; - const flowdata = getFlowdata(ctx); - _.forEach(ctx.luckysheet_select_save, (obj_s) => { - // 如果右键在选区内, 停止mousedown处理 - if ( + const isInSelection = _.some( + ctx.luckysheet_select_save, + (obj_s) => obj_s.column != null && col_index >= obj_s.column[0] && col_index <= obj_s.column[1] && obj_s.row[0] === 0 && obj_s.row[1] === (flowdata?.length ?? 0) - 1 - ) { - isInSelection = true; - return false; - } - return true; - }); - - if (isInSelection) { - return; - } + ); + if (isInSelection) return; } let left = col_pre; @@ -5093,7 +5063,7 @@ export function handleColumnHeaderMouseDown( ctx.luckysheet_select_save![ctx.luckysheet_select_save!.length - 1] = last; - } else if (e.ctrlKey) { + } else if (e.ctrlKey || e.metaKey) { // 选区添加 ctx.luckysheet_select_save?.push({ left, diff --git a/packages/core/src/locale/en.ts b/packages/core/src/locale/en.ts index f86c3938..c3736017 100644 --- a/packages/core/src/locale/en.ts +++ b/packages/core/src/locale/en.ts @@ -1,4 +1,9 @@ export default { + generalDialog: { + partiallyError: "Cannot perform this operation on partially merged cells", + readOnlyError: "Cannot perform this operation in read-only mode", + dataNullError: "Cannot perform this operation on data that does not exist", + }, functionlist: [ { n: "SUMIF", @@ -11493,6 +11498,8 @@ export default { byRow: "By row", byCol: "By column", generateNewMatrix: "Generate new matrix", + noMulti: + "Cannot perform this operation on multiple selection areas, please select a single area", }, comment: { insert: "Insert", diff --git a/packages/core/src/locale/es.ts b/packages/core/src/locale/es.ts index 8f0c5b8a..55db1f7e 100644 --- a/packages/core/src/locale/es.ts +++ b/packages/core/src/locale/es.ts @@ -1,4 +1,10 @@ export default { + generalDialog: { + partiallyError: + "No se puede realizar esta operación en celdas parcialmente unidas", + readOnlyError: "No se puede realizar esto en modo de solo lectura", + dataNullError: "No se puede hacer esto con datos que no existen", + }, functionlist: [ { n: "SUMIF", @@ -11467,6 +11473,8 @@ export default { byRow: "Por fila", byCol: "Por columna", generateNewMatrix: "Generar nueva matriz", + noMulti: + "No se puede realizar esta operación en varias áreas de selección, selecciona una sola área", }, comment: { insert: "Insertar", diff --git a/packages/core/src/locale/zh.ts b/packages/core/src/locale/zh.ts index a520a5d7..a1d759d7 100644 --- a/packages/core/src/locale/zh.ts +++ b/packages/core/src/locale/zh.ts @@ -1,4 +1,9 @@ export default { + generalDialog: { + partiallyError: "无法对部分合并单元格执行此操作", + readOnlyError: "无法对只读模式执行此操作", + dataNullError: "无法对不存在的数据执行此操作", + }, functionlist: [ { n: "SUMIF", @@ -11528,6 +11533,7 @@ export default { byRow: "按行", byCol: "按列", generateNewMatrix: "生成新矩阵", + noMulti: "无法对多重选择区域执行此操作,请选择单个区域", }, comment: { insert: "新建批注", diff --git a/packages/core/src/locale/zh_tw.ts b/packages/core/src/locale/zh_tw.ts index 378586c2..f32b7a48 100644 --- a/packages/core/src/locale/zh_tw.ts +++ b/packages/core/src/locale/zh_tw.ts @@ -1,4 +1,9 @@ export default { + generalDialog: { + partiallyError: "無法對部分合併儲存格執行此操作", + readOnlyError: "無法對只讀模式執行此操作", + dataNullError: "無法對不存在的數據執行此操作", + }, functionlist: [ { n: "SUMIF", @@ -11498,6 +11503,7 @@ export default { byRow: "按行", byCol: "按列", generateNewMatrix: "生成新矩陣", + noMulti: "無法對多重選擇區域執行此操作,請選擇單個區域", }, comment: { insert: "新建批註", diff --git a/packages/core/src/modules/cell.ts b/packages/core/src/modules/cell.ts index e7c60b70..8962e9df 100644 --- a/packages/core/src/modules/cell.ts +++ b/packages/core/src/modules/cell.ts @@ -1573,3 +1573,30 @@ export function luckysheetUpdateCell( ) { ctx.luckysheetCellUpdate = [row_index, col_index]; } + +export function getDataBySelectionNoCopy(ctx: Context, range: Selection) { + if (!range || !range.row || range.row.length === 0) return []; + const data = []; + const flowData = getFlowdata(ctx); + if (!flowData) return []; + for (let r = range.row[0]; r <= range.row[1]; r += 1) { + const row = []; + if (ctx.config.rowhidden != null && ctx.config.rowhidden[r] != null) { + continue; + } + for (let c = range.column[0]; c <= range.column[1]; c += 1) { + let value = null; + if (ctx.config.colhidden != null && ctx.config.colhidden[c] != null) { + continue; + } + if (flowData[r] != null && flowData[r][c] != null) { + value = flowData[r][c]; + } + + row.push(value); + } + + data.push(row); + } + return data; +} diff --git a/packages/core/src/modules/formula.ts b/packages/core/src/modules/formula.ts index fa8f9456..f6d20432 100644 --- a/packages/core/src/modules/formula.ts +++ b/packages/core/src/modules/formula.ts @@ -939,7 +939,7 @@ function checkBracketNum(fp: string) { return true; } -function insertUpdateFunctionGroup( +export function insertUpdateFunctionGroup( ctx: Context, r: number, c: number, diff --git a/packages/core/src/modules/selection.ts b/packages/core/src/modules/selection.ts index 2807d3dd..be6fae77 100644 --- a/packages/core/src/modules/selection.ts +++ b/packages/core/src/modules/selection.ts @@ -4,6 +4,7 @@ import { Context, getFlowdata } from "../context"; import { getCellValue, getdatabyselection, + getDataBySelectionNoCopy, getStyleByCell, mergeBorder, } from "./cell"; @@ -13,6 +14,8 @@ import { getBorderInfoCompute } from "./border"; import { getSheetIndex, replaceHtml } from "../utils"; import { hasPartMC } from "./validation"; import { update } from "./format"; +// @ts-ignore +import SSF from "./ssf"; export const selectionCache = { isPasteAction: false, @@ -1500,7 +1503,7 @@ export function copy(ctx: Context) { } } -export function deleteSelectedCellText(ctx: Context) { +export function deleteSelectedCellText(ctx: Context): string { // if ( // !checkProtectionLockedRangeList( // ctx.luckysheet_select_save, @@ -1514,13 +1517,13 @@ export function deleteSelectedCellText(ctx: Context) { // luckysheetContainerFocus(); if (ctx.allowEdit === false) { - return; + return "allowEdit"; } const selection = ctx.luckysheet_select_save; if (selection && !_.isEmpty(selection)) { const d = getFlowdata(ctx); - if (!d) return; + if (!d) return "dataNullError"; let has_PartMC = false; @@ -1535,7 +1538,6 @@ export function deleteSelectedCellText(ctx: Context) { break; } } - if (has_PartMC) { // const locale_drag = locale().drag; @@ -1545,7 +1547,7 @@ export function deleteSelectedCellText(ctx: Context) { // tooltip.info(locale_drag.noPartMerge, ""); // } - return; + return "partMC"; } const hyperlinkMap = ctx.luckysheetfile[getSheetIndex(ctx, ctx.currentSheetId)!].hyperlink; @@ -1587,13 +1589,13 @@ export function deleteSelectedCellText(ctx: Context) { } } } - // jfrefreshgrid(d, ctx.luckysheet_select_save); // // 清空编辑框的内容 // // 备注:在functionInputHanddler方法中会把该标签的内容拷贝到 #luckysheet-functionbox-cell // $("#luckysheet-rich-text-editor").html(""); } + return "success"; } // 选区是否重叠 @@ -1763,3 +1765,38 @@ export function getSelectionStyle( } return ret; } + +export function calcSelectionInfo(ctx: Context) { + const selection = ctx.luckysheet_select_save!; + let numberC = 0; + let count = 0; + let sum = 0; + let max = -Infinity; + let min = Infinity; + for (let s = 0; s < selection.length; s += 1) { + const data = getDataBySelectionNoCopy(ctx, selection[s]); + for (let r = 0; r < data.length; r += 1) { + for (let c = 0; c < data[0].length; c += 1) { + // 防止选区长度超出data + if (r >= data.length || c >= data[0].length) break; + const value = data![r][c]?.m as string; + // 判断是不是数字 + if (parseFloat(value).toString() !== "NaN") { + const valueNumber = parseFloat(value); + count += 1; + sum += valueNumber; + max = Math.max(valueNumber, max); + min = Math.min(valueNumber, min); + numberC += 1; + } else if (value != null) { + count += 1; + } + } + } + } + const average: string = SSF.format("w0.00", sum / numberC); + sum = SSF.format("w0.00", sum); + max = SSF.format("w0.00", max); + min = SSF.format("w0.00", min); + return { numberC, count, sum, max, min, average }; +} diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index 09cd4567..48bfc566 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -141,6 +141,7 @@ export type Settings = { defaultFontSize?: number; toolbarItems?: string[]; cellContextMenu?: string[]; + colContextMenu?: string[]; sheetTabContextMenu?: string[]; filterContextMenu?: string[]; generateSheetId?: () => string; @@ -216,6 +217,9 @@ export const defaultSettings: Required = { "delete-row", // 删除选中行 "delete-column", // 删除选中列 "delete-cell", // 删除单元格 + "set-row-height", // 设置行高 + "set-column-width", // 设置列宽 + "|", "hide-row", // 隐藏选中行和显示选中行 "hide-column", // 隐藏选中列和显示选中列 "|", @@ -230,6 +234,24 @@ export const defaultSettings: Required = { "data", // 数据验证 "cell-format", // 设置单元格格式 ], // 自定义单元格右键菜单 + colContextMenu: [ + "copy", // 复制 + "paste", // 粘贴 + "|", + "insert-row", // 插入行 + "insert-column", // 插入列 + "delete-row", // 删除选中行 + "delete-column", // 删除选中列 + "delete-cell", // 删除单元格 + "hide-row", // 隐藏选中行和显示选中行 + "hide-column", // 隐藏选中列和显示选中列 + "set-column-width", // 设置列宽 + "|", + "clear", // 清除内容 + "sort", // 排序选区 + "orderAZ", // 升序 + "orderZA", // 降序 + ], // header菜单 sheetTabContextMenu: [ "delete", "copy", diff --git a/packages/react/src/components/ContextMenu/index.tsx b/packages/react/src/components/ContextMenu/index.tsx index b2e31c87..42ce8a0e 100644 --- a/packages/react/src/components/ContextMenu/index.tsx +++ b/packages/react/src/components/ContextMenu/index.tsx @@ -15,6 +15,7 @@ import { } from "@fortune-sheet/core"; import _ from "lodash"; import React, { useContext, useRef, useLayoutEffect, useCallback } from "react"; +import { setColumnWidth, setRowHeight } from "../../../../core/src/api"; import WorkbookContext, { SetContextOptions } from "../../context"; import { useAlert } from "../../hooks/useAlert"; import { useDialog } from "../../hooks/useDialog"; @@ -28,7 +29,7 @@ const ContextMenu: React.FC = () => { const { context, setContext, settings } = useContext(WorkbookContext); const { contextMenu } = context; const { showAlert } = useAlert(); - const { rightclick, drag } = locale(context); + const { rightclick, drag, generalDialog, info } = locale(context); const getMenuElement = useCallback( (name: string, i: number) => { const selection = context.luckysheet_select_save?.[0]; @@ -41,6 +42,11 @@ const ContextMenu: React.FC = () => { key={name} onClick={() => { setContext((draftCtx) => { + if (draftCtx.luckysheet_select_save?.length! > 1) { + showAlert(rightclick.noMulti, "ok"); + draftCtx.contextMenu = undefined; + return; + } handleCopy(draftCtx); draftCtx.contextMenu = undefined; }); @@ -214,6 +220,11 @@ const ContextMenu: React.FC = () => { }; setContext( (draftCtx) => { + if (draftCtx.luckysheet_select_save?.length! > 1) { + showAlert(rightclick.noMulti, "ok"); + draftCtx.contextMenu = undefined; + return; + } deleteRowCol(draftCtx, deleteRowColOp); draftCtx.contextMenu = undefined; }, @@ -241,6 +252,11 @@ const ContextMenu: React.FC = () => { }; setContext( (draftCtx) => { + if (draftCtx.luckysheet_select_save?.length! > 1) { + showAlert(rightclick.noMulti, "ok"); + draftCtx.contextMenu = undefined; + return; + } deleteRowCol(draftCtx, deleteRowColOp); draftCtx.contextMenu = undefined; }, @@ -305,6 +321,126 @@ const ContextMenu: React.FC = () => { )) ); } + if (name === "set-row-height") { + const rowHeight = selection?.height || context.defaultrowlen; + const shownRowHeight = context.luckysheet_select_save?.some( + (section) => + section.height_move !== + (rowHeight + 1) * (section.row[1] - section.row[0] + 1) - 1 + ) + ? "" + : rowHeight; + return context.luckysheet_select_save?.some( + (section) => section.row_select + ) ? ( + { + const targetRowHeight = container.querySelector("input")?.value; + setContext((draftCtx) => { + if ( + _.isUndefined(targetRowHeight) || + targetRowHeight === "" || + parseInt(targetRowHeight, 10) <= 0 || + parseInt(targetRowHeight, 10) > 545 + ) { + showAlert(info.tipRowHeightLimit, "ok"); + draftCtx.contextMenu = undefined; + return; + } + const numRowHeight = parseInt(targetRowHeight, 10); + const rowHeightList: Record = {}; + _.forEach(draftCtx.luckysheet_select_save, (section) => { + for ( + let rowNum = section.row[0]; + rowNum <= section.row[1]; + rowNum += 1 + ) { + rowHeightList[rowNum] = numRowHeight; + } + }); + setRowHeight(draftCtx, rowHeightList); + draftCtx.contextMenu = undefined; + }); + }} + > + {rightclick.row} + {rightclick.height} + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + type="number" + min={1} + max={545} + className="luckysheet-mousedown-cancel" + placeholder={rightclick.number} + defaultValue={shownRowHeight} + style={{ width: "40px" }} + /> + px + + ) : null; + } + if (name === "set-column-width") { + const colWidth = selection?.width || context.defaultcollen; + const shownColWidth = context.luckysheet_select_save?.some( + (section) => + section.width_move !== + (colWidth + 1) * (section.column[1] - section.column[0] + 1) - 1 + ) + ? "" + : colWidth; + return context.luckysheet_select_save?.some( + (section) => section.column_select + ) ? ( + { + const targetColWidth = container.querySelector("input")?.value; + setContext((draftCtx) => { + if ( + _.isUndefined(targetColWidth) || + targetColWidth === "" || + parseInt(targetColWidth, 10) <= 0 || + parseInt(targetColWidth, 10) > 2038 + ) { + showAlert(info.tipColumnWidthLimit, "ok"); + draftCtx.contextMenu = undefined; + return; + } + const numColWidth = parseInt(targetColWidth, 10); + const colWidthList: Record = {}; + _.forEach(draftCtx.luckysheet_select_save, (section) => { + for ( + let colNum = section.column[0]; + colNum <= section.column[1]; + colNum += 1 + ) { + colWidthList[colNum] = numColWidth; + } + }); + setColumnWidth(draftCtx, colWidthList); + draftCtx.contextMenu = undefined; + }); + }} + > + {rightclick.column} + {rightclick.width} + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + type="number" + min={1} + max={545} + className="luckysheet-mousedown-cancel" + placeholder={rightclick.number} + defaultValue={shownColWidth} + style={{ width: "40px" }} + /> + px + + ) : null; + } if (name === "clear") { return ( { if (draftCtx.activeImg?.id != null) { removeActiveImage(draftCtx); } else { - deleteSelectedCellText(draftCtx); + const msg = deleteSelectedCellText(draftCtx); + if (msg === "partMC") { + showDialog(generalDialog.partiallyError, "ok"); + } else if (msg === "allowEdit") { + showDialog(generalDialog.readOnlyError, "ok"); + } else if (msg === "dataNullError") { + showDialog(generalDialog.dataNullError, "ok"); + } } draftCtx.contextMenu = undefined; }); @@ -421,11 +564,15 @@ const ContextMenu: React.FC = () => { context.currentSheetId, context.lang, context.luckysheet_select_save, + context.defaultrowlen, + context.defaultcollen, rightclick, + info, setContext, showAlert, showDialog, drag, + generalDialog, ] ); @@ -458,7 +605,11 @@ const ContextMenu: React.FC = () => { if (hasOverflow) { setContext((draftCtx) => { draftCtx.contextMenu.x = left; - draftCtx.contextMenu.y = top; + if (draftCtx.contextMenu.headerMenu === true) { + draftCtx.contextMenu.y = 90; + } else { + draftCtx.contextMenu.y = top; + } }); } }, [contextMenu, setContext]); @@ -472,7 +623,9 @@ const ContextMenu: React.FC = () => { onContextMenu={(e) => e.stopPropagation()} style={{ left: contextMenu.x, top: contextMenu.y }} > - {settings.cellContextMenu.map((menu, i) => getMenuElement(menu, i))} + {context.contextMenu.headerMenu === true + ? settings.colContextMenu.map((menu, i) => getMenuElement(menu, i)) + : settings.cellContextMenu.map((menu, i) => getMenuElement(menu, i))} ); }; diff --git a/packages/react/src/components/SVGDefines.tsx b/packages/react/src/components/SVGDefines.tsx index 474d12f9..f65ae9cb 100644 --- a/packages/react/src/components/SVGDefines.tsx +++ b/packages/react/src/components/SVGDefines.tsx @@ -1189,6 +1189,12 @@ const SVGDefines: React.FC = () => ( p-id="2683" /> + + + ); diff --git a/packages/react/src/components/SheetOverlay/ColumnHeader.tsx b/packages/react/src/components/SheetOverlay/ColumnHeader.tsx index 22b0b1a8..264a8746 100644 --- a/packages/react/src/components/SheetOverlay/ColumnHeader.tsx +++ b/packages/react/src/components/SheetOverlay/ColumnHeader.tsx @@ -16,6 +16,7 @@ import React, { useEffect, } from "react"; import WorkbookContext from "../../context"; +import SVGIcon from "../SVGIcon"; const ColumnHeader: React.FC = () => { const { context, setContext, settings, refs } = useContext(WorkbookContext); @@ -171,7 +172,22 @@ const ColumnHeader: React.FC = () => { width: hoverLocation.col - hoverLocation.col_pre - 1, display: "block", }} - /> + > + { + setContext((ctx) => { + ctx.contextMenu = { + x: e.pageX, + y: 90, + headerMenu: true, + }; + }); + }} + > + + + ) : null} {selectedLocation.map(({ col, col_pre }, i) => (
{ const cellAreaMouseDown = useCallback( (e: React.MouseEvent) => { const { nativeEvent } = e; - setContext((draftCtx) => { - handleCellAreaMouseDown( - draftCtx, - refs.globalCache, - nativeEvent, - refs.cellInput.current!, - refs.cellArea.current!, - refs.fxInput.current! - ); - }); + if (e.button !== 2) { + // onContextMenu event will not call onMouseDown + setContext((draftCtx) => { + handleCellAreaMouseDown( + draftCtx, + refs.globalCache, + nativeEvent, + refs.cellInput.current!, + refs.cellArea.current!, + refs.fxInput.current! + ); + }); + } }, [refs.cellArea, refs.cellInput, refs.globalCache, refs.fxInput, setContext] ); @@ -265,7 +268,13 @@ const SheetOverlay: React.FC = () => { useEffect(() => { refs.cellArea.current!.scrollLeft = context.scrollLeft; refs.cellArea.current!.scrollTop = context.scrollTop; - }, [context.scrollLeft, context.scrollTop, refs.cellArea]); + }, [ + context.scrollLeft, + context.scrollTop, + refs.cellArea, + refs.cellArea.current?.scrollLeft, + refs.cellArea.current?.scrollTop, + ]); useEffect(() => { // ensure cell input is always focused to accept first key stroke on cell @@ -663,6 +672,16 @@ const SheetOverlay: React.FC = () => { ({info.addLast}) + { + setContext((ctx) => { + ctx.scrollTop = 0; + }); + }} + > + {info.backTop} +
diff --git a/packages/react/src/components/SheetTab/index.css b/packages/react/src/components/SheetTab/index.css index 49534351..7c843e56 100644 --- a/packages/react/src/components/SheetTab/index.css +++ b/packages/react/src/components/SheetTab/index.css @@ -247,7 +247,7 @@ } .luckysheet-sheet-selection-calInfo { - max-width: 280%; + max-width: 22%; display: flex; font-size: 12px; justify-content: space-between; diff --git a/packages/react/src/components/SheetTab/index.tsx b/packages/react/src/components/SheetTab/index.tsx index a3d47f78..6449c765 100644 --- a/packages/react/src/components/SheetTab/index.tsx +++ b/packages/react/src/components/SheetTab/index.tsx @@ -6,9 +6,13 @@ import React, { useRef, useState, } from "react"; -import { updateCell, addSheet, locale, getFlowdata } from "@fortune-sheet/core"; +import { + updateCell, + addSheet, + locale, + calcSelectionInfo, +} from "@fortune-sheet/core"; // @ts-ignore -import SSF from "@fortune-sheet/core/src/modules/ssf.js"; import WorkbookContext from "../../context"; import SVGIcon from "../SVGIcon"; import "./index.css"; @@ -24,13 +28,21 @@ const SheetTab: React.FC = () => { const [sheetScrollStep] = useState(150); const [isShowScrollBtn, setIsShowScrollBtn] = useState(false); const [isShowBoundary, setIsShowBoundary] = useState(true); - const [numberCount, setNumberCount] = useState(0); - const [showCalInfo, setShowCalInfo] = useState(false); - const [countInfo, setCountInfo] = useState(undefined); - const [sumInfo, setSumInfo] = useState(undefined); - const [averageInfo, setAverageInfo] = useState(undefined); - const [maxInfo, setMaxInfo] = useState(undefined); - const [minInfo, setMinInfo] = useState(undefined); + const [calInfo, setCalInfo] = useState<{ + numberC: number; + count: number; + sum: number; + max: number; + min: number; + average: string; + }>({ + numberC: 0, + count: 0, + sum: 0, + max: 0, + min: 0, + average: "", + }); const scrollToLeft = useCallback( (moveType: string) => { @@ -79,60 +91,15 @@ const SheetTab: React.FC = () => { useEffect(() => { const tabCurrent = tabContainerRef.current; - setIsShowScrollBtn(tabCurrent!.scrollWidth > tabCurrent!.clientWidth); + setIsShowScrollBtn(tabCurrent!.scrollWidth - 2 > tabCurrent!.clientWidth); }, [context.luckysheetfile]); // 计算选区的信息 useEffect(() => { const selection = context.luckysheet_select_save; if (selection) { - const data = getFlowdata(context, context.currentSheetId); - if (data == null) return; - const row = selection[0].row ?? []; - const column = selection[0].column ?? []; - if (row[0] !== row[1] || column[0] !== column[1]) { - let numberC = 0; - let count = 0; - let sum = 0; - let max = Number.MIN_VALUE; - let min = Number.MAX_VALUE; - for (let r = row[0]; r <= row[1]; r += 1) { - for (let c = column[0]; c <= column[1]; c += 1) { - if (r >= data.length || c >= data[0].length) break; - const value = data![r][c]?.m as string; - // 判断是不是数字 - if (parseFloat(value).toString() !== "NaN") { - const valueNumber = parseFloat(value); - count += 1; - sum += valueNumber; - max = Math.max(valueNumber, max); - min = Math.min(valueNumber, min); - numberC += 1; - } else if (value != null) { - count += 1; - } - } - } - const average = SSF.format("w0.00", sum / numberC); - sum = SSF.format("w0.00", sum); - max = SSF.format("w0.00", max === Number.MIN_VALUE ? 0 : max); - min = SSF.format("w0.00", min === Number.MAX_VALUE ? 0 : min); - setCountInfo(count.toString()); - setSumInfo(sum.toString()); - setAverageInfo(average.toString()); - setMaxInfo(max.toString()); - setMinInfo(min.toString()); - setShowCalInfo(true); - setNumberCount(numberC); - } else { - setCountInfo(undefined); - setSumInfo(undefined); - setAverageInfo(undefined); - setMaxInfo(undefined); - setMinInfo(undefined); - setShowCalInfo(false); - setNumberCount(0); - } + const re = calcSelectionInfo(context); + setCalInfo(re); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [context.luckysheet_select_save]); @@ -249,35 +216,33 @@ const SheetTab: React.FC = () => { )} - {showCalInfo && ( -
- {!!countInfo && ( -
- {formula.count}: {countInfo} -
- )} - {!!numberCount && !!sumInfo && ( -
- {formula.sum}: {sumInfo} -
- )} - {!!numberCount && !!averageInfo && ( -
- {formula.average}: {averageInfo} -
- )} - {!!numberCount && !!maxInfo && ( -
- {formula.max}: {maxInfo} -
- )} - {!!numberCount && !!minInfo && ( -
- {formula.min}: {minInfo} -
- )} -
- )} +
+ {!!calInfo.count && ( +
+ {formula.count}: {calInfo.count} +
+ )} + {!!calInfo.numberC && !!calInfo.sum && ( +
+ {formula.sum}: {calInfo.sum} +
+ )} + {!!calInfo.numberC && !!calInfo.average && ( +
+ {formula.average}: {calInfo.average} +
+ )} + {!!calInfo.numberC && !!calInfo.max && ( +
+ {formula.max}: {calInfo.max} +
+ )} + {!!calInfo.numberC && !!calInfo.min && ( +
+ {formula.min}: {calInfo.min} +
+ )} +
); diff --git a/packages/react/src/components/Toolbar/index.tsx b/packages/react/src/components/Toolbar/index.tsx index ed9f853b..4f437810 100644 --- a/packages/react/src/components/Toolbar/index.tsx +++ b/packages/react/src/components/Toolbar/index.tsx @@ -245,7 +245,14 @@ const Toolbar: React.FC<{ ); } if (name === "font") { - let current = cell?.ff?.toString() ?? fontarray[0]; + let current = fontarray[0]; + if (cell?.ff != null) { + if (_.isNumber(cell.ff)) { + current = fontarray[cell.ff]; + } else { + current = cell.ff; + } + } return ( {(setOpen) => ( diff --git a/packages/react/src/components/Workbook/api.ts b/packages/react/src/components/Workbook/api.ts index 3cce667c..d3f4964c 100644 --- a/packages/react/src/components/Workbook/api.ts +++ b/packages/react/src/components/Workbook/api.ts @@ -331,5 +331,13 @@ export function generateAPIs( }); } }, + + calculateFormula: () => { + setContext((draftCtx) => { + _.forEach(draftCtx.luckysheetfile, (sheet_obj) => { + api.calculateSheetFromula(draftCtx, sheet_obj.id as string); + }); + }); + }, }; } diff --git a/packages/react/src/components/Workbook/index.tsx b/packages/react/src/components/Workbook/index.tsx index 0d791e4a..b84fc8ba 100644 --- a/packages/react/src/components/Workbook/index.tsx +++ b/packages/react/src/components/Workbook/index.tsx @@ -105,6 +105,8 @@ const Workbook = React.forwardRef( }); draftCtx.luckysheetfile = produce(draftCtx.luckysheetfile, (d) => { d[index!].data = expandedData; + delete d[index!].celldata; + return d; }); return expandedData; } diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index e26e7552..f422b523 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -1,3 +1,4 @@ -import Workbook, { WorkbookInstance } from "./Workbook"; +import Workbook from "./Workbook"; -export { Workbook, WorkbookInstance }; +export { Workbook }; +export type { WorkbookInstance } from "./Workbook"; diff --git a/stories/API.stories.tsx b/stories/API.stories.tsx index 31d4829b..3741dd91 100644 --- a/stories/API.stories.tsx +++ b/stories/API.stories.tsx @@ -48,8 +48,10 @@ export const GetCellValue: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [[{ v: "fortune" }]], + celldata: [{ r: 0, c: 0, v: { v: "fortune" } }], order: 0, + row: 1, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -102,8 +104,12 @@ export const ClearCell: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [[{ bg: "green", v: "fortune", m: "fortune" }]], + celldata: [ + { r: 0, c: 0, v: { bg: "green", v: "fortune", m: "fortune" } }, + ], order: 0, + row: 1, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -125,8 +131,11 @@ export const SetCellFormat: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [[{ v: "set bg = green" }]], + celldata: [{ r: 0, c: 0, v: { v: "set bg = green" } }], order: 0, + row: 1, + column: 1, + config: { columnlen: { "0": 120 } }, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -148,26 +157,15 @@ export const AutoFillCell: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [ - [ - { m: "1", v: 1, ct: { t: "n", fa: "General" } }, - { m: "2", v: 2, ct: { t: "n", fa: "General" } }, - ], - [ - { m: "2", v: 2, ct: { t: "n", fa: "General" } }, - { m: "4", v: 4, ct: { t: "n", fa: "General" } }, - ], - [null, null], - [null, null], - [null, null], - [null, null], - [null, null], - [null, null], - [null, null], - [null, null], - [null, null], + celldata: [ + { r: 0, c: 0, v: { m: "1", v: 1, ct: { t: "n", fa: "General" } } }, + { r: 0, c: 1, v: { m: "2", v: 2, ct: { t: "n", fa: "General" } } }, + { r: 1, c: 0, v: { m: "2", v: 2, ct: { t: "n", fa: "General" } } }, + { r: 1, c: 1, v: { m: "4", v: 4, ct: { t: "n", fa: "General" } } }, ], order: 0, + row: 10, + column: 2, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -215,8 +213,10 @@ export const InsertRowCol: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [[{ v: "original" }]], + celldata: [{ r: 0, c: 0, v: { v: "original" } }], order: 0, + row: 1, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -241,14 +241,16 @@ export const DeleteRowCol: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [ - [{ v: "0" }], - [{ v: "1" }], - [{ v: "2" }], - [{ v: "3" }], - [{ v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 1, c: 0, v: { v: "1" } }, + { r: 2, c: 0, v: { v: "2" } }, + { r: 3, c: 0, v: { v: "3" } }, + { r: 4, c: 0, v: { v: "4" } }, ], order: 0, + row: 5, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -271,14 +273,16 @@ export const GetRowHeight: ComponentStory = () => { { name: "Sheet1", config: { rowlen: { 2: 200 } }, - data: [ - [{ v: "0" }], - [{ v: "1" }], - [{ v: "2" }], - [{ v: "3" }], - [{ v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 1, c: 0, v: { v: "1" } }, + { r: 2, c: 0, v: { v: "2" } }, + { r: 3, c: 0, v: { v: "3" } }, + { r: 4, c: 0, v: { v: "4" } }, ], order: 0, + row: 5, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -301,8 +305,16 @@ export const GetColumnWidth: ComponentStory = () => { { name: "Sheet1", config: { columnlen: { 2: 200 } }, - data: [[{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }]], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 0, c: 1, v: { v: "1" } }, + { r: 0, c: 2, v: { v: "2" } }, + { r: 0, c: 3, v: { v: "3" } }, + { r: 0, c: 4, v: { v: "4" } }, + ], order: 0, + row: 1, + column: 5, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -324,14 +336,16 @@ export const SetRowHeight: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [ - [{ v: "0" }], - [{ v: "1" }], - [{ v: "height = 100" }], - [{ v: "3" }], - [{ v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 1, c: 0, v: { v: "1" } }, + { r: 2, c: 0, v: { v: "height = 100" } }, + { r: 3, c: 0, v: { v: "3" } }, + { r: 4, c: 0, v: { v: "4" } }, ], order: 0, + row: 5, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -353,10 +367,16 @@ export const SetColumnWidth: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [ - [{ v: "0" }, { v: "1" }, { v: "width = 200" }, { v: "3" }, { v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 0, c: 1, v: { v: "1" } }, + { r: 0, c: 2, v: { v: "width = 200" } }, + { r: 0, c: 3, v: { v: "3" } }, + { r: 0, c: 4, v: { v: "4" } }, ], order: 0, + row: 1, + column: 5, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -379,11 +399,21 @@ export const GetSelection: ComponentStory = () => { { name: "Sheet1", luckysheet_select_save: [{ row: [0, 1], column: [1, 2] }], - data: [ - [{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }], - [{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 0, c: 1, v: { v: "1" } }, + { r: 0, c: 2, v: { v: "2" } }, + { r: 0, c: 3, v: { v: "3" } }, + { r: 0, c: 4, v: { v: "4" } }, + { r: 1, c: 0, v: { v: "0" } }, + { r: 1, c: 1, v: { v: "1" } }, + { r: 1, c: 2, v: { v: "2" } }, + { r: 1, c: 3, v: { v: "3" } }, + { r: 1, c: 4, v: { v: "4" } }, ], order: 0, + row: 2, + column: 5, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -405,11 +435,21 @@ export const SetSelection: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [ - [{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }], - [{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 0, c: 1, v: { v: "1" } }, + { r: 0, c: 2, v: { v: "2" } }, + { r: 0, c: 3, v: { v: "3" } }, + { r: 0, c: 4, v: { v: "4" } }, + { r: 1, c: 0, v: { v: "0" } }, + { r: 1, c: 1, v: { v: "1" } }, + { r: 1, c: 2, v: { v: "2" } }, + { r: 1, c: 3, v: { v: "3" } }, + { r: 1, c: 4, v: { v: "4" } }, ], order: 0, + row: 2, + column: 5, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -431,11 +471,21 @@ export const MergeCells: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [ - [{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }], - [{ v: "0" }, { v: "1" }, { v: "2" }, { v: "3" }, { v: "4" }], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 0, c: 1, v: { v: "1" } }, + { r: 0, c: 2, v: { v: "2" } }, + { r: 0, c: 3, v: { v: "3" } }, + { r: 0, c: 4, v: { v: "4" } }, + { r: 1, c: 0, v: { v: "0" } }, + { r: 1, c: 1, v: { v: "1" } }, + { r: 1, c: 2, v: { v: "2" } }, + { r: 1, c: 3, v: { v: "3" } }, + { r: 1, c: 4, v: { v: "4" } }, ], order: 0, + row: 2, + column: 5, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -457,13 +507,23 @@ export const GetAllSheets: ComponentStory = () => { const [data, setData] = useState([ { name: "Sheet1", - data: [[{ v: "0" }, { v: "1" }]], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 0, c: 1, v: { v: "1" } }, + ], order: 0, + row: 1, + column: 2, }, { name: "Sheet2", - data: [[{ v: "0" }], [{ v: "1" }]], + celldata: [ + { r: 0, c: 0, v: { v: "0" } }, + { r: 1, c: 0, v: { v: "1" } }, + ], order: 1, + row: 2, + column: 1, }, ]); const onChange = useCallback((d: Sheet[]) => { @@ -483,7 +543,14 @@ export const GetAllSheets: ComponentStory = () => { export const AddSheet: ComponentStory = () => { const ref = useRef(null); const [data, setData] = useState([ - { id: "1", name: "Sheet1", data: [[{ v: "1" }]], order: 0 }, + { + id: "1", + name: "Sheet1", + celldata: [{ r: 0, c: 0, v: { v: "1" } }], + order: 0, + row: 1, + column: 1, + }, ]); const onChange = useCallback((d: Sheet[]) => { setData(d); @@ -502,8 +569,22 @@ export const AddSheet: ComponentStory = () => { export const DeleteSheet: ComponentStory = () => { const ref = useRef(null); const [data, setData] = useState([ - { id: "1", name: "Sheet1", data: [[{ v: "1" }]], order: 0 }, - { id: "2", name: "Sheet2", data: [[{ v: "2" }]], order: 1 }, + { + id: "1", + name: "Sheet1", + celldata: [{ r: 0, c: 0, v: { v: "1" } }], + order: 0, + row: 1, + column: 1, + }, + { + id: "2", + name: "Sheet2", + celldata: [{ r: 0, c: 0, v: { v: "2" } }], + order: 1, + row: 1, + column: 1, + }, ]); const onChange = useCallback((d: Sheet[]) => { setData(d); @@ -522,7 +603,12 @@ export const DeleteSheet: ComponentStory = () => { export const UpdateSheet: ComponentStory = () => { const ref = useRef(null); const [data, setData] = useState([ - { id: "1", name: "sheet1", data: [[{ v: "1" }]], order: 0 }, + { + id: "1", + name: "sheet1", + celldata: [{ r: 0, c: 0, v: { v: "1" } }], + order: 0, + }, ]); const onChange = useCallback((d: Sheet[]) => { setData(d); @@ -636,8 +722,22 @@ export const UpdateSheet: ComponentStory = () => { export const ActivateSheet: ComponentStory = () => { const ref = useRef(null); const [data, setData] = useState([ - { id: "1", name: "Sheet1", data: [[{ v: "1" }]], order: 0 }, - { id: "2", name: "Sheet2", data: [[{ v: "2" }]], order: 1 }, + { + id: "1", + name: "Sheet1", + celldata: [{ r: 0, c: 0, v: { v: "1" } }], + order: 0, + row: 1, + column: 1, + }, + { + id: "2", + name: "Sheet2", + celldata: [{ r: 0, c: 0, v: { v: "2" } }], + order: 1, + row: 1, + column: 1, + }, ]); const onChange = useCallback((d: Sheet[]) => { setData(d); diff --git a/stories/data/cell.ts b/stories/data/cell.ts index 4087087d..884b6c68 100644 --- a/stories/data/cell.ts +++ b/stories/data/cell.ts @@ -1,7 +1,7 @@ const data = { name: "Cell", config: { - rowReadOnly: [2,3,4], + rowReadOnly: [2, 3, 4], merge: { "13_5": { r: 13,