@a3s-lab/office 0.40.0 → 0.41.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,17 @@ and [CLI reference](./docs/latest/en/cli-reference.md).
309
309
 
310
310
  ## Current release
311
311
 
312
+ Version `0.41.0` aligns Spreadsheet data-validation alerts with the local,
313
+ testable interaction model used by Traditional Office:
314
+
315
+ - Stop blocks an invalid edit with an accessible notice. Warning and
316
+ Information ask whether the invalid value should be kept, using the authored
317
+ title and message plus the current input so the decision is explicit.
318
+ - Keeping a Warning or Information value commits it once through the controlled
319
+ workbook API; cancelling restores the original value. Selection, focus,
320
+ Undo, and collaboration remain on the same bounded edit path, and native
321
+ XLSX `errorStyle` metadata remains lossless on reopen.
322
+
312
323
  Version `0.40.0` extends Writer's local, testable review workflow with atomic
313
324
  ordered-list numbering revisions:
314
325
 
@@ -26838,7 +26838,7 @@ function SpreadsheetDataValidationDialog({ source, restoreFocusTarget, onApply,
26838
26838
  children: [
26839
26839
  /*#__PURE__*/ jsx(field_Field, {
26840
26840
  label: "错误警告样式",
26841
- description: "三种样式会写入原生文件;当前浏览器网格统一阻止无效值。",
26841
+ description: "停止会阻止无效输入;警告和信息会询问是否保留。三种样式都会写入原生文件。",
26842
26842
  children: /*#__PURE__*/ jsxs("select", {
26843
26843
  "aria-label": "错误警告样式",
26844
26844
  value: value.errorStyle,
@@ -27073,6 +27073,55 @@ function use_spreadsheet_data_validation_spreadsheetDataValidationError(content,
27073
27073
  value
27074
27074
  }) ?? fallback;
27075
27075
  }
27076
+ function spreadsheetDataValidationErrorStyle(item) {
27077
+ return 'warning' === item.errorStyle || 'information' === item.errorStyle ? item.errorStyle : 'stop';
27078
+ }
27079
+ function spreadsheetDataValidationInteraction(style) {
27080
+ return 'stop' === style ? 'notice' : 'confirm';
27081
+ }
27082
+ function spreadsheetDataValidationDialogTitle(item) {
27083
+ const customTitle = item.errorTitle?.trim();
27084
+ if (customTitle) return customTitle;
27085
+ switch(spreadsheetDataValidationErrorStyle(item)){
27086
+ case 'warning':
27087
+ return '数据验证警告';
27088
+ case 'information':
27089
+ return '数据验证提示';
27090
+ default:
27091
+ return '数据验证错误';
27092
+ }
27093
+ }
27094
+ function spreadsheetDataValidationDialogDescription(failureText, value, item) {
27095
+ const customTitle = item?.errorTitle?.trim();
27096
+ const customMessage = item?.errorMessage?.trim();
27097
+ let message = customMessage || failureText.trim();
27098
+ if (customTitle && message.startsWith(`${customTitle}\n`)) message = message.slice(customTitle.length + 1).trim();
27099
+ message ||= '当前输入不符合此单元格的数据验证规则。';
27100
+ const valueText = spreadsheetDataValidationValueText(value);
27101
+ return valueText ? `${message}\n\n当前输入:${valueText}` : message;
27102
+ }
27103
+ function spreadsheetDataValidationConfirmLabels(style) {
27104
+ return 'warning' === style ? {
27105
+ cancelLabel: '取消',
27106
+ confirmLabel: '继续输入'
27107
+ } : {
27108
+ cancelLabel: '返回修改',
27109
+ confirmLabel: '保留输入'
27110
+ };
27111
+ }
27112
+ function spreadsheetDataValidationValueText(value) {
27113
+ if ('string' == typeof value) return value.trim();
27114
+ if ('number' == typeof value || 'boolean' == typeof value) return String(value);
27115
+ return '';
27116
+ }
27117
+ function cloneSpreadsheetDataValidationItem(item) {
27118
+ return {
27119
+ ...item,
27120
+ errorTitle: item.errorTitle,
27121
+ errorMessage: item.errorMessage,
27122
+ hintTitle: item.hintTitle
27123
+ };
27124
+ }
27076
27125
  const spreadsheetCellFormatAttributes = [
27077
27126
  'bl',
27078
27127
  'it',
@@ -29935,6 +29984,28 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
29935
29984
  const activeSheetId = viewActiveSheetId && content.sheets.some((sheet)=>sheet.id === viewActiveSheetId && 1 !== sheet.hide) ? viewActiveSheetId : contentActiveSheetId;
29936
29985
  const activeSheetIdRef = useRef(activeSheetId);
29937
29986
  const focusedSheetIdRef = useRef(null);
29987
+ const pendingDataValidationRef = useRef(null);
29988
+ const [pendingDataValidation, setPendingDataValidation] = useState(null);
29989
+ const queueDataValidationEdit = useCallback((row, column, value, item, failureText, sheetId)=>{
29990
+ const requestSheetId = sheetId ?? activeSheetIdRef.current;
29991
+ if (!requestSheetId || !item || 'object' != typeof item || pendingDataValidationRef.current) return pendingDataValidationRef.current ? false : void 0;
29992
+ const validationItem = item;
29993
+ const style = spreadsheetDataValidationErrorStyle(validationItem);
29994
+ const request = {
29995
+ column,
29996
+ failureText,
29997
+ interaction: spreadsheetDataValidationInteraction(style),
29998
+ item: cloneSpreadsheetDataValidationItem(validationItem),
29999
+ row,
30000
+ sheetId: requestSheetId,
30001
+ value
30002
+ };
30003
+ pendingDataValidationRef.current = request;
30004
+ queueMicrotask(()=>{
30005
+ if (pendingDataValidationRef.current === request) setPendingDataValidation(request);
30006
+ });
30007
+ return false;
30008
+ }, []);
29938
30009
  contentRef.current = materializedContent;
29939
30010
  previewRef.current = preview;
29940
30011
  const controlledHistory = useOfficeHistory({
@@ -29992,6 +30063,100 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
29992
30063
  }, [
29993
30064
  activeSheetId
29994
30065
  ]);
30066
+ useEffect(()=>{
30067
+ const request = pendingDataValidation;
30068
+ if (!request) return;
30069
+ let cancelled = false;
30070
+ const style = spreadsheetDataValidationErrorStyle(request.item);
30071
+ const description = spreadsheetDataValidationDialogDescription(request.failureText, request.value, request.item);
30072
+ const restoreValidationSelection = ()=>{
30073
+ const selection = {
30074
+ row: [
30075
+ request.row,
30076
+ request.row
30077
+ ],
30078
+ column: [
30079
+ request.column,
30080
+ request.column
30081
+ ],
30082
+ row_focus: request.row,
30083
+ column_focus: request.column
30084
+ };
30085
+ const nextSelection = {
30086
+ sheetId: request.sheetId,
30087
+ selection
30088
+ };
30089
+ selectionStateRef.current.current = nextSelection;
30090
+ selectionStateRef.current.requested = {
30091
+ sheetId: request.sheetId,
30092
+ selection: {
30093
+ ...selection,
30094
+ row: [
30095
+ ...selection.row
30096
+ ],
30097
+ column: [
30098
+ ...selection.column
30099
+ ]
30100
+ }
30101
+ };
30102
+ setSelectionState(nextSelection);
30103
+ try {
30104
+ workbookRef.current?.setSelection([
30105
+ {
30106
+ row: selection.row,
30107
+ column: selection.column
30108
+ }
30109
+ ], {
30110
+ id: request.sheetId
30111
+ });
30112
+ } catch {}
30113
+ };
30114
+ const finish = (accepted)=>{
30115
+ if (cancelled || pendingDataValidationRef.current !== request) return;
30116
+ pendingDataValidationRef.current = null;
30117
+ setPendingDataValidation(null);
30118
+ if (accepted) try {
30119
+ workbookRef.current?.setCellValue(request.row, request.column, request.value, {
30120
+ id: request.sheetId,
30121
+ skipDataValidation: true
30122
+ });
30123
+ } catch {
30124
+ showToast('无法保留此输入,请重试。', 'error');
30125
+ }
30126
+ restoreValidationSelection();
30127
+ };
30128
+ const showValidationDialog = async ()=>{
30129
+ if ('notice' === request.interaction) {
30130
+ await officeDialog.notice({
30131
+ title: spreadsheetDataValidationDialogTitle(request.item),
30132
+ description,
30133
+ confirmLabel: '知道了',
30134
+ restoreFocusTarget: getSpreadsheetDialogGridFocusTarget
30135
+ });
30136
+ finish(false);
30137
+ return;
30138
+ }
30139
+ const labels = spreadsheetDataValidationConfirmLabels(style);
30140
+ const accepted = await officeDialog.confirm({
30141
+ title: spreadsheetDataValidationDialogTitle(request.item),
30142
+ description,
30143
+ confirmLabel: labels.confirmLabel,
30144
+ cancelLabel: labels.cancelLabel,
30145
+ confirmTone: 'warning' === style ? 'primary' : 'secondary',
30146
+ restoreFocusTarget: getSpreadsheetDialogGridFocusTarget
30147
+ });
30148
+ finish(accepted);
30149
+ };
30150
+ showValidationDialog();
30151
+ return ()=>{
30152
+ cancelled = true;
30153
+ };
30154
+ }, [
30155
+ getSpreadsheetDialogGridFocusTarget,
30156
+ officeDialog.confirm,
30157
+ officeDialog.notice,
30158
+ pendingDataValidation
30159
+ ]);
29995
30160
  useEffect(()=>{
29996
30161
  if (preview || !activeSheetId || focusedSheetIdRef.current === activeSheetId) return;
29997
30162
  const initialFocus = null === focusedSheetIdRef.current;
@@ -30077,6 +30242,7 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
30077
30242
  const sheet = contentRef.current.sheets.find((candidate)=>candidate.id === activeSheetIdRef.current);
30078
30243
  return !sheet || !spreadsheetPivotOutputContains(sheet, row, column);
30079
30244
  },
30245
+ beforeDataValidation: queueDataValidationEdit,
30080
30246
  beforePaste: (selections)=>{
30081
30247
  const sheet = contentRef.current.sheets.find((candidate)=>candidate.id === activeSheetIdRef.current);
30082
30248
  if (!sheet) return true;
@@ -30116,7 +30282,8 @@ function SpreadsheetEditorSurface({ autoFocus = true, content, collaborationHist
30116
30282
  }
30117
30283
  }
30118
30284
  }), [
30119
- collaborationView
30285
+ collaborationView,
30286
+ queueDataValidationEdit
30120
30287
  ]);
30121
30288
  const conditionalFormatKey = useMemo(()=>content.sheets.map((sheet)=>`${sheet.id}:${JSON.stringify(sheet.luckysheet_conditionformat_save ?? [])}`).join('|'), [
30122
30289
  content.sheets
@@ -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 {};
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Open-source collaborative browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",