@beyondwork/docx-react-component 1.0.21 → 1.0.23
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 +763 -38
- package/package.json +25 -36
- package/src/api/public-types.ts +66 -1
- package/src/core/commands/index.ts +574 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +661 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
insertText,
|
|
34
34
|
splitParagraph,
|
|
35
35
|
} from "./text-commands.ts";
|
|
36
|
+
import type { RevisionRecord as CanonicalRevisionRecord } from "../../model/canonical-document.ts";
|
|
36
37
|
import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
|
|
37
38
|
import { applyRevisionRuntimeCommand } from "../../runtime/revision-runtime.ts";
|
|
38
39
|
import type { RevisionStore } from "../../review/store/revision-store.ts";
|
|
@@ -180,6 +181,8 @@ export interface TransactionEffects {
|
|
|
180
181
|
commentBodyEdited?: { commentId: string };
|
|
181
182
|
changeAccepted?: { changeId: string };
|
|
182
183
|
changeRejected?: { changeId: string };
|
|
184
|
+
revisionAuthored?: { changeId: string; kind: "insertion" | "deletion" };
|
|
185
|
+
commandBlocked?: { code: string; message: string };
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
export interface EditorTransaction {
|
|
@@ -192,6 +195,8 @@ export interface EditorTransaction {
|
|
|
192
195
|
|
|
193
196
|
export interface CommandExecutionContext {
|
|
194
197
|
timestamp: string;
|
|
198
|
+
documentMode?: "editing" | "suggesting" | "viewing";
|
|
199
|
+
defaultAuthorId?: string;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
export function executeEditorCommand(
|
|
@@ -253,27 +258,64 @@ export function executeEditorCommand(
|
|
|
253
258
|
},
|
|
254
259
|
);
|
|
255
260
|
}
|
|
256
|
-
case "text.insert":
|
|
261
|
+
case "text.insert": {
|
|
262
|
+
const suggestingResult = context.documentMode === "suggesting"
|
|
263
|
+
? applySuggestingInsert(state, command.text, context)
|
|
264
|
+
: undefined;
|
|
265
|
+
if (suggestingResult) return suggestingResult;
|
|
257
266
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
258
267
|
insertText(document, selection, command.text, context),
|
|
259
268
|
);
|
|
260
|
-
|
|
269
|
+
}
|
|
270
|
+
case "text.delete-backward": {
|
|
271
|
+
const suggestingResult = context.documentMode === "suggesting"
|
|
272
|
+
? applySuggestingDelete(state, "backward", context)
|
|
273
|
+
: undefined;
|
|
274
|
+
if (suggestingResult) return suggestingResult;
|
|
261
275
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
262
276
|
deleteSelectionOrBackward(document, selection, context),
|
|
263
277
|
);
|
|
264
|
-
|
|
278
|
+
}
|
|
279
|
+
case "text.delete-forward": {
|
|
280
|
+
const suggestingResult = context.documentMode === "suggesting"
|
|
281
|
+
? applySuggestingDelete(state, "forward", context)
|
|
282
|
+
: undefined;
|
|
283
|
+
if (suggestingResult) return suggestingResult;
|
|
265
284
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
266
285
|
deleteSelectionOrForward(document, selection, context),
|
|
267
286
|
);
|
|
268
|
-
|
|
287
|
+
}
|
|
288
|
+
case "text.insert-tab": {
|
|
289
|
+
const suggestingResult = context.documentMode === "suggesting"
|
|
290
|
+
? applySuggestingInsertUnit(state, "tab", context)
|
|
291
|
+
: undefined;
|
|
292
|
+
if (suggestingResult) return suggestingResult;
|
|
269
293
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
270
294
|
insertTab(document, selection, context),
|
|
271
295
|
);
|
|
272
|
-
|
|
296
|
+
}
|
|
297
|
+
case "text.insert-hard-break": {
|
|
298
|
+
const suggestingResult = context.documentMode === "suggesting"
|
|
299
|
+
? applySuggestingInsertUnit(state, "hard_break", context)
|
|
300
|
+
: undefined;
|
|
301
|
+
if (suggestingResult) return suggestingResult;
|
|
273
302
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
274
303
|
insertHardBreak(document, selection, context),
|
|
275
304
|
);
|
|
305
|
+
}
|
|
276
306
|
case "paragraph.split":
|
|
307
|
+
if (context.documentMode === "suggesting") {
|
|
308
|
+
return createTransaction(state, {
|
|
309
|
+
historyBoundary: "skip",
|
|
310
|
+
markDirty: false,
|
|
311
|
+
effects: {
|
|
312
|
+
commandBlocked: {
|
|
313
|
+
code: "suggesting_unsupported",
|
|
314
|
+
message: "Paragraph splits are not supported in suggesting mode.",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
277
319
|
return applyTextCommand(state, context.timestamp, (document, selection) =>
|
|
278
320
|
splitParagraph(document, selection, context),
|
|
279
321
|
);
|
|
@@ -666,6 +708,8 @@ function createTransaction(
|
|
|
666
708
|
commentResolved: options.effects?.commentResolved,
|
|
667
709
|
changeAccepted: options.effects?.changeAccepted,
|
|
668
710
|
changeRejected: options.effects?.changeRejected,
|
|
711
|
+
revisionAuthored: options.effects?.revisionAuthored,
|
|
712
|
+
commandBlocked: options.effects?.commandBlocked,
|
|
669
713
|
},
|
|
670
714
|
};
|
|
671
715
|
}
|
|
@@ -1010,6 +1054,7 @@ function createRevisionStoreFromState(
|
|
|
1010
1054
|
warningIds: [...(revision.warningIds ?? [])],
|
|
1011
1055
|
metadata: {
|
|
1012
1056
|
source: revision.metadata?.source ?? "runtime",
|
|
1057
|
+
storyTarget: revision.metadata?.storyTarget,
|
|
1013
1058
|
preserveOnlyReason: revision.metadata?.preserveOnlyReason,
|
|
1014
1059
|
importedRevisionForm: revision.metadata?.importedRevisionForm,
|
|
1015
1060
|
originalRevisionType: revision.metadata?.originalRevisionType,
|
|
@@ -1036,6 +1081,7 @@ function toEditorRevisionRecords(
|
|
|
1036
1081
|
warningIds: [...revision.warningIds],
|
|
1037
1082
|
metadata: {
|
|
1038
1083
|
source: revision.metadata.source,
|
|
1084
|
+
storyTarget: revision.metadata.storyTarget,
|
|
1039
1085
|
preserveOnlyReason: revision.metadata.preserveOnlyReason,
|
|
1040
1086
|
importedRevisionForm: revision.metadata.importedRevisionForm,
|
|
1041
1087
|
originalRevisionType: revision.metadata.originalRevisionType,
|
|
@@ -1135,3 +1181,526 @@ function combineMappingSteps(
|
|
|
1135
1181
|
}
|
|
1136
1182
|
: createEmptyMapping();
|
|
1137
1183
|
}
|
|
1184
|
+
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
// Suggesting mode: creates revision records instead of (or alongside) text mutations
|
|
1187
|
+
// ---------------------------------------------------------------------------
|
|
1188
|
+
|
|
1189
|
+
let suggestingRevisionCounter = 0;
|
|
1190
|
+
|
|
1191
|
+
function createSuggestingRevisionId(
|
|
1192
|
+
existing: Record<string, unknown>,
|
|
1193
|
+
timestamp: string,
|
|
1194
|
+
): string {
|
|
1195
|
+
suggestingRevisionCounter += 1;
|
|
1196
|
+
const ts = timestamp.replace(/[^0-9]/gu, "");
|
|
1197
|
+
let id = `change-${ts}-s${suggestingRevisionCounter}`;
|
|
1198
|
+
while (existing[id]) {
|
|
1199
|
+
suggestingRevisionCounter += 1;
|
|
1200
|
+
id = `change-${ts}-s${suggestingRevisionCounter}`;
|
|
1201
|
+
}
|
|
1202
|
+
return id;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function createAuthoredRevision(
|
|
1206
|
+
existing: Record<string, unknown>,
|
|
1207
|
+
kind: "insertion" | "deletion",
|
|
1208
|
+
from: number,
|
|
1209
|
+
to: number,
|
|
1210
|
+
authorId: string,
|
|
1211
|
+
timestamp: string,
|
|
1212
|
+
): CanonicalRevisionRecord {
|
|
1213
|
+
const changeId = createSuggestingRevisionId(existing, timestamp);
|
|
1214
|
+
return {
|
|
1215
|
+
changeId,
|
|
1216
|
+
kind,
|
|
1217
|
+
anchor: {
|
|
1218
|
+
kind: "range",
|
|
1219
|
+
range: { from, to },
|
|
1220
|
+
assoc: { start: 1, end: -1 },
|
|
1221
|
+
},
|
|
1222
|
+
authorId,
|
|
1223
|
+
createdAt: timestamp,
|
|
1224
|
+
warningIds: [],
|
|
1225
|
+
metadata: {
|
|
1226
|
+
source: "runtime",
|
|
1227
|
+
},
|
|
1228
|
+
status: "open",
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function createSuggestingUnsupportedTransaction(
|
|
1233
|
+
state: EditorState,
|
|
1234
|
+
message: string,
|
|
1235
|
+
): EditorTransaction {
|
|
1236
|
+
return createTransaction(state, {
|
|
1237
|
+
historyBoundary: "skip",
|
|
1238
|
+
markDirty: false,
|
|
1239
|
+
effects: {
|
|
1240
|
+
commandBlocked: {
|
|
1241
|
+
code: "suggesting_unsupported",
|
|
1242
|
+
message,
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function isSingleParagraphSuggestingRange(
|
|
1249
|
+
document: CanonicalDocumentEnvelope,
|
|
1250
|
+
from: number,
|
|
1251
|
+
to: number,
|
|
1252
|
+
): boolean {
|
|
1253
|
+
const ranges: Array<{ start: number; end: number }> = [];
|
|
1254
|
+
let cursor = 0;
|
|
1255
|
+
let previousWasParagraph = false;
|
|
1256
|
+
|
|
1257
|
+
for (const block of document.content.children) {
|
|
1258
|
+
if (block.type === "paragraph") {
|
|
1259
|
+
if (previousWasParagraph) {
|
|
1260
|
+
cursor += 1;
|
|
1261
|
+
}
|
|
1262
|
+
const start = cursor;
|
|
1263
|
+
cursor += block.children.reduce<number>((size, child) => {
|
|
1264
|
+
if (child.type === "text") {
|
|
1265
|
+
return size + child.text.length;
|
|
1266
|
+
}
|
|
1267
|
+
if (child.type === "hyperlink") {
|
|
1268
|
+
return size + child.children.reduce<number>((childSize, entry) => {
|
|
1269
|
+
if (entry.type === "text") {
|
|
1270
|
+
return childSize + entry.text.length;
|
|
1271
|
+
}
|
|
1272
|
+
return childSize + 1;
|
|
1273
|
+
}, 0);
|
|
1274
|
+
}
|
|
1275
|
+
return size + 1;
|
|
1276
|
+
}, 0);
|
|
1277
|
+
ranges.push({ start, end: cursor });
|
|
1278
|
+
previousWasParagraph = true;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
cursor += 1;
|
|
1283
|
+
previousWasParagraph = false;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return ranges.some((range) => from >= range.start && to <= range.end);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function applySuggestingInsert(
|
|
1290
|
+
state: EditorState,
|
|
1291
|
+
text: string,
|
|
1292
|
+
context: CommandExecutionContext,
|
|
1293
|
+
): EditorTransaction | undefined {
|
|
1294
|
+
if (state.readOnly) {
|
|
1295
|
+
return createTransaction(state, { historyBoundary: "skip", markDirty: false });
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const authorId = context.defaultAuthorId ?? "unknown";
|
|
1299
|
+
const selection = state.selection;
|
|
1300
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
1301
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
1302
|
+
const isCollapsed = from === to;
|
|
1303
|
+
|
|
1304
|
+
if (!isCollapsed && !isSingleParagraphSuggestingRange(state.document, from, to)) {
|
|
1305
|
+
return createSuggestingUnsupportedTransaction(
|
|
1306
|
+
state,
|
|
1307
|
+
"Suggesting mode does not yet support multi-paragraph replacement ranges.",
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (isCollapsed) {
|
|
1312
|
+
// Pure insertion at cursor: apply normally, then create insertion revision
|
|
1313
|
+
const result = insertText(state.document, selection, text, { timestamp: context.timestamp });
|
|
1314
|
+
const insertedFrom = from;
|
|
1315
|
+
const insertedTo = from + Array.from(text).length;
|
|
1316
|
+
|
|
1317
|
+
// Remap existing review state first (comments, pre-existing revisions)
|
|
1318
|
+
const reviewState = remapReviewStateAfterContentChange(
|
|
1319
|
+
state,
|
|
1320
|
+
result.document,
|
|
1321
|
+
result.mapping,
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
// Create the revision with pre-mapping positions — it refers to content
|
|
1325
|
+
// that was just inserted, so its anchors are already correct in the new document
|
|
1326
|
+
const revision = createAuthoredRevision(
|
|
1327
|
+
reviewState.document.review.revisions,
|
|
1328
|
+
"insertion",
|
|
1329
|
+
insertedFrom,
|
|
1330
|
+
insertedTo,
|
|
1331
|
+
authorId,
|
|
1332
|
+
context.timestamp,
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1336
|
+
...reviewState.document,
|
|
1337
|
+
review: {
|
|
1338
|
+
...reviewState.document.review,
|
|
1339
|
+
revisions: {
|
|
1340
|
+
...reviewState.document.review.revisions,
|
|
1341
|
+
[revision.changeId]: revision,
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
return createTransaction(
|
|
1347
|
+
{
|
|
1348
|
+
...state,
|
|
1349
|
+
document: finalDocument,
|
|
1350
|
+
selection: result.selection,
|
|
1351
|
+
warnings: reviewState.warnings,
|
|
1352
|
+
runtime: {
|
|
1353
|
+
...state.runtime,
|
|
1354
|
+
activeCommentId: reviewState.activeCommentId,
|
|
1355
|
+
},
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
historyBoundary: "push",
|
|
1359
|
+
markDirty: true,
|
|
1360
|
+
mapping: result.mapping,
|
|
1361
|
+
effects: {
|
|
1362
|
+
...reviewState.effects,
|
|
1363
|
+
revisionAuthored: { changeId: revision.changeId, kind: "insertion" },
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Non-collapsed selection: mark selected text as deletion, insert new text after selection
|
|
1370
|
+
// Step 1: Insert new text at the end of the selection
|
|
1371
|
+
const insertSelection = createSelectionSnapshot(to, to);
|
|
1372
|
+
const result = insertText(state.document, insertSelection, text, { timestamp: context.timestamp });
|
|
1373
|
+
const insertedFrom = to;
|
|
1374
|
+
const insertedTo = to + Array.from(text).length;
|
|
1375
|
+
|
|
1376
|
+
// Step 2: Remap existing review state through the mapping first
|
|
1377
|
+
const reviewState = remapReviewStateAfterContentChange(
|
|
1378
|
+
state,
|
|
1379
|
+
result.document,
|
|
1380
|
+
result.mapping,
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
// Step 3: Create deletion revision for the selected range (text stays in place).
|
|
1384
|
+
// Deletion range uses pre-mapping positions since content was not removed.
|
|
1385
|
+
const deletionRevision = createAuthoredRevision(
|
|
1386
|
+
reviewState.document.review.revisions,
|
|
1387
|
+
"deletion",
|
|
1388
|
+
from,
|
|
1389
|
+
to,
|
|
1390
|
+
authorId,
|
|
1391
|
+
context.timestamp,
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
// Step 4: Create insertion revision for the new text (positions already correct)
|
|
1395
|
+
const insertionRevision = createAuthoredRevision(
|
|
1396
|
+
{
|
|
1397
|
+
...reviewState.document.review.revisions,
|
|
1398
|
+
[deletionRevision.changeId]: deletionRevision,
|
|
1399
|
+
},
|
|
1400
|
+
"insertion",
|
|
1401
|
+
insertedFrom,
|
|
1402
|
+
insertedTo,
|
|
1403
|
+
authorId,
|
|
1404
|
+
context.timestamp,
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1408
|
+
...reviewState.document,
|
|
1409
|
+
review: {
|
|
1410
|
+
...reviewState.document.review,
|
|
1411
|
+
revisions: {
|
|
1412
|
+
...reviewState.document.review.revisions,
|
|
1413
|
+
[deletionRevision.changeId]: deletionRevision,
|
|
1414
|
+
[insertionRevision.changeId]: insertionRevision,
|
|
1415
|
+
},
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
// Move cursor to after inserted text
|
|
1420
|
+
const caretPos = insertedTo;
|
|
1421
|
+
|
|
1422
|
+
return createTransaction(
|
|
1423
|
+
{
|
|
1424
|
+
...state,
|
|
1425
|
+
document: finalDocument,
|
|
1426
|
+
selection: createSelectionSnapshot(caretPos, caretPos),
|
|
1427
|
+
warnings: reviewState.warnings,
|
|
1428
|
+
runtime: {
|
|
1429
|
+
...state.runtime,
|
|
1430
|
+
activeCommentId: reviewState.activeCommentId,
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
historyBoundary: "push",
|
|
1435
|
+
markDirty: true,
|
|
1436
|
+
mapping: result.mapping,
|
|
1437
|
+
effects: {
|
|
1438
|
+
...reviewState.effects,
|
|
1439
|
+
revisionAuthored: { changeId: insertionRevision.changeId, kind: "insertion" },
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function applySuggestingDelete(
|
|
1446
|
+
state: EditorState,
|
|
1447
|
+
direction: "backward" | "forward",
|
|
1448
|
+
context: CommandExecutionContext,
|
|
1449
|
+
): EditorTransaction | undefined {
|
|
1450
|
+
if (state.readOnly) {
|
|
1451
|
+
return createTransaction(state, { historyBoundary: "skip", markDirty: false });
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const authorId = context.defaultAuthorId ?? "unknown";
|
|
1455
|
+
const selection = state.selection;
|
|
1456
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
1457
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
1458
|
+
const isCollapsed = from === to;
|
|
1459
|
+
|
|
1460
|
+
if (!isCollapsed && !isSingleParagraphSuggestingRange(state.document, from, to)) {
|
|
1461
|
+
return createSuggestingUnsupportedTransaction(
|
|
1462
|
+
state,
|
|
1463
|
+
"Suggesting mode does not yet support multi-paragraph deletion ranges.",
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
let deleteFrom: number;
|
|
1468
|
+
let deleteTo: number;
|
|
1469
|
+
|
|
1470
|
+
if (isCollapsed) {
|
|
1471
|
+
if (direction === "backward") {
|
|
1472
|
+
deleteFrom = Math.max(0, from - 1);
|
|
1473
|
+
deleteTo = from;
|
|
1474
|
+
} else {
|
|
1475
|
+
deleteFrom = from;
|
|
1476
|
+
deleteTo = from + 1;
|
|
1477
|
+
}
|
|
1478
|
+
} else {
|
|
1479
|
+
deleteFrom = from;
|
|
1480
|
+
deleteTo = to;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Validate the deletion range doesn't cross protected content
|
|
1484
|
+
if (deleteFrom === deleteTo) {
|
|
1485
|
+
return createTransaction(state, { historyBoundary: "skip", markDirty: false });
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Check if the range overlaps an existing deletion revision from this suggesting session
|
|
1489
|
+
// If so, extend the existing revision rather than creating a new one
|
|
1490
|
+
const existingDeletion = findOverlappingAuthoredDeletion(
|
|
1491
|
+
state.document.review.revisions,
|
|
1492
|
+
deleteFrom,
|
|
1493
|
+
deleteTo,
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
if (existingDeletion) {
|
|
1497
|
+
// Extend the existing deletion revision to cover the new range
|
|
1498
|
+
const extendedFrom = Math.min(
|
|
1499
|
+
existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.range.from : deleteFrom,
|
|
1500
|
+
deleteFrom,
|
|
1501
|
+
);
|
|
1502
|
+
const extendedTo = Math.max(
|
|
1503
|
+
existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.range.to : deleteTo,
|
|
1504
|
+
deleteTo,
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
const updatedRevision: CanonicalRevisionRecord = {
|
|
1508
|
+
...existingDeletion,
|
|
1509
|
+
anchor: {
|
|
1510
|
+
kind: "range",
|
|
1511
|
+
range: { from: extendedFrom, to: extendedTo },
|
|
1512
|
+
assoc: { start: 1, end: -1 },
|
|
1513
|
+
},
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
1517
|
+
...state.document,
|
|
1518
|
+
updatedAt: context.timestamp,
|
|
1519
|
+
review: {
|
|
1520
|
+
...state.document.review,
|
|
1521
|
+
revisions: {
|
|
1522
|
+
...state.document.review.revisions,
|
|
1523
|
+
[updatedRevision.changeId]: updatedRevision,
|
|
1524
|
+
},
|
|
1525
|
+
},
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const caretPos = direction === "backward" ? deleteFrom : deleteTo;
|
|
1529
|
+
|
|
1530
|
+
return createTransaction(
|
|
1531
|
+
{
|
|
1532
|
+
...state,
|
|
1533
|
+
document: nextDocument,
|
|
1534
|
+
selection: createSelectionSnapshot(caretPos, caretPos),
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
historyBoundary: "push",
|
|
1538
|
+
markDirty: true,
|
|
1539
|
+
effects: {
|
|
1540
|
+
revisionAuthored: { changeId: updatedRevision.changeId, kind: "deletion" },
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// Create a new deletion revision — text stays in the document
|
|
1547
|
+
const revision = createAuthoredRevision(
|
|
1548
|
+
state.document.review.revisions,
|
|
1549
|
+
"deletion",
|
|
1550
|
+
deleteFrom,
|
|
1551
|
+
deleteTo,
|
|
1552
|
+
authorId,
|
|
1553
|
+
context.timestamp,
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
1557
|
+
...state.document,
|
|
1558
|
+
updatedAt: context.timestamp,
|
|
1559
|
+
review: {
|
|
1560
|
+
...state.document.review,
|
|
1561
|
+
revisions: {
|
|
1562
|
+
...state.document.review.revisions,
|
|
1563
|
+
[revision.changeId]: revision,
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const caretPos = direction === "backward" ? deleteFrom : deleteTo;
|
|
1569
|
+
|
|
1570
|
+
return createTransaction(
|
|
1571
|
+
{
|
|
1572
|
+
...state,
|
|
1573
|
+
document: nextDocument,
|
|
1574
|
+
selection: createSelectionSnapshot(caretPos, caretPos),
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
historyBoundary: "push",
|
|
1578
|
+
markDirty: true,
|
|
1579
|
+
effects: {
|
|
1580
|
+
revisionAuthored: { changeId: revision.changeId, kind: "deletion" },
|
|
1581
|
+
},
|
|
1582
|
+
},
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function applySuggestingInsertUnit(
|
|
1587
|
+
state: EditorState,
|
|
1588
|
+
unitType: "tab" | "hard_break",
|
|
1589
|
+
context: CommandExecutionContext,
|
|
1590
|
+
): EditorTransaction | undefined {
|
|
1591
|
+
if (state.readOnly) {
|
|
1592
|
+
return createTransaction(state, { historyBoundary: "skip", markDirty: false });
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const authorId = context.defaultAuthorId ?? "unknown";
|
|
1596
|
+
const selection = state.selection;
|
|
1597
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
1598
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
1599
|
+
|
|
1600
|
+
if (from !== to && !isSingleParagraphSuggestingRange(state.document, from, to)) {
|
|
1601
|
+
return createSuggestingUnsupportedTransaction(
|
|
1602
|
+
state,
|
|
1603
|
+
"Suggesting mode does not yet support multi-paragraph replacement ranges.",
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Insert the unit at the end of the selection (or at cursor if collapsed)
|
|
1608
|
+
const insertPos = to;
|
|
1609
|
+
const insertSelection = createSelectionSnapshot(insertPos, insertPos);
|
|
1610
|
+
const applyFn = unitType === "tab" ? insertTab : insertHardBreak;
|
|
1611
|
+
const result = applyFn(state.document, insertSelection, { timestamp: context.timestamp });
|
|
1612
|
+
|
|
1613
|
+
// Remap existing review state first, before adding new revisions
|
|
1614
|
+
const reviewState = remapReviewStateAfterContentChange(
|
|
1615
|
+
state,
|
|
1616
|
+
result.document,
|
|
1617
|
+
result.mapping,
|
|
1618
|
+
);
|
|
1619
|
+
|
|
1620
|
+
// If non-collapsed, mark selected range as deletion (positions are pre-mapping, content preserved)
|
|
1621
|
+
const deletionRevision = from !== to
|
|
1622
|
+
? createAuthoredRevision(
|
|
1623
|
+
reviewState.document.review.revisions,
|
|
1624
|
+
"deletion",
|
|
1625
|
+
from,
|
|
1626
|
+
to,
|
|
1627
|
+
authorId,
|
|
1628
|
+
context.timestamp,
|
|
1629
|
+
)
|
|
1630
|
+
: undefined;
|
|
1631
|
+
|
|
1632
|
+
// Create insertion revision for the single inserted unit (positions already correct)
|
|
1633
|
+
const insertionRevision = createAuthoredRevision(
|
|
1634
|
+
{
|
|
1635
|
+
...reviewState.document.review.revisions,
|
|
1636
|
+
...(deletionRevision ? { [deletionRevision.changeId]: deletionRevision } : {}),
|
|
1637
|
+
},
|
|
1638
|
+
"insertion",
|
|
1639
|
+
insertPos,
|
|
1640
|
+
insertPos + 1,
|
|
1641
|
+
authorId,
|
|
1642
|
+
context.timestamp,
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1646
|
+
...reviewState.document,
|
|
1647
|
+
review: {
|
|
1648
|
+
...reviewState.document.review,
|
|
1649
|
+
revisions: {
|
|
1650
|
+
...reviewState.document.review.revisions,
|
|
1651
|
+
...(deletionRevision ? { [deletionRevision.changeId]: deletionRevision } : {}),
|
|
1652
|
+
[insertionRevision.changeId]: insertionRevision,
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
return createTransaction(
|
|
1658
|
+
{
|
|
1659
|
+
...state,
|
|
1660
|
+
document: finalDocument,
|
|
1661
|
+
selection: result.selection,
|
|
1662
|
+
warnings: reviewState.warnings,
|
|
1663
|
+
runtime: {
|
|
1664
|
+
...state.runtime,
|
|
1665
|
+
activeCommentId: reviewState.activeCommentId,
|
|
1666
|
+
},
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
historyBoundary: "push",
|
|
1670
|
+
markDirty: true,
|
|
1671
|
+
mapping: result.mapping,
|
|
1672
|
+
effects: {
|
|
1673
|
+
...reviewState.effects,
|
|
1674
|
+
revisionAuthored: { changeId: insertionRevision.changeId, kind: "insertion" },
|
|
1675
|
+
},
|
|
1676
|
+
},
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function findOverlappingAuthoredDeletion(
|
|
1681
|
+
revisions: Record<string, CanonicalRevisionRecord>,
|
|
1682
|
+
from: number,
|
|
1683
|
+
to: number,
|
|
1684
|
+
): CanonicalRevisionRecord | undefined {
|
|
1685
|
+
for (const revision of Object.values(revisions)) {
|
|
1686
|
+
if (
|
|
1687
|
+
revision.kind === "deletion" &&
|
|
1688
|
+
revision.status === "open" &&
|
|
1689
|
+
revision.metadata?.source === "runtime" &&
|
|
1690
|
+
revision.anchor.kind === "range"
|
|
1691
|
+
) {
|
|
1692
|
+
const revFrom = revision.anchor.range.from;
|
|
1693
|
+
const revTo = revision.anchor.range.to;
|
|
1694
|
+
// Adjacent or overlapping: the deletion is directly next to or overlapping the range
|
|
1695
|
+
if (
|
|
1696
|
+
(from >= revFrom && from <= revTo) ||
|
|
1697
|
+
(to >= revFrom && to <= revTo) ||
|
|
1698
|
+
(from === revTo) ||
|
|
1699
|
+
(to === revFrom)
|
|
1700
|
+
) {
|
|
1701
|
+
return revision;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return undefined;
|
|
1706
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type {
|
|
|
11
11
|
EditorSessionState,
|
|
12
12
|
EditorHostAdapter,
|
|
13
13
|
WordReviewEditorProps,
|
|
14
|
+
WordReviewEditorChromeVisibility,
|
|
14
15
|
WordReviewEditorRef,
|
|
15
16
|
EditorUser,
|
|
16
17
|
EditorDatastoreAdapter,
|
|
@@ -93,4 +94,8 @@ export type {
|
|
|
93
94
|
WorkflowMarkupItem,
|
|
94
95
|
WorkflowMarkupSnapshot,
|
|
95
96
|
WorkflowCandidateRangeOptions,
|
|
97
|
+
HostAnnotationKind,
|
|
98
|
+
HostAnnotationItem,
|
|
99
|
+
HostAnnotationOverlay,
|
|
100
|
+
HostAnnotationSnapshot,
|
|
96
101
|
} from "./api/public-types.ts";
|