@a3s-lab/office 0.40.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 CHANGED
@@ -309,6 +309,32 @@ 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
+
327
+ Version `0.41.0` aligns Spreadsheet data-validation alerts with the local,
328
+ testable interaction model used by Traditional Office:
329
+
330
+ - Stop blocks an invalid edit with an accessible notice. Warning and
331
+ Information ask whether the invalid value should be kept, using the authored
332
+ title and message plus the current input so the decision is explicit.
333
+ - Keeping a Warning or Information value commits it once through the controlled
334
+ workbook API; cancelling restores the original value. Selection, focus,
335
+ Undo, and collaboration remain on the same bounded edit path, and native
336
+ XLSX `errorStyle` metadata remains lossless on reopen.
337
+
312
338
  Version `0.40.0` extends Writer's local, testable review workflow with atomic
313
339
  ordered-list numbering revisions:
314
340
 
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,
@@ -26838,7 +26861,7 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
26838
26861
  children: [
26839
26862
  /*#__PURE__*/ jsx(field_Field, {
26840
26863
  label: "错误警告样式",
26841
- description: "三种样式会写入原生文件;当前浏览器网格统一阻止无效值。",
26864
+ description: "停止会阻止无效输入;警告和信息会询问是否保留。三种样式都会写入原生文件。",
26842
26865
  children: /*#__PURE__*/ jsxs("select", {
26843
26866
  "aria-label": "错误警告样式",
26844
26867
  value: value.errorStyle,
@@ -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';
@@ -27073,6 +27135,219 @@ function use_spreadsheet_data_validation_spreadsheetDataValidationError(content,
27073
27135
  value
27074
27136
  }) ?? fallback;
27075
27137
  }
27138
+ function spreadsheetDataValidationErrorStyle(item) {
27139
+ return 'warning' === item.errorStyle || 'information' === item.errorStyle ? item.errorStyle : 'stop';
27140
+ }
27141
+ function spreadsheetDataValidationInteraction(style) {
27142
+ return 'stop' === style ? 'notice' : 'confirm';
27143
+ }
27144
+ function spreadsheetDataValidationDialogTitle(item) {
27145
+ const customTitle = item.errorTitle?.trim();
27146
+ if (customTitle) return customTitle;
27147
+ switch(spreadsheetDataValidationErrorStyle(item)){
27148
+ case 'warning':
27149
+ return '数据验证警告';
27150
+ case 'information':
27151
+ return '数据验证提示';
27152
+ default:
27153
+ return '数据验证错误';
27154
+ }
27155
+ }
27156
+ function spreadsheetDataValidationDialogDescription(failureText, value, item) {
27157
+ const customTitle = item?.errorTitle?.trim();
27158
+ const customMessage = item?.errorMessage?.trim();
27159
+ let message = customMessage || failureText.trim();
27160
+ if (customTitle && message.startsWith(`${customTitle}\n`)) message = message.slice(customTitle.length + 1).trim();
27161
+ message ||= '当前输入不符合此单元格的数据验证规则。';
27162
+ const valueText = spreadsheetDataValidationValueText(value);
27163
+ return valueText ? `${message}\n\n当前输入:${valueText}` : message;
27164
+ }
27165
+ function spreadsheetDataValidationConfirmLabels(style) {
27166
+ return 'warning' === style ? {
27167
+ cancelLabel: '取消',
27168
+ confirmLabel: '继续输入'
27169
+ } : {
27170
+ cancelLabel: '返回修改',
27171
+ confirmLabel: '保留输入'
27172
+ };
27173
+ }
27174
+ function spreadsheetDataValidationValueText(value) {
27175
+ if ('string' == typeof value) return value.trim();
27176
+ if ('number' == typeof value || 'boolean' == typeof value) return String(value);
27177
+ return '';
27178
+ }
27179
+ function cloneSpreadsheetDataValidationItem(item) {
27180
+ return {
27181
+ ...item,
27182
+ errorTitle: item.errorTitle,
27183
+ errorMessage: item.errorMessage,
27184
+ hintTitle: item.hintTitle
27185
+ };
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
+ }
27076
27351
  const spreadsheetCellFormatAttributes = [
27077
27352
  'bl',
27078
27353
  'it',
@@ -29935,6 +30210,49 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
29935
30210
  const activeSheetId = viewActiveSheetId && content.sheets.some((sheet)=>sheet.id === viewActiveSheetId && 1 !== sheet.hide) ? viewActiveSheetId : contentActiveSheetId;
29936
30211
  const activeSheetIdRef = useRef(activeSheetId);
29937
30212
  const focusedSheetIdRef = useRef(null);
30213
+ const pendingDataValidationRef = useRef(null);
30214
+ const [pendingDataValidation, setPendingDataValidation] = useState(null);
30215
+ const queueDataValidationEdit = useCallback((row, column, value, item, failureText, sheetId)=>{
30216
+ const requestSheetId = sheetId ?? activeSheetIdRef.current;
30217
+ if (!requestSheetId || !item || 'object' != typeof item || pendingDataValidationRef.current) return pendingDataValidationRef.current ? false : void 0;
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
+ }
30240
+ const style = spreadsheetDataValidationErrorStyle(validationItem);
30241
+ const request = {
30242
+ column,
30243
+ failureText,
30244
+ interaction: spreadsheetDataValidationInteraction(style),
30245
+ item: cloneSpreadsheetDataValidationItem(validationItem),
30246
+ row,
30247
+ sheetId: requestSheetId,
30248
+ value
30249
+ };
30250
+ pendingDataValidationRef.current = request;
30251
+ queueMicrotask(()=>{
30252
+ if (pendingDataValidationRef.current === request) setPendingDataValidation(request);
30253
+ });
30254
+ return false;
30255
+ }, []);
29938
30256
  contentRef.current = materializedContent;
29939
30257
  previewRef.current = preview;
29940
30258
  const controlledHistory = useOfficeHistory({
@@ -29992,6 +30310,100 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
29992
30310
  }, [
29993
30311
  activeSheetId
29994
30312
  ]);
30313
+ useEffect(()=>{
30314
+ const request = pendingDataValidation;
30315
+ if (!request) return;
30316
+ let cancelled = false;
30317
+ const style = spreadsheetDataValidationErrorStyle(request.item);
30318
+ const description = spreadsheetDataValidationDialogDescription(request.failureText, request.value, request.item);
30319
+ const restoreValidationSelection = ()=>{
30320
+ const selection = {
30321
+ row: [
30322
+ request.row,
30323
+ request.row
30324
+ ],
30325
+ column: [
30326
+ request.column,
30327
+ request.column
30328
+ ],
30329
+ row_focus: request.row,
30330
+ column_focus: request.column
30331
+ };
30332
+ const nextSelection = {
30333
+ sheetId: request.sheetId,
30334
+ selection
30335
+ };
30336
+ selectionStateRef.current.current = nextSelection;
30337
+ selectionStateRef.current.requested = {
30338
+ sheetId: request.sheetId,
30339
+ selection: {
30340
+ ...selection,
30341
+ row: [
30342
+ ...selection.row
30343
+ ],
30344
+ column: [
30345
+ ...selection.column
30346
+ ]
30347
+ }
30348
+ };
30349
+ setSelectionState(nextSelection);
30350
+ try {
30351
+ workbookRef.current?.setSelection([
30352
+ {
30353
+ row: selection.row,
30354
+ column: selection.column
30355
+ }
30356
+ ], {
30357
+ id: request.sheetId
30358
+ });
30359
+ } catch {}
30360
+ };
30361
+ const finish = (accepted)=>{
30362
+ if (cancelled || pendingDataValidationRef.current !== request) return;
30363
+ pendingDataValidationRef.current = null;
30364
+ setPendingDataValidation(null);
30365
+ if (accepted) try {
30366
+ workbookRef.current?.setCellValue(request.row, request.column, request.value, {
30367
+ id: request.sheetId,
30368
+ skipDataValidation: true
30369
+ });
30370
+ } catch {
30371
+ showToast('无法保留此输入,请重试。', 'error');
30372
+ }
30373
+ restoreValidationSelection();
30374
+ };
30375
+ const showValidationDialog = async ()=>{
30376
+ if ('notice' === request.interaction) {
30377
+ await officeDialog.notice({
30378
+ title: spreadsheetDataValidationDialogTitle(request.item),
30379
+ description,
30380
+ confirmLabel: '知道了',
30381
+ restoreFocusTarget: getSpreadsheetDialogGridFocusTarget
30382
+ });
30383
+ finish(false);
30384
+ return;
30385
+ }
30386
+ const labels = spreadsheetDataValidationConfirmLabels(style);
30387
+ const accepted = await officeDialog.confirm({
30388
+ title: spreadsheetDataValidationDialogTitle(request.item),
30389
+ description,
30390
+ confirmLabel: labels.confirmLabel,
30391
+ cancelLabel: labels.cancelLabel,
30392
+ confirmTone: 'warning' === style ? 'primary' : 'secondary',
30393
+ restoreFocusTarget: getSpreadsheetDialogGridFocusTarget
30394
+ });
30395
+ finish(accepted);
30396
+ };
30397
+ showValidationDialog();
30398
+ return ()=>{
30399
+ cancelled = true;
30400
+ };
30401
+ }, [
30402
+ getSpreadsheetDialogGridFocusTarget,
30403
+ officeDialog.confirm,
30404
+ officeDialog.notice,
30405
+ pendingDataValidation
30406
+ ]);
29995
30407
  useEffect(()=>{
29996
30408
  if (preview || !activeSheetId || focusedSheetIdRef.current === activeSheetId) return;
29997
30409
  const initialFocus = null === focusedSheetIdRef.current;
@@ -30077,6 +30489,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
30077
30489
  const sheet = contentRef.current.sheets.find((candidate)=>candidate.id === activeSheetIdRef.current);
30078
30490
  return !sheet || !spreadsheetPivotOutputContains(sheet, row, column);
30079
30491
  },
30492
+ beforeDataValidation: queueDataValidationEdit,
30080
30493
  beforePaste: (selections)=>{
30081
30494
  const sheet = contentRef.current.sheets.find((candidate)=>candidate.id === activeSheetIdRef.current);
30082
30495
  if (!sheet) return true;
@@ -30116,7 +30529,8 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
30116
30529
  }
30117
30530
  }
30118
30531
  }), [
30119
- collaborationView
30532
+ collaborationView,
30533
+ queueDataValidationEdit
30120
30534
  ]);
30121
30535
  const conditionalFormatKey = useMemo(()=>content.sheets.map((sheet)=>`${sheet.id}:${JSON.stringify(sheet.luckysheet_conditionformat_save ?? [])}`).join('|'), [
30122
30536
  content.sheets
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;
@@ -0,0 +1,23 @@
1
+ import type { WorkSpreadsheetDataValidationErrorStyle, WorkSpreadsheetDataValidationItem } from '../work-types';
2
+ export type SpreadsheetDataValidationInteraction = 'notice' | 'confirm';
3
+ export interface SpreadsheetDataValidationEditRequest {
4
+ column: number;
5
+ failureText: string;
6
+ interaction: SpreadsheetDataValidationInteraction;
7
+ item: WorkSpreadsheetDataValidationItem;
8
+ row: number;
9
+ sheetId: string;
10
+ value: unknown;
11
+ }
12
+ type SpreadsheetDataValidationMessageSource = Pick<WorkSpreadsheetDataValidationItem, 'errorMessage' | 'errorTitle'>;
13
+ export declare function spreadsheetDataValidationErrorStyle(item: Pick<WorkSpreadsheetDataValidationItem, 'errorStyle'>): WorkSpreadsheetDataValidationErrorStyle;
14
+ export declare function spreadsheetDataValidationInteraction(style: WorkSpreadsheetDataValidationErrorStyle): SpreadsheetDataValidationInteraction;
15
+ export declare function spreadsheetDataValidationDialogTitle(item: Pick<WorkSpreadsheetDataValidationItem, 'errorStyle' | 'errorTitle'>): string;
16
+ export declare function spreadsheetDataValidationDialogDescription(failureText: string, value: unknown, item?: SpreadsheetDataValidationMessageSource): string;
17
+ export declare function spreadsheetDataValidationConfirmLabels(style: WorkSpreadsheetDataValidationErrorStyle): {
18
+ cancelLabel: string;
19
+ confirmLabel: string;
20
+ };
21
+ export declare function spreadsheetDataValidationValueText(value: unknown): string;
22
+ export declare function cloneSpreadsheetDataValidationItem(item: WorkSpreadsheetDataValidationItem): WorkSpreadsheetDataValidationItem;
23
+ export {};
@@ -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;
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.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",