@beyondwork/docx-react-component 1.0.28 → 1.0.30
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 +26 -37
- package/src/api/public-types.ts +531 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/index.ts +201 -79
- package/src/core/commands/table-structure-commands.ts +138 -5
- package/src/core/state/text-transaction.ts +370 -3
- package/src/index.ts +41 -0
- package/src/io/docx-session.ts +318 -25
- package/src/io/export/serialize-footnotes.ts +41 -46
- package/src/io/export/serialize-headers-footers.ts +36 -40
- package/src/io/export/serialize-main-document.ts +55 -89
- package/src/io/export/serialize-numbering.ts +104 -4
- package/src/io/export/serialize-runtime-revisions.ts +196 -2
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
- package/src/io/export/table-properties-xml.ts +318 -0
- package/src/io/normalize/normalize-text.ts +34 -3
- package/src/io/ooxml/parse-comments.ts +6 -0
- package/src/io/ooxml/parse-footnotes.ts +69 -13
- package/src/io/ooxml/parse-headers-footers.ts +54 -11
- package/src/io/ooxml/parse-main-document.ts +112 -42
- package/src/io/ooxml/parse-numbering.ts +341 -26
- package/src/io/ooxml/parse-revisions.ts +118 -4
- package/src/io/ooxml/parse-styles.ts +176 -0
- package/src/io/ooxml/parse-tables.ts +34 -25
- package/src/io/ooxml/revision-boundaries.ts +127 -3
- package/src/io/ooxml/workflow-payload.ts +544 -0
- package/src/model/canonical-document.ts +91 -1
- package/src/model/snapshot.ts +112 -1
- package/src/preservation/store.ts +73 -3
- package/src/review/store/comment-store.ts +19 -1
- package/src/review/store/revision-actions.ts +29 -0
- package/src/review/store/revision-store.ts +12 -1
- package/src/review/store/revision-types.ts +11 -0
- package/src/runtime/context-analytics.ts +824 -0
- package/src/runtime/document-locations.ts +521 -0
- package/src/runtime/document-navigation.ts +14 -1
- package/src/runtime/document-outline.ts +440 -0
- package/src/runtime/document-runtime.ts +941 -45
- package/src/runtime/event-refresh-hints.ts +137 -0
- package/src/runtime/numbering-prefix.ts +67 -39
- package/src/runtime/page-layout-estimation.ts +100 -7
- package/src/runtime/resolved-numbering-geometry.ts +293 -0
- package/src/runtime/session-capabilities.ts +2 -2
- package/src/runtime/suggestions-snapshot.ts +137 -0
- package/src/runtime/surface-projection.ts +223 -27
- package/src/runtime/table-style-resolver.ts +409 -0
- package/src/runtime/view-state.ts +17 -1
- package/src/runtime/workflow-markup.ts +54 -14
- package/src/ui/WordReviewEditor.tsx +1269 -87
- package/src/ui/editor-command-bag.ts +7 -0
- package/src/ui/editor-runtime-boundary.ts +111 -10
- package/src/ui/editor-shell-view.tsx +17 -15
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-tool-context.ts +19 -0
- package/src/ui/headless/selection-tool-resolver.ts +752 -0
- package/src/ui/headless/selection-tool-types.ts +129 -0
- package/src/ui/headless/selection-toolbar-model.ts +10 -33
- package/src/ui/runtime-shortcut-dispatch.ts +365 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
- package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
- package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
- package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
- package/src/ui-tailwind/theme/editor-theme.css +58 -40
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
- package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
- package/src/validation/compatibility-engine.ts +246 -2
- package/src/validation/docx-comment-proof.ts +24 -11
package/src/api/session-state.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function editorSessionStateFromPersistedSnapshot(
|
|
|
33
33
|
warningLog: snapshot.warningLog,
|
|
34
34
|
protectionSnapshot: snapshot.protectionSnapshot,
|
|
35
35
|
sourcePackage: snapshot.sourcePackage,
|
|
36
|
+
workflowMetadata: snapshot.workflowMetadata,
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -56,5 +57,6 @@ export function persistedSnapshotFromEditorSessionState(
|
|
|
56
57
|
warningLog: sessionState.warningLog,
|
|
57
58
|
protectionSnapshot: sessionState.protectionSnapshot,
|
|
58
59
|
sourcePackage: sessionState.sourcePackage,
|
|
60
|
+
workflowMetadata: sessionState.workflowMetadata,
|
|
59
61
|
});
|
|
60
62
|
}
|
|
@@ -305,16 +305,8 @@ export function executeEditorCommand(
|
|
|
305
305
|
}
|
|
306
306
|
case "paragraph.split":
|
|
307
307
|
if (context.documentMode === "suggesting") {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
markDirty: false,
|
|
311
|
-
effects: {
|
|
312
|
-
commandBlocked: {
|
|
313
|
-
code: "suggesting_unsupported",
|
|
314
|
-
message: "Paragraph splits are not supported in suggesting mode.",
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
});
|
|
308
|
+
const suggestingResult = applySuggestingParagraphSplit(state, context);
|
|
309
|
+
if (suggestingResult) return suggestingResult;
|
|
318
310
|
}
|
|
319
311
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
320
312
|
splitParagraph(document, selection, context),
|
|
@@ -722,10 +714,31 @@ function normalizeSelection(selection: SelectionSnapshot): SelectionSnapshot {
|
|
|
722
714
|
);
|
|
723
715
|
|
|
724
716
|
if (activeRange.kind === "range") {
|
|
717
|
+
const rangeFrom = activeRange.range.from;
|
|
718
|
+
const rangeTo = activeRange.range.to;
|
|
719
|
+
const collapsed = rangeFrom === rangeTo;
|
|
720
|
+
const anchorWithinRange = selection.anchor >= rangeFrom && selection.anchor <= rangeTo;
|
|
721
|
+
const headWithinRange = selection.head >= rangeFrom && selection.head <= rangeTo;
|
|
722
|
+
const preserveDirectionalEndpoints = anchorWithinRange && headWithinRange;
|
|
723
|
+
const fallbackForward = selection.anchor <= selection.head;
|
|
724
|
+
const anchor = collapsed
|
|
725
|
+
? rangeFrom
|
|
726
|
+
: preserveDirectionalEndpoints
|
|
727
|
+
? selection.anchor
|
|
728
|
+
: fallbackForward
|
|
729
|
+
? rangeFrom
|
|
730
|
+
: rangeTo;
|
|
731
|
+
const head = collapsed
|
|
732
|
+
? rangeTo
|
|
733
|
+
: preserveDirectionalEndpoints
|
|
734
|
+
? selection.head
|
|
735
|
+
: fallbackForward
|
|
736
|
+
? rangeTo
|
|
737
|
+
: rangeFrom;
|
|
725
738
|
return {
|
|
726
|
-
anchor
|
|
727
|
-
head
|
|
728
|
-
isCollapsed:
|
|
739
|
+
anchor,
|
|
740
|
+
head,
|
|
741
|
+
isCollapsed: collapsed,
|
|
729
742
|
activeRange,
|
|
730
743
|
};
|
|
731
744
|
}
|
|
@@ -1056,6 +1069,10 @@ function createRevisionStoreFromState(
|
|
|
1056
1069
|
source: revision.metadata?.source ?? "runtime",
|
|
1057
1070
|
storyTarget: revision.metadata?.storyTarget,
|
|
1058
1071
|
preserveOnlyReason: revision.metadata?.preserveOnlyReason,
|
|
1072
|
+
suggestionId: revision.metadata?.suggestionId,
|
|
1073
|
+
semanticKind: revision.metadata?.semanticKind,
|
|
1074
|
+
linkedRevisionIds: revision.metadata?.linkedRevisionIds,
|
|
1075
|
+
predecessorSuggestionId: revision.metadata?.predecessorSuggestionId,
|
|
1059
1076
|
importedRevisionForm: revision.metadata?.importedRevisionForm,
|
|
1060
1077
|
originalRevisionType: revision.metadata?.originalRevisionType,
|
|
1061
1078
|
ooxmlRevisionId: revision.metadata?.ooxmlRevisionId,
|
|
@@ -1083,6 +1100,10 @@ function toEditorRevisionRecords(
|
|
|
1083
1100
|
source: revision.metadata.source,
|
|
1084
1101
|
storyTarget: revision.metadata.storyTarget,
|
|
1085
1102
|
preserveOnlyReason: revision.metadata.preserveOnlyReason,
|
|
1103
|
+
suggestionId: revision.metadata.suggestionId,
|
|
1104
|
+
semanticKind: revision.metadata.semanticKind,
|
|
1105
|
+
linkedRevisionIds: revision.metadata.linkedRevisionIds,
|
|
1106
|
+
predecessorSuggestionId: revision.metadata.predecessorSuggestionId,
|
|
1086
1107
|
importedRevisionForm: revision.metadata.importedRevisionForm,
|
|
1087
1108
|
originalRevisionType: revision.metadata.originalRevisionType,
|
|
1088
1109
|
ooxmlRevisionId: revision.metadata.ooxmlRevisionId,
|
|
@@ -1209,6 +1230,7 @@ function createAuthoredRevision(
|
|
|
1209
1230
|
to: number,
|
|
1210
1231
|
authorId: string,
|
|
1211
1232
|
timestamp: string,
|
|
1233
|
+
metadata: Partial<NonNullable<CanonicalRevisionRecord["metadata"]>> = {},
|
|
1212
1234
|
): CanonicalRevisionRecord {
|
|
1213
1235
|
const changeId = createSuggestingRevisionId(existing, timestamp);
|
|
1214
1236
|
return {
|
|
@@ -1224,11 +1246,33 @@ function createAuthoredRevision(
|
|
|
1224
1246
|
warningIds: [],
|
|
1225
1247
|
metadata: {
|
|
1226
1248
|
source: "runtime",
|
|
1249
|
+
...metadata,
|
|
1227
1250
|
},
|
|
1228
1251
|
status: "open",
|
|
1229
1252
|
};
|
|
1230
1253
|
}
|
|
1231
1254
|
|
|
1255
|
+
function createSuggestionMetadata(args: {
|
|
1256
|
+
suggestionId?: string;
|
|
1257
|
+
semanticKind:
|
|
1258
|
+
| "insertion"
|
|
1259
|
+
| "deletion"
|
|
1260
|
+
| "replacement"
|
|
1261
|
+
| "formatting-change"
|
|
1262
|
+
| "paragraph-property-change"
|
|
1263
|
+
| "structural-change"
|
|
1264
|
+
| "object-change";
|
|
1265
|
+
linkedRevisionIds?: string[];
|
|
1266
|
+
predecessorSuggestionId?: string;
|
|
1267
|
+
}): Partial<NonNullable<CanonicalRevisionRecord["metadata"]>> {
|
|
1268
|
+
return {
|
|
1269
|
+
suggestionId: args.suggestionId,
|
|
1270
|
+
semanticKind: args.semanticKind,
|
|
1271
|
+
linkedRevisionIds: args.linkedRevisionIds,
|
|
1272
|
+
predecessorSuggestionId: args.predecessorSuggestionId,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1232
1276
|
function createSuggestingUnsupportedTransaction(
|
|
1233
1277
|
state: EditorState,
|
|
1234
1278
|
message: string,
|
|
@@ -1309,7 +1353,7 @@ function applySuggestingInsert(
|
|
|
1309
1353
|
}
|
|
1310
1354
|
|
|
1311
1355
|
if (isCollapsed) {
|
|
1312
|
-
// Pure insertion at cursor: apply normally, then create
|
|
1356
|
+
// Pure insertion at cursor: apply normally, then create insertion revision
|
|
1313
1357
|
const result = insertText(state.document, selection, text, { timestamp: context.timestamp });
|
|
1314
1358
|
const insertedFrom = from;
|
|
1315
1359
|
const insertedTo = from + Array.from(text).length;
|
|
@@ -1321,53 +1365,28 @@ function applySuggestingInsert(
|
|
|
1321
1365
|
result.mapping,
|
|
1322
1366
|
);
|
|
1323
1367
|
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
|
|
1327
|
-
// This groups consecutive keystrokes into a single tracked change.
|
|
1328
|
-
const adjacentInsertion = findAdjacentAuthoredInsertion(
|
|
1368
|
+
// Create the revision with pre-mapping positions — it refers to content
|
|
1369
|
+
// that was just inserted, so its anchors are already correct in the new document
|
|
1370
|
+
const revision = createAuthoredRevision(
|
|
1329
1371
|
reviewState.document.review.revisions,
|
|
1372
|
+
"insertion",
|
|
1330
1373
|
insertedFrom,
|
|
1374
|
+
insertedTo,
|
|
1375
|
+
authorId,
|
|
1376
|
+
context.timestamp,
|
|
1377
|
+
createSuggestionMetadata({
|
|
1378
|
+
semanticKind: "insertion",
|
|
1379
|
+
}),
|
|
1331
1380
|
);
|
|
1332
1381
|
|
|
1333
|
-
let finalRevisionId: string;
|
|
1334
|
-
let finalRevisions: Record<string, CanonicalRevisionRecord>;
|
|
1335
|
-
|
|
1336
|
-
if (adjacentInsertion && adjacentInsertion.anchor.kind === "range") {
|
|
1337
|
-
const extended: CanonicalRevisionRecord = {
|
|
1338
|
-
...adjacentInsertion,
|
|
1339
|
-
anchor: {
|
|
1340
|
-
kind: "range",
|
|
1341
|
-
range: { from: adjacentInsertion.anchor.range.from, to: insertedTo },
|
|
1342
|
-
assoc: { start: 1, end: -1 },
|
|
1343
|
-
},
|
|
1344
|
-
};
|
|
1345
|
-
finalRevisionId = extended.changeId;
|
|
1346
|
-
finalRevisions = {
|
|
1347
|
-
...reviewState.document.review.revisions,
|
|
1348
|
-
[extended.changeId]: extended,
|
|
1349
|
-
};
|
|
1350
|
-
} else {
|
|
1351
|
-
const revision = createAuthoredRevision(
|
|
1352
|
-
reviewState.document.review.revisions,
|
|
1353
|
-
"insertion",
|
|
1354
|
-
insertedFrom,
|
|
1355
|
-
insertedTo,
|
|
1356
|
-
authorId,
|
|
1357
|
-
context.timestamp,
|
|
1358
|
-
);
|
|
1359
|
-
finalRevisionId = revision.changeId;
|
|
1360
|
-
finalRevisions = {
|
|
1361
|
-
...reviewState.document.review.revisions,
|
|
1362
|
-
[revision.changeId]: revision,
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
1382
|
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1367
1383
|
...reviewState.document,
|
|
1368
1384
|
review: {
|
|
1369
1385
|
...reviewState.document.review,
|
|
1370
|
-
revisions:
|
|
1386
|
+
revisions: {
|
|
1387
|
+
...reviewState.document.review.revisions,
|
|
1388
|
+
[revision.changeId]: revision,
|
|
1389
|
+
},
|
|
1371
1390
|
},
|
|
1372
1391
|
};
|
|
1373
1392
|
|
|
@@ -1388,7 +1407,7 @@ function applySuggestingInsert(
|
|
|
1388
1407
|
mapping: result.mapping,
|
|
1389
1408
|
effects: {
|
|
1390
1409
|
...reviewState.effects,
|
|
1391
|
-
revisionAuthored: { changeId:
|
|
1410
|
+
revisionAuthored: { changeId: revision.changeId, kind: "insertion" },
|
|
1392
1411
|
},
|
|
1393
1412
|
},
|
|
1394
1413
|
);
|
|
@@ -1407,16 +1426,24 @@ function applySuggestingInsert(
|
|
|
1407
1426
|
result.document,
|
|
1408
1427
|
result.mapping,
|
|
1409
1428
|
);
|
|
1429
|
+
const replacementSuggestionId = createSuggestingRevisionId(
|
|
1430
|
+
reviewState.document.review.revisions,
|
|
1431
|
+
context.timestamp,
|
|
1432
|
+
);
|
|
1410
1433
|
|
|
1411
1434
|
// Step 3: Create deletion revision for the selected range (text stays in place).
|
|
1412
1435
|
// Deletion range uses pre-mapping positions since content was not removed.
|
|
1413
|
-
|
|
1436
|
+
let deletionRevision = createAuthoredRevision(
|
|
1414
1437
|
reviewState.document.review.revisions,
|
|
1415
1438
|
"deletion",
|
|
1416
1439
|
from,
|
|
1417
1440
|
to,
|
|
1418
1441
|
authorId,
|
|
1419
1442
|
context.timestamp,
|
|
1443
|
+
createSuggestionMetadata({
|
|
1444
|
+
suggestionId: replacementSuggestionId,
|
|
1445
|
+
semanticKind: "replacement",
|
|
1446
|
+
}),
|
|
1420
1447
|
);
|
|
1421
1448
|
|
|
1422
1449
|
// Step 4: Create insertion revision for the new text (positions already correct)
|
|
@@ -1430,7 +1457,19 @@ function applySuggestingInsert(
|
|
|
1430
1457
|
insertedTo,
|
|
1431
1458
|
authorId,
|
|
1432
1459
|
context.timestamp,
|
|
1460
|
+
createSuggestionMetadata({
|
|
1461
|
+
suggestionId: replacementSuggestionId,
|
|
1462
|
+
semanticKind: "replacement",
|
|
1463
|
+
linkedRevisionIds: [deletionRevision.changeId],
|
|
1464
|
+
}),
|
|
1433
1465
|
);
|
|
1466
|
+
deletionRevision = {
|
|
1467
|
+
...deletionRevision,
|
|
1468
|
+
metadata: {
|
|
1469
|
+
...deletionRevision.metadata,
|
|
1470
|
+
linkedRevisionIds: [insertionRevision.changeId],
|
|
1471
|
+
},
|
|
1472
|
+
};
|
|
1434
1473
|
|
|
1435
1474
|
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1436
1475
|
...reviewState.document,
|
|
@@ -1579,6 +1618,9 @@ function applySuggestingDelete(
|
|
|
1579
1618
|
deleteTo,
|
|
1580
1619
|
authorId,
|
|
1581
1620
|
context.timestamp,
|
|
1621
|
+
createSuggestionMetadata({
|
|
1622
|
+
semanticKind: "deletion",
|
|
1623
|
+
}),
|
|
1582
1624
|
);
|
|
1583
1625
|
|
|
1584
1626
|
const nextDocument: CanonicalDocumentEnvelope = {
|
|
@@ -1644,9 +1686,12 @@ function applySuggestingInsertUnit(
|
|
|
1644
1686
|
result.document,
|
|
1645
1687
|
result.mapping,
|
|
1646
1688
|
);
|
|
1689
|
+
const replacementSuggestionId = from !== to
|
|
1690
|
+
? createSuggestingRevisionId(reviewState.document.review.revisions, context.timestamp)
|
|
1691
|
+
: undefined;
|
|
1647
1692
|
|
|
1648
1693
|
// If non-collapsed, mark selected range as deletion (positions are pre-mapping, content preserved)
|
|
1649
|
-
|
|
1694
|
+
let deletionRevision = from !== to
|
|
1650
1695
|
? createAuthoredRevision(
|
|
1651
1696
|
reviewState.document.review.revisions,
|
|
1652
1697
|
"deletion",
|
|
@@ -1654,6 +1699,10 @@ function applySuggestingInsertUnit(
|
|
|
1654
1699
|
to,
|
|
1655
1700
|
authorId,
|
|
1656
1701
|
context.timestamp,
|
|
1702
|
+
createSuggestionMetadata({
|
|
1703
|
+
suggestionId: replacementSuggestionId,
|
|
1704
|
+
semanticKind: "replacement",
|
|
1705
|
+
}),
|
|
1657
1706
|
)
|
|
1658
1707
|
: undefined;
|
|
1659
1708
|
|
|
@@ -1668,7 +1717,27 @@ function applySuggestingInsertUnit(
|
|
|
1668
1717
|
insertPos + 1,
|
|
1669
1718
|
authorId,
|
|
1670
1719
|
context.timestamp,
|
|
1720
|
+
createSuggestionMetadata(
|
|
1721
|
+
from !== to
|
|
1722
|
+
? {
|
|
1723
|
+
suggestionId: replacementSuggestionId,
|
|
1724
|
+
semanticKind: "replacement",
|
|
1725
|
+
linkedRevisionIds: deletionRevision ? [deletionRevision.changeId] : undefined,
|
|
1726
|
+
}
|
|
1727
|
+
: {
|
|
1728
|
+
semanticKind: "insertion",
|
|
1729
|
+
},
|
|
1730
|
+
),
|
|
1671
1731
|
);
|
|
1732
|
+
if (deletionRevision) {
|
|
1733
|
+
deletionRevision = {
|
|
1734
|
+
...deletionRevision,
|
|
1735
|
+
metadata: {
|
|
1736
|
+
...deletionRevision.metadata,
|
|
1737
|
+
linkedRevisionIds: [insertionRevision.changeId],
|
|
1738
|
+
},
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1672
1741
|
|
|
1673
1742
|
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1674
1743
|
...reviewState.document,
|
|
@@ -1705,6 +1774,82 @@ function applySuggestingInsertUnit(
|
|
|
1705
1774
|
);
|
|
1706
1775
|
}
|
|
1707
1776
|
|
|
1777
|
+
function applySuggestingParagraphSplit(
|
|
1778
|
+
state: EditorState,
|
|
1779
|
+
context: CommandExecutionContext,
|
|
1780
|
+
): EditorTransaction | undefined {
|
|
1781
|
+
if (state.readOnly) {
|
|
1782
|
+
return createTransaction(state, { historyBoundary: "skip", markDirty: false });
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const selection = state.selection;
|
|
1786
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
1787
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
1788
|
+
if (from !== to) {
|
|
1789
|
+
return createSuggestingUnsupportedTransaction(
|
|
1790
|
+
state,
|
|
1791
|
+
"Suggesting mode paragraph split currently requires a collapsed selection.",
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const authorId = context.defaultAuthorId ?? "unknown";
|
|
1796
|
+
const result = splitParagraph(state.document, selection, { timestamp: context.timestamp });
|
|
1797
|
+
|
|
1798
|
+
const reviewState = remapReviewStateAfterContentChange(
|
|
1799
|
+
state,
|
|
1800
|
+
result.document,
|
|
1801
|
+
result.mapping,
|
|
1802
|
+
);
|
|
1803
|
+
const revision = createAuthoredRevision(
|
|
1804
|
+
reviewState.document.review.revisions,
|
|
1805
|
+
"insertion",
|
|
1806
|
+
from,
|
|
1807
|
+
from,
|
|
1808
|
+
authorId,
|
|
1809
|
+
context.timestamp,
|
|
1810
|
+
{
|
|
1811
|
+
...createSuggestionMetadata({
|
|
1812
|
+
semanticKind: "structural-change",
|
|
1813
|
+
}),
|
|
1814
|
+
importedRevisionForm: "paragraph-insertion",
|
|
1815
|
+
originalRevisionType: "paragraph-ins",
|
|
1816
|
+
},
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1820
|
+
...reviewState.document,
|
|
1821
|
+
review: {
|
|
1822
|
+
...reviewState.document.review,
|
|
1823
|
+
revisions: {
|
|
1824
|
+
...reviewState.document.review.revisions,
|
|
1825
|
+
[revision.changeId]: revision,
|
|
1826
|
+
},
|
|
1827
|
+
},
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
return createTransaction(
|
|
1831
|
+
{
|
|
1832
|
+
...state,
|
|
1833
|
+
document: finalDocument,
|
|
1834
|
+
selection: result.selection,
|
|
1835
|
+
warnings: reviewState.warnings,
|
|
1836
|
+
runtime: {
|
|
1837
|
+
...state.runtime,
|
|
1838
|
+
activeCommentId: reviewState.activeCommentId,
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
historyBoundary: "push",
|
|
1843
|
+
markDirty: true,
|
|
1844
|
+
mapping: result.mapping,
|
|
1845
|
+
effects: {
|
|
1846
|
+
...reviewState.effects,
|
|
1847
|
+
revisionAuthored: { changeId: revision.changeId, kind: "insertion" },
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1708
1853
|
function findOverlappingAuthoredDeletion(
|
|
1709
1854
|
revisions: Record<string, CanonicalRevisionRecord>,
|
|
1710
1855
|
from: number,
|
|
@@ -1732,26 +1877,3 @@ function findOverlappingAuthoredDeletion(
|
|
|
1732
1877
|
}
|
|
1733
1878
|
return undefined;
|
|
1734
1879
|
}
|
|
1735
|
-
|
|
1736
|
-
/**
|
|
1737
|
-
* Find an open authored insertion revision whose end position equals `cursorAt`.
|
|
1738
|
-
* Used to extend an existing insertion when the user keeps typing at the same
|
|
1739
|
-
* position rather than creating a new revision for every character.
|
|
1740
|
-
*/
|
|
1741
|
-
function findAdjacentAuthoredInsertion(
|
|
1742
|
-
revisions: Record<string, CanonicalRevisionRecord>,
|
|
1743
|
-
cursorAt: number,
|
|
1744
|
-
): CanonicalRevisionRecord | undefined {
|
|
1745
|
-
for (const revision of Object.values(revisions)) {
|
|
1746
|
-
if (
|
|
1747
|
-
revision.kind === "insertion" &&
|
|
1748
|
-
revision.status === "open" &&
|
|
1749
|
-
revision.metadata?.source === "runtime" &&
|
|
1750
|
-
revision.anchor.kind === "range" &&
|
|
1751
|
-
revision.anchor.range.to === cursorAt
|
|
1752
|
-
) {
|
|
1753
|
-
return revision;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return undefined;
|
|
1757
|
-
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
RuntimeRenderSnapshot as PublicRuntimeRenderSnapshot,
|
|
3
|
+
TableOperationCapabilitySnapshot,
|
|
4
|
+
TableStructureContextSnapshot,
|
|
5
|
+
} from "../../api/public-types";
|
|
2
6
|
import type {
|
|
3
7
|
DocumentRootNode,
|
|
4
8
|
ParagraphNode,
|
|
@@ -82,6 +86,110 @@ export function applyTableStructureOperation(
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
export function getTableStructureContext(
|
|
90
|
+
document: CanonicalDocumentEnvelope,
|
|
91
|
+
snapshot: PublicRuntimeRenderSnapshot,
|
|
92
|
+
selectionDescriptor: TableSelectionDescriptor | null,
|
|
93
|
+
): TableStructureContextSnapshot | null {
|
|
94
|
+
const root = document.content;
|
|
95
|
+
if (!root || root.type !== "doc") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const effectiveSelection = selectionDescriptor ?? resolveTableSelectionFromSnapshot(snapshot);
|
|
100
|
+
if (!effectiveSelection) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const target = root.children[effectiveSelection.tableBlockIndex];
|
|
105
|
+
if (!target || target.type !== "table") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const row = target.rows[effectiveSelection.anchorCell.rowIndex];
|
|
110
|
+
const cellRef = row
|
|
111
|
+
? findCellAtColumn(row, effectiveSelection.anchorCell.columnIndex)
|
|
112
|
+
: null;
|
|
113
|
+
if (!row || !cellRef) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const simpleTable = isSimpleTable(target);
|
|
118
|
+
const columnCount = getLogicalColumnCount(target);
|
|
119
|
+
const splitWidth = Math.max(1, cellRef.cell.gridSpan ?? 1);
|
|
120
|
+
const splitHeight = Math.max(
|
|
121
|
+
1,
|
|
122
|
+
computeRowSpan(
|
|
123
|
+
target,
|
|
124
|
+
effectiveSelection.anchorCell.rowIndex,
|
|
125
|
+
effectiveSelection.anchorCell.columnIndex,
|
|
126
|
+
splitWidth,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
const selectedCellCount =
|
|
130
|
+
effectiveSelection.selectionKind === "cell"
|
|
131
|
+
? Math.max(
|
|
132
|
+
1,
|
|
133
|
+
(effectiveSelection.rect.bottom - effectiveSelection.rect.top) *
|
|
134
|
+
(effectiveSelection.rect.right - effectiveSelection.rect.left),
|
|
135
|
+
)
|
|
136
|
+
: 1;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
tableBlockIndex: effectiveSelection.tableBlockIndex,
|
|
140
|
+
currentStyleId: target.styleId ?? null,
|
|
141
|
+
selectionKind: effectiveSelection.selectionKind,
|
|
142
|
+
rowCount: target.rows.length,
|
|
143
|
+
columnCount,
|
|
144
|
+
selectedCellCount,
|
|
145
|
+
isSimpleTable: simpleTable,
|
|
146
|
+
currentCell: {
|
|
147
|
+
rowIndex: effectiveSelection.anchorCell.rowIndex,
|
|
148
|
+
columnIndex: effectiveSelection.anchorCell.columnIndex,
|
|
149
|
+
isHeader: row.isHeader === true,
|
|
150
|
+
},
|
|
151
|
+
operations: {
|
|
152
|
+
setTableStyle: enabledCapability(),
|
|
153
|
+
setCellBackground: enabledCapability(),
|
|
154
|
+
addRowBefore: simpleTable
|
|
155
|
+
? enabledCapability()
|
|
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."),
|
|
160
|
+
deleteRow:
|
|
161
|
+
!simpleTable
|
|
162
|
+
? disabledCapability("Only simple rectangular tables support row deletion right now.")
|
|
163
|
+
: target.rows.length <= 1
|
|
164
|
+
? disabledCapability("At least two rows are required before a row can be deleted.")
|
|
165
|
+
: enabledCapability(),
|
|
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."),
|
|
172
|
+
deleteColumn:
|
|
173
|
+
!simpleTable
|
|
174
|
+
? disabledCapability("Only simple rectangular tables support column deletion right now.")
|
|
175
|
+
: columnCount <= 1
|
|
176
|
+
? disabledCapability("At least two columns are required before a column can be deleted.")
|
|
177
|
+
: enabledCapability(),
|
|
178
|
+
mergeCells:
|
|
179
|
+
!simpleTable
|
|
180
|
+
? disabledCapability("Only simple rectangular tables support merging right now.")
|
|
181
|
+
: effectiveSelection.selectionKind !== "cell" || selectedCellCount <= 1
|
|
182
|
+
? disabledCapability("Select more than one cell to merge them.")
|
|
183
|
+
: enabledCapability(),
|
|
184
|
+
splitCell:
|
|
185
|
+
splitWidth === 1 && splitHeight === 1
|
|
186
|
+
? disabledCapability("Select a merged or spanning cell to split it.")
|
|
187
|
+
: enabledCapability(),
|
|
188
|
+
deleteTable: enabledCapability(),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
85
193
|
function addRow(
|
|
86
194
|
document: CanonicalDocumentEnvelope,
|
|
87
195
|
root: DocumentRootNode,
|
|
@@ -405,13 +513,27 @@ function setCellBackground(
|
|
|
405
513
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
406
514
|
}
|
|
407
515
|
|
|
516
|
+
const activeRow = table.rows[selection.anchorCell.rowIndex];
|
|
517
|
+
const activeCellRef = activeRow
|
|
518
|
+
? findCellAtColumn(activeRow, selection.anchorCell.columnIndex)
|
|
519
|
+
: null;
|
|
520
|
+
const activeCellWidth = Math.max(1, activeCellRef?.cell.gridSpan ?? 1);
|
|
521
|
+
const activeCellHeight = Math.max(
|
|
522
|
+
1,
|
|
523
|
+
computeRowSpan(
|
|
524
|
+
table,
|
|
525
|
+
selection.anchorCell.rowIndex,
|
|
526
|
+
selection.anchorCell.columnIndex,
|
|
527
|
+
activeCellWidth,
|
|
528
|
+
),
|
|
529
|
+
);
|
|
408
530
|
const targetRect =
|
|
409
531
|
selection.selectionKind === "text"
|
|
410
532
|
? {
|
|
411
|
-
top:
|
|
412
|
-
left:
|
|
413
|
-
bottom:
|
|
414
|
-
right:
|
|
533
|
+
top: selection.anchorCell.rowIndex,
|
|
534
|
+
left: selection.anchorCell.columnIndex,
|
|
535
|
+
bottom: selection.anchorCell.rowIndex + activeCellHeight,
|
|
536
|
+
right: selection.anchorCell.columnIndex + activeCellWidth,
|
|
415
537
|
}
|
|
416
538
|
: selection.rect;
|
|
417
539
|
let changed = false;
|
|
@@ -611,6 +733,17 @@ function toInternalSelectionSnapshot(
|
|
|
611
733
|
};
|
|
612
734
|
}
|
|
613
735
|
|
|
736
|
+
function enabledCapability(): TableOperationCapabilitySnapshot {
|
|
737
|
+
return { enabled: true };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function disabledCapability(reason: string): TableOperationCapabilitySnapshot {
|
|
741
|
+
return {
|
|
742
|
+
enabled: false,
|
|
743
|
+
reason,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
614
747
|
function isSimpleTable(table: TableNode): boolean {
|
|
615
748
|
const width = getLogicalColumnCount(table);
|
|
616
749
|
return table.rows.every((row) => {
|