@beyondwork/docx-react-component 1.0.37 → 1.0.39
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/package.json +41 -31
- package/src/api/public-types.ts +496 -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 +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -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 +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -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 +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- 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/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -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 +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -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.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -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,73 @@ 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
|
+
: mergeRectCoverage &&
|
|
362
|
+
mergeRectCoverage.fullyCoveredOrigins.some(
|
|
363
|
+
(origin) => origin.columnSpan > 1 || origin.rowSpan > 1,
|
|
364
|
+
)
|
|
365
|
+
? disabledCapability(
|
|
366
|
+
"Selection encloses an already-merged cell. Split it first, then merge.",
|
|
367
|
+
)
|
|
368
|
+
: enabledCapability(),
|
|
184
369
|
splitCell:
|
|
185
370
|
splitWidth === 1 && splitHeight === 1
|
|
186
371
|
? disabledCapability("Select a merged or spanning cell to split it.")
|
|
187
372
|
: enabledCapability(),
|
|
188
373
|
deleteTable: enabledCapability(),
|
|
374
|
+
// P2g — table-level
|
|
375
|
+
setTableWidth: enabledCapability(),
|
|
376
|
+
setTableAlignment: enabledCapability(),
|
|
377
|
+
setTableIndent: enabledCapability(),
|
|
378
|
+
setTableLayoutMode: enabledCapability(),
|
|
379
|
+
setTableCellMargins: enabledCapability(),
|
|
380
|
+
setTableBorders: enabledCapability(),
|
|
381
|
+
setTableCaption: enabledCapability(),
|
|
382
|
+
setTableDescription: enabledCapability(),
|
|
383
|
+
// P2h — column/row sizing + row props
|
|
384
|
+
setColumnWidth:
|
|
385
|
+
target.gridColumns.length > 0
|
|
386
|
+
? enabledCapability()
|
|
387
|
+
: disabledCapability("Table has no grid columns to size."),
|
|
388
|
+
distributeColumnsEvenly:
|
|
389
|
+
target.gridColumns.length > 1
|
|
390
|
+
? enabledCapability()
|
|
391
|
+
: disabledCapability("At least two columns are required to distribute width."),
|
|
392
|
+
setRowHeight: enabledCapability(),
|
|
393
|
+
setRowCantSplit: enabledCapability(),
|
|
394
|
+
setRowIsHeader: enabledCapability(),
|
|
395
|
+
setRowAlignment: enabledCapability(),
|
|
396
|
+
insertRows: enabledCapability(),
|
|
397
|
+
insertColumns: enabledCapability(),
|
|
398
|
+
// P2i — cell-level
|
|
399
|
+
setCellBorders: enabledCapability(),
|
|
400
|
+
setCellShading: enabledCapability(),
|
|
401
|
+
clearCellShading: enabledCapability(),
|
|
402
|
+
setCellMargins: enabledCapability(),
|
|
403
|
+
setCellVerticalAlign: enabledCapability(),
|
|
404
|
+
setCellTextDirection: enabledCapability(),
|
|
405
|
+
setCellNoWrap: enabledCapability(),
|
|
406
|
+
setCellFitText: enabledCapability(),
|
|
189
407
|
},
|
|
190
408
|
};
|
|
191
409
|
}
|
|
@@ -198,17 +416,15 @@ function addRow(
|
|
|
198
416
|
position: "before" | "after",
|
|
199
417
|
fallbackSelection: SelectionSnapshot,
|
|
200
418
|
): StructuralMutationResult {
|
|
201
|
-
|
|
419
|
+
const grid = buildLogicalGrid(table);
|
|
420
|
+
const columnCount = grid.columnCount;
|
|
421
|
+
if (columnCount === 0) {
|
|
202
422
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
203
423
|
}
|
|
204
424
|
|
|
205
|
-
const columnCount = getLogicalColumnCount(table);
|
|
206
425
|
const insertIndex =
|
|
207
426
|
position === "before" ? selection.anchorCell.rowIndex : selection.anchorCell.rowIndex + 1;
|
|
208
|
-
const nextRow
|
|
209
|
-
type: "table_row",
|
|
210
|
-
cells: Array.from({ length: columnCount }, () => createEmptyCell()),
|
|
211
|
-
};
|
|
427
|
+
const nextRow = buildInsertedRow(grid, insertIndex);
|
|
212
428
|
const nextTable: TableNode = {
|
|
213
429
|
...table,
|
|
214
430
|
rows: [
|
|
@@ -229,6 +445,58 @@ function addRow(
|
|
|
229
445
|
);
|
|
230
446
|
}
|
|
231
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Build the cells for a new row inserted at `insertIndex` (index in the
|
|
450
|
+
* original table, before the insertion is applied). For each logical column,
|
|
451
|
+
* if both the row above and the row below belong to the same vMerge chain,
|
|
452
|
+
* the new row extends that chain with a `verticalMerge: "continue"` cell.
|
|
453
|
+
* Otherwise an empty cell is emitted with gridSpan=1.
|
|
454
|
+
*/
|
|
455
|
+
function buildInsertedRow(grid: LogicalGrid, insertIndex: number): TableRowNode {
|
|
456
|
+
const cells: TableCellNode[] = [];
|
|
457
|
+
const columnCount = grid.columnCount;
|
|
458
|
+
let column = 0;
|
|
459
|
+
while (column < columnCount) {
|
|
460
|
+
const above = insertIndex > 0 ? originAt(grid, insertIndex - 1, column) : null;
|
|
461
|
+
const below =
|
|
462
|
+
insertIndex < grid.rowCount ? originAt(grid, insertIndex, column) : null;
|
|
463
|
+
|
|
464
|
+
// A vMerge chain continues across the insertion point when:
|
|
465
|
+
// - both above and below resolve to the same origin, and
|
|
466
|
+
// - that origin lives above the insertion point (otherwise the chain
|
|
467
|
+
// starts at `below` and is fully below the insertion point).
|
|
468
|
+
if (
|
|
469
|
+
above &&
|
|
470
|
+
below &&
|
|
471
|
+
above === below &&
|
|
472
|
+
above.rowIndex < insertIndex
|
|
473
|
+
) {
|
|
474
|
+
const span = above.columnSpan;
|
|
475
|
+
// Align to the origin's leftmost column: if we started iteration
|
|
476
|
+
// mid-span, still treat the full span as the unit.
|
|
477
|
+
if (column !== above.columnIndex) {
|
|
478
|
+
// We're scanning forward, so this shouldn't happen — if it does,
|
|
479
|
+
// fall through to an empty cell for this column only.
|
|
480
|
+
cells.push(createEmptyCell());
|
|
481
|
+
column += 1;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
cells.push({
|
|
485
|
+
type: "table_cell",
|
|
486
|
+
children: [createEmptyParagraph()],
|
|
487
|
+
verticalMerge: "continue",
|
|
488
|
+
...(span > 1 ? { gridSpan: span } : {}),
|
|
489
|
+
});
|
|
490
|
+
column += span;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
cells.push(createEmptyCell());
|
|
495
|
+
column += 1;
|
|
496
|
+
}
|
|
497
|
+
return { type: "table_row", cells };
|
|
498
|
+
}
|
|
499
|
+
|
|
232
500
|
function deleteRow(
|
|
233
501
|
document: CanonicalDocumentEnvelope,
|
|
234
502
|
root: DocumentRootNode,
|
|
@@ -236,12 +504,52 @@ function deleteRow(
|
|
|
236
504
|
selection: TableSelectionDescriptor,
|
|
237
505
|
fallbackSelection: SelectionSnapshot,
|
|
238
506
|
): StructuralMutationResult {
|
|
239
|
-
if (
|
|
507
|
+
if (table.rows.length <= 1) {
|
|
240
508
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
241
509
|
}
|
|
242
510
|
|
|
243
511
|
const deleteIndex = selection.anchorCell.rowIndex;
|
|
244
|
-
const
|
|
512
|
+
const grid = buildLogicalGrid(table);
|
|
513
|
+
|
|
514
|
+
// For every logical column, check if this row is the origin of a vMerge
|
|
515
|
+
// chain that extends beyond this row. If so, we must promote the first
|
|
516
|
+
// following continue cell to "restart" and copy over the origin's
|
|
517
|
+
// content/properties so the chain survives the deletion.
|
|
518
|
+
//
|
|
519
|
+
// For columns where this row is a mid-chain continue, the chain's owner
|
|
520
|
+
// origin stays in place — we just need to drop 1 from its effective
|
|
521
|
+
// rowSpan (which happens automatically when the row is removed from
|
|
522
|
+
// the canonical tree).
|
|
523
|
+
const promotions: Array<{
|
|
524
|
+
originColumnIndex: number;
|
|
525
|
+
originColumnSpan: number;
|
|
526
|
+
promotedRowIndex: number;
|
|
527
|
+
originCell: TableCellNode;
|
|
528
|
+
}> = [];
|
|
529
|
+
|
|
530
|
+
for (let column = 0; column < grid.columnCount; ) {
|
|
531
|
+
const origin = originAt(grid, deleteIndex, column);
|
|
532
|
+
if (!origin) {
|
|
533
|
+
column += 1;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (origin.rowIndex === deleteIndex && origin.rowSpan > 1) {
|
|
537
|
+
promotions.push({
|
|
538
|
+
originColumnIndex: origin.columnIndex,
|
|
539
|
+
originColumnSpan: origin.columnSpan,
|
|
540
|
+
promotedRowIndex: deleteIndex + 1,
|
|
541
|
+
originCell: origin.cell,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
column = origin.columnIndex + origin.columnSpan;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Apply promotions to the row immediately after the deleted one.
|
|
548
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
549
|
+
if (rowIndex !== deleteIndex + 1 || promotions.length === 0) return row;
|
|
550
|
+
return applyVMergePromotions(row, promotions);
|
|
551
|
+
}).filter((_, rowIndex) => rowIndex !== deleteIndex);
|
|
552
|
+
|
|
245
553
|
const focusRowIndex = Math.max(0, Math.min(deleteIndex, nextRows.length - 1));
|
|
246
554
|
const nextTable: TableNode = {
|
|
247
555
|
...table,
|
|
@@ -259,6 +567,43 @@ function deleteRow(
|
|
|
259
567
|
);
|
|
260
568
|
}
|
|
261
569
|
|
|
570
|
+
function applyVMergePromotions(
|
|
571
|
+
row: TableRowNode,
|
|
572
|
+
promotions: Array<{
|
|
573
|
+
originColumnIndex: number;
|
|
574
|
+
originColumnSpan: number;
|
|
575
|
+
promotedRowIndex: number;
|
|
576
|
+
originCell: TableCellNode;
|
|
577
|
+
}>,
|
|
578
|
+
): TableRowNode {
|
|
579
|
+
const nextCells = row.cells.map((cell, cellIndex) => {
|
|
580
|
+
let cursor = row.gridBefore ?? 0;
|
|
581
|
+
// Recompute the cell's logical column position.
|
|
582
|
+
for (let i = 0; i < cellIndex; i += 1) {
|
|
583
|
+
cursor += Math.max(1, row.cells[i]!.gridSpan ?? 1);
|
|
584
|
+
}
|
|
585
|
+
const logicalColumn = cursor;
|
|
586
|
+
|
|
587
|
+
const promotion = promotions.find(
|
|
588
|
+
(p) =>
|
|
589
|
+
p.originColumnIndex === logicalColumn &&
|
|
590
|
+
(cell.gridSpan ?? 1) === p.originColumnSpan &&
|
|
591
|
+
cell.verticalMerge === "continue",
|
|
592
|
+
);
|
|
593
|
+
if (!promotion) return cell;
|
|
594
|
+
|
|
595
|
+
// Promote: copy origin's cell content + properties, mark as restart.
|
|
596
|
+
const { children, ...properties } = promotion.originCell;
|
|
597
|
+
return {
|
|
598
|
+
...properties,
|
|
599
|
+
children: structuredClone(children),
|
|
600
|
+
verticalMerge: "restart" as const,
|
|
601
|
+
...(promotion.originColumnSpan > 1 ? { gridSpan: promotion.originColumnSpan } : {}),
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
return { ...row, cells: nextCells };
|
|
605
|
+
}
|
|
606
|
+
|
|
262
607
|
function addColumn(
|
|
263
608
|
document: CanonicalDocumentEnvelope,
|
|
264
609
|
root: DocumentRootNode,
|
|
@@ -267,25 +612,90 @@ function addColumn(
|
|
|
267
612
|
position: "before" | "after",
|
|
268
613
|
fallbackSelection: SelectionSnapshot,
|
|
269
614
|
): StructuralMutationResult {
|
|
270
|
-
|
|
615
|
+
const grid = buildLogicalGrid(table);
|
|
616
|
+
const columnCount = grid.columnCount;
|
|
617
|
+
if (columnCount === 0) {
|
|
271
618
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
272
619
|
}
|
|
273
620
|
|
|
274
|
-
const
|
|
621
|
+
const insertColumn =
|
|
275
622
|
position === "before"
|
|
276
623
|
? selection.anchorCell.columnIndex
|
|
277
624
|
: selection.anchorCell.columnIndex + 1;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
625
|
+
|
|
626
|
+
// Rewrite each row. For rows where the insert column falls strictly
|
|
627
|
+
// inside an existing horizontal span (origin.columnIndex < insertColumn
|
|
628
|
+
// < origin.columnIndex + origin.columnSpan), extend that span by 1.
|
|
629
|
+
// Otherwise insert a plain empty cell at the appropriate cellIndex.
|
|
630
|
+
const insertedOriginsExtended = new Set<GridOriginSlot>();
|
|
631
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
632
|
+
// Find the origin that covers the insertion column in this row, if any.
|
|
633
|
+
const leftOrigin =
|
|
634
|
+
insertColumn > 0 ? originAt(grid, rowIndex, insertColumn - 1) : null;
|
|
635
|
+
const rightSlot = slotAt(grid, rowIndex, insertColumn);
|
|
636
|
+
const rightOrigin = rightSlot
|
|
637
|
+
? rightSlot.kind === "origin"
|
|
638
|
+
? rightSlot
|
|
639
|
+
: rightSlot.origin
|
|
640
|
+
: null;
|
|
641
|
+
|
|
642
|
+
const insideHSpan =
|
|
643
|
+
leftOrigin &&
|
|
644
|
+
rightOrigin &&
|
|
645
|
+
leftOrigin === rightOrigin &&
|
|
646
|
+
leftOrigin.columnSpan > 1;
|
|
647
|
+
|
|
648
|
+
if (insideHSpan && leftOrigin && !insertedOriginsExtended.has(leftOrigin)) {
|
|
649
|
+
// Extend the owner cell's gridSpan by 1 (only once per origin across
|
|
650
|
+
// rows — each origin lives in exactly one row).
|
|
651
|
+
const ownerCellIndex = leftOrigin.cellIndex;
|
|
652
|
+
insertedOriginsExtended.add(leftOrigin);
|
|
653
|
+
return {
|
|
654
|
+
...row,
|
|
655
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
656
|
+
cellIndex === ownerCellIndex
|
|
657
|
+
? { ...cell, gridSpan: (cell.gridSpan ?? 1) + 1 }
|
|
658
|
+
: cell,
|
|
659
|
+
),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (insideHSpan) {
|
|
664
|
+
// The owner has already been extended in its origin row; this row
|
|
665
|
+
// is a vMerge continuation covered by the same column range. Extend
|
|
666
|
+
// the continue cell's gridSpan so the logical grid stays consistent.
|
|
667
|
+
const continueRef = findCellIndexAtColumn(row, leftOrigin!.columnIndex);
|
|
668
|
+
if (continueRef) {
|
|
669
|
+
return {
|
|
670
|
+
...row,
|
|
671
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
672
|
+
cellIndex === continueRef.cellIndex
|
|
673
|
+
? { ...cell, gridSpan: (cell.gridSpan ?? 1) + 1 }
|
|
674
|
+
: cell,
|
|
675
|
+
),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return row;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Not inside an hspan — insert a plain empty cell at the cellIndex
|
|
682
|
+
// corresponding to the insertion column. Account for gridBefore
|
|
683
|
+
// padding and any earlier cell spans.
|
|
684
|
+
const insertCellIndex = resolveInsertCellIndex(row, insertColumn);
|
|
685
|
+
return {
|
|
282
686
|
...row,
|
|
283
687
|
cells: [
|
|
284
|
-
...row.cells.slice(0,
|
|
688
|
+
...row.cells.slice(0, insertCellIndex),
|
|
285
689
|
createEmptyCell(),
|
|
286
|
-
...row.cells.slice(
|
|
690
|
+
...row.cells.slice(insertCellIndex),
|
|
287
691
|
],
|
|
288
|
-
}
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const nextTable: TableNode = {
|
|
696
|
+
...table,
|
|
697
|
+
gridColumns: insertGridColumn(table.gridColumns, insertColumn),
|
|
698
|
+
rows: nextRows,
|
|
289
699
|
};
|
|
290
700
|
|
|
291
701
|
return commitTableChange(
|
|
@@ -295,10 +705,24 @@ function addColumn(
|
|
|
295
705
|
nextTable,
|
|
296
706
|
fallbackSelection,
|
|
297
707
|
selection.anchorCell.rowIndex,
|
|
298
|
-
|
|
708
|
+
insertColumn,
|
|
299
709
|
);
|
|
300
710
|
}
|
|
301
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Find the index in row.cells where a new cell should be inserted so it
|
|
714
|
+
* lands at logical column `logicalColumn`. Accounts for gridBefore padding
|
|
715
|
+
* and any earlier cell spans.
|
|
716
|
+
*/
|
|
717
|
+
function resolveInsertCellIndex(row: TableRowNode, logicalColumn: number): number {
|
|
718
|
+
let cursor = row.gridBefore ?? 0;
|
|
719
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
720
|
+
if (logicalColumn <= cursor) return cellIndex;
|
|
721
|
+
cursor += Math.max(1, row.cells[cellIndex]!.gridSpan ?? 1);
|
|
722
|
+
}
|
|
723
|
+
return row.cells.length;
|
|
724
|
+
}
|
|
725
|
+
|
|
302
726
|
function deleteColumn(
|
|
303
727
|
document: CanonicalDocumentEnvelope,
|
|
304
728
|
root: DocumentRootNode,
|
|
@@ -306,20 +730,67 @@ function deleteColumn(
|
|
|
306
730
|
selection: TableSelectionDescriptor,
|
|
307
731
|
fallbackSelection: SelectionSnapshot,
|
|
308
732
|
): StructuralMutationResult {
|
|
309
|
-
if (
|
|
733
|
+
if (getLogicalColumnCount(table) <= 1) {
|
|
310
734
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
311
735
|
}
|
|
312
736
|
|
|
313
|
-
const
|
|
737
|
+
const deleteColumn = selection.anchorCell.columnIndex;
|
|
738
|
+
const grid = buildLogicalGrid(table);
|
|
739
|
+
|
|
740
|
+
// For each row, determine whether the deleted column hits a cell that
|
|
741
|
+
// is a pure single-column cell (drop it) or is inside a gridSpan > 1
|
|
742
|
+
// cell (decrement the span). Rows where the column lies outside the
|
|
743
|
+
// cell range (gridBefore/gridAfter padding) simply pass through.
|
|
744
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
745
|
+
const origin = originAt(grid, rowIndex, deleteColumn);
|
|
746
|
+
if (!origin) return row;
|
|
747
|
+
|
|
748
|
+
const cellRef = findCellIndexAtColumn(row, origin.columnIndex);
|
|
749
|
+
if (!cellRef) return row;
|
|
750
|
+
|
|
751
|
+
if (origin.columnSpan <= 1) {
|
|
752
|
+
// Pure single-column cell — drop it from the row.
|
|
753
|
+
return {
|
|
754
|
+
...row,
|
|
755
|
+
cells: row.cells.filter((_, cellIndex) => cellIndex !== cellRef.cellIndex),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Inside an hspan — decrement gridSpan. If the new span would be 1,
|
|
760
|
+
// drop the gridSpan attribute entirely so the canonical model stays
|
|
761
|
+
// tidy.
|
|
762
|
+
const nextSpan = origin.columnSpan - 1;
|
|
763
|
+
const nextCell: TableCellNode = {
|
|
764
|
+
...cellRef.cell,
|
|
765
|
+
...(nextSpan > 1 ? { gridSpan: nextSpan } : { gridSpan: undefined }),
|
|
766
|
+
};
|
|
767
|
+
// Remove the explicit gridSpan field when nextSpan === 1.
|
|
768
|
+
if (nextSpan === 1) {
|
|
769
|
+
const { gridSpan: _drop, ...rest } = nextCell;
|
|
770
|
+
return {
|
|
771
|
+
...row,
|
|
772
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
773
|
+
cellIndex === cellRef.cellIndex ? (rest as TableCellNode) : cell,
|
|
774
|
+
),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
...row,
|
|
779
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
780
|
+
cellIndex === cellRef.cellIndex ? nextCell : cell,
|
|
781
|
+
),
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
|
|
314
785
|
const nextTable: TableNode = {
|
|
315
786
|
...table,
|
|
316
|
-
gridColumns: deleteGridColumn(table.gridColumns,
|
|
317
|
-
rows:
|
|
318
|
-
...row,
|
|
319
|
-
cells: row.cells.filter((_, cellIndex) => cellIndex !== deleteIndex),
|
|
320
|
-
})),
|
|
787
|
+
gridColumns: deleteGridColumn(table.gridColumns, deleteColumn),
|
|
788
|
+
rows: nextRows,
|
|
321
789
|
};
|
|
322
|
-
const focusColumnIndex = Math.max(
|
|
790
|
+
const focusColumnIndex = Math.max(
|
|
791
|
+
0,
|
|
792
|
+
Math.min(deleteColumn, getLogicalColumnCount(nextTable) - 1),
|
|
793
|
+
);
|
|
323
794
|
|
|
324
795
|
return commitTableChange(
|
|
325
796
|
document,
|
|
@@ -339,14 +810,36 @@ function mergeSelectedCells(
|
|
|
339
810
|
selection: TableSelectionDescriptor,
|
|
340
811
|
fallbackSelection: SelectionSnapshot,
|
|
341
812
|
): StructuralMutationResult {
|
|
813
|
+
// R1a: the merge command was previously gated on `!isSimpleTable(table)`,
|
|
814
|
+
// which blocked merging on any table that already carried a single merged
|
|
815
|
+
// span anywhere in the table, even when the user's current selection was
|
|
816
|
+
// over a simple region. The correct gate is whether the rect itself is:
|
|
817
|
+
// (a) clean — every origin it touches is fully enclosed (analyzeRect);
|
|
818
|
+
// (b) internally simple — no pre-existing merged origin lives inside
|
|
819
|
+
// the rect. findCellAtColumn in the splice body below assumes cells
|
|
820
|
+
// at logical column C correspond 1:1 with row.cells entries; that
|
|
821
|
+
// assumption holds iff the rect's internal cells are all gridSpan=1
|
|
822
|
+
// and verticalMerge=undefined.
|
|
823
|
+
// A rect that satisfies both is safe to merge even on a table that has
|
|
824
|
+
// merged spans elsewhere (the common imported-agreement case).
|
|
342
825
|
if (
|
|
343
|
-
!isSimpleTable(table) ||
|
|
344
826
|
selection.selectionKind !== "cell" ||
|
|
345
827
|
selection.rect.bottom - selection.rect.top < 1 ||
|
|
346
828
|
selection.rect.right - selection.rect.left < 1
|
|
347
829
|
) {
|
|
348
830
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
349
831
|
}
|
|
832
|
+
const grid = buildLogicalGrid(table);
|
|
833
|
+
const coverage = analyzeRect(grid, selection.rect);
|
|
834
|
+
if (!coverage.clean) {
|
|
835
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
836
|
+
}
|
|
837
|
+
const rectInternallySimple = coverage.fullyCoveredOrigins.every(
|
|
838
|
+
(origin) => origin.columnSpan === 1 && origin.rowSpan === 1,
|
|
839
|
+
);
|
|
840
|
+
if (!rectInternallySimple) {
|
|
841
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
842
|
+
}
|
|
350
843
|
|
|
351
844
|
const height = selection.rect.bottom - selection.rect.top;
|
|
352
845
|
const width = selection.rect.right - selection.rect.left;
|
|
@@ -852,3 +1345,299 @@ function normalizeFillColor(color: string): string | null {
|
|
|
852
1345
|
const normalized = color.trim().replace(/^#/, "");
|
|
853
1346
|
return /^[0-9A-Fa-f]{3,8}$/.test(normalized) ? normalized.toUpperCase() : null;
|
|
854
1347
|
}
|
|
1348
|
+
|
|
1349
|
+
// ─── P2g helpers: table-level patches ────────────────────────────────────────
|
|
1350
|
+
|
|
1351
|
+
function patchTable(
|
|
1352
|
+
document: CanonicalDocumentEnvelope,
|
|
1353
|
+
root: DocumentRootNode,
|
|
1354
|
+
table: TableNode,
|
|
1355
|
+
selection: TableSelectionDescriptor,
|
|
1356
|
+
patch: Partial<TableNode>,
|
|
1357
|
+
fallbackSelection: SelectionSnapshot,
|
|
1358
|
+
): StructuralMutationResult {
|
|
1359
|
+
const nextTable: TableNode = { ...table, ...patch };
|
|
1360
|
+
return commitTableChange(
|
|
1361
|
+
document,
|
|
1362
|
+
root,
|
|
1363
|
+
selection.tableBlockIndex,
|
|
1364
|
+
nextTable,
|
|
1365
|
+
fallbackSelection,
|
|
1366
|
+
selection.anchorCell.rowIndex,
|
|
1367
|
+
selection.anchorCell.columnIndex,
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function mergeTableBorders(
|
|
1372
|
+
base: TableBorders | undefined,
|
|
1373
|
+
override: Partial<TableBorders>,
|
|
1374
|
+
): TableBorders {
|
|
1375
|
+
return { ...(base ?? {}), ...override };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function mergeCellBorders(
|
|
1379
|
+
base: TableCellBorders | undefined,
|
|
1380
|
+
override: Partial<TableCellBorders>,
|
|
1381
|
+
): TableCellBorders {
|
|
1382
|
+
return { ...(base ?? {}), ...override };
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function mergeTableCellMargins(
|
|
1386
|
+
base: TableCellMargins | undefined,
|
|
1387
|
+
override: Partial<TableCellMargins>,
|
|
1388
|
+
): TableCellMargins {
|
|
1389
|
+
return { ...(base ?? {}), ...override };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// ─── P2h helpers: column/row sizing + row-level patches ──────────────────────
|
|
1393
|
+
|
|
1394
|
+
function setColumnWidth(
|
|
1395
|
+
document: CanonicalDocumentEnvelope,
|
|
1396
|
+
root: DocumentRootNode,
|
|
1397
|
+
table: TableNode,
|
|
1398
|
+
selection: TableSelectionDescriptor,
|
|
1399
|
+
columnIndex: number,
|
|
1400
|
+
twips: number,
|
|
1401
|
+
fallbackSelection: SelectionSnapshot,
|
|
1402
|
+
): StructuralMutationResult {
|
|
1403
|
+
if (columnIndex < 0 || columnIndex >= table.gridColumns.length) {
|
|
1404
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1405
|
+
}
|
|
1406
|
+
const clamped = Math.max(0, Math.round(twips));
|
|
1407
|
+
const nextGridColumns = table.gridColumns.map((w, i) => (i === columnIndex ? clamped : w));
|
|
1408
|
+
return patchTable(document, root, table, selection, { gridColumns: nextGridColumns }, fallbackSelection);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function distributeColumnsEvenly(
|
|
1412
|
+
document: CanonicalDocumentEnvelope,
|
|
1413
|
+
root: DocumentRootNode,
|
|
1414
|
+
table: TableNode,
|
|
1415
|
+
selection: TableSelectionDescriptor,
|
|
1416
|
+
columnRange: { from: number; to: number } | undefined,
|
|
1417
|
+
fallbackSelection: SelectionSnapshot,
|
|
1418
|
+
): StructuralMutationResult {
|
|
1419
|
+
const gridColumns = table.gridColumns;
|
|
1420
|
+
if (gridColumns.length === 0) {
|
|
1421
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1422
|
+
}
|
|
1423
|
+
const from = columnRange ? Math.max(0, columnRange.from) : 0;
|
|
1424
|
+
const to = columnRange ? Math.min(gridColumns.length, columnRange.to) : gridColumns.length;
|
|
1425
|
+
if (to - from <= 1) {
|
|
1426
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1427
|
+
}
|
|
1428
|
+
const total = gridColumns.slice(from, to).reduce((sum, w) => sum + w, 0);
|
|
1429
|
+
const even = Math.floor(total / (to - from));
|
|
1430
|
+
const remainder = total - even * (to - from);
|
|
1431
|
+
const nextGridColumns = gridColumns.map((w, i) => {
|
|
1432
|
+
if (i < from || i >= to) return w;
|
|
1433
|
+
const offset = i - from;
|
|
1434
|
+
return offset === (to - from - 1) ? even + remainder : even;
|
|
1435
|
+
});
|
|
1436
|
+
return patchTable(document, root, table, selection, { gridColumns: nextGridColumns }, fallbackSelection);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function patchRow(
|
|
1440
|
+
document: CanonicalDocumentEnvelope,
|
|
1441
|
+
root: DocumentRootNode,
|
|
1442
|
+
table: TableNode,
|
|
1443
|
+
selection: TableSelectionDescriptor,
|
|
1444
|
+
rowIndex: number,
|
|
1445
|
+
patch: Partial<TableRowNode>,
|
|
1446
|
+
fallbackSelection: SelectionSnapshot,
|
|
1447
|
+
): StructuralMutationResult {
|
|
1448
|
+
if (rowIndex < 0 || rowIndex >= table.rows.length) {
|
|
1449
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1450
|
+
}
|
|
1451
|
+
const nextRows = table.rows.map((row, i) => (i === rowIndex ? { ...row, ...patch } : row));
|
|
1452
|
+
return patchTable(document, root, table, selection, { rows: nextRows }, fallbackSelection);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function insertRows(
|
|
1456
|
+
document: CanonicalDocumentEnvelope,
|
|
1457
|
+
root: DocumentRootNode,
|
|
1458
|
+
table: TableNode,
|
|
1459
|
+
selection: TableSelectionDescriptor,
|
|
1460
|
+
rowIndex: number,
|
|
1461
|
+
at: "before" | "after",
|
|
1462
|
+
count: number,
|
|
1463
|
+
fallbackSelection: SelectionSnapshot,
|
|
1464
|
+
): StructuralMutationResult {
|
|
1465
|
+
if (count <= 0) {
|
|
1466
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1467
|
+
}
|
|
1468
|
+
let current = table;
|
|
1469
|
+
const insertAt = at === "before" ? rowIndex : rowIndex + 1;
|
|
1470
|
+
for (let i = 0; i < count; i += 1) {
|
|
1471
|
+
const grid = buildLogicalGrid(current);
|
|
1472
|
+
const inserted = buildInsertedRow(grid, insertAt + i);
|
|
1473
|
+
current = {
|
|
1474
|
+
...current,
|
|
1475
|
+
rows: [
|
|
1476
|
+
...current.rows.slice(0, insertAt + i),
|
|
1477
|
+
inserted,
|
|
1478
|
+
...current.rows.slice(insertAt + i),
|
|
1479
|
+
],
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
return patchTable(document, root, current, selection, {}, fallbackSelection);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function insertColumns(
|
|
1486
|
+
document: CanonicalDocumentEnvelope,
|
|
1487
|
+
root: DocumentRootNode,
|
|
1488
|
+
table: TableNode,
|
|
1489
|
+
selection: TableSelectionDescriptor,
|
|
1490
|
+
columnIndex: number,
|
|
1491
|
+
at: "before" | "after",
|
|
1492
|
+
count: number,
|
|
1493
|
+
widths: readonly number[] | undefined,
|
|
1494
|
+
fallbackSelection: SelectionSnapshot,
|
|
1495
|
+
): StructuralMutationResult {
|
|
1496
|
+
if (count <= 0) {
|
|
1497
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1498
|
+
}
|
|
1499
|
+
let current = table;
|
|
1500
|
+
const baseInsert = at === "before" ? columnIndex : columnIndex + 1;
|
|
1501
|
+
for (let i = 0; i < count; i += 1) {
|
|
1502
|
+
const insertColumnIndex = baseInsert + i;
|
|
1503
|
+
const result = addColumn(
|
|
1504
|
+
{ ...document, content: { ...root, children: [...root.children] } } as CanonicalDocumentEnvelope,
|
|
1505
|
+
root,
|
|
1506
|
+
current,
|
|
1507
|
+
{
|
|
1508
|
+
...selection,
|
|
1509
|
+
anchorCell: {
|
|
1510
|
+
rowIndex: selection.anchorCell.rowIndex,
|
|
1511
|
+
columnIndex: at === "before" ? insertColumnIndex : insertColumnIndex - 1,
|
|
1512
|
+
},
|
|
1513
|
+
} as TableSelectionDescriptor,
|
|
1514
|
+
at,
|
|
1515
|
+
fallbackSelection,
|
|
1516
|
+
);
|
|
1517
|
+
if (!result.changed) break;
|
|
1518
|
+
current = result.document.content.children[selection.tableBlockIndex] as TableNode;
|
|
1519
|
+
}
|
|
1520
|
+
// Apply explicit widths if provided (overwrites widths at the inserted positions).
|
|
1521
|
+
if (widths && widths.length > 0) {
|
|
1522
|
+
const nextGridColumns = [...current.gridColumns];
|
|
1523
|
+
for (let i = 0; i < count && i < widths.length; i += 1) {
|
|
1524
|
+
const idx = baseInsert + i;
|
|
1525
|
+
if (idx >= 0 && idx < nextGridColumns.length) {
|
|
1526
|
+
nextGridColumns[idx] = Math.max(0, Math.round(widths[i] ?? 0));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
current = { ...current, gridColumns: nextGridColumns };
|
|
1530
|
+
}
|
|
1531
|
+
return patchTable(document, root, current, selection, {}, fallbackSelection);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// ─── P2i helpers: cell-level patches over a locator ──────────────────────────
|
|
1535
|
+
|
|
1536
|
+
function patchCells(
|
|
1537
|
+
document: CanonicalDocumentEnvelope,
|
|
1538
|
+
root: DocumentRootNode,
|
|
1539
|
+
table: TableNode,
|
|
1540
|
+
selection: TableSelectionDescriptor,
|
|
1541
|
+
locator: CellLocator,
|
|
1542
|
+
transform: (cell: TableCellNode) => TableCellNode,
|
|
1543
|
+
fallbackSelection: SelectionSnapshot,
|
|
1544
|
+
): StructuralMutationResult {
|
|
1545
|
+
const grid = buildLogicalGrid(table);
|
|
1546
|
+
const targets = resolveLocatorTargets(grid, selection, locator);
|
|
1547
|
+
if (targets.size === 0) {
|
|
1548
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Group by row so we can do one map per row.
|
|
1552
|
+
const byRow = new Map<number, Set<number>>();
|
|
1553
|
+
for (const { rowIndex, cellIndex } of targets) {
|
|
1554
|
+
let set = byRow.get(rowIndex);
|
|
1555
|
+
if (!set) {
|
|
1556
|
+
set = new Set();
|
|
1557
|
+
byRow.set(rowIndex, set);
|
|
1558
|
+
}
|
|
1559
|
+
set.add(cellIndex);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
1563
|
+
const indices = byRow.get(rowIndex);
|
|
1564
|
+
if (!indices) return row;
|
|
1565
|
+
return {
|
|
1566
|
+
...row,
|
|
1567
|
+
cells: row.cells.map((cell, cellIndex) =>
|
|
1568
|
+
indices.has(cellIndex) ? transform(cell) : cell,
|
|
1569
|
+
),
|
|
1570
|
+
};
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
return patchTable(document, root, table, selection, { rows: nextRows }, fallbackSelection);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function resolveLocatorTargets(
|
|
1577
|
+
grid: LogicalGrid,
|
|
1578
|
+
selection: TableSelectionDescriptor,
|
|
1579
|
+
locator: CellLocator,
|
|
1580
|
+
): Set<{ rowIndex: number; cellIndex: number }> {
|
|
1581
|
+
const targets = new Set<{ rowIndex: number; cellIndex: number }>();
|
|
1582
|
+
const seenOrigins = new Set<GridOriginSlot>();
|
|
1583
|
+
|
|
1584
|
+
const addOrigin = (origin: GridOriginSlot): void => {
|
|
1585
|
+
if (seenOrigins.has(origin)) return;
|
|
1586
|
+
seenOrigins.add(origin);
|
|
1587
|
+
targets.add({ rowIndex: origin.rowIndex, cellIndex: origin.cellIndex });
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
if (locator.kind === "anchor") {
|
|
1591
|
+
const origin = originAt(grid, selection.anchorCell.rowIndex, selection.anchorCell.columnIndex);
|
|
1592
|
+
if (origin) addOrigin(origin);
|
|
1593
|
+
return targets;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (locator.kind === "index") {
|
|
1597
|
+
const origin = originAt(grid, locator.rowIndex, locator.columnIndex);
|
|
1598
|
+
if (origin) addOrigin(origin);
|
|
1599
|
+
return targets;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (locator.kind === "row") {
|
|
1603
|
+
const rowIndex = locator.rowIndex;
|
|
1604
|
+
for (let c = 0; c < grid.columnCount; c += 1) {
|
|
1605
|
+
const origin = originAt(grid, rowIndex, c);
|
|
1606
|
+
if (origin && origin.rowIndex === rowIndex) addOrigin(origin);
|
|
1607
|
+
}
|
|
1608
|
+
return targets;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (locator.kind === "column") {
|
|
1612
|
+
const columnIndex = locator.columnIndex;
|
|
1613
|
+
for (let r = 0; r < grid.rowCount; r += 1) {
|
|
1614
|
+
const origin = originAt(grid, r, columnIndex);
|
|
1615
|
+
if (origin && origin.columnIndex === columnIndex) addOrigin(origin);
|
|
1616
|
+
}
|
|
1617
|
+
return targets;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if (locator.kind === "rect") {
|
|
1621
|
+
for (let r = locator.rect.top; r < locator.rect.bottom; r += 1) {
|
|
1622
|
+
for (let c = locator.rect.left; c < locator.rect.right; c += 1) {
|
|
1623
|
+
const origin = originAt(grid, r, c);
|
|
1624
|
+
if (origin) addOrigin(origin);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return targets;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// "selection" — use the descriptor's rect or the anchor cell.
|
|
1631
|
+
if (selection.selectionKind === "cell") {
|
|
1632
|
+
for (let r = selection.rect.top; r < selection.rect.bottom; r += 1) {
|
|
1633
|
+
for (let c = selection.rect.left; c < selection.rect.right; c += 1) {
|
|
1634
|
+
const origin = originAt(grid, r, c);
|
|
1635
|
+
if (origin) addOrigin(origin);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
const origin = originAt(grid, selection.anchorCell.rowIndex, selection.anchorCell.columnIndex);
|
|
1640
|
+
if (origin) addOrigin(origin);
|
|
1641
|
+
}
|
|
1642
|
+
return targets;
|
|
1643
|
+
}
|