@a3s-lab/office 0.41.0 → 0.42.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 +15 -0
- package/dist/0~7048.js +1 -1
- package/dist/0~spreadsheet-editor.js +252 -5
- package/dist/4121.js +27 -1
- package/dist/core.js +2 -0
- 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-data-validation.d.ts +1 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/styles.css +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -309,6 +309,21 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
|
|
|
309
309
|
|
|
310
310
|
## Current release
|
|
311
311
|
|
|
312
|
+
Version `0.42.0` extends Spreadsheet data validation with a local custom-formula
|
|
313
|
+
workflow that remains bounded, inspectable, and native-file compatible:
|
|
314
|
+
|
|
315
|
+
- Author a formula with an optional `=` prefix in the same accessible dialog;
|
|
316
|
+
relative references are anchored per selected range and the proposed value
|
|
317
|
+
is evaluated before it enters the controlled workbook.
|
|
318
|
+
- Common local functions, cell/range references, and sheet-qualified references
|
|
319
|
+
are evaluated synchronously with a 255-character formula and 1,024-cell read
|
|
320
|
+
budget. External, whole-row/column, missing-sheet, and uncached-formula
|
|
321
|
+
references fail closed instead of silently accepting an unsafe edit.
|
|
322
|
+
- Custom rules round-trip through XLSX and are demonstrated in the public
|
|
323
|
+
**新建 → 数据验证** template, alongside the existing list, date, and alert
|
|
324
|
+
branches. Paste and object-level batch writes remain separate preflight
|
|
325
|
+
boundaries.
|
|
326
|
+
|
|
312
327
|
Version `0.41.0` aligns Spreadsheet data-validation alerts with the local,
|
|
313
328
|
testable interaction model used by Traditional Office:
|
|
314
329
|
|
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 };
|
|
@@ -4,12 +4,13 @@ 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
6
|
import { convertSpanToShareString, genarate, indexToColumnChar, update as core_update } from "@fortune-sheet/core";
|
|
7
|
+
import { Parser, SUPPORTED_FORMULAS } from "@fortune-sheet/formula-parser";
|
|
7
8
|
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
9
|
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
10
|
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
11
|
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
12
|
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
|
+
import { useOfficeTaskPaneEscape, OfficeCheckbox, CommittedOfficeNumberField, useOfficeDialog, useOfficeTaskPaneModal, createOfficeKernelClient, officeFontFamilies, CollectionState, OfficeNumberField, handleOfficeTaskPaneKeyDown, normalizeFormulaForFortuneParser } 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";
|
|
@@ -8036,7 +8037,7 @@ const dateOperators = [
|
|
|
8036
8037
|
'noLaterThan'
|
|
8037
8038
|
];
|
|
8038
8039
|
function spreadsheetDataValidationOperators(type) {
|
|
8039
|
-
if ('dropdown' === type) return [];
|
|
8040
|
+
if ('custom' === type || 'dropdown' === type) return [];
|
|
8040
8041
|
return 'date' === type ? dateOperators : numericOperators;
|
|
8041
8042
|
}
|
|
8042
8043
|
function createSpreadsheetDataValidationDialogSource(content, target) {
|
|
@@ -8121,8 +8122,8 @@ function spreadsheetDataValidationTargetError(sheet, ranges) {
|
|
|
8121
8122
|
}
|
|
8122
8123
|
function normalizeSpreadsheetDataValidationItem(content, sheet, ranges, value) {
|
|
8123
8124
|
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');
|
|
8125
|
+
const type2 = 'dropdown' === value.type || 'custom' === value.type ? '' : value.type2;
|
|
8126
|
+
if ('dropdown' !== value.type && 'custom' !== value.type && !operators.includes(type2)) return spreadsheetDataValidationError('invalid-operator');
|
|
8126
8127
|
const normalizedValues = normalizeSpreadsheetDataValidationValues(content, sheet, value.type, type2, value.value1, value.value2);
|
|
8127
8128
|
if (!normalizedValues.ok) return normalizedValues;
|
|
8128
8129
|
return {
|
|
@@ -8153,6 +8154,15 @@ function normalizeSpreadsheetDataValidationValues(content, sheet, type, type2, r
|
|
|
8153
8154
|
const value2 = rawValue2.trim();
|
|
8154
8155
|
if (!value1) return spreadsheetDataValidationError('missing-value');
|
|
8155
8156
|
if ('dropdown' === type) return normalizeSpreadsheetListValidation(content, sheet, value1);
|
|
8157
|
+
if ('custom' === type) {
|
|
8158
|
+
const formula = value1.replace(/^=/, '').trim();
|
|
8159
|
+
if (!formula || Array.from(formula).length > 255 || /[\u0000-\u001f\u007f]/u.test(formula)) return spreadsheetDataValidationError('invalid-custom-formula');
|
|
8160
|
+
return {
|
|
8161
|
+
ok: true,
|
|
8162
|
+
value1: formula,
|
|
8163
|
+
value2: ''
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8156
8166
|
const needsSecond = 'between' === type2 || 'notBetween' === type2;
|
|
8157
8167
|
if (needsSecond && !value2) return spreadsheetDataValidationError('missing-value');
|
|
8158
8168
|
if ('date' === type) {
|
|
@@ -8256,6 +8266,7 @@ function spreadsheetDataValidationDialogValue(value) {
|
|
|
8256
8266
|
}
|
|
8257
8267
|
function spreadsheetDataValidationType(value) {
|
|
8258
8268
|
return [
|
|
8269
|
+
'custom',
|
|
8259
8270
|
'date',
|
|
8260
8271
|
'dropdown',
|
|
8261
8272
|
'number',
|
|
@@ -8443,6 +8454,7 @@ function cloneSpreadsheetCellRange(range) {
|
|
|
8443
8454
|
}
|
|
8444
8455
|
function spreadsheetDataValidationError(code) {
|
|
8445
8456
|
const messages = {
|
|
8457
|
+
'invalid-custom-formula': '自定义公式必须是 255 个字符以内的本地公式,且不能包含控制字符。',
|
|
8446
8458
|
'invalid-date': '请输入有效的日期、Excel 日期序号或 DATE(...) 表达式。',
|
|
8447
8459
|
'invalid-list-source': '请输入不超过 255 个字符的逗号分隔列表,或有效的单行/单列区域。',
|
|
8448
8460
|
'invalid-number': '请输入有效的数字;整数验证不能使用小数边界。',
|
|
@@ -26523,6 +26535,10 @@ function pruneSpreadsheetCollaborationViewState(view, content) {
|
|
|
26523
26535
|
for (const sheetId of view.zoomBySheet.keys())if (!sheetIds.has(sheetId)) view.zoomBySheet.delete(sheetId);
|
|
26524
26536
|
}
|
|
26525
26537
|
const validationTypes = [
|
|
26538
|
+
{
|
|
26539
|
+
value: 'custom',
|
|
26540
|
+
label: '自定义公式'
|
|
26541
|
+
},
|
|
26526
26542
|
{
|
|
26527
26543
|
value: 'dropdown',
|
|
26528
26544
|
label: '序列'
|
|
@@ -26679,7 +26695,7 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
|
|
|
26679
26695
|
}, option.value))
|
|
26680
26696
|
})
|
|
26681
26697
|
}),
|
|
26682
|
-
'dropdown' !== value.type && /*#__PURE__*/ jsx(field_Field, {
|
|
26698
|
+
'dropdown' !== value.type && 'custom' !== value.type && /*#__PURE__*/ jsx(field_Field, {
|
|
26683
26699
|
label: "数据",
|
|
26684
26700
|
children: /*#__PURE__*/ jsx("select", {
|
|
26685
26701
|
value: value.type2,
|
|
@@ -26741,6 +26757,13 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
|
|
|
26741
26757
|
setTouched(true);
|
|
26742
26758
|
}
|
|
26743
26759
|
})
|
|
26760
|
+
}) : 'custom' === value.type ? /*#__PURE__*/ jsx(SpreadsheetDataValidationCustomFormulaField, {
|
|
26761
|
+
value: value.value1,
|
|
26762
|
+
error: visibleError,
|
|
26763
|
+
onTouched: ()=>setTouched(true),
|
|
26764
|
+
onValueChange: (value1)=>update({
|
|
26765
|
+
value1
|
|
26766
|
+
})
|
|
26744
26767
|
}) : /*#__PURE__*/ jsx(SpreadsheetDataValidationBoundaryFields, {
|
|
26745
26768
|
type: value.type,
|
|
26746
26769
|
type2: value.type2,
|
|
@@ -26900,6 +26923,45 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
|
|
|
26900
26923
|
})
|
|
26901
26924
|
});
|
|
26902
26925
|
}
|
|
26926
|
+
function SpreadsheetDataValidationCustomFormulaField({ error, onTouched, onValueChange, value }) {
|
|
26927
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
26928
|
+
className: "work-spreadsheet-data-validation-custom-formula",
|
|
26929
|
+
children: [
|
|
26930
|
+
/*#__PURE__*/ jsx(field_Field, {
|
|
26931
|
+
label: "公式",
|
|
26932
|
+
required: true,
|
|
26933
|
+
description: "公式必须返回 TRUE;相对引用以每个选定区域的左上角为基准。仅计算本地单元格和区域,不访问网络。",
|
|
26934
|
+
error: error ?? void 0,
|
|
26935
|
+
children: /*#__PURE__*/ jsx("textarea", {
|
|
26936
|
+
"aria-label": "公式",
|
|
26937
|
+
rows: 2,
|
|
26938
|
+
maxLength: 255,
|
|
26939
|
+
placeholder: '例如:=AND(A1<>"",A1<=100)',
|
|
26940
|
+
autoCapitalize: "none",
|
|
26941
|
+
spellCheck: false,
|
|
26942
|
+
value: value,
|
|
26943
|
+
onBlur: onTouched,
|
|
26944
|
+
onChange: (event)=>{
|
|
26945
|
+
onValueChange(event.currentTarget.value);
|
|
26946
|
+
onTouched();
|
|
26947
|
+
}
|
|
26948
|
+
})
|
|
26949
|
+
}),
|
|
26950
|
+
/*#__PURE__*/ jsxs("p", {
|
|
26951
|
+
className: "work-spreadsheet-data-validation-formula-note",
|
|
26952
|
+
children: [
|
|
26953
|
+
/*#__PURE__*/ jsx(Sigma, {
|
|
26954
|
+
size: 14,
|
|
26955
|
+
"aria-hidden": "true"
|
|
26956
|
+
}),
|
|
26957
|
+
/*#__PURE__*/ jsx("span", {
|
|
26958
|
+
children: "支持常用 Excel 函数、单元格引用和区域引用;无法安全求值时会阻止输入。"
|
|
26959
|
+
})
|
|
26960
|
+
]
|
|
26961
|
+
})
|
|
26962
|
+
]
|
|
26963
|
+
});
|
|
26964
|
+
}
|
|
26903
26965
|
function SpreadsheetDataValidationBoundaryFields({ error, onTouched, onValueChange, type, type2, value1, value2 }) {
|
|
26904
26966
|
const needsSecond = spreadsheetDataValidationNeedsSecondValue(type2);
|
|
26905
26967
|
const inputMode = 'date' === type ? 'text' : 'decimal';
|
|
@@ -27122,6 +27184,170 @@ function cloneSpreadsheetDataValidationItem(item) {
|
|
|
27122
27184
|
hintTitle: item.hintTitle
|
|
27123
27185
|
};
|
|
27124
27186
|
}
|
|
27187
|
+
const MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH = 255;
|
|
27188
|
+
const MAX_SPREADSHEET_CUSTOM_VALIDATION_REFERENCED_CELLS = 1024;
|
|
27189
|
+
function evaluateSpreadsheetCustomValidation(content, sheetId, row, column, item, value) {
|
|
27190
|
+
const sheet = content.sheets.find((candidate)=>candidate.id === sheetId);
|
|
27191
|
+
if (!sheet) return unsupportedCustomValidation('找不到自定义公式所在的工作表。');
|
|
27192
|
+
const proposed = spreadsheetCustomValidationValue(value);
|
|
27193
|
+
const normalizedProposed = spreadsheetCustomValidationBlank(proposed) ? '' : proposed;
|
|
27194
|
+
if (false !== item.allowBlank && '' === normalizedProposed) return {
|
|
27195
|
+
supported: true,
|
|
27196
|
+
valid: true
|
|
27197
|
+
};
|
|
27198
|
+
const formula = item.value1.trim().replace(/^=/, '').trim();
|
|
27199
|
+
if (!formula) return unsupportedCustomValidation('自定义公式为空。');
|
|
27200
|
+
if (Array.from(formula).length > MAX_SPREADSHEET_CUSTOM_VALIDATION_FORMULA_LENGTH) return unsupportedCustomValidation('自定义公式超过 255 个字符。');
|
|
27201
|
+
const ranges = parseSpreadsheetCellRanges(item.rangeTxt);
|
|
27202
|
+
const anchor = spreadsheetCustomValidationAnchor(ranges, row, column);
|
|
27203
|
+
const evaluator = new SpreadsheetCustomFormulaEvaluator(content, sheet, row, column, anchor, normalizedProposed);
|
|
27204
|
+
return evaluator.evaluate(formula);
|
|
27205
|
+
}
|
|
27206
|
+
class SpreadsheetCustomFormulaEvaluator {
|
|
27207
|
+
content;
|
|
27208
|
+
currentSheet;
|
|
27209
|
+
targetRow;
|
|
27210
|
+
targetColumn;
|
|
27211
|
+
anchor;
|
|
27212
|
+
proposedValue;
|
|
27213
|
+
sheetsByName = new Map();
|
|
27214
|
+
referencedCells = 0;
|
|
27215
|
+
unsupportedMessage;
|
|
27216
|
+
constructor(content, currentSheet, targetRow, targetColumn, anchor, proposedValue){
|
|
27217
|
+
this.content = content;
|
|
27218
|
+
this.currentSheet = currentSheet;
|
|
27219
|
+
this.targetRow = targetRow;
|
|
27220
|
+
this.targetColumn = targetColumn;
|
|
27221
|
+
this.anchor = anchor;
|
|
27222
|
+
this.proposedValue = proposedValue;
|
|
27223
|
+
for (const sheet of content.sheets)this.sheetsByName.set(sheet.name.trim().toLocaleLowerCase(), sheet);
|
|
27224
|
+
}
|
|
27225
|
+
evaluate(formula) {
|
|
27226
|
+
const allowedFunctions = new Set(SUPPORTED_FORMULAS.map((name)=>name.toUpperCase()));
|
|
27227
|
+
const parser = new Parser();
|
|
27228
|
+
parser.setFunction('ROW', (parameters)=>parameters.length ? null : this.targetRow + 1).setFunction('COLUMN', (parameters)=>parameters.length ? null : this.targetColumn + 1);
|
|
27229
|
+
let unsupportedFunction;
|
|
27230
|
+
parser.on('callFunction', (name)=>{
|
|
27231
|
+
const normalized = name.toUpperCase();
|
|
27232
|
+
if (!allowedFunctions.has(normalized)) unsupportedFunction ??= normalized;
|
|
27233
|
+
});
|
|
27234
|
+
parser.on('callCellValue', (coordinate, _options, done)=>{
|
|
27235
|
+
const resolved = this.resolveCoordinate(this.currentSheet, coordinate);
|
|
27236
|
+
if (!resolved) return void done(null);
|
|
27237
|
+
done(this.readCell(resolved));
|
|
27238
|
+
});
|
|
27239
|
+
parser.on('callRangeValue', (start, end, _options, done)=>{
|
|
27240
|
+
done(this.readRange(start, end));
|
|
27241
|
+
});
|
|
27242
|
+
const parsed = parser.parse(normalizeFormulaForFortuneParser(formula), {
|
|
27243
|
+
sheetId: this.currentSheet.id
|
|
27244
|
+
});
|
|
27245
|
+
if (unsupportedFunction) return unsupportedCustomValidation(`自定义公式函数“${unsupportedFunction}”无法在浏览器本地求值。`);
|
|
27246
|
+
if (this.unsupportedMessage) return unsupportedCustomValidation(this.unsupportedMessage);
|
|
27247
|
+
if (parsed.error) return unsupportedCustomValidation('自定义公式无法求值,请检查函数、引用和括号。');
|
|
27248
|
+
return {
|
|
27249
|
+
supported: true,
|
|
27250
|
+
valid: spreadsheetCustomValidationBoolean(parsed.result)
|
|
27251
|
+
};
|
|
27252
|
+
}
|
|
27253
|
+
resolveCoordinate(fallbackSheet, coordinate) {
|
|
27254
|
+
const sheet = coordinate.sheetName ? this.sheetsByName.get(coordinate.sheetName.trim().toLocaleLowerCase()) : fallbackSheet;
|
|
27255
|
+
if (!sheet) {
|
|
27256
|
+
this.unsupportedMessage = '自定义公式引用了不存在的工作表。';
|
|
27257
|
+
return null;
|
|
27258
|
+
}
|
|
27259
|
+
if (coordinate.row.index < 0 || coordinate.column.index < 0) {
|
|
27260
|
+
this.unsupportedMessage = '自定义公式暂不支持整行或整列引用,请改用有限区域。';
|
|
27261
|
+
return null;
|
|
27262
|
+
}
|
|
27263
|
+
const resolvedRow = coordinate.row.isAbsolute ? coordinate.row.index : this.targetRow + (coordinate.row.index - this.anchor.row);
|
|
27264
|
+
const resolvedColumn = coordinate.column.isAbsolute ? coordinate.column.index : this.targetColumn + (coordinate.column.index - this.anchor.column);
|
|
27265
|
+
if (resolvedRow < 0 || resolvedColumn < 0 || resolvedRow >= 1048576 || resolvedColumn >= 16384) {
|
|
27266
|
+
this.unsupportedMessage = '自定义公式引用超出了工作表边界。';
|
|
27267
|
+
return null;
|
|
27268
|
+
}
|
|
27269
|
+
return {
|
|
27270
|
+
sheet,
|
|
27271
|
+
row: resolvedRow,
|
|
27272
|
+
column: resolvedColumn
|
|
27273
|
+
};
|
|
27274
|
+
}
|
|
27275
|
+
readRange(start, end) {
|
|
27276
|
+
const startCoordinate = this.resolveCoordinate(this.currentSheet, start);
|
|
27277
|
+
const rangeSheet = start.sheetName ? this.sheetsByName.get(start.sheetName.trim().toLocaleLowerCase()) : this.currentSheet;
|
|
27278
|
+
const endCoordinate = rangeSheet ? this.resolveCoordinate(rangeSheet, end) : null;
|
|
27279
|
+
if (!startCoordinate || !endCoordinate || startCoordinate.sheet.id !== endCoordinate.sheet.id) {
|
|
27280
|
+
this.unsupportedMessage ??= '自定义公式区域引用必须位于同一工作表。';
|
|
27281
|
+
return [];
|
|
27282
|
+
}
|
|
27283
|
+
const rowStart = Math.min(startCoordinate.row, endCoordinate.row);
|
|
27284
|
+
const rowEnd = Math.max(startCoordinate.row, endCoordinate.row);
|
|
27285
|
+
const columnStart = Math.min(startCoordinate.column, endCoordinate.column);
|
|
27286
|
+
const columnEnd = Math.max(startCoordinate.column, endCoordinate.column);
|
|
27287
|
+
const area = (rowEnd - rowStart + 1) * (columnEnd - columnStart + 1);
|
|
27288
|
+
if (area <= 0 || area > MAX_SPREADSHEET_CUSTOM_VALIDATION_REFERENCED_CELLS - this.referencedCells) {
|
|
27289
|
+
this.unsupportedMessage = '自定义公式一次最多读取 1,024 个单元格。';
|
|
27290
|
+
return [];
|
|
27291
|
+
}
|
|
27292
|
+
const values = [];
|
|
27293
|
+
for(let currentRow = rowStart; currentRow <= rowEnd; currentRow += 1){
|
|
27294
|
+
const row = [];
|
|
27295
|
+
for(let currentColumn = columnStart; currentColumn <= columnEnd; currentColumn += 1)row.push(this.readCell({
|
|
27296
|
+
sheet: startCoordinate.sheet,
|
|
27297
|
+
row: currentRow,
|
|
27298
|
+
column: currentColumn
|
|
27299
|
+
}));
|
|
27300
|
+
values.push(row);
|
|
27301
|
+
}
|
|
27302
|
+
return values;
|
|
27303
|
+
}
|
|
27304
|
+
readCell(coordinate) {
|
|
27305
|
+
this.referencedCells += 1;
|
|
27306
|
+
if (this.referencedCells > MAX_SPREADSHEET_CUSTOM_VALIDATION_REFERENCED_CELLS) {
|
|
27307
|
+
this.unsupportedMessage = '自定义公式一次最多读取 1,024 个单元格。';
|
|
27308
|
+
return null;
|
|
27309
|
+
}
|
|
27310
|
+
if (coordinate.sheet.id === this.currentSheet.id && coordinate.row === this.targetRow && coordinate.column === this.targetColumn) return this.proposedValue;
|
|
27311
|
+
const cell = spreadsheetCustomValidationCellAt(coordinate.sheet, coordinate.row, coordinate.column);
|
|
27312
|
+
if (!cell) return null;
|
|
27313
|
+
if (cell.f && void 0 === cell.v) {
|
|
27314
|
+
this.unsupportedMessage ??= '自定义公式引用了尚未计算的公式单元格。';
|
|
27315
|
+
return null;
|
|
27316
|
+
}
|
|
27317
|
+
return cell.v ?? null;
|
|
27318
|
+
}
|
|
27319
|
+
}
|
|
27320
|
+
function spreadsheetCustomValidationAnchor(ranges, row, column) {
|
|
27321
|
+
const range = ranges?.find((candidate)=>spreadsheetCellRangeContains(candidate, row, column));
|
|
27322
|
+
return {
|
|
27323
|
+
row: range?.row[0] ?? row,
|
|
27324
|
+
column: range?.column[0] ?? column
|
|
27325
|
+
};
|
|
27326
|
+
}
|
|
27327
|
+
function spreadsheetCustomValidationCellAt(sheet, row, column) {
|
|
27328
|
+
if (void 0 !== sheet.data) return sheet.data[row]?.[column] ?? null;
|
|
27329
|
+
return sheet.celldata?.find((entry)=>entry.r === row && entry.c === column)?.v ?? null;
|
|
27330
|
+
}
|
|
27331
|
+
function spreadsheetCustomValidationValue(value) {
|
|
27332
|
+
if (!value || 'object' != typeof value || Array.isArray(value)) return value;
|
|
27333
|
+
if ('v' in value) return value.v;
|
|
27334
|
+
return value;
|
|
27335
|
+
}
|
|
27336
|
+
function spreadsheetCustomValidationBlank(value) {
|
|
27337
|
+
return null == value || 'string' == typeof value && '' === value.trim();
|
|
27338
|
+
}
|
|
27339
|
+
function spreadsheetCustomValidationBoolean(value) {
|
|
27340
|
+
if ('boolean' == typeof value) return value;
|
|
27341
|
+
if ('number' == typeof value) return Number.isFinite(value) && 0 !== value;
|
|
27342
|
+
return 'string' == typeof value && 'TRUE' === value.trim().toUpperCase();
|
|
27343
|
+
}
|
|
27344
|
+
function unsupportedCustomValidation(message) {
|
|
27345
|
+
return {
|
|
27346
|
+
supported: false,
|
|
27347
|
+
valid: false,
|
|
27348
|
+
message
|
|
27349
|
+
};
|
|
27350
|
+
}
|
|
27125
27351
|
const spreadsheetCellFormatAttributes = [
|
|
27126
27352
|
'bl',
|
|
27127
27353
|
'it',
|
|
@@ -29990,6 +30216,27 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
|
|
|
29990
30216
|
const requestSheetId = sheetId ?? activeSheetIdRef.current;
|
|
29991
30217
|
if (!requestSheetId || !item || 'object' != typeof item || pendingDataValidationRef.current) return pendingDataValidationRef.current ? false : void 0;
|
|
29992
30218
|
const validationItem = item;
|
|
30219
|
+
if ('custom' === validationItem.type) {
|
|
30220
|
+
const evaluation = evaluateSpreadsheetCustomValidation(contentRef.current, requestSheetId, row, column, validationItem, value);
|
|
30221
|
+
if (evaluation.supported && evaluation.valid) return true;
|
|
30222
|
+
const displayItem = cloneSpreadsheetDataValidationItem(validationItem);
|
|
30223
|
+
if (!displayItem.errorMessage?.trim() && evaluation.message) displayItem.errorMessage = evaluation.message;
|
|
30224
|
+
const style = spreadsheetDataValidationErrorStyle(displayItem);
|
|
30225
|
+
const request = {
|
|
30226
|
+
column,
|
|
30227
|
+
failureText: evaluation.message ?? failureText,
|
|
30228
|
+
interaction: spreadsheetDataValidationInteraction(style),
|
|
30229
|
+
item: displayItem,
|
|
30230
|
+
row,
|
|
30231
|
+
sheetId: requestSheetId,
|
|
30232
|
+
value
|
|
30233
|
+
};
|
|
30234
|
+
pendingDataValidationRef.current = request;
|
|
30235
|
+
queueMicrotask(()=>{
|
|
30236
|
+
if (pendingDataValidationRef.current === request) setPendingDataValidation(request);
|
|
30237
|
+
});
|
|
30238
|
+
return false;
|
|
30239
|
+
}
|
|
29993
30240
|
const style = spreadsheetDataValidationErrorStyle(validationItem);
|
|
29994
30241
|
const request = {
|
|
29995
30242
|
column,
|
package/dist/4121.js
CHANGED
|
@@ -2902,7 +2902,7 @@ 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
2908
|
{
|
|
@@ -3566,6 +3566,32 @@ function dataValidationTemplateSheets() {
|
|
|
3566
3566
|
hintTitle: 'Priority',
|
|
3567
3567
|
hintValue: '1 is highest priority; 5 is lowest.'
|
|
3568
3568
|
})
|
|
3569
|
+
},
|
|
3570
|
+
{
|
|
3571
|
+
ranges: [
|
|
3572
|
+
{
|
|
3573
|
+
row: [
|
|
3574
|
+
1,
|
|
3575
|
+
5
|
|
3576
|
+
],
|
|
3577
|
+
column: [
|
|
3578
|
+
4,
|
|
3579
|
+
4
|
|
3580
|
+
]
|
|
3581
|
+
}
|
|
3582
|
+
],
|
|
3583
|
+
item: dataValidationTemplateItem({
|
|
3584
|
+
type: 'custom',
|
|
3585
|
+
rangeTxt: 'E2:E6',
|
|
3586
|
+
value1: 'LEN(E2)>0',
|
|
3587
|
+
allowBlank: false,
|
|
3588
|
+
errorStyle: 'stop',
|
|
3589
|
+
errorTitle: 'Owner required',
|
|
3590
|
+
errorMessage: 'Enter an owner for every task.',
|
|
3591
|
+
hintShow: true,
|
|
3592
|
+
hintTitle: 'Owner',
|
|
3593
|
+
hintValue: 'The custom formula checks that this cell is not blank.'
|
|
3594
|
+
})
|
|
3569
3595
|
}
|
|
3570
3596
|
],
|
|
3571
3597
|
luckysheet_select_save: [
|
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',
|
|
@@ -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;
|
|
@@ -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;
|
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;
|