@beyondwork/docx-react-component 1.0.13 → 1.0.15

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.
@@ -0,0 +1,206 @@
1
+ import { addSheet, getSheetByName, moveSheet, removeSheet, renameSheet, type CanonicalWorkbook } from "../model/workbook.ts";
2
+ import {
3
+ cloneWorkbook,
4
+ createEmptyWorkbookMapping,
5
+ createWorkbookSelection,
6
+ createWorkbookTransaction,
7
+ getDefaultSelection,
8
+ normalizeWorkbookSelection,
9
+ remapFormulasForMutation,
10
+ type WorkbookSelection,
11
+ type WorkbookTransaction,
12
+ } from "./workbook-transaction.ts";
13
+
14
+ export type WorkbookSheetCommand =
15
+ | {
16
+ type: "sheet.add";
17
+ sheetId?: string;
18
+ name: string;
19
+ atIndex?: number;
20
+ activate?: boolean;
21
+ }
22
+ | {
23
+ type: "sheet.remove";
24
+ sheetId: string;
25
+ }
26
+ | {
27
+ type: "sheet.rename";
28
+ sheetId: string;
29
+ name: string;
30
+ }
31
+ | {
32
+ type: "sheet.reorder";
33
+ sheetId: string;
34
+ toIndex: number;
35
+ };
36
+
37
+ export function applySheetCommand(
38
+ workbook: CanonicalWorkbook,
39
+ selection: WorkbookSelection,
40
+ command: WorkbookSheetCommand,
41
+ options?: {
42
+ createSheetId?: () => string;
43
+ },
44
+ ): WorkbookTransaction {
45
+ const nextWorkbook = cloneWorkbook(workbook);
46
+ const mapping = createEmptyWorkbookMapping();
47
+
48
+ switch (command.type) {
49
+ case "sheet.add": {
50
+ assertUniqueSheetName(nextWorkbook, command.name);
51
+ const sheetId = command.sheetId ?? createSheetId(nextWorkbook, options?.createSheetId);
52
+ const insertIndex = command.atIndex ?? nextWorkbook.sheetOrder.length;
53
+ addSheet(nextWorkbook, sheetId, command.name, command.atIndex);
54
+ mapping.operations.push({
55
+ type: "sheet-add",
56
+ sheetId,
57
+ index: clampIndex(insertIndex, nextWorkbook.sheetOrder.length - 1),
58
+ name: command.name,
59
+ });
60
+ const nextSelection = command.activate || workbook.sheetOrder.length === 0
61
+ ? createWorkbookSelection(sheetId)
62
+ : selection;
63
+ return createWorkbookTransaction(
64
+ nextWorkbook,
65
+ normalizeWorkbookSelection(nextWorkbook, nextSelection),
66
+ mapping,
67
+ "sheet.add",
68
+ );
69
+ }
70
+ case "sheet.remove": {
71
+ const currentIndex = nextWorkbook.sheetOrder.indexOf(command.sheetId);
72
+ if (currentIndex === -1) {
73
+ throw new Error(`Sheet not found: ${command.sheetId}`);
74
+ }
75
+ const removedSheet = nextWorkbook.sheets.get(command.sheetId);
76
+ if (!removedSheet) {
77
+ throw new Error(`Sheet not found: ${command.sheetId}`);
78
+ }
79
+ const fallbackSheetId = resolveFallbackSheetId(nextWorkbook, command.sheetId);
80
+ remapFormulasForMutation(
81
+ nextWorkbook,
82
+ command.sheetId,
83
+ { kind: "remove-sheet", removedName: removedSheet.name },
84
+ mapping,
85
+ );
86
+ nextWorkbook.namedItems = nextWorkbook.namedItems.filter((item) => item.localSheetId !== command.sheetId);
87
+ removeSheet(nextWorkbook, command.sheetId);
88
+ mapping.operations.push({
89
+ type: "sheet-remove",
90
+ sheetId: command.sheetId,
91
+ index: currentIndex,
92
+ name: removedSheet.name,
93
+ fallbackSheetId,
94
+ });
95
+ return createWorkbookTransaction(
96
+ nextWorkbook,
97
+ normalizeWorkbookSelection(nextWorkbook, selection.sheetId === command.sheetId
98
+ ? (fallbackSheetId ? createWorkbookSelection(fallbackSheetId) : { mode: "sheet" })
99
+ : selection),
100
+ mapping,
101
+ "sheet.remove",
102
+ );
103
+ }
104
+ case "sheet.rename": {
105
+ const sheet = nextWorkbook.sheets.get(command.sheetId);
106
+ if (!sheet) {
107
+ throw new Error(`Sheet not found: ${command.sheetId}`);
108
+ }
109
+ if (sheet.name !== command.name) {
110
+ assertUniqueSheetName(nextWorkbook, command.name, command.sheetId);
111
+ }
112
+ const beforeName = sheet.name;
113
+ renameSheet(nextWorkbook, command.sheetId, command.name);
114
+ mapping.operations.push({
115
+ type: "sheet-rename",
116
+ sheetId: command.sheetId,
117
+ beforeName,
118
+ afterName: command.name,
119
+ });
120
+ remapFormulasForMutation(
121
+ nextWorkbook,
122
+ command.sheetId,
123
+ { kind: "rename-sheet", beforeName, afterName: command.name },
124
+ mapping,
125
+ );
126
+ return createWorkbookTransaction(
127
+ nextWorkbook,
128
+ normalizeWorkbookSelection(nextWorkbook, selection),
129
+ mapping,
130
+ "sheet.rename",
131
+ );
132
+ }
133
+ case "sheet.reorder": {
134
+ const fromIndex = nextWorkbook.sheetOrder.indexOf(command.sheetId);
135
+ if (fromIndex === -1) {
136
+ throw new Error(`Sheet not found: ${command.sheetId}`);
137
+ }
138
+ const toIndex = clampIndex(command.toIndex, nextWorkbook.sheetOrder.length - 1);
139
+ moveSheet(nextWorkbook, command.sheetId, command.toIndex);
140
+ mapping.operations.push({
141
+ type: "sheet-reorder",
142
+ sheetId: command.sheetId,
143
+ fromIndex,
144
+ toIndex,
145
+ });
146
+ return createWorkbookTransaction(
147
+ nextWorkbook,
148
+ normalizeWorkbookSelection(nextWorkbook, selection),
149
+ mapping,
150
+ "sheet.reorder",
151
+ );
152
+ }
153
+ }
154
+ }
155
+
156
+ function resolveFallbackSheetId(
157
+ workbook: CanonicalWorkbook,
158
+ removedSheetId: string,
159
+ ): string | undefined {
160
+ const currentIndex = workbook.sheetOrder.indexOf(removedSheetId);
161
+ if (currentIndex === -1) {
162
+ return getDefaultSelection(workbook).sheetId;
163
+ }
164
+
165
+ if (workbook.sheetOrder.length === 1) {
166
+ return undefined;
167
+ }
168
+
169
+ return workbook.sheetOrder[currentIndex + 1] ?? workbook.sheetOrder[currentIndex - 1];
170
+ }
171
+
172
+ function assertUniqueSheetName(
173
+ workbook: CanonicalWorkbook,
174
+ name: string,
175
+ currentSheetId?: string,
176
+ ): void {
177
+ const existing = getSheetByName(workbook, name);
178
+ if (existing && existing.sheetId !== currentSheetId) {
179
+ throw new Error(`Sheet name already exists: ${name}`);
180
+ }
181
+ }
182
+
183
+ function createSheetId(
184
+ workbook: CanonicalWorkbook,
185
+ factory?: () => string,
186
+ ): string {
187
+ if (factory) {
188
+ const nextId = factory();
189
+ if (workbook.sheets.has(nextId)) {
190
+ throw new Error(`Sheet id already exists: ${nextId}`);
191
+ }
192
+ return nextId;
193
+ }
194
+
195
+ let counter = workbook.sheets.size + 1;
196
+ let sheetId = `sheet-${counter}`;
197
+ while (workbook.sheets.has(sheetId)) {
198
+ counter += 1;
199
+ sheetId = `sheet-${counter}`;
200
+ }
201
+ return sheetId;
202
+ }
203
+
204
+ function clampIndex(value: number, maxIndex: number): number {
205
+ return Math.max(0, Math.min(value, maxIndex));
206
+ }
@@ -0,0 +1,177 @@
1
+ import type { CanonicalWorkbook } from "../model/workbook.ts";
2
+ import { applyCellCommand, type WorkbookCellCommand } from "./cell-commands.ts";
3
+ import { applySheetCommand, type WorkbookSheetCommand } from "./sheet-commands.ts";
4
+ import {
5
+ cloneWorkbook,
6
+ getDefaultSelection,
7
+ normalizeWorkbookSelection,
8
+ type WorkbookSelection,
9
+ type WorkbookTransaction,
10
+ } from "./workbook-transaction.ts";
11
+
12
+ export type WorkbookCommand = WorkbookCellCommand | WorkbookSheetCommand;
13
+
14
+ export type WorkbookRuntimeUnsubscribe = () => void;
15
+
16
+ export interface WorkbookRuntimeSnapshot {
17
+ workbook: CanonicalWorkbook;
18
+ selection: WorkbookSelection;
19
+ commandState: {
20
+ canUndo: boolean;
21
+ canRedo: boolean;
22
+ activeSheetId?: string;
23
+ };
24
+ }
25
+
26
+ export interface WorkbookRuntime {
27
+ subscribe(listener: () => void): WorkbookRuntimeUnsubscribe;
28
+ getSnapshot(): WorkbookRuntimeSnapshot;
29
+ dispatch(command: WorkbookCommand): WorkbookTransaction;
30
+ undo(): WorkbookTransaction | null;
31
+ redo(): WorkbookTransaction | null;
32
+ }
33
+
34
+ export interface CreateWorkbookRuntimeOptions {
35
+ workbook: CanonicalWorkbook;
36
+ selection?: WorkbookSelection;
37
+ createSheetId?: () => string;
38
+ }
39
+
40
+ interface WorkbookHistoryEntry {
41
+ beforeWorkbook: CanonicalWorkbook;
42
+ beforeSelection: WorkbookSelection;
43
+ transaction: WorkbookTransaction;
44
+ }
45
+
46
+ export function createWorkbookRuntime(
47
+ options: CreateWorkbookRuntimeOptions,
48
+ ): WorkbookRuntime {
49
+ const listeners = new Set<() => void>();
50
+ const history = {
51
+ past: [] as WorkbookHistoryEntry[],
52
+ future: [] as WorkbookHistoryEntry[],
53
+ };
54
+
55
+ let workbook = cloneWorkbook(options.workbook);
56
+ let selection = normalizeWorkbookSelection(workbook, options.selection ?? getDefaultSelection(workbook));
57
+ let cachedSnapshot = createSnapshot(workbook, selection, history);
58
+
59
+ return {
60
+ subscribe(listener) {
61
+ listeners.add(listener);
62
+ return () => {
63
+ listeners.delete(listener);
64
+ };
65
+ },
66
+ getSnapshot() {
67
+ return cachedSnapshot;
68
+ },
69
+ dispatch(command) {
70
+ const beforeWorkbook = cloneWorkbook(workbook);
71
+ const beforeSelection = cloneSelection(selection);
72
+ const transaction = isCellCommand(command)
73
+ ? applyCellCommand(workbook, selection, command)
74
+ : applySheetCommand(workbook, selection, command, {
75
+ createSheetId: options.createSheetId,
76
+ });
77
+
78
+ workbook = transaction.nextWorkbook;
79
+ selection = transaction.nextSelection;
80
+ history.past.push({
81
+ beforeWorkbook,
82
+ beforeSelection,
83
+ transaction,
84
+ });
85
+ history.future = [];
86
+ cachedSnapshot = createSnapshot(workbook, selection, history);
87
+ notifyListeners(listeners);
88
+ return transaction;
89
+ },
90
+ undo() {
91
+ const entry = history.past.pop();
92
+ if (!entry) {
93
+ return null;
94
+ }
95
+
96
+ const redoEntry: WorkbookHistoryEntry = {
97
+ beforeWorkbook: cloneWorkbook(workbook),
98
+ beforeSelection: cloneSelection(selection),
99
+ transaction: entry.transaction,
100
+ };
101
+ history.future.push(redoEntry);
102
+
103
+ workbook = cloneWorkbook(entry.beforeWorkbook);
104
+ selection = cloneSelection(entry.beforeSelection);
105
+ cachedSnapshot = createSnapshot(workbook, selection, history);
106
+ notifyListeners(listeners);
107
+ return {
108
+ ...entry.transaction,
109
+ nextWorkbook: cloneWorkbook(workbook),
110
+ nextSelection: cloneSelection(selection),
111
+ };
112
+ },
113
+ redo() {
114
+ const entry = history.future.pop();
115
+ if (!entry) {
116
+ return null;
117
+ }
118
+
119
+ history.past.push({
120
+ beforeWorkbook: cloneWorkbook(workbook),
121
+ beforeSelection: cloneSelection(selection),
122
+ transaction: entry.transaction,
123
+ });
124
+
125
+ workbook = cloneWorkbook(entry.transaction.nextWorkbook);
126
+ selection = cloneSelection(entry.transaction.nextSelection);
127
+ cachedSnapshot = createSnapshot(workbook, selection, history);
128
+ notifyListeners(listeners);
129
+ return {
130
+ ...entry.transaction,
131
+ nextWorkbook: cloneWorkbook(workbook),
132
+ nextSelection: cloneSelection(selection),
133
+ };
134
+ },
135
+ };
136
+ }
137
+
138
+ function createSnapshot(
139
+ workbook: CanonicalWorkbook,
140
+ selection: WorkbookSelection,
141
+ history: {
142
+ past: WorkbookHistoryEntry[];
143
+ future: WorkbookHistoryEntry[];
144
+ },
145
+ ): WorkbookRuntimeSnapshot {
146
+ return {
147
+ workbook,
148
+ selection,
149
+ commandState: {
150
+ canUndo: history.past.length > 0,
151
+ canRedo: history.future.length > 0,
152
+ activeSheetId: selection.sheetId,
153
+ },
154
+ };
155
+ }
156
+
157
+ function notifyListeners(listeners: Set<() => void>): void {
158
+ for (const listener of listeners) {
159
+ listener();
160
+ }
161
+ }
162
+
163
+ function isCellCommand(command: WorkbookCommand): command is WorkbookCellCommand {
164
+ return command.type.startsWith("cell.")
165
+ || command.type.startsWith("row.")
166
+ || command.type.startsWith("column.")
167
+ || command.type.startsWith("cells.");
168
+ }
169
+
170
+ function cloneSelection(selection: WorkbookSelection): WorkbookSelection {
171
+ return {
172
+ mode: selection.mode,
173
+ sheetId: selection.sheetId,
174
+ activeCell: selection.activeCell ? { ...selection.activeCell } : undefined,
175
+ range: selection.range ? { ...selection.range } : undefined,
176
+ };
177
+ }