@particle-academy/fancy-sheets 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +183 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +183 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -123,12 +123,41 @@ function lexFormula(input) {
|
|
|
123
123
|
if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_") {
|
|
124
124
|
const pos = i;
|
|
125
125
|
i++;
|
|
126
|
-
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_"))
|
|
127
|
-
|
|
126
|
+
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9" || input[i] === "_" || input[i] === " ")) {
|
|
127
|
+
if (input[i] === " ") {
|
|
128
|
+
let lookAhead = i + 1;
|
|
129
|
+
while (lookAhead < len && input[lookAhead] === " ") lookAhead++;
|
|
130
|
+
if (lookAhead < len && (input[lookAhead] >= "A" && input[lookAhead] <= "Z" || input[lookAhead] >= "a" && input[lookAhead] <= "z" || input[lookAhead] >= "0" && input[lookAhead] <= "9" || input[lookAhead] === "!")) {
|
|
131
|
+
i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
let word = input.slice(pos, i).trimEnd();
|
|
139
|
+
i = pos + word.length;
|
|
128
140
|
if (word.toUpperCase() === "TRUE" || word.toUpperCase() === "FALSE") {
|
|
129
141
|
tokens.push({ type: "boolean", value: word.toUpperCase(), position: pos });
|
|
130
142
|
continue;
|
|
131
143
|
}
|
|
144
|
+
if (i < len && input[i] === "!") {
|
|
145
|
+
const sheetName = word;
|
|
146
|
+
i++;
|
|
147
|
+
const refStart = i;
|
|
148
|
+
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
|
|
149
|
+
const ref1 = input.slice(refStart, i);
|
|
150
|
+
if (i < len && input[i] === ":") {
|
|
151
|
+
i++;
|
|
152
|
+
const ref2Start = i;
|
|
153
|
+
while (i < len && (input[i] >= "A" && input[i] <= "Z" || input[i] >= "a" && input[i] <= "z" || input[i] >= "0" && input[i] <= "9")) i++;
|
|
154
|
+
const ref2 = input.slice(ref2Start, i);
|
|
155
|
+
tokens.push({ type: "sheetRangeRef", value: sheetName + "!" + ref1 + ":" + ref2, position: pos });
|
|
156
|
+
} else {
|
|
157
|
+
tokens.push({ type: "sheetCellRef", value: sheetName + "!" + ref1.toUpperCase(), position: pos });
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
132
161
|
if (i < len && input[i] === ":") {
|
|
133
162
|
const colonPos = i;
|
|
134
163
|
i++;
|
|
@@ -272,6 +301,19 @@ function parseFormula(tokens) {
|
|
|
272
301
|
const parts = t.value.split(":");
|
|
273
302
|
return { type: "rangeRef", start: parts[0], end: parts[1] };
|
|
274
303
|
}
|
|
304
|
+
if (t.type === "sheetCellRef") {
|
|
305
|
+
advance();
|
|
306
|
+
const bangIdx = t.value.indexOf("!");
|
|
307
|
+
return { type: "sheetCellRef", sheet: t.value.slice(0, bangIdx), address: t.value.slice(bangIdx + 1).toUpperCase() };
|
|
308
|
+
}
|
|
309
|
+
if (t.type === "sheetRangeRef") {
|
|
310
|
+
advance();
|
|
311
|
+
const bangIdx = t.value.indexOf("!");
|
|
312
|
+
const sheetName = t.value.slice(0, bangIdx);
|
|
313
|
+
const rangePart = t.value.slice(bangIdx + 1);
|
|
314
|
+
const parts = rangePart.split(":");
|
|
315
|
+
return { type: "sheetRangeRef", sheet: sheetName, start: parts[0].toUpperCase(), end: parts[1].toUpperCase() };
|
|
316
|
+
}
|
|
275
317
|
if (t.type === "function") {
|
|
276
318
|
const name = advance().value;
|
|
277
319
|
expect("paren", "(");
|
|
@@ -1005,7 +1047,7 @@ registerFunction("TYPE", (args) => {
|
|
|
1005
1047
|
});
|
|
1006
1048
|
|
|
1007
1049
|
// src/engine/formula/evaluator.ts
|
|
1008
|
-
function evaluateAST(node, getCellValue, getRangeValues) {
|
|
1050
|
+
function evaluateAST(node, getCellValue, getRangeValues, ctx) {
|
|
1009
1051
|
switch (node.type) {
|
|
1010
1052
|
case "number":
|
|
1011
1053
|
return node.value;
|
|
@@ -1015,9 +1057,19 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
1015
1057
|
return node.value;
|
|
1016
1058
|
case "cellRef":
|
|
1017
1059
|
return getCellValue(node.address);
|
|
1018
|
-
case "rangeRef":
|
|
1060
|
+
case "rangeRef": {
|
|
1019
1061
|
const vals = getRangeValues(node.start, node.end);
|
|
1020
1062
|
return vals[0] ?? null;
|
|
1063
|
+
}
|
|
1064
|
+
case "sheetCellRef": {
|
|
1065
|
+
if (!ctx?.getSheetCellValue) return "#REF!";
|
|
1066
|
+
return ctx.getSheetCellValue(node.sheet, node.address);
|
|
1067
|
+
}
|
|
1068
|
+
case "sheetRangeRef": {
|
|
1069
|
+
if (!ctx?.getSheetRangeValues) return "#REF!";
|
|
1070
|
+
const vals = ctx.getSheetRangeValues(node.sheet, node.start, node.end);
|
|
1071
|
+
return vals[0] ?? null;
|
|
1072
|
+
}
|
|
1021
1073
|
case "functionCall": {
|
|
1022
1074
|
const entry = getFunction(node.name);
|
|
1023
1075
|
if (!entry) return `#NAME?`;
|
|
@@ -1025,7 +1077,11 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
1025
1077
|
if (arg.type === "rangeRef") {
|
|
1026
1078
|
return getRangeValues(arg.start, arg.end);
|
|
1027
1079
|
}
|
|
1028
|
-
|
|
1080
|
+
if (arg.type === "sheetRangeRef") {
|
|
1081
|
+
if (!ctx?.getSheetRangeValues) return ["#REF!"];
|
|
1082
|
+
return ctx.getSheetRangeValues(arg.sheet, arg.start, arg.end);
|
|
1083
|
+
}
|
|
1084
|
+
const val = evaluateAST(arg, getCellValue, getRangeValues, ctx);
|
|
1029
1085
|
return [val];
|
|
1030
1086
|
});
|
|
1031
1087
|
try {
|
|
@@ -1035,8 +1091,8 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
1035
1091
|
}
|
|
1036
1092
|
}
|
|
1037
1093
|
case "binaryOp": {
|
|
1038
|
-
const left = evaluateAST(node.left, getCellValue, getRangeValues);
|
|
1039
|
-
const right = evaluateAST(node.right, getCellValue, getRangeValues);
|
|
1094
|
+
const left = evaluateAST(node.left, getCellValue, getRangeValues, ctx);
|
|
1095
|
+
const right = evaluateAST(node.right, getCellValue, getRangeValues, ctx);
|
|
1040
1096
|
const lNum = typeof left === "number" ? left : Number(left);
|
|
1041
1097
|
const rNum = typeof right === "number" ? right : Number(right);
|
|
1042
1098
|
switch (node.operator) {
|
|
@@ -1069,7 +1125,7 @@ function evaluateAST(node, getCellValue, getRangeValues) {
|
|
|
1069
1125
|
}
|
|
1070
1126
|
}
|
|
1071
1127
|
case "unaryOp": {
|
|
1072
|
-
const operand = evaluateAST(node.operand, getCellValue, getRangeValues);
|
|
1128
|
+
const operand = evaluateAST(node.operand, getCellValue, getRangeValues, ctx);
|
|
1073
1129
|
const num = typeof operand === "number" ? operand : Number(operand);
|
|
1074
1130
|
if (isNaN(num)) return "#VALUE!";
|
|
1075
1131
|
return node.operator === "-" ? -num : num;
|
|
@@ -1183,7 +1239,7 @@ function getRecalculationOrder(graph) {
|
|
|
1183
1239
|
function recalculateWorkbook(workbook) {
|
|
1184
1240
|
return {
|
|
1185
1241
|
...workbook,
|
|
1186
|
-
sheets: workbook.sheets.map(recalculateSheet)
|
|
1242
|
+
sheets: workbook.sheets.map((s) => recalculateSheet(s, workbook.sheets))
|
|
1187
1243
|
};
|
|
1188
1244
|
}
|
|
1189
1245
|
function createInitialState(data) {
|
|
@@ -1213,7 +1269,7 @@ function pushUndo(state) {
|
|
|
1213
1269
|
if (stack.length > 50) stack.shift();
|
|
1214
1270
|
return { undoStack: stack, redoStack: [] };
|
|
1215
1271
|
}
|
|
1216
|
-
function recalculateSheet(sheet) {
|
|
1272
|
+
function recalculateSheet(sheet, allSheets) {
|
|
1217
1273
|
const graph = buildDependencyGraph(sheet.cells);
|
|
1218
1274
|
if (graph.size === 0) return sheet;
|
|
1219
1275
|
const circular = detectCircularRefs(graph);
|
|
@@ -1229,6 +1285,28 @@ function recalculateSheet(sheet) {
|
|
|
1229
1285
|
const addresses = expandRange(startAddr, endAddr);
|
|
1230
1286
|
return addresses.map(getCellValue);
|
|
1231
1287
|
};
|
|
1288
|
+
const getSheetCellValue = (sheetName, addr) => {
|
|
1289
|
+
if (!allSheets) return "#REF!";
|
|
1290
|
+
const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
|
|
1291
|
+
if (!target) return "#REF!";
|
|
1292
|
+
const c = target.cells[addr];
|
|
1293
|
+
if (!c) return null;
|
|
1294
|
+
if (c.formula && c.computedValue !== void 0) return c.computedValue;
|
|
1295
|
+
return c.value;
|
|
1296
|
+
};
|
|
1297
|
+
const getSheetRangeValues = (sheetName, startAddr, endAddr) => {
|
|
1298
|
+
if (!allSheets) return [];
|
|
1299
|
+
const target = allSheets.find((s) => s.name === sheetName || s.id === sheetName);
|
|
1300
|
+
if (!target) return [];
|
|
1301
|
+
const addresses = expandRange(startAddr, endAddr);
|
|
1302
|
+
return addresses.map((a) => {
|
|
1303
|
+
const c = target.cells[a];
|
|
1304
|
+
if (!c) return null;
|
|
1305
|
+
if (c.formula && c.computedValue !== void 0) return c.computedValue;
|
|
1306
|
+
return c.value;
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
const ctx = { getSheetCellValue, getSheetRangeValues };
|
|
1232
1310
|
for (const addr of order) {
|
|
1233
1311
|
const cell = cells[addr];
|
|
1234
1312
|
if (!cell?.formula) continue;
|
|
@@ -1239,7 +1317,7 @@ function recalculateSheet(sheet) {
|
|
|
1239
1317
|
try {
|
|
1240
1318
|
const tokens = lexFormula(cell.formula);
|
|
1241
1319
|
const ast = parseFormula(tokens);
|
|
1242
|
-
const result = evaluateAST(ast, getCellValue, getRangeValues);
|
|
1320
|
+
const result = evaluateAST(ast, getCellValue, getRangeValues, ctx);
|
|
1243
1321
|
cells[addr] = { ...cell, computedValue: result };
|
|
1244
1322
|
} catch {
|
|
1245
1323
|
cells[addr] = { ...cell, computedValue: "#ERROR!" };
|
|
@@ -1264,7 +1342,7 @@ function reducer(state, action) {
|
|
|
1264
1342
|
if (existing?.format) cellData.format = existing.format;
|
|
1265
1343
|
const workbook = updateActiveSheet(state, (s) => {
|
|
1266
1344
|
const updated = { ...s, cells: { ...s.cells, [action.address]: cellData } };
|
|
1267
|
-
return recalculateSheet(updated);
|
|
1345
|
+
return recalculateSheet(updated, state.workbook.sheets);
|
|
1268
1346
|
});
|
|
1269
1347
|
return { ...state, workbook, ...history };
|
|
1270
1348
|
}
|
|
@@ -1420,6 +1498,20 @@ function reducer(state, action) {
|
|
|
1420
1498
|
selection: { activeCell: "A1", ranges: [{ start: "A1", end: "A1" }] },
|
|
1421
1499
|
editingCell: null
|
|
1422
1500
|
};
|
|
1501
|
+
case "SET_FROZEN_ROWS": {
|
|
1502
|
+
const workbook = updateActiveSheet(state, (s) => ({
|
|
1503
|
+
...s,
|
|
1504
|
+
frozenRows: Math.max(0, action.count)
|
|
1505
|
+
}));
|
|
1506
|
+
return { ...state, workbook };
|
|
1507
|
+
}
|
|
1508
|
+
case "SET_FROZEN_COLS": {
|
|
1509
|
+
const workbook = updateActiveSheet(state, (s) => ({
|
|
1510
|
+
...s,
|
|
1511
|
+
frozenCols: Math.max(0, action.count)
|
|
1512
|
+
}));
|
|
1513
|
+
return { ...state, workbook };
|
|
1514
|
+
}
|
|
1423
1515
|
case "UNDO": {
|
|
1424
1516
|
if (state.undoStack.length === 0) return state;
|
|
1425
1517
|
const prev = state.undoStack[state.undoStack.length - 1];
|
|
@@ -1464,6 +1556,8 @@ function useSpreadsheetStore(initialData) {
|
|
|
1464
1556
|
addSheet: () => dispatch({ type: "ADD_SHEET" }),
|
|
1465
1557
|
renameSheet: (sheetId, name) => dispatch({ type: "RENAME_SHEET", sheetId, name }),
|
|
1466
1558
|
deleteSheet: (sheetId) => dispatch({ type: "DELETE_SHEET", sheetId }),
|
|
1559
|
+
setFrozenRows: (count) => dispatch({ type: "SET_FROZEN_ROWS", count }),
|
|
1560
|
+
setFrozenCols: (count) => dispatch({ type: "SET_FROZEN_COLS", count }),
|
|
1467
1561
|
setActiveSheet: (sheetId) => dispatch({ type: "SET_ACTIVE_SHEET", sheetId }),
|
|
1468
1562
|
undo: () => dispatch({ type: "UNDO" }),
|
|
1469
1563
|
redo: () => dispatch({ type: "REDO" }),
|
|
@@ -1552,11 +1646,49 @@ function RowHeader({ rowIndex }) {
|
|
|
1552
1646
|
);
|
|
1553
1647
|
}
|
|
1554
1648
|
RowHeader.displayName = "RowHeader";
|
|
1649
|
+
var EXCEL_EPOCH2 = new Date(1899, 11, 30).getTime();
|
|
1650
|
+
function serialToDateStr(serial) {
|
|
1651
|
+
const d = new Date(EXCEL_EPOCH2 + Math.floor(serial) * 864e5);
|
|
1652
|
+
const y = d.getFullYear();
|
|
1653
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1654
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1655
|
+
return `${y}-${m}-${day}`;
|
|
1656
|
+
}
|
|
1657
|
+
function serialToDateTimeStr(serial) {
|
|
1658
|
+
const date = serialToDateStr(serial);
|
|
1659
|
+
const fraction = serial % 1;
|
|
1660
|
+
const totalSeconds = Math.round(fraction * 86400);
|
|
1661
|
+
const h = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
|
|
1662
|
+
const min = String(Math.floor(totalSeconds % 3600 / 60)).padStart(2, "0");
|
|
1663
|
+
const s = String(totalSeconds % 60).padStart(2, "0");
|
|
1664
|
+
return `${date} ${h}:${min}:${s}`;
|
|
1665
|
+
}
|
|
1666
|
+
function isDateFormula(formula) {
|
|
1667
|
+
if (!formula) return false;
|
|
1668
|
+
const f = formula.toUpperCase();
|
|
1669
|
+
return /^(TODAY|NOW|DATE|EDATE)\b/.test(f) || /\b(TODAY|NOW|DATE|EDATE)\s*\(/.test(f);
|
|
1670
|
+
}
|
|
1671
|
+
function formatCellValue(val, cell) {
|
|
1672
|
+
if (val === null || val === void 0) return "";
|
|
1673
|
+
const fmt = cell?.format?.displayFormat;
|
|
1674
|
+
if (typeof val === "number") {
|
|
1675
|
+
if (fmt === "date") return serialToDateStr(val);
|
|
1676
|
+
if (fmt === "datetime") return serialToDateTimeStr(val);
|
|
1677
|
+
if (fmt === "percentage") return (val * 100).toFixed(1) + "%";
|
|
1678
|
+
if (fmt === "currency") return "$" + val.toFixed(2);
|
|
1679
|
+
if (fmt === "auto" || !fmt) {
|
|
1680
|
+
if (cell?.formula && isDateFormula(cell.formula)) {
|
|
1681
|
+
return val % 1 === 0 ? serialToDateStr(val) : serialToDateTimeStr(val);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
|
|
1686
|
+
return String(val);
|
|
1687
|
+
}
|
|
1555
1688
|
function getCellDisplayValue2(cell) {
|
|
1556
1689
|
if (!cell) return "";
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
return String(cell.value);
|
|
1690
|
+
const val = cell.formula && cell.computedValue !== void 0 ? cell.computedValue : cell.value;
|
|
1691
|
+
return formatCellValue(val, cell);
|
|
1560
1692
|
}
|
|
1561
1693
|
var Cell = react.memo(function Cell2({ address, row, col }) {
|
|
1562
1694
|
const {
|
|
@@ -1852,13 +1984,42 @@ function SpreadsheetGrid({ className }) {
|
|
|
1852
1984
|
children: [
|
|
1853
1985
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaders, {}) }),
|
|
1854
1986
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
1855
|
-
Array.from({ length: rowCount }, (_, rowIdx) =>
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1987
|
+
Array.from({ length: rowCount }, (_, rowIdx) => {
|
|
1988
|
+
const isFrozenRow = rowIdx < activeSheet.frozenRows;
|
|
1989
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1990
|
+
"div",
|
|
1991
|
+
{
|
|
1992
|
+
className: "flex",
|
|
1993
|
+
style: isFrozenRow ? {
|
|
1994
|
+
position: "sticky",
|
|
1995
|
+
top: rowHeight + rowIdx * rowHeight,
|
|
1996
|
+
zIndex: 8,
|
|
1997
|
+
backgroundColor: "inherit"
|
|
1998
|
+
} : void 0,
|
|
1999
|
+
children: [
|
|
2000
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky left-0 z-[5]", children: /* @__PURE__ */ jsxRuntime.jsx(RowHeader, { rowIndex: rowIdx }) }),
|
|
2001
|
+
Array.from({ length: columnCount }, (_2, colIdx) => {
|
|
2002
|
+
const addr = toAddress(rowIdx, colIdx);
|
|
2003
|
+
const isFrozenCol = colIdx < activeSheet.frozenCols;
|
|
2004
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2005
|
+
"div",
|
|
2006
|
+
{
|
|
2007
|
+
style: isFrozenCol ? {
|
|
2008
|
+
position: "sticky",
|
|
2009
|
+
left: 48 + Array.from({ length: colIdx }, (_3, c) => getColumnWidth(c)).reduce((a, b) => a + b, 0),
|
|
2010
|
+
zIndex: isFrozenRow ? 9 : 6,
|
|
2011
|
+
backgroundColor: "inherit"
|
|
2012
|
+
} : void 0,
|
|
2013
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Cell, { address: addr, row: rowIdx, col: colIdx })
|
|
2014
|
+
},
|
|
2015
|
+
addr
|
|
2016
|
+
);
|
|
2017
|
+
})
|
|
2018
|
+
]
|
|
2019
|
+
},
|
|
2020
|
+
rowIdx
|
|
2021
|
+
);
|
|
2022
|
+
}),
|
|
1862
2023
|
/* @__PURE__ */ jsxRuntime.jsx(SelectionOverlay, {}),
|
|
1863
2024
|
editorPosition && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1864
2025
|
"div",
|