@beyondwork/docx-react-component 1.0.36 → 1.0.38
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 +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -6,9 +6,15 @@ import type {
|
|
|
6
6
|
import type {
|
|
7
7
|
DocumentRootNode,
|
|
8
8
|
ParagraphNode,
|
|
9
|
+
TableBorders,
|
|
10
|
+
TableCellBorders,
|
|
11
|
+
TableCellMargins,
|
|
9
12
|
TableCellNode,
|
|
13
|
+
TableIndent,
|
|
10
14
|
TableNode,
|
|
11
15
|
TableRowNode,
|
|
16
|
+
TableWidth,
|
|
17
|
+
CellShading,
|
|
12
18
|
} from "../../model/canonical-document.ts";
|
|
13
19
|
import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
|
|
14
20
|
import type { CanonicalDocumentEnvelope, SelectionSnapshot } from "../state/editor-state.ts";
|
|
@@ -24,8 +30,26 @@ import {
|
|
|
24
30
|
findTopLevelParagraphSelectionNearBlock,
|
|
25
31
|
type StructuralMutationResult,
|
|
26
32
|
} from "./structural-helpers.ts";
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
import {
|
|
34
|
+
analyzeRect,
|
|
35
|
+
buildLogicalGrid,
|
|
36
|
+
computeLogicalColumnCount as computeLogicalColumnCountFromGrid,
|
|
37
|
+
findCellIndexAtColumn,
|
|
38
|
+
originAt,
|
|
39
|
+
slotAt,
|
|
40
|
+
type GridOriginSlot,
|
|
41
|
+
type LogicalGrid,
|
|
42
|
+
} from "./table-grid.ts";
|
|
43
|
+
|
|
44
|
+
export type CellLocator =
|
|
45
|
+
| { kind: "anchor" }
|
|
46
|
+
| { kind: "index"; rowIndex: number; columnIndex: number }
|
|
47
|
+
| { kind: "rect"; rect: { top: number; left: number; bottom: number; right: number } }
|
|
48
|
+
| { kind: "row"; rowIndex: number }
|
|
49
|
+
| { kind: "column"; columnIndex: number }
|
|
50
|
+
| { kind: "selection" };
|
|
51
|
+
|
|
52
|
+
export type TableStructureOperation =
|
|
29
53
|
| { type: "add-row-before" }
|
|
30
54
|
| { type: "add-row-after" }
|
|
31
55
|
| { type: "add-column-before" }
|
|
@@ -35,7 +59,35 @@ type TableStructureOperation =
|
|
|
35
59
|
| { type: "delete-table" }
|
|
36
60
|
| { type: "merge-cells" }
|
|
37
61
|
| { type: "split-cell" }
|
|
38
|
-
| { type: "set-cell-background"; color: string }
|
|
62
|
+
| { type: "set-cell-background"; color: string }
|
|
63
|
+
// P2g — table-level
|
|
64
|
+
| { type: "set-table-width"; width: TableWidth }
|
|
65
|
+
| { type: "set-table-alignment"; alignment: "left" | "center" | "right" }
|
|
66
|
+
| { type: "set-table-indent"; indent: TableIndent }
|
|
67
|
+
| { type: "set-table-layout-mode"; mode: "fixed" | "autofit" }
|
|
68
|
+
| { type: "set-table-cell-margins"; margins: Partial<TableCellMargins> }
|
|
69
|
+
| { type: "set-table-borders"; borders: Partial<TableBorders> }
|
|
70
|
+
| { type: "set-table-style"; styleId: string | null }
|
|
71
|
+
| { type: "set-table-caption"; caption: string | null }
|
|
72
|
+
| { type: "set-table-description"; description: string | null }
|
|
73
|
+
// P2h — column / row sizing + row props
|
|
74
|
+
| { type: "set-column-width"; columnIndex: number; twips: number }
|
|
75
|
+
| { type: "distribute-columns-evenly"; columnRange?: { from: number; to: number } }
|
|
76
|
+
| { type: "set-row-height"; rowIndex: number; twips: number; rule: "auto" | "atLeast" | "exact" }
|
|
77
|
+
| { type: "set-row-cant-split"; rowIndex: number; value: boolean }
|
|
78
|
+
| { type: "set-row-is-header"; rowIndex: number; value: boolean }
|
|
79
|
+
| { type: "set-row-alignment"; rowIndex: number; alignment: "left" | "center" | "right" }
|
|
80
|
+
| { type: "insert-rows"; rowIndex: number; at: "before" | "after"; count: number }
|
|
81
|
+
| { type: "insert-columns"; columnIndex: number; at: "before" | "after"; count: number; widths?: readonly number[] }
|
|
82
|
+
// P2i — cell-level
|
|
83
|
+
| { type: "set-cell-borders"; locator: CellLocator; borders: Partial<TableCellBorders> }
|
|
84
|
+
| { type: "set-cell-shading"; locator: CellLocator; shading: Partial<CellShading> | null }
|
|
85
|
+
| { type: "clear-cell-shading"; locator: CellLocator }
|
|
86
|
+
| { type: "set-cell-margins"; locator: CellLocator; margins: Partial<TableCellMargins> }
|
|
87
|
+
| { type: "set-cell-vertical-align"; locator: CellLocator; align: "top" | "center" | "bottom" }
|
|
88
|
+
| { type: "set-cell-text-direction"; locator: CellLocator; direction: "lrTb" | "tbRl" | "btLr" }
|
|
89
|
+
| { type: "set-cell-no-wrap"; locator: CellLocator; value: boolean }
|
|
90
|
+
| { type: "set-cell-fit-text"; locator: CellLocator; value: boolean };
|
|
39
91
|
|
|
40
92
|
export function applyTableStructureOperation(
|
|
41
93
|
document: CanonicalDocumentEnvelope,
|
|
@@ -81,6 +133,127 @@ export function applyTableStructureOperation(
|
|
|
81
133
|
return addColumn(document, root, target, effectiveSelection, "after", fallbackSelection);
|
|
82
134
|
case "delete-column":
|
|
83
135
|
return deleteColumn(document, root, target, effectiveSelection, fallbackSelection);
|
|
136
|
+
case "set-table-width":
|
|
137
|
+
return patchTable(document, root, target, effectiveSelection, { width: operation.width }, fallbackSelection);
|
|
138
|
+
case "set-table-alignment":
|
|
139
|
+
return patchTable(document, root, target, effectiveSelection, { alignment: operation.alignment }, fallbackSelection);
|
|
140
|
+
case "set-table-indent":
|
|
141
|
+
return patchTable(document, root, target, effectiveSelection, { indent: operation.indent }, fallbackSelection);
|
|
142
|
+
case "set-table-layout-mode":
|
|
143
|
+
return patchTable(document, root, target, effectiveSelection, { layoutMode: operation.mode }, fallbackSelection);
|
|
144
|
+
case "set-table-cell-margins":
|
|
145
|
+
return patchTable(
|
|
146
|
+
document,
|
|
147
|
+
root,
|
|
148
|
+
target,
|
|
149
|
+
effectiveSelection,
|
|
150
|
+
{ cellMargins: mergeTableCellMargins(target.cellMargins, operation.margins) },
|
|
151
|
+
fallbackSelection,
|
|
152
|
+
);
|
|
153
|
+
case "set-table-borders":
|
|
154
|
+
return patchTable(
|
|
155
|
+
document,
|
|
156
|
+
root,
|
|
157
|
+
target,
|
|
158
|
+
effectiveSelection,
|
|
159
|
+
{ borders: mergeTableBorders(target.borders, operation.borders) },
|
|
160
|
+
fallbackSelection,
|
|
161
|
+
);
|
|
162
|
+
case "set-table-style":
|
|
163
|
+
return patchTable(
|
|
164
|
+
document,
|
|
165
|
+
root,
|
|
166
|
+
target,
|
|
167
|
+
effectiveSelection,
|
|
168
|
+
{ styleId: operation.styleId ?? undefined },
|
|
169
|
+
fallbackSelection,
|
|
170
|
+
);
|
|
171
|
+
case "set-table-caption":
|
|
172
|
+
return patchTable(
|
|
173
|
+
document,
|
|
174
|
+
root,
|
|
175
|
+
target,
|
|
176
|
+
effectiveSelection,
|
|
177
|
+
{ caption: operation.caption ?? undefined },
|
|
178
|
+
fallbackSelection,
|
|
179
|
+
);
|
|
180
|
+
case "set-table-description":
|
|
181
|
+
return patchTable(
|
|
182
|
+
document,
|
|
183
|
+
root,
|
|
184
|
+
target,
|
|
185
|
+
effectiveSelection,
|
|
186
|
+
{ description: operation.description ?? undefined },
|
|
187
|
+
fallbackSelection,
|
|
188
|
+
);
|
|
189
|
+
case "set-column-width":
|
|
190
|
+
return setColumnWidth(document, root, target, effectiveSelection, operation.columnIndex, operation.twips, fallbackSelection);
|
|
191
|
+
case "distribute-columns-evenly":
|
|
192
|
+
return distributeColumnsEvenly(document, root, target, effectiveSelection, operation.columnRange, fallbackSelection);
|
|
193
|
+
case "set-row-height":
|
|
194
|
+
return patchRow(document, root, target, effectiveSelection, operation.rowIndex, {
|
|
195
|
+
height: operation.twips,
|
|
196
|
+
heightRule: operation.rule,
|
|
197
|
+
}, fallbackSelection);
|
|
198
|
+
case "set-row-cant-split":
|
|
199
|
+
return patchRow(document, root, target, effectiveSelection, operation.rowIndex, {
|
|
200
|
+
cantSplit: operation.value,
|
|
201
|
+
}, fallbackSelection);
|
|
202
|
+
case "set-row-is-header":
|
|
203
|
+
return patchRow(document, root, target, effectiveSelection, operation.rowIndex, {
|
|
204
|
+
isHeader: operation.value,
|
|
205
|
+
}, fallbackSelection);
|
|
206
|
+
case "set-row-alignment":
|
|
207
|
+
return patchRow(document, root, target, effectiveSelection, operation.rowIndex, {
|
|
208
|
+
horizontalAlignment: operation.alignment,
|
|
209
|
+
}, fallbackSelection);
|
|
210
|
+
case "insert-rows":
|
|
211
|
+
return insertRows(document, root, target, effectiveSelection, operation.rowIndex, operation.at, operation.count, fallbackSelection);
|
|
212
|
+
case "insert-columns":
|
|
213
|
+
return insertColumns(document, root, target, effectiveSelection, operation.columnIndex, operation.at, operation.count, operation.widths, fallbackSelection);
|
|
214
|
+
case "set-cell-borders":
|
|
215
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => ({
|
|
216
|
+
...cell,
|
|
217
|
+
borders: mergeCellBorders(cell.borders, operation.borders),
|
|
218
|
+
}), fallbackSelection);
|
|
219
|
+
case "set-cell-shading":
|
|
220
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => {
|
|
221
|
+
if (operation.shading === null) {
|
|
222
|
+
const { shading: _drop, ...rest } = cell;
|
|
223
|
+
return rest as TableCellNode;
|
|
224
|
+
}
|
|
225
|
+
return { ...cell, shading: { ...(cell.shading ?? {}), ...operation.shading } };
|
|
226
|
+
}, fallbackSelection);
|
|
227
|
+
case "clear-cell-shading":
|
|
228
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => {
|
|
229
|
+
const { shading: _drop, ...rest } = cell;
|
|
230
|
+
return rest as TableCellNode;
|
|
231
|
+
}, fallbackSelection);
|
|
232
|
+
case "set-cell-margins":
|
|
233
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => ({
|
|
234
|
+
...cell,
|
|
235
|
+
margins: { ...(cell.margins ?? {}), ...operation.margins },
|
|
236
|
+
}), fallbackSelection);
|
|
237
|
+
case "set-cell-vertical-align":
|
|
238
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => ({
|
|
239
|
+
...cell,
|
|
240
|
+
verticalAlign: operation.align,
|
|
241
|
+
}), fallbackSelection);
|
|
242
|
+
case "set-cell-text-direction":
|
|
243
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => ({
|
|
244
|
+
...cell,
|
|
245
|
+
textDirection: operation.direction,
|
|
246
|
+
}), fallbackSelection);
|
|
247
|
+
case "set-cell-no-wrap":
|
|
248
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => ({
|
|
249
|
+
...cell,
|
|
250
|
+
noWrap: operation.value,
|
|
251
|
+
}), fallbackSelection);
|
|
252
|
+
case "set-cell-fit-text":
|
|
253
|
+
return patchCells(document, root, target, effectiveSelection, operation.locator, (cell) => ({
|
|
254
|
+
...cell,
|
|
255
|
+
fitText: operation.value,
|
|
256
|
+
}), fallbackSelection);
|
|
84
257
|
default:
|
|
85
258
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
86
259
|
}
|
|
@@ -135,6 +308,19 @@ export function getTableStructureContext(
|
|
|
135
308
|
)
|
|
136
309
|
: 1;
|
|
137
310
|
|
|
311
|
+
// Build the logical grid once so merge-aware capability checks share the
|
|
312
|
+
// same traversal as the commands they gate.
|
|
313
|
+
const grid = buildLogicalGrid(target);
|
|
314
|
+
const mergeRectCoverage =
|
|
315
|
+
effectiveSelection.selectionKind === "cell" && selectedCellCount > 1
|
|
316
|
+
? analyzeRect(grid, {
|
|
317
|
+
top: effectiveSelection.rect.top,
|
|
318
|
+
left: effectiveSelection.rect.left,
|
|
319
|
+
bottom: effectiveSelection.rect.bottom,
|
|
320
|
+
right: effectiveSelection.rect.right,
|
|
321
|
+
})
|
|
322
|
+
: null;
|
|
323
|
+
|
|
138
324
|
return {
|
|
139
325
|
tableBlockIndex: effectiveSelection.tableBlockIndex,
|
|
140
326
|
currentStyleId: target.styleId ?? null,
|
|
@@ -151,41 +337,66 @@ export function getTableStructureContext(
|
|
|
151
337
|
operations: {
|
|
152
338
|
setTableStyle: enabledCapability(),
|
|
153
339
|
setCellBackground: enabledCapability(),
|
|
154
|
-
addRowBefore:
|
|
155
|
-
|
|
156
|
-
: disabledCapability("Only simple rectangular tables support row insertion right now."),
|
|
157
|
-
addRowAfter: simpleTable
|
|
158
|
-
? enabledCapability()
|
|
159
|
-
: disabledCapability("Only simple rectangular tables support row insertion right now."),
|
|
340
|
+
addRowBefore: enabledCapability(),
|
|
341
|
+
addRowAfter: enabledCapability(),
|
|
160
342
|
deleteRow:
|
|
161
|
-
|
|
162
|
-
? disabledCapability("
|
|
163
|
-
:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
addColumnBefore: simpleTable
|
|
167
|
-
? enabledCapability()
|
|
168
|
-
: disabledCapability("Only simple rectangular tables support column insertion right now."),
|
|
169
|
-
addColumnAfter: simpleTable
|
|
170
|
-
? enabledCapability()
|
|
171
|
-
: disabledCapability("Only simple rectangular tables support column insertion right now."),
|
|
343
|
+
target.rows.length <= 1
|
|
344
|
+
? disabledCapability("At least two rows are required before a row can be deleted.")
|
|
345
|
+
: enabledCapability(),
|
|
346
|
+
addColumnBefore: enabledCapability(),
|
|
347
|
+
addColumnAfter: enabledCapability(),
|
|
172
348
|
deleteColumn:
|
|
173
|
-
|
|
174
|
-
? disabledCapability("
|
|
175
|
-
:
|
|
176
|
-
? disabledCapability("At least two columns are required before a column can be deleted.")
|
|
177
|
-
: enabledCapability(),
|
|
349
|
+
columnCount <= 1
|
|
350
|
+
? disabledCapability("At least two columns are required before a column can be deleted.")
|
|
351
|
+
: enabledCapability(),
|
|
178
352
|
mergeCells:
|
|
179
|
-
|
|
180
|
-
? disabledCapability("
|
|
181
|
-
:
|
|
353
|
+
effectiveSelection.selectionKind !== "cell"
|
|
354
|
+
? disabledCapability("Select more than one cell to merge them.")
|
|
355
|
+
: selectedCellCount <= 1
|
|
182
356
|
? disabledCapability("Select more than one cell to merge them.")
|
|
183
|
-
:
|
|
357
|
+
: mergeRectCoverage && !mergeRectCoverage.clean
|
|
358
|
+
? disabledCapability(
|
|
359
|
+
"Selection cuts through a merged cell. Extend the selection to fully enclose the span.",
|
|
360
|
+
)
|
|
361
|
+
: enabledCapability(),
|
|
184
362
|
splitCell:
|
|
185
363
|
splitWidth === 1 && splitHeight === 1
|
|
186
364
|
? disabledCapability("Select a merged or spanning cell to split it.")
|
|
187
365
|
: enabledCapability(),
|
|
188
366
|
deleteTable: enabledCapability(),
|
|
367
|
+
// P2g — table-level
|
|
368
|
+
setTableWidth: enabledCapability(),
|
|
369
|
+
setTableAlignment: enabledCapability(),
|
|
370
|
+
setTableIndent: enabledCapability(),
|
|
371
|
+
setTableLayoutMode: enabledCapability(),
|
|
372
|
+
setTableCellMargins: enabledCapability(),
|
|
373
|
+
setTableBorders: enabledCapability(),
|
|
374
|
+
setTableCaption: enabledCapability(),
|
|
375
|
+
setTableDescription: enabledCapability(),
|
|
376
|
+
// P2h — column/row sizing + row props
|
|
377
|
+
setColumnWidth:
|
|
378
|
+
target.gridColumns.length > 0
|
|
379
|
+
? enabledCapability()
|
|
380
|
+
: disabledCapability("Table has no grid columns to size."),
|
|
381
|
+
distributeColumnsEvenly:
|
|
382
|
+
target.gridColumns.length > 1
|
|
383
|
+
? enabledCapability()
|
|
384
|
+
: disabledCapability("At least two columns are required to distribute width."),
|
|
385
|
+
setRowHeight: enabledCapability(),
|
|
386
|
+
setRowCantSplit: enabledCapability(),
|
|
387
|
+
setRowIsHeader: enabledCapability(),
|
|
388
|
+
setRowAlignment: enabledCapability(),
|
|
389
|
+
insertRows: enabledCapability(),
|
|
390
|
+
insertColumns: enabledCapability(),
|
|
391
|
+
// P2i — cell-level
|
|
392
|
+
setCellBorders: enabledCapability(),
|
|
393
|
+
setCellShading: enabledCapability(),
|
|
394
|
+
clearCellShading: enabledCapability(),
|
|
395
|
+
setCellMargins: enabledCapability(),
|
|
396
|
+
setCellVerticalAlign: enabledCapability(),
|
|
397
|
+
setCellTextDirection: enabledCapability(),
|
|
398
|
+
setCellNoWrap: enabledCapability(),
|
|
399
|
+
setCellFitText: enabledCapability(),
|
|
189
400
|
},
|
|
190
401
|
};
|
|
191
402
|
}
|
|
@@ -198,17 +409,15 @@ function addRow(
|
|
|
198
409
|
position: "before" | "after",
|
|
199
410
|
fallbackSelection: SelectionSnapshot,
|
|
200
411
|
): StructuralMutationResult {
|
|
201
|
-
|
|
412
|
+
const grid = buildLogicalGrid(table);
|
|
413
|
+
const columnCount = grid.columnCount;
|
|
414
|
+
if (columnCount === 0) {
|
|
202
415
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
203
416
|
}
|
|
204
417
|
|
|
205
|
-
const columnCount = getLogicalColumnCount(table);
|
|
206
418
|
const insertIndex =
|
|
207
419
|
position === "before" ? selection.anchorCell.rowIndex : selection.anchorCell.rowIndex + 1;
|
|
208
|
-
const nextRow
|
|
209
|
-
type: "table_row",
|
|
210
|
-
cells: Array.from({ length: columnCount }, () => createEmptyCell()),
|
|
211
|
-
};
|
|
420
|
+
const nextRow = buildInsertedRow(grid, insertIndex);
|
|
212
421
|
const nextTable: TableNode = {
|
|
213
422
|
...table,
|
|
214
423
|
rows: [
|
|
@@ -229,6 +438,58 @@ function addRow(
|
|
|
229
438
|
);
|
|
230
439
|
}
|
|
231
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Build the cells for a new row inserted at `insertIndex` (index in the
|
|
443
|
+
* original table, before the insertion is applied). For each logical column,
|
|
444
|
+
* if both the row above and the row below belong to the same vMerge chain,
|
|
445
|
+
* the new row extends that chain with a `verticalMerge: "continue"` cell.
|
|
446
|
+
* Otherwise an empty cell is emitted with gridSpan=1.
|
|
447
|
+
*/
|
|
448
|
+
function buildInsertedRow(grid: LogicalGrid, insertIndex: number): TableRowNode {
|
|
449
|
+
const cells: TableCellNode[] = [];
|
|
450
|
+
const columnCount = grid.columnCount;
|
|
451
|
+
let column = 0;
|
|
452
|
+
while (column < columnCount) {
|
|
453
|
+
const above = insertIndex > 0 ? originAt(grid, insertIndex - 1, column) : null;
|
|
454
|
+
const below =
|
|
455
|
+
insertIndex < grid.rowCount ? originAt(grid, insertIndex, column) : null;
|
|
456
|
+
|
|
457
|
+
// A vMerge chain continues across the insertion point when:
|
|
458
|
+
// - both above and below resolve to the same origin, and
|
|
459
|
+
// - that origin lives above the insertion point (otherwise the chain
|
|
460
|
+
// starts at `below` and is fully below the insertion point).
|
|
461
|
+
if (
|
|
462
|
+
above &&
|
|
463
|
+
below &&
|
|
464
|
+
above === below &&
|
|
465
|
+
above.rowIndex < insertIndex
|
|
466
|
+
) {
|
|
467
|
+
const span = above.columnSpan;
|
|
468
|
+
// Align to the origin's leftmost column: if we started iteration
|
|
469
|
+
// mid-span, still treat the full span as the unit.
|
|
470
|
+
if (column !== above.columnIndex) {
|
|
471
|
+
// We're scanning forward, so this shouldn't happen — if it does,
|
|
472
|
+
// fall through to an empty cell for this column only.
|
|
473
|
+
cells.push(createEmptyCell());
|
|
474
|
+
column += 1;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
cells.push({
|
|
478
|
+
type: "table_cell",
|
|
479
|
+
children: [createEmptyParagraph()],
|
|
480
|
+
verticalMerge: "continue",
|
|
481
|
+
...(span > 1 ? { gridSpan: span } : {}),
|
|
482
|
+
});
|
|
483
|
+
column += span;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
cells.push(createEmptyCell());
|
|
488
|
+
column += 1;
|
|
489
|
+
}
|
|
490
|
+
return { type: "table_row", cells };
|
|
491
|
+
}
|
|
492
|
+
|
|
232
493
|
function deleteRow(
|
|
233
494
|
document: CanonicalDocumentEnvelope,
|
|
234
495
|
root: DocumentRootNode,
|
|
@@ -236,12 +497,52 @@ function deleteRow(
|
|
|
236
497
|
selection: TableSelectionDescriptor,
|
|
237
498
|
fallbackSelection: SelectionSnapshot,
|
|
238
499
|
): StructuralMutationResult {
|
|
239
|
-
if (
|
|
500
|
+
if (table.rows.length <= 1) {
|
|
240
501
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
241
502
|
}
|
|
242
503
|
|
|
243
504
|
const deleteIndex = selection.anchorCell.rowIndex;
|
|
244
|
-
const
|
|
505
|
+
const grid = buildLogicalGrid(table);
|
|
506
|
+
|
|
507
|
+
// For every logical column, check if this row is the origin of a vMerge
|
|
508
|
+
// chain that extends beyond this row. If so, we must promote the first
|
|
509
|
+
// following continue cell to "restart" and copy over the origin's
|
|
510
|
+
// content/properties so the chain survives the deletion.
|
|
511
|
+
//
|
|
512
|
+
// For columns where this row is a mid-chain continue, the chain's owner
|
|
513
|
+
// origin stays in place — we just need to drop 1 from its effective
|
|
514
|
+
// rowSpan (which happens automatically when the row is removed from
|
|
515
|
+
// the canonical tree).
|
|
516
|
+
const promotions: Array<{
|
|
517
|
+
originColumnIndex: number;
|
|
518
|
+
originColumnSpan: number;
|
|
519
|
+
promotedRowIndex: number;
|
|
520
|
+
originCell: TableCellNode;
|
|
521
|
+
}> = [];
|
|
522
|
+
|
|
523
|
+
for (let column = 0; column < grid.columnCount; ) {
|
|
524
|
+
const origin = originAt(grid, deleteIndex, column);
|
|
525
|
+
if (!origin) {
|
|
526
|
+
column += 1;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (origin.rowIndex === deleteIndex && origin.rowSpan > 1) {
|
|
530
|
+
promotions.push({
|
|
531
|
+
originColumnIndex: origin.columnIndex,
|
|
532
|
+
originColumnSpan: origin.columnSpan,
|
|
533
|
+
promotedRowIndex: deleteIndex + 1,
|
|
534
|
+
originCell: origin.cell,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
column = origin.columnIndex + origin.columnSpan;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Apply promotions to the row immediately after the deleted one.
|
|
541
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
542
|
+
if (rowIndex !== deleteIndex + 1 || promotions.length === 0) return row;
|
|
543
|
+
return applyVMergePromotions(row, promotions);
|
|
544
|
+
}).filter((_, rowIndex) => rowIndex !== deleteIndex);
|
|
545
|
+
|
|
245
546
|
const focusRowIndex = Math.max(0, Math.min(deleteIndex, nextRows.length - 1));
|
|
246
547
|
const nextTable: TableNode = {
|
|
247
548
|
...table,
|
|
@@ -259,6 +560,43 @@ function deleteRow(
|
|
|
259
560
|
);
|
|
260
561
|
}
|
|
261
562
|
|
|
563
|
+
function applyVMergePromotions(
|
|
564
|
+
row: TableRowNode,
|
|
565
|
+
promotions: Array<{
|
|
566
|
+
originColumnIndex: number;
|
|
567
|
+
originColumnSpan: number;
|
|
568
|
+
promotedRowIndex: number;
|
|
569
|
+
originCell: TableCellNode;
|
|
570
|
+
}>,
|
|
571
|
+
): TableRowNode {
|
|
572
|
+
const nextCells = row.cells.map((cell, cellIndex) => {
|
|
573
|
+
let cursor = row.gridBefore ?? 0;
|
|
574
|
+
// Recompute the cell's logical column position.
|
|
575
|
+
for (let i = 0; i < cellIndex; i += 1) {
|
|
576
|
+
cursor += Math.max(1, row.cells[i]!.gridSpan ?? 1);
|
|
577
|
+
}
|
|
578
|
+
const logicalColumn = cursor;
|
|
579
|
+
|
|
580
|
+
const promotion = promotions.find(
|
|
581
|
+
(p) =>
|
|
582
|
+
p.originColumnIndex === logicalColumn &&
|
|
583
|
+
(cell.gridSpan ?? 1) === p.originColumnSpan &&
|
|
584
|
+
cell.verticalMerge === "continue",
|
|
585
|
+
);
|
|
586
|
+
if (!promotion) return cell;
|
|
587
|
+
|
|
588
|
+
// Promote: copy origin's cell content + properties, mark as restart.
|
|
589
|
+
const { children, ...properties } = promotion.originCell;
|
|
590
|
+
return {
|
|
591
|
+
...properties,
|
|
592
|
+
children: structuredClone(children),
|
|
593
|
+
verticalMerge: "restart" as const,
|
|
594
|
+
...(promotion.originColumnSpan > 1 ? { gridSpan: promotion.originColumnSpan } : {}),
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
return { ...row, cells: nextCells };
|
|
598
|
+
}
|
|
599
|
+
|
|
262
600
|
function addColumn(
|
|
263
601
|
document: CanonicalDocumentEnvelope,
|
|
264
602
|
root: DocumentRootNode,
|
|
@@ -267,25 +605,90 @@ function addColumn(
|
|
|
267
605
|
position: "before" | "after",
|
|
268
606
|
fallbackSelection: SelectionSnapshot,
|
|
269
607
|
): StructuralMutationResult {
|
|
270
|
-
|
|
608
|
+
const grid = buildLogicalGrid(table);
|
|
609
|
+
const columnCount = grid.columnCount;
|
|
610
|
+
if (columnCount === 0) {
|
|
271
611
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
272
612
|
}
|
|
273
613
|
|
|
274
|
-
const
|
|
614
|
+
const insertColumn =
|
|
275
615
|
position === "before"
|
|
276
616
|
? selection.anchorCell.columnIndex
|
|
277
617
|
: selection.anchorCell.columnIndex + 1;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
618
|
+
|
|
619
|
+
// Rewrite each row. For rows where the insert column falls strictly
|
|
620
|
+
// inside an existing horizontal span (origin.columnIndex < insertColumn
|
|
621
|
+
// < origin.columnIndex + origin.columnSpan), extend that span by 1.
|
|
622
|
+
// Otherwise insert a plain empty cell at the appropriate cellIndex.
|
|
623
|
+
const insertedOriginsExtended = new Set<GridOriginSlot>();
|
|
624
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
625
|
+
// Find the origin that covers the insertion column in this row, if any.
|
|
626
|
+
const leftOrigin =
|
|
627
|
+
insertColumn > 0 ? originAt(grid, rowIndex, insertColumn - 1) : null;
|
|
628
|
+
const rightSlot = slotAt(grid, rowIndex, insertColumn);
|
|
629
|
+
const rightOrigin = rightSlot
|
|
630
|
+
? rightSlot.kind === "origin"
|
|
631
|
+
? rightSlot
|
|
632
|
+
: rightSlot.origin
|
|
633
|
+
: null;
|
|
634
|
+
|
|
635
|
+
const insideHSpan =
|
|
636
|
+
leftOrigin &&
|
|
637
|
+
rightOrigin &&
|
|
638
|
+
leftOrigin === rightOrigin &&
|
|
639
|
+
leftOrigin.columnSpan > 1;
|
|
640
|
+
|
|
641
|
+
if (insideHSpan && leftOrigin && !insertedOriginsExtended.has(leftOrigin)) {
|
|
642
|
+
// Extend the owner cell's gridSpan by 1 (only once per origin across
|
|
643
|
+
// rows — each origin lives in exactly one row).
|
|
644
|
+
const ownerCellIndex = leftOrigin.cellIndex;
|
|
645
|
+
insertedOriginsExtended.add(leftOrigin);
|
|
646
|
+
return {
|
|
647
|
+
...row,
|
|
648
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
649
|
+
cellIndex === ownerCellIndex
|
|
650
|
+
? { ...cell, gridSpan: (cell.gridSpan ?? 1) + 1 }
|
|
651
|
+
: cell,
|
|
652
|
+
),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (insideHSpan) {
|
|
657
|
+
// The owner has already been extended in its origin row; this row
|
|
658
|
+
// is a vMerge continuation covered by the same column range. Extend
|
|
659
|
+
// the continue cell's gridSpan so the logical grid stays consistent.
|
|
660
|
+
const continueRef = findCellIndexAtColumn(row, leftOrigin!.columnIndex);
|
|
661
|
+
if (continueRef) {
|
|
662
|
+
return {
|
|
663
|
+
...row,
|
|
664
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
665
|
+
cellIndex === continueRef.cellIndex
|
|
666
|
+
? { ...cell, gridSpan: (cell.gridSpan ?? 1) + 1 }
|
|
667
|
+
: cell,
|
|
668
|
+
),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return row;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Not inside an hspan — insert a plain empty cell at the cellIndex
|
|
675
|
+
// corresponding to the insertion column. Account for gridBefore
|
|
676
|
+
// padding and any earlier cell spans.
|
|
677
|
+
const insertCellIndex = resolveInsertCellIndex(row, insertColumn);
|
|
678
|
+
return {
|
|
282
679
|
...row,
|
|
283
680
|
cells: [
|
|
284
|
-
...row.cells.slice(0,
|
|
681
|
+
...row.cells.slice(0, insertCellIndex),
|
|
285
682
|
createEmptyCell(),
|
|
286
|
-
...row.cells.slice(
|
|
683
|
+
...row.cells.slice(insertCellIndex),
|
|
287
684
|
],
|
|
288
|
-
}
|
|
685
|
+
};
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const nextTable: TableNode = {
|
|
689
|
+
...table,
|
|
690
|
+
gridColumns: insertGridColumn(table.gridColumns, insertColumn),
|
|
691
|
+
rows: nextRows,
|
|
289
692
|
};
|
|
290
693
|
|
|
291
694
|
return commitTableChange(
|
|
@@ -295,10 +698,24 @@ function addColumn(
|
|
|
295
698
|
nextTable,
|
|
296
699
|
fallbackSelection,
|
|
297
700
|
selection.anchorCell.rowIndex,
|
|
298
|
-
|
|
701
|
+
insertColumn,
|
|
299
702
|
);
|
|
300
703
|
}
|
|
301
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Find the index in row.cells where a new cell should be inserted so it
|
|
707
|
+
* lands at logical column `logicalColumn`. Accounts for gridBefore padding
|
|
708
|
+
* and any earlier cell spans.
|
|
709
|
+
*/
|
|
710
|
+
function resolveInsertCellIndex(row: TableRowNode, logicalColumn: number): number {
|
|
711
|
+
let cursor = row.gridBefore ?? 0;
|
|
712
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
713
|
+
if (logicalColumn <= cursor) return cellIndex;
|
|
714
|
+
cursor += Math.max(1, row.cells[cellIndex]!.gridSpan ?? 1);
|
|
715
|
+
}
|
|
716
|
+
return row.cells.length;
|
|
717
|
+
}
|
|
718
|
+
|
|
302
719
|
function deleteColumn(
|
|
303
720
|
document: CanonicalDocumentEnvelope,
|
|
304
721
|
root: DocumentRootNode,
|
|
@@ -306,20 +723,67 @@ function deleteColumn(
|
|
|
306
723
|
selection: TableSelectionDescriptor,
|
|
307
724
|
fallbackSelection: SelectionSnapshot,
|
|
308
725
|
): StructuralMutationResult {
|
|
309
|
-
if (
|
|
726
|
+
if (getLogicalColumnCount(table) <= 1) {
|
|
310
727
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
311
728
|
}
|
|
312
729
|
|
|
313
|
-
const
|
|
730
|
+
const deleteColumn = selection.anchorCell.columnIndex;
|
|
731
|
+
const grid = buildLogicalGrid(table);
|
|
732
|
+
|
|
733
|
+
// For each row, determine whether the deleted column hits a cell that
|
|
734
|
+
// is a pure single-column cell (drop it) or is inside a gridSpan > 1
|
|
735
|
+
// cell (decrement the span). Rows where the column lies outside the
|
|
736
|
+
// cell range (gridBefore/gridAfter padding) simply pass through.
|
|
737
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
738
|
+
const origin = originAt(grid, rowIndex, deleteColumn);
|
|
739
|
+
if (!origin) return row;
|
|
740
|
+
|
|
741
|
+
const cellRef = findCellIndexAtColumn(row, origin.columnIndex);
|
|
742
|
+
if (!cellRef) return row;
|
|
743
|
+
|
|
744
|
+
if (origin.columnSpan <= 1) {
|
|
745
|
+
// Pure single-column cell — drop it from the row.
|
|
746
|
+
return {
|
|
747
|
+
...row,
|
|
748
|
+
cells: row.cells.filter((_, cellIndex) => cellIndex !== cellRef.cellIndex),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Inside an hspan — decrement gridSpan. If the new span would be 1,
|
|
753
|
+
// drop the gridSpan attribute entirely so the canonical model stays
|
|
754
|
+
// tidy.
|
|
755
|
+
const nextSpan = origin.columnSpan - 1;
|
|
756
|
+
const nextCell: TableCellNode = {
|
|
757
|
+
...cellRef.cell,
|
|
758
|
+
...(nextSpan > 1 ? { gridSpan: nextSpan } : { gridSpan: undefined }),
|
|
759
|
+
};
|
|
760
|
+
// Remove the explicit gridSpan field when nextSpan === 1.
|
|
761
|
+
if (nextSpan === 1) {
|
|
762
|
+
const { gridSpan: _drop, ...rest } = nextCell;
|
|
763
|
+
return {
|
|
764
|
+
...row,
|
|
765
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
766
|
+
cellIndex === cellRef.cellIndex ? (rest as TableCellNode) : cell,
|
|
767
|
+
),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
...row,
|
|
772
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
773
|
+
cellIndex === cellRef.cellIndex ? nextCell : cell,
|
|
774
|
+
),
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
|
|
314
778
|
const nextTable: TableNode = {
|
|
315
779
|
...table,
|
|
316
|
-
gridColumns: deleteGridColumn(table.gridColumns,
|
|
317
|
-
rows:
|
|
318
|
-
...row,
|
|
319
|
-
cells: row.cells.filter((_, cellIndex) => cellIndex !== deleteIndex),
|
|
320
|
-
})),
|
|
780
|
+
gridColumns: deleteGridColumn(table.gridColumns, deleteColumn),
|
|
781
|
+
rows: nextRows,
|
|
321
782
|
};
|
|
322
|
-
const focusColumnIndex = Math.max(
|
|
783
|
+
const focusColumnIndex = Math.max(
|
|
784
|
+
0,
|
|
785
|
+
Math.min(deleteColumn, getLogicalColumnCount(nextTable) - 1),
|
|
786
|
+
);
|
|
323
787
|
|
|
324
788
|
return commitTableChange(
|
|
325
789
|
document,
|
|
@@ -852,3 +1316,299 @@ function normalizeFillColor(color: string): string | null {
|
|
|
852
1316
|
const normalized = color.trim().replace(/^#/, "");
|
|
853
1317
|
return /^[0-9A-Fa-f]{3,8}$/.test(normalized) ? normalized.toUpperCase() : null;
|
|
854
1318
|
}
|
|
1319
|
+
|
|
1320
|
+
// ─── P2g helpers: table-level patches ────────────────────────────────────────
|
|
1321
|
+
|
|
1322
|
+
function patchTable(
|
|
1323
|
+
document: CanonicalDocumentEnvelope,
|
|
1324
|
+
root: DocumentRootNode,
|
|
1325
|
+
table: TableNode,
|
|
1326
|
+
selection: TableSelectionDescriptor,
|
|
1327
|
+
patch: Partial<TableNode>,
|
|
1328
|
+
fallbackSelection: SelectionSnapshot,
|
|
1329
|
+
): StructuralMutationResult {
|
|
1330
|
+
const nextTable: TableNode = { ...table, ...patch };
|
|
1331
|
+
return commitTableChange(
|
|
1332
|
+
document,
|
|
1333
|
+
root,
|
|
1334
|
+
selection.tableBlockIndex,
|
|
1335
|
+
nextTable,
|
|
1336
|
+
fallbackSelection,
|
|
1337
|
+
selection.anchorCell.rowIndex,
|
|
1338
|
+
selection.anchorCell.columnIndex,
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function mergeTableBorders(
|
|
1343
|
+
base: TableBorders | undefined,
|
|
1344
|
+
override: Partial<TableBorders>,
|
|
1345
|
+
): TableBorders {
|
|
1346
|
+
return { ...(base ?? {}), ...override };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function mergeCellBorders(
|
|
1350
|
+
base: TableCellBorders | undefined,
|
|
1351
|
+
override: Partial<TableCellBorders>,
|
|
1352
|
+
): TableCellBorders {
|
|
1353
|
+
return { ...(base ?? {}), ...override };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function mergeTableCellMargins(
|
|
1357
|
+
base: TableCellMargins | undefined,
|
|
1358
|
+
override: Partial<TableCellMargins>,
|
|
1359
|
+
): TableCellMargins {
|
|
1360
|
+
return { ...(base ?? {}), ...override };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// ─── P2h helpers: column/row sizing + row-level patches ──────────────────────
|
|
1364
|
+
|
|
1365
|
+
function setColumnWidth(
|
|
1366
|
+
document: CanonicalDocumentEnvelope,
|
|
1367
|
+
root: DocumentRootNode,
|
|
1368
|
+
table: TableNode,
|
|
1369
|
+
selection: TableSelectionDescriptor,
|
|
1370
|
+
columnIndex: number,
|
|
1371
|
+
twips: number,
|
|
1372
|
+
fallbackSelection: SelectionSnapshot,
|
|
1373
|
+
): StructuralMutationResult {
|
|
1374
|
+
if (columnIndex < 0 || columnIndex >= table.gridColumns.length) {
|
|
1375
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1376
|
+
}
|
|
1377
|
+
const clamped = Math.max(0, Math.round(twips));
|
|
1378
|
+
const nextGridColumns = table.gridColumns.map((w, i) => (i === columnIndex ? clamped : w));
|
|
1379
|
+
return patchTable(document, root, table, selection, { gridColumns: nextGridColumns }, fallbackSelection);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function distributeColumnsEvenly(
|
|
1383
|
+
document: CanonicalDocumentEnvelope,
|
|
1384
|
+
root: DocumentRootNode,
|
|
1385
|
+
table: TableNode,
|
|
1386
|
+
selection: TableSelectionDescriptor,
|
|
1387
|
+
columnRange: { from: number; to: number } | undefined,
|
|
1388
|
+
fallbackSelection: SelectionSnapshot,
|
|
1389
|
+
): StructuralMutationResult {
|
|
1390
|
+
const gridColumns = table.gridColumns;
|
|
1391
|
+
if (gridColumns.length === 0) {
|
|
1392
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1393
|
+
}
|
|
1394
|
+
const from = columnRange ? Math.max(0, columnRange.from) : 0;
|
|
1395
|
+
const to = columnRange ? Math.min(gridColumns.length, columnRange.to) : gridColumns.length;
|
|
1396
|
+
if (to - from <= 1) {
|
|
1397
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1398
|
+
}
|
|
1399
|
+
const total = gridColumns.slice(from, to).reduce((sum, w) => sum + w, 0);
|
|
1400
|
+
const even = Math.floor(total / (to - from));
|
|
1401
|
+
const remainder = total - even * (to - from);
|
|
1402
|
+
const nextGridColumns = gridColumns.map((w, i) => {
|
|
1403
|
+
if (i < from || i >= to) return w;
|
|
1404
|
+
const offset = i - from;
|
|
1405
|
+
return offset === (to - from - 1) ? even + remainder : even;
|
|
1406
|
+
});
|
|
1407
|
+
return patchTable(document, root, table, selection, { gridColumns: nextGridColumns }, fallbackSelection);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function patchRow(
|
|
1411
|
+
document: CanonicalDocumentEnvelope,
|
|
1412
|
+
root: DocumentRootNode,
|
|
1413
|
+
table: TableNode,
|
|
1414
|
+
selection: TableSelectionDescriptor,
|
|
1415
|
+
rowIndex: number,
|
|
1416
|
+
patch: Partial<TableRowNode>,
|
|
1417
|
+
fallbackSelection: SelectionSnapshot,
|
|
1418
|
+
): StructuralMutationResult {
|
|
1419
|
+
if (rowIndex < 0 || rowIndex >= table.rows.length) {
|
|
1420
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1421
|
+
}
|
|
1422
|
+
const nextRows = table.rows.map((row, i) => (i === rowIndex ? { ...row, ...patch } : row));
|
|
1423
|
+
return patchTable(document, root, table, selection, { rows: nextRows }, fallbackSelection);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function insertRows(
|
|
1427
|
+
document: CanonicalDocumentEnvelope,
|
|
1428
|
+
root: DocumentRootNode,
|
|
1429
|
+
table: TableNode,
|
|
1430
|
+
selection: TableSelectionDescriptor,
|
|
1431
|
+
rowIndex: number,
|
|
1432
|
+
at: "before" | "after",
|
|
1433
|
+
count: number,
|
|
1434
|
+
fallbackSelection: SelectionSnapshot,
|
|
1435
|
+
): StructuralMutationResult {
|
|
1436
|
+
if (count <= 0) {
|
|
1437
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1438
|
+
}
|
|
1439
|
+
let current = table;
|
|
1440
|
+
const insertAt = at === "before" ? rowIndex : rowIndex + 1;
|
|
1441
|
+
for (let i = 0; i < count; i += 1) {
|
|
1442
|
+
const grid = buildLogicalGrid(current);
|
|
1443
|
+
const inserted = buildInsertedRow(grid, insertAt + i);
|
|
1444
|
+
current = {
|
|
1445
|
+
...current,
|
|
1446
|
+
rows: [
|
|
1447
|
+
...current.rows.slice(0, insertAt + i),
|
|
1448
|
+
inserted,
|
|
1449
|
+
...current.rows.slice(insertAt + i),
|
|
1450
|
+
],
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
return patchTable(document, root, current, selection, {}, fallbackSelection);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function insertColumns(
|
|
1457
|
+
document: CanonicalDocumentEnvelope,
|
|
1458
|
+
root: DocumentRootNode,
|
|
1459
|
+
table: TableNode,
|
|
1460
|
+
selection: TableSelectionDescriptor,
|
|
1461
|
+
columnIndex: number,
|
|
1462
|
+
at: "before" | "after",
|
|
1463
|
+
count: number,
|
|
1464
|
+
widths: readonly number[] | undefined,
|
|
1465
|
+
fallbackSelection: SelectionSnapshot,
|
|
1466
|
+
): StructuralMutationResult {
|
|
1467
|
+
if (count <= 0) {
|
|
1468
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1469
|
+
}
|
|
1470
|
+
let current = table;
|
|
1471
|
+
const baseInsert = at === "before" ? columnIndex : columnIndex + 1;
|
|
1472
|
+
for (let i = 0; i < count; i += 1) {
|
|
1473
|
+
const insertColumnIndex = baseInsert + i;
|
|
1474
|
+
const result = addColumn(
|
|
1475
|
+
{ ...document, content: { ...root, children: [...root.children] } } as CanonicalDocumentEnvelope,
|
|
1476
|
+
root,
|
|
1477
|
+
current,
|
|
1478
|
+
{
|
|
1479
|
+
...selection,
|
|
1480
|
+
anchorCell: {
|
|
1481
|
+
rowIndex: selection.anchorCell.rowIndex,
|
|
1482
|
+
columnIndex: at === "before" ? insertColumnIndex : insertColumnIndex - 1,
|
|
1483
|
+
},
|
|
1484
|
+
} as TableSelectionDescriptor,
|
|
1485
|
+
at,
|
|
1486
|
+
fallbackSelection,
|
|
1487
|
+
);
|
|
1488
|
+
if (!result.changed) break;
|
|
1489
|
+
current = result.document.content.children[selection.tableBlockIndex] as TableNode;
|
|
1490
|
+
}
|
|
1491
|
+
// Apply explicit widths if provided (overwrites widths at the inserted positions).
|
|
1492
|
+
if (widths && widths.length > 0) {
|
|
1493
|
+
const nextGridColumns = [...current.gridColumns];
|
|
1494
|
+
for (let i = 0; i < count && i < widths.length; i += 1) {
|
|
1495
|
+
const idx = baseInsert + i;
|
|
1496
|
+
if (idx >= 0 && idx < nextGridColumns.length) {
|
|
1497
|
+
nextGridColumns[idx] = Math.max(0, Math.round(widths[i] ?? 0));
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
current = { ...current, gridColumns: nextGridColumns };
|
|
1501
|
+
}
|
|
1502
|
+
return patchTable(document, root, current, selection, {}, fallbackSelection);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// ─── P2i helpers: cell-level patches over a locator ──────────────────────────
|
|
1506
|
+
|
|
1507
|
+
function patchCells(
|
|
1508
|
+
document: CanonicalDocumentEnvelope,
|
|
1509
|
+
root: DocumentRootNode,
|
|
1510
|
+
table: TableNode,
|
|
1511
|
+
selection: TableSelectionDescriptor,
|
|
1512
|
+
locator: CellLocator,
|
|
1513
|
+
transform: (cell: TableCellNode) => TableCellNode,
|
|
1514
|
+
fallbackSelection: SelectionSnapshot,
|
|
1515
|
+
): StructuralMutationResult {
|
|
1516
|
+
const grid = buildLogicalGrid(table);
|
|
1517
|
+
const targets = resolveLocatorTargets(grid, selection, locator);
|
|
1518
|
+
if (targets.size === 0) {
|
|
1519
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Group by row so we can do one map per row.
|
|
1523
|
+
const byRow = new Map<number, Set<number>>();
|
|
1524
|
+
for (const { rowIndex, cellIndex } of targets) {
|
|
1525
|
+
let set = byRow.get(rowIndex);
|
|
1526
|
+
if (!set) {
|
|
1527
|
+
set = new Set();
|
|
1528
|
+
byRow.set(rowIndex, set);
|
|
1529
|
+
}
|
|
1530
|
+
set.add(cellIndex);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
1534
|
+
const indices = byRow.get(rowIndex);
|
|
1535
|
+
if (!indices) return row;
|
|
1536
|
+
return {
|
|
1537
|
+
...row,
|
|
1538
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
1539
|
+
indices.has(cellIndex) ? transform(cell) : cell,
|
|
1540
|
+
),
|
|
1541
|
+
};
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
return patchTable(document, root, table, selection, { rows: nextRows }, fallbackSelection);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function resolveLocatorTargets(
|
|
1548
|
+
grid: LogicalGrid,
|
|
1549
|
+
selection: TableSelectionDescriptor,
|
|
1550
|
+
locator: CellLocator,
|
|
1551
|
+
): Set<{ rowIndex: number; cellIndex: number }> {
|
|
1552
|
+
const targets = new Set<{ rowIndex: number; cellIndex: number }>();
|
|
1553
|
+
const seenOrigins = new Set<GridOriginSlot>();
|
|
1554
|
+
|
|
1555
|
+
const addOrigin = (origin: GridOriginSlot): void => {
|
|
1556
|
+
if (seenOrigins.has(origin)) return;
|
|
1557
|
+
seenOrigins.add(origin);
|
|
1558
|
+
targets.add({ rowIndex: origin.rowIndex, cellIndex: origin.cellIndex });
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
if (locator.kind === "anchor") {
|
|
1562
|
+
const origin = originAt(grid, selection.anchorCell.rowIndex, selection.anchorCell.columnIndex);
|
|
1563
|
+
if (origin) addOrigin(origin);
|
|
1564
|
+
return targets;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (locator.kind === "index") {
|
|
1568
|
+
const origin = originAt(grid, locator.rowIndex, locator.columnIndex);
|
|
1569
|
+
if (origin) addOrigin(origin);
|
|
1570
|
+
return targets;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (locator.kind === "row") {
|
|
1574
|
+
const rowIndex = locator.rowIndex;
|
|
1575
|
+
for (let c = 0; c < grid.columnCount; c += 1) {
|
|
1576
|
+
const origin = originAt(grid, rowIndex, c);
|
|
1577
|
+
if (origin && origin.rowIndex === rowIndex) addOrigin(origin);
|
|
1578
|
+
}
|
|
1579
|
+
return targets;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (locator.kind === "column") {
|
|
1583
|
+
const columnIndex = locator.columnIndex;
|
|
1584
|
+
for (let r = 0; r < grid.rowCount; r += 1) {
|
|
1585
|
+
const origin = originAt(grid, r, columnIndex);
|
|
1586
|
+
if (origin && origin.columnIndex === columnIndex) addOrigin(origin);
|
|
1587
|
+
}
|
|
1588
|
+
return targets;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (locator.kind === "rect") {
|
|
1592
|
+
for (let r = locator.rect.top; r < locator.rect.bottom; r += 1) {
|
|
1593
|
+
for (let c = locator.rect.left; c < locator.rect.right; c += 1) {
|
|
1594
|
+
const origin = originAt(grid, r, c);
|
|
1595
|
+
if (origin) addOrigin(origin);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return targets;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// "selection" — use the descriptor's rect or the anchor cell.
|
|
1602
|
+
if (selection.selectionKind === "cell") {
|
|
1603
|
+
for (let r = selection.rect.top; r < selection.rect.bottom; r += 1) {
|
|
1604
|
+
for (let c = selection.rect.left; c < selection.rect.right; c += 1) {
|
|
1605
|
+
const origin = originAt(grid, r, c);
|
|
1606
|
+
if (origin) addOrigin(origin);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
} else {
|
|
1610
|
+
const origin = originAt(grid, selection.anchorCell.rowIndex, selection.anchorCell.columnIndex);
|
|
1611
|
+
if (origin) addOrigin(origin);
|
|
1612
|
+
}
|
|
1613
|
+
return targets;
|
|
1614
|
+
}
|