@a3s-lab/office 0.41.0 → 0.43.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/README.md +30 -0
- package/dist/0~7048.js +1 -1
- package/dist/0~spreadsheet-editor.js +471 -58
- package/dist/4121.js +280 -1
- package/dist/core.js +2 -0
- package/dist/internal/features/work/editors/spreadsheet-conditional-format-model.d.ts +6 -1
- package/dist/internal/features/work/editors/spreadsheet-data-validation-custom.d.ts +18 -0
- package/dist/internal/features/work/editors/spreadsheet-data-validation.d.ts +2 -2
- package/dist/internal/features/work/work-spreadsheet-conditional-format.d.ts +3 -1
- package/dist/internal/features/work/work-spreadsheet-data-validation.d.ts +1 -0
- package/dist/internal/features/work/work-spreadsheet-local-formula.d.ts +43 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/styles.css +21 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -309,6 +309,36 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
|
|
|
309
309
|
|
|
310
310
|
## Current release
|
|
311
311
|
|
|
312
|
+
Version `0.43.0` makes Spreadsheet formula conditional formatting a local,
|
|
313
|
+
editable workflow instead of a file-only preservation path:
|
|
314
|
+
|
|
315
|
+
- **Home → Conditional Formatting → Custom formula** authors rules with
|
|
316
|
+
relative/absolute references, finite cross-sheet references, independent
|
|
317
|
+
text/fill colors, ordered precedence, and Stop-if-true behavior. Imported
|
|
318
|
+
XLSX expression rules reopen in the same editor.
|
|
319
|
+
- The shared synchronous evaluator reads only cached workbook values, caps
|
|
320
|
+
formulas at 255 Unicode characters and each decision at 1,024 cells, scans
|
|
321
|
+
bounded blank ranges without densifying the workbook, and fails closed for
|
|
322
|
+
external, whole-row/column, missing-sheet, or uncached-formula references.
|
|
323
|
+
- Native XLSX differential styles, `sqref`, priorities, and formulas round-trip
|
|
324
|
+
through export/reopen. The public **新建 → 公式条件格式** template shows a
|
|
325
|
+
blocking rule and a cross-sheet threshold without any remote service.
|
|
326
|
+
|
|
327
|
+
Version `0.42.0` extends Spreadsheet data validation with a local custom-formula
|
|
328
|
+
workflow that remains bounded, inspectable, and native-file compatible:
|
|
329
|
+
|
|
330
|
+
- Author a formula with an optional `=` prefix in the same accessible dialog;
|
|
331
|
+
relative references are anchored per selected range and the proposed value
|
|
332
|
+
is evaluated before it enters the controlled workbook.
|
|
333
|
+
- Common local functions, cell/range references, and sheet-qualified references
|
|
334
|
+
are evaluated synchronously with a 255-character formula and 1,024-cell read
|
|
335
|
+
budget. External, whole-row/column, missing-sheet, and uncached-formula
|
|
336
|
+
references fail closed instead of silently accepting an unsafe edit.
|
|
337
|
+
- Custom rules round-trip through XLSX and are demonstrated in the public
|
|
338
|
+
**新建 → 数据验证** template, alongside the existing list, date, and alert
|
|
339
|
+
branches. Paste and object-level batch writes remain separate preflight
|
|
340
|
+
boundaries.
|
|
341
|
+
|
|
312
342
|
Version `0.41.0` aligns Spreadsheet data-validation alerts with the local,
|
|
313
343
|
testable interaction model used by Traditional Office:
|
|
314
344
|
|
package/dist/0~7048.js
CHANGED
|
@@ -2733,4 +2733,4 @@ function abortError() {
|
|
|
2733
2733
|
function disposedError() {
|
|
2734
2734
|
return new Error('Office kernel client is disposed.');
|
|
2735
2735
|
}
|
|
2736
|
-
export { CollectionState, CommittedOfficeNumberField, OfficeCheckbox, OfficeNumberField, createOfficeKernelClient, handleOfficeTaskPaneKeyDown, normalizeOfficeFontFamily, officeFontFamilies, officeFontFamilyLabel, useOfficeDialog, useOfficeTaskPaneEscape, useOfficeTaskPaneModal };
|
|
2736
|
+
export { CollectionState, CommittedOfficeNumberField, OfficeCheckbox, OfficeNumberField, createOfficeKernelClient, handleOfficeTaskPaneKeyDown, normalizeFormulaForFortuneParser, normalizeOfficeFontFamily, officeFontFamilies, officeFontFamilyLabel, useOfficeDialog, useOfficeTaskPaneEscape, useOfficeTaskPaneModal };
|
|
@@ -3,13 +3,14 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Workbook } from "@fortune-sheet/react";
|
|
4
4
|
import { AArrowDown, AArrowUp, AlignCenter, AlignLeft, AlignRight, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, ArrowDown, ArrowDownToLine, ArrowLeft, ArrowLeftToLine, ArrowRight, ArrowRightToLine, ArrowUp, ArrowUpToLine, BadgeJapaneseYen, BarChart3, BetweenHorizontalEnd, BetweenHorizontalStart, BetweenVerticalEnd, BetweenVerticalStart, Bold, Bookmark, Calculator, CalendarClock, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, ClipboardPaste, Clock, Cloud, Columns3, Copy, CopyPlus, DecimalsArrowLeft, DecimalsArrowRight, Eraser, Eye, EyeOff, FileX2, Globe2, Grid2X2, Grid3X3, Hash, Info, Italic, KeyRound, Layers2, Link2, Link2Off, ListChecks, ListFilter, ListOrdered, ListPlus, LocateFixed, Merge, MessageSquareX, MoveHorizontal, MoveVertical, Paintbrush, Palette, PanelLeft, PanelTop, PanelsTopLeft, Pencil, Percent, Plus, Printer, Redo2, RefreshCw, Rows3, Save, Scissors, Search, Settings2, ShieldCheck, Sigma, SlidersHorizontal, SortAsc, SortDesc, Strikethrough, SwatchBook, Table2, TableProperties, Trash2, TriangleAlert, Underline, Undo2, WrapText, X } from "lucide-react";
|
|
5
5
|
import { Fragment as external_react_Fragment, cloneElement, createElement, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Parser, SUPPORTED_FORMULAS } from "@fortune-sheet/formula-parser";
|
|
6
7
|
import { convertSpanToShareString, genarate, indexToColumnChar, update as core_update } from "@fortune-sheet/core";
|
|
8
|
+
import { useOfficeTaskPaneEscape, OfficeCheckbox, useOfficeDialog, CommittedOfficeNumberField, useOfficeTaskPaneModal, createOfficeKernelClient, officeFontFamilies, CollectionState, OfficeNumberField, handleOfficeTaskPaneKeyDown, normalizeFormulaForFortuneParser } from "./0~7048.js";
|
|
7
9
|
import { freezeImportedSpreadsheetCell, normalizeXlsxSemanticColorOrigin, spreadsheetMatrixProfile, withXlsxPatternFill, withoutEditableRange, normalizeSpreadsheetConditionalVisualOptions, xlsxSemanticColorMatchesValue, withXlsxGradientFill, effectiveSpreadsheetPageSetup, attachSpreadsheetShownCommentCells, normalizeXlsxPatternFill, withEditableRange, sameSpreadsheetHistoryValue, isSpreadsheetConditionalComparisonOperator, spreadsheetConditionalIconForValue, defaultSpreadsheetConditionalIconThresholds, xlsxPatternFillTypes, sheetProtectionAuthority, normalizeXlsxGradientFill, xlsxGradientFillFallbackColor, editableRangeCellCount, unlockedCellCount, registerDerivedSpreadsheetMatrix, editableRangeRequiresCredentials, SPREADSHEET_CONDITIONAL_COMPARISON_OPERATORS, normalizeSpreadsheetConditionalIconSetFormat, defaultSpreadsheetDataBarOptions, spreadsheetConditionalThresholdValue, normalizeSheetProtectionAuthority, withSheetProtection, spreadsheetProtectionKey, defaultSpreadsheetColorScaleThresholds, protectedSheetCount, SPREADSHEET_CONDITIONAL_ICON_SETS, withSheetSelectionPermissions, spreadsheetConditionalComparisonNeedsUpperValue, drawSpreadsheetConditionalIcon, isSpreadsheetConditionalIconSetName, spreadsheetConditionalThresholdsEqual, spreadsheetConditionalIconSetCount, spreadsheetConditionalIconAppearance } from "./9333.js";
|
|
8
10
|
import { workSpreadsheetChartSupportsGrouping, spreadsheetFormulaRangesForSelection, createSpreadsheetChartFromSelection, normalizeWorkSpreadsheetDataLabelPosition, normalizeSpreadsheetPrintTitleColumns, normalizeSpreadsheetPrintTitleRows, formulaHasStructuredReference, normalizeWorkSpreadsheetErrorBars, defaultPivotValueCaption, workSpreadsheetChartSupportsBarSpacing, normalizeWorkSpreadsheetChartGrouping, spreadsheetGridSize, normalizeWorkSpreadsheetChartAxisGroup, normalizeWorkSpreadsheetScatterStyle, cloneSparseMatrix, spreadsheetPivotAggregationLabel, workSpreadsheetChartAxisIsCategoryAxis, reconcileSpreadsheetChartPreviews, normalizeWorkSpreadsheetChartAxes, normalizeWorkSpreadsheetDoughnutHoleSize, normalizeWorkSpreadsheetChartLegendPosition, spreadsheetChartCount, workSpreadsheetChartTypeLabel, sparseArrayEntries, normalizeWorkSpreadsheetTrendline, effectiveSpreadsheetCalculationSettings, spreadsheetPivotIntersects, normalizeWorkSpreadsheetDataLabels, spreadsheetPivotOutputContains, normalizeSpreadsheetPrintArea, spreadsheetCellAddress, formatSpreadsheetCellRanges, normalizeWorkSpreadsheetChartLayout, spreadsheetFormulaRangeForCell, workSpreadsheetChartAxisIsValueAxis, normalizeWorkSpreadsheetChartSmoothLines, spreadsheetPivotFilterValueKey, normalizeWorkSpreadsheetBubbleScale, normalizeWorkSpreadsheetChartSeriesStyle, volatileSpreadsheetFormulaFunctions, sparseArrayIndexes, normalizeWorkSpreadsheetBubbleSizeRepresents, workSpreadsheetChartSupportsTrendlines, normalizeWorkSpreadsheetChartLegendOverlay, parseSpreadsheetChartReference, workSpreadsheetChartGroupingIsStacked, normalizeWorkSpreadsheetChartGapWidth, workSpreadsheetChartSupportsSmoothLines, refreshSpreadsheetPivotTables, createSpreadsheetPivotFromSelection, reconcileSpreadsheetPivots, spreadsheetPivotCount, sparseMatrixColumnCount, workSpreadsheetChartSupportsErrorBars, formulaHasExternalReference, isValidSpreadsheetDefinedName, spreadsheetPivotValidation, deleteSpreadsheetPivotTable, workSpreadsheetChartAxisDefaultLabelPosition, workSpreadsheetChartSupportsSeriesAnalysis, parseSpreadsheetCellRanges, spreadsheetPivotFilterItems, spreadsheetPivotFields, normalizeWorkSpreadsheetChartOverlap, spreadsheetFormulaRangeConflict, spreadsheetSheetsWithChartPreviews, workSpreadsheetChartUsesNumericXAxis, workSpreadsheetChartAxisShowsMajorGridlinesByDefault, workSpreadsheetChartSupportsAxes, normalizeWorkSpreadsheetCombinationSeriesType, normalizeWorkSpreadsheetRadarStyle } from "./8715.js";
|
|
9
11
|
import { workSpreadsheetFilterHiddenRows, xlsxRichTextCellText, spreadsheetTextOrientationFromAngle, spreadsheetCellValueWithDiagonalBorder, spreadsheetUnderlineCellValue, workSpreadsheetAutoFilterCriteriaEntries, spreadsheetTableTotalsFunctionLabel, withDefaultSpreadsheetTableTotalsLabel, normalizeXlsxRichTextEditSource, spreadsheetTableTotalsColumnPatch, SPREADSHEET_TABLE_TOTALS_FUNCTIONS, normalizeXlsxRichTextCell, activeXlsxNativeFill, normalizeSpreadsheetDateValidationBoundary, spreadsheetTextOrientationChoiceFromCell, parseSpreadsheetSortCustomList, workSpreadsheetFilterTextCharacters, sameXlsxRichTextRunStyle, spreadsheetSortCustomListMatchKey, createSpreadsheetSortCustomList, reconcileSpreadsheetTableTotalsColumns, spreadsheetTextOrientationFromCell, patchSpreadsheetRichTextFontRuns, synchronizeSpreadsheetTableTotalsRow, workSpreadsheetAutoFilterManuallyHiddenRows, normalizedWorkSpreadsheetAutoFilterRange, workSpreadsheetSheetWithoutAutoFilterCriteria, isSpreadsheetTextOrientationId, reconcileSpreadsheetTableCalculatedColumns, spreadsheetVisibleTextRotationFromCell, spreadsheetTextOrientationFromChoice, normalizeWorkSpreadsheetFilterCriteria, readOfficeSpreadsheetCollaboration as readWorkOfficeSpreadsheetCollaboration, normalizeSpreadsheetDataValidationErrorStyle, workSpreadsheetAutoFilterCriteria, normalizeXlsxRichTextColor, isHighSurrogate, xlsxNativeFillCellKeys, workSpreadsheetSheetWithReappliedAutoFilterCriteria, workSpreadsheetSheetWithAutoFilterCriteria, createOfficeSpreadsheetCollaborationBinding as createWorkOfficeSpreadsheetCollaborationBinding, boundedSpreadsheetDataValidationText, spreadsheetUnderlineStyle, workSpreadsheetCellIsDate, workSpreadsheetAutoFilterOwnedRows, deleteXlsxNativeFills, spreadsheetTextOrientationCellStyle, normalizeXlsxRichTextRun, spreadsheetDiagonalBorderFromCellValue, isLowSurrogate, mergeSpreadsheetSortCustomLists, spreadsheetTableTotalsRowHasContent, validateSpreadsheetSortCustomList, normalizeStoredSpreadsheetSortCustomLists, coalesceXlsxRichTextRuns, fillSpreadsheetTableCalculatedColumns, replaceOfficeSpreadsheetCollaboration as replaceWorkOfficeSpreadsheetCollaboration, validXlsxRichText, spreadsheetSortCustomListsEqual, isSpreadsheetUnderlineStyle } from "./5416.js";
|
|
10
12
|
import { moveOfficeMenuFocus, stepOfficeZoom, useOfficeRemoteParticipants, Popover, Dialog, officePresenceColor, useOfficePublishPresenceLocation, button_Button, icon_button_IconButton, moveOfficeGridMenuFocus, office_text_field_OfficeTextField, useDialogFocusScope, useOfficeEditorWheelZoom } from "./0~8136.js";
|
|
11
13
|
import { isOfficeKernelSpreadsheetError, normalizeCssColor, OFFICE_KERNEL_SPREADSHEET_MAX_ROWS, createOfficeId as createWorkId } from "./4121.js";
|
|
12
|
-
import { useOfficeTaskPaneEscape, OfficeCheckbox, CommittedOfficeNumberField, useOfficeDialog, useOfficeTaskPaneModal, createOfficeKernelClient, officeFontFamilies, CollectionState, OfficeNumberField, handleOfficeTaskPaneKeyDown } from "./0~7048.js";
|
|
13
14
|
import { isOfficeShortcutBlocked, OfficeColorPicker } from "./0~3635.js";
|
|
14
15
|
import { createOfficeEditorExtension, useOfficeEditorRuntime, useOfficeEditorKeyboardShortcuts } from "./0~3557.js";
|
|
15
16
|
import { showToast } from "./6489.js";
|
|
@@ -158,30 +159,187 @@ function displayLength(valueRatio, minLength, maxLength) {
|
|
|
158
159
|
const normalized = Math.max(0, Math.min(1, valueRatio));
|
|
159
160
|
return minLength + (maxLength - minLength) * normalized;
|
|
160
161
|
}
|
|
161
|
-
|
|
162
|
+
const MAX_SPREADSHEET_LOCAL_FORMULA_LENGTH = 255;
|
|
163
|
+
const work_spreadsheet_local_formula_MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS = 1024;
|
|
164
|
+
const MAX_SHEET_ROW = 1048576;
|
|
165
|
+
const MAX_SHEET_COLUMN = 16384;
|
|
166
|
+
const LOCAL_FORMULA_FUNCTIONS = new Set(SUPPORTED_FORMULAS.map((name)=>name.toUpperCase()));
|
|
167
|
+
function evaluateSpreadsheetLocalFormula(formula, options) {
|
|
168
|
+
const source = formula.trim().replace(/^=/, '').trim();
|
|
169
|
+
if (!source) return unsupportedLocalFormula('公式为空。');
|
|
170
|
+
if (Array.from(source).length > MAX_SPREADSHEET_LOCAL_FORMULA_LENGTH) return unsupportedLocalFormula('公式超过 255 个字符。');
|
|
171
|
+
const currentSheet = options.sheets.find((candidate)=>candidate.id === options.sheetId);
|
|
172
|
+
if (!currentSheet) return unsupportedLocalFormula('找不到公式所在的工作表。');
|
|
173
|
+
const evaluator = new SpreadsheetLocalFormulaEvaluator(options, currentSheet);
|
|
174
|
+
return evaluator.evaluate(source);
|
|
175
|
+
}
|
|
176
|
+
function spreadsheetLocalFormulaBoolean(value) {
|
|
177
|
+
if ('boolean' == typeof value) return value;
|
|
178
|
+
if ('number' == typeof value) return Number.isFinite(value) && 0 !== value;
|
|
179
|
+
return 'string' == typeof value && 'TRUE' === value.trim().toUpperCase();
|
|
180
|
+
}
|
|
181
|
+
class SpreadsheetLocalFormulaEvaluator {
|
|
182
|
+
options;
|
|
183
|
+
currentSheet;
|
|
184
|
+
sheetsByName = new Map();
|
|
185
|
+
anchor;
|
|
186
|
+
referencedCells = 0;
|
|
187
|
+
unsupportedMessage;
|
|
188
|
+
celldataBySheet = new Map();
|
|
189
|
+
constructor(options, currentSheet){
|
|
190
|
+
this.options = options;
|
|
191
|
+
this.currentSheet = currentSheet;
|
|
192
|
+
this.anchor = {
|
|
193
|
+
row: options.anchorRow ?? options.row,
|
|
194
|
+
column: options.anchorColumn ?? options.column
|
|
195
|
+
};
|
|
196
|
+
for (const sheet of options.sheets)this.sheetsByName.set(sheet.name.trim().toLocaleLowerCase(), sheet);
|
|
197
|
+
}
|
|
198
|
+
evaluate(formula) {
|
|
199
|
+
const parser = new Parser();
|
|
200
|
+
parser.setFunction('ROW', (parameters)=>parameters.length ? null : this.options.row + 1).setFunction('COLUMN', (parameters)=>parameters.length ? null : this.options.column + 1);
|
|
201
|
+
let unsupportedFunction;
|
|
202
|
+
parser.on('callFunction', (name)=>{
|
|
203
|
+
const normalized = name.toUpperCase();
|
|
204
|
+
if (!LOCAL_FORMULA_FUNCTIONS.has(normalized)) unsupportedFunction ??= normalized;
|
|
205
|
+
});
|
|
206
|
+
parser.on('callCellValue', (coordinate, _parserOptions, done)=>{
|
|
207
|
+
const resolved = this.resolveCoordinate(this.currentSheet, coordinate);
|
|
208
|
+
if (!resolved) return void done(null);
|
|
209
|
+
done(this.readCell(resolved));
|
|
210
|
+
});
|
|
211
|
+
parser.on('callRangeValue', (start, end, _parserOptions, done)=>{
|
|
212
|
+
done(this.readRange(start, end));
|
|
213
|
+
});
|
|
214
|
+
const parsed = parser.parse(normalizeFormulaForFortuneParser(formula), {
|
|
215
|
+
sheetId: this.currentSheet.id
|
|
216
|
+
});
|
|
217
|
+
if (unsupportedFunction) return unsupportedLocalFormula(`公式函数“${unsupportedFunction}”无法在浏览器本地求值。`);
|
|
218
|
+
if (this.unsupportedMessage) return unsupportedLocalFormula(this.unsupportedMessage);
|
|
219
|
+
if (parsed.error || void 0 === parsed.result) return unsupportedLocalFormula('公式无法求值,请检查函数、引用和括号。');
|
|
220
|
+
return {
|
|
221
|
+
supported: true,
|
|
222
|
+
value: parsed.result
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
resolveCoordinate(fallbackSheet, coordinate) {
|
|
226
|
+
const sheet = coordinate.sheetName ? this.sheetsByName.get(coordinate.sheetName.trim().toLocaleLowerCase()) : fallbackSheet;
|
|
227
|
+
if (!sheet) {
|
|
228
|
+
this.unsupportedMessage = '公式引用了不存在的工作表。';
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (coordinate.row.index < 0 || coordinate.column.index < 0) {
|
|
232
|
+
this.unsupportedMessage = '公式暂不支持整行或整列引用,请改用有限区域。';
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const row = coordinate.row.isAbsolute ? coordinate.row.index : this.options.row + (coordinate.row.index - this.anchor.row);
|
|
236
|
+
const column = coordinate.column.isAbsolute ? coordinate.column.index : this.options.column + (coordinate.column.index - this.anchor.column);
|
|
237
|
+
if (row < 0 || column < 0 || row >= MAX_SHEET_ROW || column >= MAX_SHEET_COLUMN) {
|
|
238
|
+
this.unsupportedMessage = '公式引用超出了工作表边界。';
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
sheet,
|
|
243
|
+
row,
|
|
244
|
+
column
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
readRange(start, end) {
|
|
248
|
+
const startCoordinate = this.resolveCoordinate(this.currentSheet, start);
|
|
249
|
+
const rangeSheet = start.sheetName ? this.sheetsByName.get(start.sheetName.trim().toLocaleLowerCase()) : this.currentSheet;
|
|
250
|
+
const endCoordinate = rangeSheet ? this.resolveCoordinate(rangeSheet, end) : null;
|
|
251
|
+
if (!startCoordinate || !endCoordinate || startCoordinate.sheet.id !== endCoordinate.sheet.id) {
|
|
252
|
+
this.unsupportedMessage ??= '公式区域引用必须位于同一工作表。';
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const rowStart = Math.min(startCoordinate.row, endCoordinate.row);
|
|
256
|
+
const rowEnd = Math.max(startCoordinate.row, endCoordinate.row);
|
|
257
|
+
const columnStart = Math.min(startCoordinate.column, endCoordinate.column);
|
|
258
|
+
const columnEnd = Math.max(startCoordinate.column, endCoordinate.column);
|
|
259
|
+
const area = (rowEnd - rowStart + 1) * (columnEnd - columnStart + 1);
|
|
260
|
+
if (area <= 0 || area > work_spreadsheet_local_formula_MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS - this.referencedCells) {
|
|
261
|
+
this.unsupportedMessage = '公式一次最多读取 1,024 个单元格。';
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
const values = [];
|
|
265
|
+
for(let row = rowStart; row <= rowEnd; row += 1){
|
|
266
|
+
const valuesRow = [];
|
|
267
|
+
for(let column = columnStart; column <= columnEnd; column += 1)valuesRow.push(this.readCell({
|
|
268
|
+
sheet: startCoordinate.sheet,
|
|
269
|
+
row,
|
|
270
|
+
column
|
|
271
|
+
}));
|
|
272
|
+
values.push(valuesRow);
|
|
273
|
+
}
|
|
274
|
+
return values;
|
|
275
|
+
}
|
|
276
|
+
readCell(coordinate) {
|
|
277
|
+
this.referencedCells += 1;
|
|
278
|
+
if (this.referencedCells > work_spreadsheet_local_formula_MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS) {
|
|
279
|
+
this.unsupportedMessage = '公式一次最多读取 1,024 个单元格。';
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (this.options.useProposedValue && coordinate.sheet.id === this.currentSheet.id && coordinate.row === this.options.row && coordinate.column === this.options.column) return this.options.proposedValue;
|
|
283
|
+
const cell = this.cellAt(coordinate.sheet, coordinate.row, coordinate.column);
|
|
284
|
+
if (!cell) return null;
|
|
285
|
+
if ('string' == typeof cell.f && cell.f.length > 0 && void 0 === cell.v) {
|
|
286
|
+
this.unsupportedMessage ??= '公式引用了尚未计算的公式单元格。';
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return cell.v ?? null;
|
|
290
|
+
}
|
|
291
|
+
cellAt(sheet, row, column) {
|
|
292
|
+
if (void 0 !== sheet.data) return sheet.data[row]?.[column] ?? null;
|
|
293
|
+
let cells = this.celldataBySheet.get(sheet.id);
|
|
294
|
+
if (!cells) {
|
|
295
|
+
cells = new Map();
|
|
296
|
+
for (const entry of sheet.celldata ?? []){
|
|
297
|
+
const value = entry.v && 'object' == typeof entry.v && 'v' in entry.v ? entry.v : {
|
|
298
|
+
v: entry.v
|
|
299
|
+
};
|
|
300
|
+
cells.set(`${entry.r}_${entry.c}`, value);
|
|
301
|
+
}
|
|
302
|
+
this.celldataBySheet.set(sheet.id, cells);
|
|
303
|
+
}
|
|
304
|
+
return cells.get(`${row}_${column}`) ?? null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function unsupportedLocalFormula(message) {
|
|
308
|
+
return {
|
|
309
|
+
supported: false,
|
|
310
|
+
value: false,
|
|
311
|
+
message
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const MAX_SPREADSHEET_CONDITIONAL_FORMULA_SCAN_CELLS = 10000;
|
|
315
|
+
function spreadsheetConditionalFormatStyles(sheet, formulaSheets) {
|
|
162
316
|
const styles = new Map();
|
|
163
317
|
const data = sheet.data ?? [];
|
|
164
|
-
|
|
318
|
+
const rules = conditionalRules(sheet);
|
|
319
|
+
if (!data.length && !rules.some(isFormulaConditionalRule)) return styles;
|
|
165
320
|
const blocked = new Set();
|
|
166
|
-
for (const rule of
|
|
321
|
+
for (const rule of rules){
|
|
167
322
|
const matched = new Set();
|
|
168
323
|
if ('colorGradation' === rule.type) applyColorScale(styles, data, rule, blocked, matched);
|
|
169
324
|
else if ('dataBar' === rule.type) applyDataBars(styles, data, rule, blocked, matched);
|
|
170
325
|
else if ('icons' === rule.type) applyIconSet(styles, data, rule, blocked, matched);
|
|
171
|
-
else applyDefaultRule(styles, data, rule, blocked, matched);
|
|
326
|
+
else applyDefaultRule(styles, data, rule, blocked, matched, formulaSheets, sheet);
|
|
172
327
|
if (rule.stopIfTrue) for (const key of matched)blocked.add(key);
|
|
173
328
|
}
|
|
174
329
|
return styles;
|
|
175
330
|
}
|
|
176
|
-
function
|
|
331
|
+
function isFormulaConditionalRule(rule) {
|
|
332
|
+
return 'default' === rule.type && 'formula' === rule.conditionName;
|
|
333
|
+
}
|
|
334
|
+
function applyDefaultRule(styles, data, rule, blocked, matched, formulaSheets, currentSheet) {
|
|
177
335
|
if ('default' !== rule.type || !isConditionalFormatStyle(rule.format) || !rule.conditionName) return;
|
|
178
336
|
const values = (rule.conditionValue ?? []).map(String);
|
|
179
337
|
const style = rule.format;
|
|
180
338
|
if ('formula' === rule.conditionName && values[0]) {
|
|
181
|
-
for (const range of rule.cellrange)
|
|
339
|
+
for (const range of rule.cellrange)forEachFormulaCell(data, [
|
|
182
340
|
range
|
|
183
341
|
], ({ row, column })=>{
|
|
184
|
-
if (evaluateFormula(values[0], row, column, range,
|
|
342
|
+
if (evaluateFormula(values[0], row, column, range, currentSheet, formulaSheets)) applyDifferentialStyle(styles, blocked, matched, row, column, style);
|
|
185
343
|
});
|
|
186
344
|
return;
|
|
187
345
|
}
|
|
@@ -360,35 +518,20 @@ function matchesCondition(value, name, conditions) {
|
|
|
360
518
|
}
|
|
361
519
|
return false;
|
|
362
520
|
}
|
|
363
|
-
function evaluateFormula(source, row, column, baseRange,
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
const comparison = /^(\$?)([A-Z]{1,3})(\$?)([1-9]\d*)(=|<>|>=|<=|>|<)(?:"((?:[^"]|"")*)"|(-?\d+(?:\.\d+)?))$/i.exec(formula);
|
|
379
|
-
if (!comparison) return false;
|
|
380
|
-
const target = formulaReference(comparison[1], comparison[2], comparison[3], comparison[4], row, column, baseRange);
|
|
381
|
-
const left = work_spreadsheet_conditional_format_cellValue(data, target.row, target.column);
|
|
382
|
-
const right = void 0 !== comparison[6] ? comparison[6].replaceAll('""', '"') : Number(comparison[7]);
|
|
383
|
-
return compareWithOperator(left, comparison[5], right);
|
|
384
|
-
}
|
|
385
|
-
function formulaReference(absoluteColumn, columnName, absoluteRow, rowNumber, row, column, baseRange) {
|
|
386
|
-
const sourceColumn = decodeColumn(columnName);
|
|
387
|
-
const sourceRow = Number(rowNumber) - 1;
|
|
388
|
-
return {
|
|
389
|
-
row: absoluteRow ? sourceRow : sourceRow + row - baseRange.row[0],
|
|
390
|
-
column: absoluteColumn ? sourceColumn : sourceColumn + column - baseRange.column[0]
|
|
391
|
-
};
|
|
521
|
+
function evaluateFormula(source, row, column, baseRange, currentSheet, formulaSheets) {
|
|
522
|
+
const sheets = [
|
|
523
|
+
toFormulaSheet(currentSheet),
|
|
524
|
+
...(formulaSheets ?? []).filter((sheet)=>sheet.id && sheet.id !== currentSheet.id).map(toFormulaSheet)
|
|
525
|
+
];
|
|
526
|
+
const result = evaluateSpreadsheetLocalFormula(source, {
|
|
527
|
+
sheets,
|
|
528
|
+
sheetId: currentSheet.id ?? '',
|
|
529
|
+
row,
|
|
530
|
+
column,
|
|
531
|
+
anchorRow: baseRange.row[0],
|
|
532
|
+
anchorColumn: baseRange.column[0]
|
|
533
|
+
});
|
|
534
|
+
return result.supported && spreadsheetLocalFormulaBoolean(result.value);
|
|
392
535
|
}
|
|
393
536
|
function applyDifferentialStyle(styles, blocked, matched, row, column, style) {
|
|
394
537
|
const patch = {};
|
|
@@ -447,6 +590,21 @@ function forEachCell(data, ranges, visit) {
|
|
|
447
590
|
column
|
|
448
591
|
});
|
|
449
592
|
}
|
|
593
|
+
function forEachFormulaCell(data, ranges, visit) {
|
|
594
|
+
for (const range of ranges){
|
|
595
|
+
const area = (range.row[1] - range.row[0] + 1) * (range.column[1] - range.column[0] + 1);
|
|
596
|
+
if (area > 0 && area <= MAX_SPREADSHEET_CONDITIONAL_FORMULA_SCAN_CELLS) {
|
|
597
|
+
for(let row = range.row[0]; row <= range.row[1]; row += 1)for(let column = range.column[0]; column <= range.column[1]; column += 1)visit({
|
|
598
|
+
row,
|
|
599
|
+
column
|
|
600
|
+
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
forEachCell(data, [
|
|
604
|
+
range
|
|
605
|
+
], visit);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
450
608
|
function work_spreadsheet_conditional_format_cellValue(data, row, column) {
|
|
451
609
|
return data[row]?.[column]?.v ?? null;
|
|
452
610
|
}
|
|
@@ -463,19 +621,13 @@ function compareValues(left, right) {
|
|
|
463
621
|
if (String(left).trim() && String(right).trim() && Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return leftNumber === rightNumber ? 0 : leftNumber > rightNumber ? 1 : -1;
|
|
464
622
|
return String(left).localeCompare(String(right));
|
|
465
623
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return '<=' === operator && comparison <= 0;
|
|
474
|
-
}
|
|
475
|
-
function decodeColumn(value) {
|
|
476
|
-
let result = 0;
|
|
477
|
-
for (const character of value.toUpperCase())result = 26 * result + character.charCodeAt(0) - 64;
|
|
478
|
-
return result - 1;
|
|
624
|
+
function toFormulaSheet(sheet) {
|
|
625
|
+
return {
|
|
626
|
+
id: sheet.id ?? '',
|
|
627
|
+
name: sheet.name,
|
|
628
|
+
data: sheet.data,
|
|
629
|
+
celldata: sheet.celldata
|
|
630
|
+
};
|
|
479
631
|
}
|
|
480
632
|
function drawSpreadsheetDiagonalDownBorder(context, bounds, line) {
|
|
481
633
|
drawSpreadsheetDiagonalBorder(context, bounds, line, 'down');
|
|
@@ -8036,7 +8188,7 @@ const dateOperators = [
|
|
|
8036
8188
|
'noLaterThan'
|
|
8037
8189
|
];
|
|
8038
8190
|
function spreadsheetDataValidationOperators(type) {
|
|
8039
|
-
if ('dropdown' === type) return [];
|
|
8191
|
+
if ('custom' === type || 'dropdown' === type) return [];
|
|
8040
8192
|
return 'date' === type ? dateOperators : numericOperators;
|
|
8041
8193
|
}
|
|
8042
8194
|
function createSpreadsheetDataValidationDialogSource(content, target) {
|
|
@@ -8121,8 +8273,8 @@ function spreadsheetDataValidationTargetError(sheet, ranges) {
|
|
|
8121
8273
|
}
|
|
8122
8274
|
function normalizeSpreadsheetDataValidationItem(content, sheet, ranges, value) {
|
|
8123
8275
|
const operators = spreadsheetDataValidationOperators(value.type);
|
|
8124
|
-
const type2 = 'dropdown' === value.type ? '' : value.type2;
|
|
8125
|
-
if ('dropdown' !== value.type && !operators.includes(type2)) return spreadsheetDataValidationError('invalid-operator');
|
|
8276
|
+
const type2 = 'dropdown' === value.type || 'custom' === value.type ? '' : value.type2;
|
|
8277
|
+
if ('dropdown' !== value.type && 'custom' !== value.type && !operators.includes(type2)) return spreadsheetDataValidationError('invalid-operator');
|
|
8126
8278
|
const normalizedValues = normalizeSpreadsheetDataValidationValues(content, sheet, value.type, type2, value.value1, value.value2);
|
|
8127
8279
|
if (!normalizedValues.ok) return normalizedValues;
|
|
8128
8280
|
return {
|
|
@@ -8153,6 +8305,15 @@ function normalizeSpreadsheetDataValidationValues(content, sheet, type, type2, r
|
|
|
8153
8305
|
const value2 = rawValue2.trim();
|
|
8154
8306
|
if (!value1) return spreadsheetDataValidationError('missing-value');
|
|
8155
8307
|
if ('dropdown' === type) return normalizeSpreadsheetListValidation(content, sheet, value1);
|
|
8308
|
+
if ('custom' === type) {
|
|
8309
|
+
const formula = value1.replace(/^=/, '').trim();
|
|
8310
|
+
if (!formula || Array.from(formula).length > 255 || /[\u0000-\u001f\u007f]/u.test(formula)) return spreadsheetDataValidationError('invalid-custom-formula');
|
|
8311
|
+
return {
|
|
8312
|
+
ok: true,
|
|
8313
|
+
value1: formula,
|
|
8314
|
+
value2: ''
|
|
8315
|
+
};
|
|
8316
|
+
}
|
|
8156
8317
|
const needsSecond = 'between' === type2 || 'notBetween' === type2;
|
|
8157
8318
|
if (needsSecond && !value2) return spreadsheetDataValidationError('missing-value');
|
|
8158
8319
|
if ('date' === type) {
|
|
@@ -8256,6 +8417,7 @@ function spreadsheetDataValidationDialogValue(value) {
|
|
|
8256
8417
|
}
|
|
8257
8418
|
function spreadsheetDataValidationType(value) {
|
|
8258
8419
|
return [
|
|
8420
|
+
'custom',
|
|
8259
8421
|
'date',
|
|
8260
8422
|
'dropdown',
|
|
8261
8423
|
'number',
|
|
@@ -8443,6 +8605,7 @@ function cloneSpreadsheetCellRange(range) {
|
|
|
8443
8605
|
}
|
|
8444
8606
|
function spreadsheetDataValidationError(code) {
|
|
8445
8607
|
const messages = {
|
|
8608
|
+
'invalid-custom-formula': '自定义公式必须是 255 个字符以内的本地公式,且不能包含控制字符。',
|
|
8446
8609
|
'invalid-date': '请输入有效的日期、Excel 日期序号或 DATE(...) 表达式。',
|
|
8447
8610
|
'invalid-list-source': '请输入不超过 255 个字符的逗号分隔列表,或有效的单行/单列区域。',
|
|
8448
8611
|
'invalid-number': '请输入有效的数字;整数验证不能使用小数边界。',
|
|
@@ -13061,6 +13224,11 @@ function newConditionalRuleDraft(sheetId) {
|
|
|
13061
13224
|
comparisonCellColor: DEFAULT_COLORS.comparisonCell,
|
|
13062
13225
|
comparisonUseTextColor: true,
|
|
13063
13226
|
comparisonUseCellColor: true,
|
|
13227
|
+
formula: '=A1>0',
|
|
13228
|
+
formulaTextColor: DEFAULT_COLORS.comparisonText,
|
|
13229
|
+
formulaCellColor: DEFAULT_COLORS.comparisonCell,
|
|
13230
|
+
formulaUseTextColor: true,
|
|
13231
|
+
formulaUseCellColor: true,
|
|
13064
13232
|
scaleSize: '3',
|
|
13065
13233
|
minimumColor: DEFAULT_COLORS.minimum,
|
|
13066
13234
|
midpointColor: DEFAULT_COLORS.midpoint,
|
|
@@ -13092,7 +13260,7 @@ function conditionalRuleDraftForRule(sheetId, rule) {
|
|
|
13092
13260
|
draft.comparisonUseCellColor = Boolean(style.cellColor);
|
|
13093
13261
|
draft.comparisonTextColor = colorInputValue(style.textColor, DEFAULT_COLORS.comparisonText);
|
|
13094
13262
|
draft.comparisonCellColor = colorInputValue(style.cellColor, DEFAULT_COLORS.comparisonCell);
|
|
13095
|
-
} else if (isToolbarConditionalRule(rule)) {
|
|
13263
|
+
} else if ('default' !== rule.type || 'formula' !== rule.conditionName || Array.isArray(rule.format)) if (isToolbarConditionalRule(rule)) {
|
|
13096
13264
|
draft.type = 'toolbarRule';
|
|
13097
13265
|
draft.preservedRule = rule;
|
|
13098
13266
|
} else if ('colorGradation' === rule.type && Array.isArray(rule.format)) {
|
|
@@ -13127,6 +13295,16 @@ function conditionalRuleDraftForRule(sheetId, rule) {
|
|
|
13127
13295
|
draft.barMaxLength = String(options.maxLength ?? 90);
|
|
13128
13296
|
}
|
|
13129
13297
|
}
|
|
13298
|
+
else {
|
|
13299
|
+
const style = rule.format;
|
|
13300
|
+
const values = (rule.conditionValue ?? []).map(String);
|
|
13301
|
+
draft.type = 'formula';
|
|
13302
|
+
draft.formula = values[0] ?? '';
|
|
13303
|
+
draft.formulaUseTextColor = Boolean(style.textColor);
|
|
13304
|
+
draft.formulaUseCellColor = Boolean(style.cellColor);
|
|
13305
|
+
draft.formulaTextColor = colorInputValue(style.textColor, DEFAULT_COLORS.comparisonText);
|
|
13306
|
+
draft.formulaCellColor = colorInputValue(style.cellColor, DEFAULT_COLORS.comparisonCell);
|
|
13307
|
+
}
|
|
13130
13308
|
return draft;
|
|
13131
13309
|
}
|
|
13132
13310
|
function buildConditionalRule(draft) {
|
|
@@ -13167,6 +13345,34 @@ function buildConditionalRule(draft) {
|
|
|
13167
13345
|
value
|
|
13168
13346
|
]
|
|
13169
13347
|
};
|
|
13348
|
+
} else if ('formula' === draft.type) {
|
|
13349
|
+
const formula = draft.formula.trim();
|
|
13350
|
+
if (!formula) return {
|
|
13351
|
+
error: '请输入条件格式公式。'
|
|
13352
|
+
};
|
|
13353
|
+
const formulaSource = formula.replace(/^=/, '').trim();
|
|
13354
|
+
if (!formulaSource) return {
|
|
13355
|
+
error: '请输入条件格式公式,例如 =A2>0。'
|
|
13356
|
+
};
|
|
13357
|
+
if (Array.from(formulaSource).length > 255) return {
|
|
13358
|
+
error: '条件格式公式不能超过 255 个字符。'
|
|
13359
|
+
};
|
|
13360
|
+
if (!draft.formulaUseTextColor && !draft.formulaUseCellColor) return {
|
|
13361
|
+
error: '至少启用一种文字或填充颜色。'
|
|
13362
|
+
};
|
|
13363
|
+
rule = {
|
|
13364
|
+
type: 'default',
|
|
13365
|
+
cellrange,
|
|
13366
|
+
format: {
|
|
13367
|
+
textColor: draft.formulaUseTextColor ? draft.formulaTextColor : null,
|
|
13368
|
+
cellColor: draft.formulaUseCellColor ? draft.formulaCellColor : null
|
|
13369
|
+
},
|
|
13370
|
+
conditionName: 'formula',
|
|
13371
|
+
conditionRange: [],
|
|
13372
|
+
conditionValue: [
|
|
13373
|
+
`=${formulaSource}`
|
|
13374
|
+
]
|
|
13375
|
+
};
|
|
13170
13376
|
} else if ('dataBar' === draft.type) {
|
|
13171
13377
|
const thresholds = savedVisualThresholds(draft.barThresholds);
|
|
13172
13378
|
const minLength = Number(draft.barMinLength);
|
|
@@ -14417,7 +14623,7 @@ function formatSpreadsheetRowPageBreaks(breaks) {
|
|
|
14417
14623
|
return normalizedBreaks(breaks).map((row)=>String(row + 1)).join(', ');
|
|
14418
14624
|
}
|
|
14419
14625
|
function parseSpreadsheetColumnPageBreaks(value, maximumColumn) {
|
|
14420
|
-
return parsePageBreakList(value, maximumColumn,
|
|
14626
|
+
return parsePageBreakList(value, maximumColumn, decodeColumn);
|
|
14421
14627
|
}
|
|
14422
14628
|
function formatSpreadsheetColumnPageBreaks(breaks) {
|
|
14423
14629
|
return normalizedBreaks(breaks).map(encodeColumn).join(', ');
|
|
@@ -14437,7 +14643,7 @@ function parsePageBreakList(value, maximum, decode) {
|
|
|
14437
14643
|
function normalizedBreaks(breaks) {
|
|
14438
14644
|
return Array.from(new Set((breaks ?? []).filter((value)=>Number.isSafeInteger(value) && value > 0))).sort((left, right)=>left - right);
|
|
14439
14645
|
}
|
|
14440
|
-
function
|
|
14646
|
+
function decodeColumn(token) {
|
|
14441
14647
|
const match = /^\$?([A-Z]+)$/i.exec(token);
|
|
14442
14648
|
if (!match) return null;
|
|
14443
14649
|
let value = 0;
|
|
@@ -21586,6 +21792,10 @@ function SpreadsheetConditionalFormatPanel({ content, onChange }) {
|
|
|
21586
21792
|
value: 'icons',
|
|
21587
21793
|
label: '图标集'
|
|
21588
21794
|
},
|
|
21795
|
+
{
|
|
21796
|
+
value: 'formula',
|
|
21797
|
+
label: '自定义公式'
|
|
21798
|
+
},
|
|
21589
21799
|
...'toolbarRule' === draft.type ? [
|
|
21590
21800
|
{
|
|
21591
21801
|
value: 'toolbarRule',
|
|
@@ -21645,6 +21855,76 @@ function SpreadsheetConditionalFormatPanel({ content, onChange }) {
|
|
|
21645
21855
|
...draft,
|
|
21646
21856
|
...patch
|
|
21647
21857
|
})
|
|
21858
|
+
}) : 'formula' === draft.type ? /*#__PURE__*/ jsxs(Fragment, {
|
|
21859
|
+
children: [
|
|
21860
|
+
/*#__PURE__*/ jsxs("div", {
|
|
21861
|
+
className: "work-office-field reference",
|
|
21862
|
+
children: [
|
|
21863
|
+
/*#__PURE__*/ jsx("span", {
|
|
21864
|
+
children: "公式"
|
|
21865
|
+
}),
|
|
21866
|
+
/*#__PURE__*/ jsx(office_text_field_OfficeTextField, {
|
|
21867
|
+
"aria-label": "条件格式公式",
|
|
21868
|
+
value: draft.formula,
|
|
21869
|
+
maxLength: 256,
|
|
21870
|
+
placeholder: "=A2>0",
|
|
21871
|
+
onChange: (event)=>setDraft({
|
|
21872
|
+
...draft,
|
|
21873
|
+
formula: event.target.value
|
|
21874
|
+
})
|
|
21875
|
+
}),
|
|
21876
|
+
/*#__PURE__*/ jsxs("small", {
|
|
21877
|
+
children: [
|
|
21878
|
+
"以应用范围左上角为相对锚点;只读取本地缓存值。",
|
|
21879
|
+
' ',
|
|
21880
|
+
Array.from(draft.formula.replace(/^=/, '')).length,
|
|
21881
|
+
"/",
|
|
21882
|
+
255
|
|
21883
|
+
]
|
|
21884
|
+
})
|
|
21885
|
+
]
|
|
21886
|
+
}),
|
|
21887
|
+
/*#__PURE__*/ jsx("p", {
|
|
21888
|
+
className: "work-office-field-hint",
|
|
21889
|
+
children: "支持常用 Excel 函数、相对/绝对引用和有限区域;无法本地求值时保持原样。"
|
|
21890
|
+
}),
|
|
21891
|
+
/*#__PURE__*/ jsx(OfficeCheckbox, {
|
|
21892
|
+
className: "toggle",
|
|
21893
|
+
ariaLabel: "公式规则使用文字颜色",
|
|
21894
|
+
checked: draft.formulaUseTextColor,
|
|
21895
|
+
onCheckedChange: (formulaUseTextColor)=>setDraft({
|
|
21896
|
+
...draft,
|
|
21897
|
+
formulaUseTextColor
|
|
21898
|
+
}),
|
|
21899
|
+
children: "使用文字颜色"
|
|
21900
|
+
}),
|
|
21901
|
+
draft.formulaUseTextColor && /*#__PURE__*/ jsx(ColorField, {
|
|
21902
|
+
label: "文字颜色",
|
|
21903
|
+
value: draft.formulaTextColor,
|
|
21904
|
+
onChange: (formulaTextColor)=>setDraft({
|
|
21905
|
+
...draft,
|
|
21906
|
+
formulaTextColor
|
|
21907
|
+
})
|
|
21908
|
+
}),
|
|
21909
|
+
/*#__PURE__*/ jsx(OfficeCheckbox, {
|
|
21910
|
+
className: "toggle",
|
|
21911
|
+
ariaLabel: "公式规则使用填充颜色",
|
|
21912
|
+
checked: draft.formulaUseCellColor,
|
|
21913
|
+
onCheckedChange: (formulaUseCellColor)=>setDraft({
|
|
21914
|
+
...draft,
|
|
21915
|
+
formulaUseCellColor
|
|
21916
|
+
}),
|
|
21917
|
+
children: "使用填充颜色"
|
|
21918
|
+
}),
|
|
21919
|
+
draft.formulaUseCellColor && /*#__PURE__*/ jsx(ColorField, {
|
|
21920
|
+
label: "填充颜色",
|
|
21921
|
+
value: draft.formulaCellColor,
|
|
21922
|
+
onChange: (formulaCellColor)=>setDraft({
|
|
21923
|
+
...draft,
|
|
21924
|
+
formulaCellColor
|
|
21925
|
+
})
|
|
21926
|
+
})
|
|
21927
|
+
]
|
|
21648
21928
|
}) : 'colorGradation' === draft.type ? /*#__PURE__*/ jsxs(Fragment, {
|
|
21649
21929
|
children: [
|
|
21650
21930
|
/*#__PURE__*/ jsxs("div", {
|
|
@@ -26523,6 +26803,10 @@ function pruneSpreadsheetCollaborationViewState(view, content) {
|
|
|
26523
26803
|
for (const sheetId of view.zoomBySheet.keys())if (!sheetIds.has(sheetId)) view.zoomBySheet.delete(sheetId);
|
|
26524
26804
|
}
|
|
26525
26805
|
const validationTypes = [
|
|
26806
|
+
{
|
|
26807
|
+
value: 'custom',
|
|
26808
|
+
label: '自定义公式'
|
|
26809
|
+
},
|
|
26526
26810
|
{
|
|
26527
26811
|
value: 'dropdown',
|
|
26528
26812
|
label: '序列'
|
|
@@ -26679,7 +26963,7 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
|
|
|
26679
26963
|
}, option.value))
|
|
26680
26964
|
})
|
|
26681
26965
|
}),
|
|
26682
|
-
'dropdown' !== value.type && /*#__PURE__*/ jsx(field_Field, {
|
|
26966
|
+
'dropdown' !== value.type && 'custom' !== value.type && /*#__PURE__*/ jsx(field_Field, {
|
|
26683
26967
|
label: "数据",
|
|
26684
26968
|
children: /*#__PURE__*/ jsx("select", {
|
|
26685
26969
|
value: value.type2,
|
|
@@ -26741,6 +27025,13 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
|
|
|
26741
27025
|
setTouched(true);
|
|
26742
27026
|
}
|
|
26743
27027
|
})
|
|
27028
|
+
}) : 'custom' === value.type ? /*#__PURE__*/ jsx(SpreadsheetDataValidationCustomFormulaField, {
|
|
27029
|
+
value: value.value1,
|
|
27030
|
+
error: visibleError,
|
|
27031
|
+
onTouched: ()=>setTouched(true),
|
|
27032
|
+
onValueChange: (value1)=>update({
|
|
27033
|
+
value1
|
|
27034
|
+
})
|
|
26744
27035
|
}) : /*#__PURE__*/ jsx(SpreadsheetDataValidationBoundaryFields, {
|
|
26745
27036
|
type: value.type,
|
|
26746
27037
|
type2: value.type2,
|
|
@@ -26900,6 +27191,45 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
|
|
|
26900
27191
|
})
|
|
26901
27192
|
});
|
|
26902
27193
|
}
|
|
27194
|
+
function SpreadsheetDataValidationCustomFormulaField({ error, onTouched, onValueChange, value }) {
|
|
27195
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
27196
|
+
className: "work-spreadsheet-data-validation-custom-formula",
|
|
27197
|
+
children: [
|
|
27198
|
+
/*#__PURE__*/ jsx(field_Field, {
|
|
27199
|
+
label: "公式",
|
|
27200
|
+
required: true,
|
|
27201
|
+
description: "公式必须返回 TRUE;相对引用以每个选定区域的左上角为基准。仅计算本地单元格和区域,不访问网络。",
|
|
27202
|
+
error: error ?? void 0,
|
|
27203
|
+
children: /*#__PURE__*/ jsx("textarea", {
|
|
27204
|
+
"aria-label": "公式",
|
|
27205
|
+
rows: 2,
|
|
27206
|
+
maxLength: 255,
|
|
27207
|
+
placeholder: '例如:=AND(A1<>"",A1<=100)',
|
|
27208
|
+
autoCapitalize: "none",
|
|
27209
|
+
spellCheck: false,
|
|
27210
|
+
value: value,
|
|
27211
|
+
onBlur: onTouched,
|
|
27212
|
+
onChange: (event)=>{
|
|
27213
|
+
onValueChange(event.currentTarget.value);
|
|
27214
|
+
onTouched();
|
|
27215
|
+
}
|
|
27216
|
+
})
|
|
27217
|
+
}),
|
|
27218
|
+
/*#__PURE__*/ jsxs("p", {
|
|
27219
|
+
className: "work-spreadsheet-data-validation-formula-note",
|
|
27220
|
+
children: [
|
|
27221
|
+
/*#__PURE__*/ jsx(Sigma, {
|
|
27222
|
+
size: 14,
|
|
27223
|
+
"aria-hidden": "true"
|
|
27224
|
+
}),
|
|
27225
|
+
/*#__PURE__*/ jsx("span", {
|
|
27226
|
+
children: "支持常用 Excel 函数、单元格引用和区域引用;无法安全求值时会阻止输入。"
|
|
27227
|
+
})
|
|
27228
|
+
]
|
|
27229
|
+
})
|
|
27230
|
+
]
|
|
27231
|
+
});
|
|
27232
|
+
}
|
|
26903
27233
|
function SpreadsheetDataValidationBoundaryFields({ error, onTouched, onValueChange, type, type2, value1, value2 }) {
|
|
26904
27234
|
const needsSecond = spreadsheetDataValidationNeedsSecondValue(type2);
|
|
26905
27235
|
const inputMode = 'date' === type ? 'text' : 'decimal';
|
|
@@ -27122,6 +27452,68 @@ function cloneSpreadsheetDataValidationItem(item) {
|
|
|
27122
27452
|
hintTitle: item.hintTitle
|
|
27123
27453
|
};
|
|
27124
27454
|
}
|
|
27455
|
+
const MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH = 255;
|
|
27456
|
+
function evaluateSpreadsheetCustomValidation(content, sheetId, row, column, item, value) {
|
|
27457
|
+
const sheet = content.sheets.find((candidate)=>candidate.id === sheetId);
|
|
27458
|
+
if (!sheet) return unsupportedCustomValidation('找不到自定义公式所在的工作表。');
|
|
27459
|
+
const proposed = spreadsheetCustomValidationValue(value);
|
|
27460
|
+
const normalizedProposed = spreadsheetCustomValidationBlank(proposed) ? '' : proposed;
|
|
27461
|
+
if (false !== item.allowBlank && '' === normalizedProposed) return {
|
|
27462
|
+
supported: true,
|
|
27463
|
+
valid: true
|
|
27464
|
+
};
|
|
27465
|
+
const formula = item.value1.trim().replace(/^=/, '').trim();
|
|
27466
|
+
if (!formula) return unsupportedCustomValidation('自定义公式为空。');
|
|
27467
|
+
if (Array.from(formula).length > MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH) return unsupportedCustomValidation('自定义公式超过 255 个字符。');
|
|
27468
|
+
const ranges = parseSpreadsheetCellRanges(item.rangeTxt);
|
|
27469
|
+
const anchor = spreadsheetCustomValidationAnchor(ranges, row, column);
|
|
27470
|
+
const result = evaluateSpreadsheetLocalFormula(formula, {
|
|
27471
|
+
sheets: content.sheets.flatMap((candidate)=>candidate.id ? [
|
|
27472
|
+
{
|
|
27473
|
+
...candidate,
|
|
27474
|
+
id: candidate.id
|
|
27475
|
+
}
|
|
27476
|
+
] : []),
|
|
27477
|
+
sheetId,
|
|
27478
|
+
row,
|
|
27479
|
+
column,
|
|
27480
|
+
anchorRow: anchor?.row,
|
|
27481
|
+
anchorColumn: anchor?.column,
|
|
27482
|
+
proposedValue: normalizedProposed,
|
|
27483
|
+
useProposedValue: true
|
|
27484
|
+
});
|
|
27485
|
+
if (!result.supported) return {
|
|
27486
|
+
supported: false,
|
|
27487
|
+
valid: false,
|
|
27488
|
+
message: result.message ? `自定义${result.message}` : result.message
|
|
27489
|
+
};
|
|
27490
|
+
return {
|
|
27491
|
+
supported: true,
|
|
27492
|
+
valid: spreadsheetLocalFormulaBoolean(result.value)
|
|
27493
|
+
};
|
|
27494
|
+
}
|
|
27495
|
+
function spreadsheetCustomValidationAnchor(ranges, row, column) {
|
|
27496
|
+
const range = ranges?.find((candidate)=>spreadsheetCellRangeContains(candidate, row, column));
|
|
27497
|
+
return {
|
|
27498
|
+
row: range?.row[0] ?? row,
|
|
27499
|
+
column: range?.column[0] ?? column
|
|
27500
|
+
};
|
|
27501
|
+
}
|
|
27502
|
+
function spreadsheetCustomValidationValue(value) {
|
|
27503
|
+
if (!value || 'object' != typeof value || Array.isArray(value)) return value;
|
|
27504
|
+
if ('v' in value) return value.v;
|
|
27505
|
+
return value;
|
|
27506
|
+
}
|
|
27507
|
+
function spreadsheetCustomValidationBlank(value) {
|
|
27508
|
+
return null == value || 'string' == typeof value && '' === value.trim();
|
|
27509
|
+
}
|
|
27510
|
+
function unsupportedCustomValidation(message) {
|
|
27511
|
+
return {
|
|
27512
|
+
supported: false,
|
|
27513
|
+
valid: false,
|
|
27514
|
+
message
|
|
27515
|
+
};
|
|
27516
|
+
}
|
|
27125
27517
|
const spreadsheetCellFormatAttributes = [
|
|
27126
27518
|
'bl',
|
|
27127
27519
|
'it',
|
|
@@ -29990,6 +30382,27 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
|
|
|
29990
30382
|
const requestSheetId = sheetId ?? activeSheetIdRef.current;
|
|
29991
30383
|
if (!requestSheetId || !item || 'object' != typeof item || pendingDataValidationRef.current) return pendingDataValidationRef.current ? false : void 0;
|
|
29992
30384
|
const validationItem = item;
|
|
30385
|
+
if ('custom' === validationItem.type) {
|
|
30386
|
+
const evaluation = evaluateSpreadsheetCustomValidation(contentRef.current, requestSheetId, row, column, validationItem, value);
|
|
30387
|
+
if (evaluation.supported && evaluation.valid) return true;
|
|
30388
|
+
const displayItem = cloneSpreadsheetDataValidationItem(validationItem);
|
|
30389
|
+
if (!displayItem.errorMessage?.trim() && evaluation.message) displayItem.errorMessage = evaluation.message;
|
|
30390
|
+
const style = spreadsheetDataValidationErrorStyle(displayItem);
|
|
30391
|
+
const request = {
|
|
30392
|
+
column,
|
|
30393
|
+
failureText: evaluation.message ?? failureText,
|
|
30394
|
+
interaction: spreadsheetDataValidationInteraction(style),
|
|
30395
|
+
item: displayItem,
|
|
30396
|
+
row,
|
|
30397
|
+
sheetId: requestSheetId,
|
|
30398
|
+
value
|
|
30399
|
+
};
|
|
30400
|
+
pendingDataValidationRef.current = request;
|
|
30401
|
+
queueMicrotask(()=>{
|
|
30402
|
+
if (pendingDataValidationRef.current === request) setPendingDataValidation(request);
|
|
30403
|
+
});
|
|
30404
|
+
return false;
|
|
30405
|
+
}
|
|
29993
30406
|
const style = spreadsheetDataValidationErrorStyle(validationItem);
|
|
29994
30407
|
const request = {
|
|
29995
30408
|
column,
|
|
@@ -30020,7 +30433,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
|
|
|
30020
30433
|
const conditionalStylesBySheet = useMemo(()=>new Map(materializedContent.sheets.flatMap((sheet)=>sheet.id ? [
|
|
30021
30434
|
[
|
|
30022
30435
|
sheet.id,
|
|
30023
|
-
spreadsheetConditionalFormatStyles(sheet)
|
|
30436
|
+
spreadsheetConditionalFormatStyles(sheet, materializedContent.sheets)
|
|
30024
30437
|
]
|
|
30025
30438
|
] : [])), [
|
|
30026
30439
|
materializedContent.sheets
|
package/dist/4121.js
CHANGED
|
@@ -2902,9 +2902,16 @@ const WORK_TEMPLATES = [
|
|
|
2902
2902
|
id: 'data-validation',
|
|
2903
2903
|
kind: 'spreadsheet',
|
|
2904
2904
|
name: '数据验证',
|
|
2905
|
-
description: '
|
|
2905
|
+
description: '自定义公式、下拉列表、输入提示与错误警告',
|
|
2906
2906
|
accent: '#13795b'
|
|
2907
2907
|
},
|
|
2908
|
+
{
|
|
2909
|
+
id: 'conditional-format',
|
|
2910
|
+
kind: 'spreadsheet',
|
|
2911
|
+
name: '公式条件格式',
|
|
2912
|
+
description: '相对引用、跨表阈值、停止后续规则与本地实时强调',
|
|
2913
|
+
accent: '#2f6fed'
|
|
2914
|
+
},
|
|
2908
2915
|
{
|
|
2909
2916
|
id: 'structured-references',
|
|
2910
2917
|
kind: 'spreadsheet',
|
|
@@ -2965,6 +2972,7 @@ function initialTitle(templateId, kind) {
|
|
|
2965
2972
|
'proofing-languages': '校对语言示例',
|
|
2966
2973
|
'quarterly-plan': '季度执行计划',
|
|
2967
2974
|
'data-validation': '数据验证示例',
|
|
2975
|
+
'conditional-format': '公式条件格式示例',
|
|
2968
2976
|
'structured-references': '结构化引用示例',
|
|
2969
2977
|
'strategy-deck': '业务策略汇报',
|
|
2970
2978
|
'animated-deck': '进入与退出动画示例'
|
|
@@ -3265,6 +3273,10 @@ function contentForTemplate(templateId) {
|
|
|
3265
3273
|
type: 'spreadsheet',
|
|
3266
3274
|
sheets: dataValidationTemplateSheets()
|
|
3267
3275
|
};
|
|
3276
|
+
if ('conditional-format' === templateId) return {
|
|
3277
|
+
type: 'spreadsheet',
|
|
3278
|
+
sheets: conditionalFormatTemplateSheets()
|
|
3279
|
+
};
|
|
3268
3280
|
if ('structured-references' === templateId) return {
|
|
3269
3281
|
type: 'spreadsheet',
|
|
3270
3282
|
sheets: structuredReferenceTemplateSheets()
|
|
@@ -3566,6 +3578,32 @@ function dataValidationTemplateSheets() {
|
|
|
3566
3578
|
hintTitle: 'Priority',
|
|
3567
3579
|
hintValue: '1 is highest priority; 5 is lowest.'
|
|
3568
3580
|
})
|
|
3581
|
+
},
|
|
3582
|
+
{
|
|
3583
|
+
ranges: [
|
|
3584
|
+
{
|
|
3585
|
+
row: [
|
|
3586
|
+
1,
|
|
3587
|
+
5
|
|
3588
|
+
],
|
|
3589
|
+
column: [
|
|
3590
|
+
4,
|
|
3591
|
+
4
|
|
3592
|
+
]
|
|
3593
|
+
}
|
|
3594
|
+
],
|
|
3595
|
+
item: dataValidationTemplateItem({
|
|
3596
|
+
type: 'custom',
|
|
3597
|
+
rangeTxt: 'E2:E6',
|
|
3598
|
+
value1: 'LEN(E2)>0',
|
|
3599
|
+
allowBlank: false,
|
|
3600
|
+
errorStyle: 'stop',
|
|
3601
|
+
errorTitle: 'Owner required',
|
|
3602
|
+
errorMessage: 'Enter an owner for every task.',
|
|
3603
|
+
hintShow: true,
|
|
3604
|
+
hintTitle: 'Owner',
|
|
3605
|
+
hintValue: 'The custom formula checks that this cell is not blank.'
|
|
3606
|
+
})
|
|
3569
3607
|
}
|
|
3570
3608
|
],
|
|
3571
3609
|
luckysheet_select_save: [
|
|
@@ -3621,6 +3659,247 @@ function dataValidationTemplateSheets() {
|
|
|
3621
3659
|
}
|
|
3622
3660
|
];
|
|
3623
3661
|
}
|
|
3662
|
+
function conditionalFormatTemplateSheets() {
|
|
3663
|
+
const status = emptyMatrix(18, 8);
|
|
3664
|
+
status[0][0] = styledCell('公式条件格式 · 发布看板', {
|
|
3665
|
+
bl: 1,
|
|
3666
|
+
fs: 16,
|
|
3667
|
+
fc: '#ffffff',
|
|
3668
|
+
bg: '#2f6fed'
|
|
3669
|
+
});
|
|
3670
|
+
status[1][0] = styledCell('公式以 A2:H7 左上角为锚点;先标记阻塞项,再强调达到阈值的任务。', {
|
|
3671
|
+
fc: '#49617f',
|
|
3672
|
+
fs: 10
|
|
3673
|
+
});
|
|
3674
|
+
[
|
|
3675
|
+
'任务',
|
|
3676
|
+
'负责人',
|
|
3677
|
+
'完成率',
|
|
3678
|
+
'状态',
|
|
3679
|
+
'预算',
|
|
3680
|
+
'实际',
|
|
3681
|
+
'偏差',
|
|
3682
|
+
'备注'
|
|
3683
|
+
].forEach((value, column)=>{
|
|
3684
|
+
status[2][column] = headerCell(value);
|
|
3685
|
+
});
|
|
3686
|
+
const rows = [
|
|
3687
|
+
[
|
|
3688
|
+
'需求确认',
|
|
3689
|
+
'Avery',
|
|
3690
|
+
1,
|
|
3691
|
+
'完成',
|
|
3692
|
+
12000,
|
|
3693
|
+
11800,
|
|
3694
|
+
'按期',
|
|
3695
|
+
'可发布'
|
|
3696
|
+
],
|
|
3697
|
+
[
|
|
3698
|
+
'接口联调',
|
|
3699
|
+
'Morgan',
|
|
3700
|
+
0.82,
|
|
3701
|
+
'进行中',
|
|
3702
|
+
18000,
|
|
3703
|
+
20100,
|
|
3704
|
+
'超支',
|
|
3705
|
+
'关注预算'
|
|
3706
|
+
],
|
|
3707
|
+
[
|
|
3708
|
+
'回归测试',
|
|
3709
|
+
'Riley',
|
|
3710
|
+
0.54,
|
|
3711
|
+
'阻塞',
|
|
3712
|
+
15000,
|
|
3713
|
+
7600,
|
|
3714
|
+
'暂停',
|
|
3715
|
+
'等待依赖'
|
|
3716
|
+
],
|
|
3717
|
+
[
|
|
3718
|
+
'文档发布',
|
|
3719
|
+
'Jordan',
|
|
3720
|
+
0.76,
|
|
3721
|
+
'进行中',
|
|
3722
|
+
9000,
|
|
3723
|
+
8800,
|
|
3724
|
+
'按期',
|
|
3725
|
+
'待审阅'
|
|
3726
|
+
],
|
|
3727
|
+
[
|
|
3728
|
+
'版本发布',
|
|
3729
|
+
'Taylor',
|
|
3730
|
+
0.38,
|
|
3731
|
+
'未开始',
|
|
3732
|
+
22000,
|
|
3733
|
+
0,
|
|
3734
|
+
'未开始',
|
|
3735
|
+
'尚未排期'
|
|
3736
|
+
],
|
|
3737
|
+
[
|
|
3738
|
+
'复盘会议',
|
|
3739
|
+
'Avery',
|
|
3740
|
+
0.9,
|
|
3741
|
+
'进行中',
|
|
3742
|
+
4000,
|
|
3743
|
+
3500,
|
|
3744
|
+
'按期',
|
|
3745
|
+
'本周完成'
|
|
3746
|
+
]
|
|
3747
|
+
];
|
|
3748
|
+
rows.forEach((row, rowIndex)=>{
|
|
3749
|
+
row.forEach((value, columnIndex)=>{
|
|
3750
|
+
status[rowIndex + 3][columnIndex] = styledCell(value, {
|
|
3751
|
+
bg: rowIndex % 2 ? '#f6f9ff' : '#ffffff',
|
|
3752
|
+
...2 === columnIndex ? {
|
|
3753
|
+
ct: {
|
|
3754
|
+
fa: '0%',
|
|
3755
|
+
t: 'n'
|
|
3756
|
+
},
|
|
3757
|
+
m: `${Math.round(100 * Number(value))}%`
|
|
3758
|
+
} : {},
|
|
3759
|
+
...4 === columnIndex || 5 === columnIndex ? {
|
|
3760
|
+
ct: {
|
|
3761
|
+
fa: '#,##0',
|
|
3762
|
+
t: 'n'
|
|
3763
|
+
},
|
|
3764
|
+
m: Number(value).toLocaleString()
|
|
3765
|
+
} : {}
|
|
3766
|
+
});
|
|
3767
|
+
});
|
|
3768
|
+
});
|
|
3769
|
+
status[11][0] = styledCell('本地规则示例', {
|
|
3770
|
+
bl: 1,
|
|
3771
|
+
fc: '#244b9b'
|
|
3772
|
+
});
|
|
3773
|
+
status[12][0] = styledCell('阻塞项(停止后续规则)');
|
|
3774
|
+
status[12][1] = styledCell('=$D4="阻塞"');
|
|
3775
|
+
status[13][0] = styledCell('达标项(跨表阈值)');
|
|
3776
|
+
status[13][1] = styledCell('=AND($C4>=Limits!$A$2,$D4<>"阻塞")');
|
|
3777
|
+
const blockRule = {
|
|
3778
|
+
type: 'default',
|
|
3779
|
+
cellrange: [
|
|
3780
|
+
{
|
|
3781
|
+
row: [
|
|
3782
|
+
3,
|
|
3783
|
+
8
|
|
3784
|
+
],
|
|
3785
|
+
column: [
|
|
3786
|
+
0,
|
|
3787
|
+
7
|
|
3788
|
+
]
|
|
3789
|
+
}
|
|
3790
|
+
],
|
|
3791
|
+
format: {
|
|
3792
|
+
textColor: '#9c0006',
|
|
3793
|
+
cellColor: '#ffc7ce'
|
|
3794
|
+
},
|
|
3795
|
+
conditionName: 'formula',
|
|
3796
|
+
conditionRange: [],
|
|
3797
|
+
conditionValue: [
|
|
3798
|
+
'=$D4="阻塞"'
|
|
3799
|
+
],
|
|
3800
|
+
stopIfTrue: true
|
|
3801
|
+
};
|
|
3802
|
+
const completedRule = {
|
|
3803
|
+
type: 'default',
|
|
3804
|
+
cellrange: [
|
|
3805
|
+
{
|
|
3806
|
+
row: [
|
|
3807
|
+
3,
|
|
3808
|
+
8
|
|
3809
|
+
],
|
|
3810
|
+
column: [
|
|
3811
|
+
0,
|
|
3812
|
+
7
|
|
3813
|
+
]
|
|
3814
|
+
}
|
|
3815
|
+
],
|
|
3816
|
+
format: {
|
|
3817
|
+
textColor: '#006100',
|
|
3818
|
+
cellColor: '#c6efce'
|
|
3819
|
+
},
|
|
3820
|
+
conditionName: 'formula',
|
|
3821
|
+
conditionRange: [],
|
|
3822
|
+
conditionValue: [
|
|
3823
|
+
'=AND($C4>=Limits!$A$2,$D4<>"阻塞")'
|
|
3824
|
+
]
|
|
3825
|
+
};
|
|
3826
|
+
return [
|
|
3827
|
+
{
|
|
3828
|
+
id: createWorkId('sheet'),
|
|
3829
|
+
name: 'Status',
|
|
3830
|
+
status: 1,
|
|
3831
|
+
order: 0,
|
|
3832
|
+
row: 18,
|
|
3833
|
+
column: 8,
|
|
3834
|
+
data: status,
|
|
3835
|
+
luckysheet_conditionformat_save: [
|
|
3836
|
+
blockRule,
|
|
3837
|
+
completedRule
|
|
3838
|
+
],
|
|
3839
|
+
config: {
|
|
3840
|
+
columnlen: {
|
|
3841
|
+
0: 132,
|
|
3842
|
+
1: 92,
|
|
3843
|
+
2: 84,
|
|
3844
|
+
3: 86,
|
|
3845
|
+
4: 92,
|
|
3846
|
+
5: 92,
|
|
3847
|
+
6: 84,
|
|
3848
|
+
7: 116
|
|
3849
|
+
},
|
|
3850
|
+
rowlen: {
|
|
3851
|
+
0: 34,
|
|
3852
|
+
1: 26,
|
|
3853
|
+
2: 28,
|
|
3854
|
+
12: 24,
|
|
3855
|
+
13: 24
|
|
3856
|
+
},
|
|
3857
|
+
merge: {
|
|
3858
|
+
'0_0': {
|
|
3859
|
+
r: 0,
|
|
3860
|
+
c: 0,
|
|
3861
|
+
rs: 1,
|
|
3862
|
+
cs: 8
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
},
|
|
3867
|
+
{
|
|
3868
|
+
id: createWorkId('sheet'),
|
|
3869
|
+
name: 'Limits',
|
|
3870
|
+
status: 0,
|
|
3871
|
+
order: 1,
|
|
3872
|
+
row: 8,
|
|
3873
|
+
column: 4,
|
|
3874
|
+
data: [
|
|
3875
|
+
[
|
|
3876
|
+
headerCell('完成率阈值'),
|
|
3877
|
+
styledCell('说明')
|
|
3878
|
+
],
|
|
3879
|
+
[
|
|
3880
|
+
styledCell(0.8, {
|
|
3881
|
+
ct: {
|
|
3882
|
+
fa: '0%',
|
|
3883
|
+
t: 'n'
|
|
3884
|
+
},
|
|
3885
|
+
m: '80%'
|
|
3886
|
+
}),
|
|
3887
|
+
styledCell('达到阈值并且未阻塞的任务使用绿色强调。')
|
|
3888
|
+
]
|
|
3889
|
+
],
|
|
3890
|
+
config: {
|
|
3891
|
+
columnlen: {
|
|
3892
|
+
0: 120,
|
|
3893
|
+
1: 300
|
|
3894
|
+
},
|
|
3895
|
+
rowlen: {
|
|
3896
|
+
0: 28,
|
|
3897
|
+
1: 36
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
];
|
|
3902
|
+
}
|
|
3624
3903
|
function structuredReferenceTemplateSheets() {
|
|
3625
3904
|
const sales = emptyMatrix(16, 10);
|
|
3626
3905
|
sales[0][0] = styledCell('Sales · 结构化引用', {
|
package/dist/core.js
CHANGED
|
@@ -3325,6 +3325,7 @@ function parseListFormula(value) {
|
|
|
3325
3325
|
}
|
|
3326
3326
|
function fortuneValidationType(value) {
|
|
3327
3327
|
const types = {
|
|
3328
|
+
custom: 'custom',
|
|
3328
3329
|
list: 'dropdown',
|
|
3329
3330
|
whole: 'number_integer',
|
|
3330
3331
|
decimal: 'number',
|
|
@@ -3335,6 +3336,7 @@ function fortuneValidationType(value) {
|
|
|
3335
3336
|
}
|
|
3336
3337
|
function xlsxValidationType(value) {
|
|
3337
3338
|
const types = {
|
|
3339
|
+
custom: 'custom',
|
|
3338
3340
|
dropdown: 'list',
|
|
3339
3341
|
number: 'decimal',
|
|
3340
3342
|
number_integer: 'whole',
|
|
@@ -15,7 +15,7 @@ export declare const SPREADSHEET_CONDITIONAL_COMPARISONS: {
|
|
|
15
15
|
}[];
|
|
16
16
|
export interface ConditionalRuleDraft {
|
|
17
17
|
sheetId: string;
|
|
18
|
-
type: 'cellComparison' | 'colorGradation' | 'dataBar' | 'icons' | 'toolbarRule';
|
|
18
|
+
type: 'cellComparison' | 'colorGradation' | 'dataBar' | 'icons' | 'formula' | 'toolbarRule';
|
|
19
19
|
reference: string;
|
|
20
20
|
stopIfTrue: boolean;
|
|
21
21
|
preservedRule: FortuneConditionalFormatRule | null;
|
|
@@ -26,6 +26,11 @@ export interface ConditionalRuleDraft {
|
|
|
26
26
|
comparisonCellColor: string;
|
|
27
27
|
comparisonUseTextColor: boolean;
|
|
28
28
|
comparisonUseCellColor: boolean;
|
|
29
|
+
formula: string;
|
|
30
|
+
formulaTextColor: string;
|
|
31
|
+
formulaCellColor: string;
|
|
32
|
+
formulaUseTextColor: boolean;
|
|
33
|
+
formulaUseCellColor: boolean;
|
|
29
34
|
scaleSize: '2' | '3';
|
|
30
35
|
minimumColor: string;
|
|
31
36
|
midpointColor: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkSpreadsheetContent, WorkSpreadsheetDataValidationItem } from '../work-types';
|
|
2
|
+
/** OOXML limits a data-validation formula to 255 characters. */
|
|
3
|
+
export declare const MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH = 255;
|
|
4
|
+
/** Keep synchronous browser validation bounded even for a hostile workbook. */
|
|
5
|
+
export declare const MAX_SPREADSHEET_CUSTOM_VALIDATION_REFERENCED_CELLS = 1024;
|
|
6
|
+
export interface SpreadsheetCustomValidationResult {
|
|
7
|
+
message?: string;
|
|
8
|
+
supported: boolean;
|
|
9
|
+
valid: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Evaluates a local Excel custom-validation formula synchronously.
|
|
13
|
+
*
|
|
14
|
+
* The editor deliberately evaluates against cached cell values. Formula
|
|
15
|
+
* recalculation remains a separate kernel concern; a formula cell without a
|
|
16
|
+
* cached value therefore fails closed instead of silently accepting input.
|
|
17
|
+
*/
|
|
18
|
+
export declare function evaluateSpreadsheetCustomValidation(content: WorkSpreadsheetContent, sheetId: string, row: number, column: number, item: WorkSpreadsheetDataValidationItem, value: unknown): SpreadsheetCustomValidationResult;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { WorkSpreadsheetContent, WorkSpreadsheetDataValidationErrorStyle, WorkSpreadsheetDataValidationItem, WorkSpreadsheetSheet } from '../work-types';
|
|
2
2
|
import { type SpreadsheetCellRange } from './spreadsheet-cell-range';
|
|
3
3
|
export declare const MAX_SPREADSHEET_DATA_VALIDATION_CELLS = 10000;
|
|
4
|
-
export type SpreadsheetDataValidationType = 'date' | 'dropdown' | 'number' | 'number_integer' | 'text_length';
|
|
4
|
+
export type SpreadsheetDataValidationType = 'custom' | 'date' | 'dropdown' | 'number' | 'number_integer' | 'text_length';
|
|
5
5
|
export type SpreadsheetDataValidationOperator = 'between' | 'equal' | 'greaterOrEqualTo' | 'lessThan' | 'lessThanOrEqualTo' | 'moreThanThe' | 'noEarlierThan' | 'noLaterThan' | 'notBetween' | 'notEqualTo' | 'earlierThan' | 'laterThan';
|
|
6
6
|
export interface SpreadsheetDataValidationDialogValue {
|
|
7
7
|
allowBlank: boolean;
|
|
@@ -46,7 +46,7 @@ export type SpreadsheetDataValidationResult = {
|
|
|
46
46
|
message: string;
|
|
47
47
|
ok: false;
|
|
48
48
|
};
|
|
49
|
-
export type SpreadsheetDataValidationErrorCode = 'invalid-date' | 'invalid-list-source' | 'invalid-number' | 'invalid-operator' | 'invalid-range' | 'invalid-text-length' | 'missing-value' | 'multiple-list-columns' | 'out-of-bounds' | 'protected-range' | 'range-too-large' | 'sheet-not-found' | 'value-order';
|
|
49
|
+
export type SpreadsheetDataValidationErrorCode = 'invalid-custom-formula' | 'invalid-date' | 'invalid-list-source' | 'invalid-number' | 'invalid-operator' | 'invalid-range' | 'invalid-text-length' | 'missing-value' | 'multiple-list-columns' | 'out-of-bounds' | 'protected-range' | 'range-too-large' | 'sheet-not-found' | 'value-order';
|
|
50
50
|
export declare function spreadsheetDataValidationOperators(type: SpreadsheetDataValidationType): readonly SpreadsheetDataValidationOperator[];
|
|
51
51
|
export declare function createSpreadsheetDataValidationDialogSource(content: WorkSpreadsheetContent, target: SpreadsheetDataValidationTarget): SpreadsheetDataValidationDialogSource | null;
|
|
52
52
|
export declare function validateSpreadsheetDataValidationRequest(content: WorkSpreadsheetContent, request: SpreadsheetDataValidationRequest): SpreadsheetDataValidationResult;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Sheet } from '@fortune-sheet/core';
|
|
2
2
|
import { type SpreadsheetConditionalDataBar } from './work-spreadsheet-conditional-data-bar';
|
|
3
3
|
import { type SpreadsheetConditionalIcon } from './work-spreadsheet-conditional-icons';
|
|
4
|
+
/** Full formula-rule scans stay bounded; larger ranges use materialized cells. */
|
|
5
|
+
export declare const MAX_SPREADSHEET_CONDITIONAL_FORMULA_SCAN_CELLS = 10000;
|
|
4
6
|
export interface SpreadsheetConditionalCellStyle {
|
|
5
7
|
textColor?: string;
|
|
6
8
|
cellColor?: string;
|
|
7
9
|
dataBar?: SpreadsheetConditionalDataBar;
|
|
8
10
|
icon?: SpreadsheetConditionalIcon;
|
|
9
11
|
}
|
|
10
|
-
export declare function spreadsheetConditionalFormatStyles(sheet: Sheet): Map<string, SpreadsheetConditionalCellStyle>;
|
|
12
|
+
export declare function spreadsheetConditionalFormatStyles(sheet: Sheet, formulaSheets?: readonly Sheet[]): Map<string, SpreadsheetConditionalCellStyle>;
|
|
@@ -2,6 +2,7 @@ import type { WorkSpreadsheetDataValidationErrorStyle } from './work-types';
|
|
|
2
2
|
export declare const SPREADSHEET_DATA_VALIDATION_TITLE_LIMIT = 32;
|
|
3
3
|
export declare const SPREADSHEET_DATA_VALIDATION_HINT_LIMIT = 255;
|
|
4
4
|
export declare const SPREADSHEET_DATA_VALIDATION_ERROR_LIMIT = 225;
|
|
5
|
+
export declare const SPREADSHEET_DATA_VALIDATION_FORMULA_LIMIT = 255;
|
|
5
6
|
export declare const SPREADSHEET_DATA_VALIDATION_ERROR_STYLES: readonly ["stop", "warning", "information"];
|
|
6
7
|
export declare function normalizeSpreadsheetDataValidationErrorStyle(value: unknown): WorkSpreadsheetDataValidationErrorStyle;
|
|
7
8
|
export declare function boundedSpreadsheetDataValidationText(value: unknown, limit: number): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CellMatrix } from '@fortune-sheet/core';
|
|
2
|
+
/** OOXML and the browser editor keep local rule formulas deliberately small. */
|
|
3
|
+
export declare const MAX_SPREADSHEET_LOCAL_FORMULA_LENGTH = 255;
|
|
4
|
+
/** Keep synchronous browser formula evaluation bounded for hostile workbooks. */
|
|
5
|
+
export declare const MAX_SPREADSHEET_LOCAL_FORMULA_REFERENCED_CELLS = 1024;
|
|
6
|
+
export interface SpreadsheetFormulaSheet {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
data?: CellMatrix;
|
|
10
|
+
celldata?: readonly SpreadsheetFormulaCell[];
|
|
11
|
+
}
|
|
12
|
+
export interface SpreadsheetFormulaCell {
|
|
13
|
+
r: number;
|
|
14
|
+
c: number;
|
|
15
|
+
v: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface SpreadsheetLocalFormulaOptions {
|
|
18
|
+
sheets: readonly SpreadsheetFormulaSheet[];
|
|
19
|
+
sheetId: string;
|
|
20
|
+
row: number;
|
|
21
|
+
column: number;
|
|
22
|
+
/** The first row of the rule/validation range. Defaults to the target row. */
|
|
23
|
+
anchorRow?: number;
|
|
24
|
+
/** The first column of the rule/validation range. Defaults to the target column. */
|
|
25
|
+
anchorColumn?: number;
|
|
26
|
+
/** Substitute the target cell with this value, as data validation does. */
|
|
27
|
+
proposedValue?: unknown;
|
|
28
|
+
useProposedValue?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface SpreadsheetLocalFormulaResult {
|
|
31
|
+
supported: boolean;
|
|
32
|
+
value?: unknown;
|
|
33
|
+
message?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Evaluate a bounded Excel formula against the workbook's cached local data.
|
|
37
|
+
*
|
|
38
|
+
* This is intentionally shared by custom data validation and conditional
|
|
39
|
+
* formatting. It never recalculates formula cells or reaches a remote source;
|
|
40
|
+
* an unavailable dependency therefore fails closed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function evaluateSpreadsheetLocalFormula(formula: string, options: SpreadsheetLocalFormulaOptions): SpreadsheetLocalFormulaResult;
|
|
43
|
+
export declare function spreadsheetLocalFormulaBoolean(value: unknown): boolean;
|
package/dist/office-kernel.wasm
CHANGED
|
Binary file
|
package/dist/styles.css
CHANGED
|
@@ -17939,6 +17939,27 @@ sub [data-office-character-position-half-points], sup [data-office-character-pos
|
|
|
17939
17939
|
display: grid;
|
|
17940
17940
|
}
|
|
17941
17941
|
|
|
17942
|
+
.work-spreadsheet-data-validation-custom-formula {
|
|
17943
|
+
gap: 8px;
|
|
17944
|
+
display: grid;
|
|
17945
|
+
}
|
|
17946
|
+
|
|
17947
|
+
.work-spreadsheet-data-validation-formula-note {
|
|
17948
|
+
color: var(--a3s-muted);
|
|
17949
|
+
align-items: flex-start;
|
|
17950
|
+
gap: 6px;
|
|
17951
|
+
margin: -2px 0 0;
|
|
17952
|
+
font-size: 10px;
|
|
17953
|
+
line-height: 1.45;
|
|
17954
|
+
display: flex;
|
|
17955
|
+
}
|
|
17956
|
+
|
|
17957
|
+
.work-spreadsheet-data-validation-formula-note svg {
|
|
17958
|
+
color: var(--work-spreadsheet-accent, #159469);
|
|
17959
|
+
flex: none;
|
|
17960
|
+
margin-top: 1px;
|
|
17961
|
+
}
|
|
17962
|
+
|
|
17942
17963
|
.work-spreadsheet-data-validation-inline-error {
|
|
17943
17964
|
color: var(--a3s-danger, #c23535);
|
|
17944
17965
|
grid-column: 1 / -1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a3s-lab/office",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"office",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"playground:visual:document-opentype": "playwright test visual-tests/document-opentype.functional.spec.ts --config playwright.config.ts",
|
|
107
107
|
"playground:visual:document-strike": "playwright test visual-tests/document-strike.functional.spec.ts --config playwright.config.ts",
|
|
108
108
|
"playground:visual:spreadsheet-data-validation": "playwright test visual-tests/spreadsheet-data-validation.functional.spec.ts --config playwright.config.ts",
|
|
109
|
+
"playground:visual:spreadsheet-conditional-format": "playwright test visual-tests/spreadsheet-conditional-format.functional.spec.ts --config playwright.config.ts",
|
|
109
110
|
"playground:visual:spreadsheet-date-time": "playwright test visual-tests/spreadsheet-date-time.functional.spec.ts --config playwright.config.ts",
|
|
110
111
|
"playground:visual:spreadsheet-copy-from-above": "playwright test visual-tests/spreadsheet-copy-from-above.functional.spec.ts --config playwright.config.ts",
|
|
111
112
|
"playground:visual:spreadsheet-custom-sort": "playwright test visual-tests/spreadsheet-custom-sort.functional.spec.ts --config playwright.config.ts",
|
|
@@ -224,6 +225,8 @@
|
|
|
224
225
|
"test:e2e:spreadsheet-go-to:check": "a3s-test check tests/e2e/spreadsheet-go-to.acl --json",
|
|
225
226
|
"test:e2e:spreadsheet-data-validation": "A3S_TEST_SUITE=tests/e2e/spreadsheet-data-validation.acl bash scripts/run-a3s-test-web-gate.sh",
|
|
226
227
|
"test:e2e:spreadsheet-data-validation:check": "a3s-test check tests/e2e/spreadsheet-data-validation.acl --json",
|
|
228
|
+
"test:e2e:spreadsheet-conditional-format": "A3S_TEST_SUITE=tests/e2e/spreadsheet-conditional-format.acl bash scripts/run-a3s-test-web-gate.sh",
|
|
229
|
+
"test:e2e:spreadsheet-conditional-format:check": "a3s-test check tests/e2e/spreadsheet-conditional-format.acl --json",
|
|
227
230
|
"test:e2e:spreadsheet-diagonal-borders": "a3s-test run tests/e2e/spreadsheet-diagonal-borders.acl --json",
|
|
228
231
|
"test:e2e:spreadsheet-diagonal-borders:check": "a3s-test check tests/e2e/spreadsheet-diagonal-borders.acl --json",
|
|
229
232
|
"test:e2e:spreadsheet-date-time": "a3s-test run tests/e2e/spreadsheet-date-time.acl --json",
|