@magic-marker/prosemirror-suggest-changes 0.3.3-wrap-unwrap.17 → 0.3.3-wrap-unwrap.19
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/dist/__tests__/playwrightPage.d.ts +31 -0
- package/dist/commands.js +24 -0
- package/dist/features/joinOnDelete/__tests__/joinOnDeleteInLists.playwright.test.d.ts +1 -0
- package/dist/features/joinOnDelete/__tests__/joinOnDeleteInListsTipTapStyle.playwright.test.d.ts +1 -0
- package/dist/features/joinOnDelete/index.d.ts +1 -17
- package/dist/features/joinOnDelete/index.js +25 -36
- package/dist/features/joinOnDelete/normalizeJoinNodesMetadata.d.ts +6 -0
- package/dist/features/joinOnDelete/normalizeJoinNodesMetadata.js +24 -0
- package/dist/features/joinOnDelete/types.d.ts +34 -0
- package/dist/features/joinOnDelete/types.js +20 -0
- package/dist/features/transactionShaping/detectSpecialTransactionShape.d.ts +3 -0
- package/dist/features/transactionShaping/detectSpecialTransactionShape.js +4 -0
- package/dist/features/transactionShaping/index.d.ts +3 -0
- package/dist/features/transactionShaping/index.js +11 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.d.ts +3 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.js +48 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.test.d.ts +1 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/detectTipTapParagraphIntoListJoin.test.js +188 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/handleTipTapParagraphIntoListJoin.d.ts +3 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/handleTipTapParagraphIntoListJoin.js +80 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/index.d.ts +2 -0
- package/dist/features/transactionShaping/tipTapParagraphIntoListJoin/index.js +2 -0
- package/dist/features/transactionShaping/types.d.ts +20 -0
- package/dist/features/transactionShaping/types.js +1 -0
- package/dist/features/wrapUnwrap/areEquivalentStructureMarks.d.ts +2 -0
- package/dist/features/wrapUnwrap/areEquivalentStructureMarks.js +17 -0
- package/dist/features/wrapUnwrap/types.d.ts +5 -0
- package/dist/features/wrapUnwrap/types.js +6 -0
- package/dist/generateId.js +30 -3
- package/dist/transformToSuggestionTransaction.d.ts +22 -0
- package/dist/transformToSuggestionTransaction.js +101 -0
- package/dist/withSuggestChanges.d.ts +3 -13
- package/dist/withSuggestChanges.js +14 -95
- package/package.json +1 -1
|
@@ -24,4 +24,35 @@ export declare class EditorPage {
|
|
|
24
24
|
}>;
|
|
25
25
|
revertAll(): Promise<void>;
|
|
26
26
|
applyAll(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Perform an action and wait for ProseMirror state to update.
|
|
29
|
+
* Stores the current state reference before pressing, then polls until
|
|
30
|
+
* editor.view.state is a different object (ProseMirror creates a new
|
|
31
|
+
* immutable state on every transaction).
|
|
32
|
+
*/
|
|
33
|
+
private doActionAndWaitForState;
|
|
34
|
+
/**
|
|
35
|
+
* Press a key and wait for ProseMirror state to update.
|
|
36
|
+
* Stores the current state reference before pressing, then polls until
|
|
37
|
+
* editor.view.state is a different object (ProseMirror creates a new
|
|
38
|
+
* immutable state on every transaction).
|
|
39
|
+
*/
|
|
40
|
+
pressKey(key: string, opts?: {
|
|
41
|
+
waitForSelectionChange?: boolean;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Insert text and wait for ProseMirror state to update.
|
|
45
|
+
* Stores the current state reference before pressing, then polls until
|
|
46
|
+
* editor.view.state is a different object (ProseMirror creates a new
|
|
47
|
+
* immutable state on every transaction).
|
|
48
|
+
*/
|
|
49
|
+
insertText(text: string, opts?: {
|
|
50
|
+
waitForSelectionChange?: boolean;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Press a key multiple times, waiting for editor state update after each press.
|
|
54
|
+
*/
|
|
55
|
+
pressKeyMultiple(key: string, count: number, opts?: {
|
|
56
|
+
waitForSelectionChange?: boolean;
|
|
57
|
+
}): Promise<void>;
|
|
27
58
|
}
|
package/dist/commands.js
CHANGED
|
@@ -146,6 +146,10 @@ export function applySuggestionsToNode(node) {
|
|
|
146
146
|
suggestionsTransform.steps.forEach((step)=>{
|
|
147
147
|
structureTransform.step(step);
|
|
148
148
|
});
|
|
149
|
+
const secondStructureTransform = applyAllStructureSuggestions(structureTransform.doc);
|
|
150
|
+
secondStructureTransform.steps.forEach((step)=>{
|
|
151
|
+
structureTransform.step(step);
|
|
152
|
+
});
|
|
149
153
|
return structureTransform.doc;
|
|
150
154
|
}
|
|
151
155
|
export function applySuggestionsToRange(doc, from, to) {
|
|
@@ -163,6 +167,10 @@ export function applySuggestionsToRange(doc, from, to) {
|
|
|
163
167
|
suggestionsTransform.steps.forEach((step)=>{
|
|
164
168
|
structureTransform.step(step);
|
|
165
169
|
});
|
|
170
|
+
const secondStructureTransform = applyAllStructureSuggestions(structureTransform.doc, structureTransform.mapping.map(from), structureTransform.mapping.map(to));
|
|
171
|
+
secondStructureTransform.steps.forEach((step)=>{
|
|
172
|
+
structureTransform.step(step);
|
|
173
|
+
});
|
|
166
174
|
return structureTransform.doc.slice(structureTransform.mapping.map(from), structureTransform.mapping.map(to));
|
|
167
175
|
}
|
|
168
176
|
/**
|
|
@@ -183,6 +191,10 @@ export function applySuggestionsToRange(doc, from, to) {
|
|
|
183
191
|
suggestionsTransform.steps.forEach((step)=>{
|
|
184
192
|
structureTransform.step(step);
|
|
185
193
|
});
|
|
194
|
+
const secondStructureTransform = applyAllStructureSuggestions(structureTransform.doc);
|
|
195
|
+
secondStructureTransform.steps.forEach((step)=>{
|
|
196
|
+
structureTransform.step(step);
|
|
197
|
+
});
|
|
186
198
|
// apply the structure transform to the transaction
|
|
187
199
|
const transaction = state.tr;
|
|
188
200
|
structureTransform.steps.forEach((step)=>{
|
|
@@ -214,6 +226,10 @@ export function applySuggestionsToRange(doc, from, to) {
|
|
|
214
226
|
suggestionsTransform.steps.forEach((step)=>{
|
|
215
227
|
structureTransform.step(step);
|
|
216
228
|
});
|
|
229
|
+
const secondStructureTransform = applyAllStructureSuggestions(structureTransform.doc, from === undefined ? undefined : structureTransform.mapping.map(from), to === undefined ? undefined : structureTransform.mapping.map(to));
|
|
230
|
+
secondStructureTransform.steps.forEach((step)=>{
|
|
231
|
+
structureTransform.step(step);
|
|
232
|
+
});
|
|
217
233
|
// apply the structure transform to the transaction
|
|
218
234
|
const transaction = state.tr;
|
|
219
235
|
structureTransform.steps.forEach((step)=>{
|
|
@@ -277,6 +293,10 @@ export function applySuggestionsToRange(doc, from, to) {
|
|
|
277
293
|
suggestionsTransform.steps.forEach((step)=>{
|
|
278
294
|
structureTransform.step(step);
|
|
279
295
|
});
|
|
296
|
+
const secondStructureTransform = revertAllStructureSuggestions(structureTransform.doc);
|
|
297
|
+
secondStructureTransform.steps.forEach((step)=>{
|
|
298
|
+
structureTransform.step(step);
|
|
299
|
+
});
|
|
280
300
|
// apply the structure transform to the transaction
|
|
281
301
|
const transaction = state.tr;
|
|
282
302
|
structureTransform.steps.forEach((step)=>{
|
|
@@ -308,6 +328,10 @@ export function applySuggestionsToRange(doc, from, to) {
|
|
|
308
328
|
suggestionsTransform.steps.forEach((step)=>{
|
|
309
329
|
structureTransform.step(step);
|
|
310
330
|
});
|
|
331
|
+
const secondStructureTransform = revertAllStructureSuggestions(structureTransform.doc, from === undefined ? undefined : structureTransform.mapping.map(from), to === undefined ? undefined : structureTransform.mapping.map(to));
|
|
332
|
+
secondStructureTransform.steps.forEach((step)=>{
|
|
333
|
+
structureTransform.step(step);
|
|
334
|
+
});
|
|
311
335
|
// apply the structure transform to the transaction
|
|
312
336
|
const transaction = state.tr;
|
|
313
337
|
structureTransform.steps.forEach((step)=>{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/features/joinOnDelete/__tests__/joinOnDeleteInListsTipTapStyle.playwright.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import { Mark, type Node, type
|
|
1
|
+
import { Mark, type Node, type MarkType, type ResolvedPos } from "prosemirror-model";
|
|
2
2
|
import { Transform } from "prosemirror-transform";
|
|
3
3
|
import { type Transaction } from "prosemirror-state";
|
|
4
4
|
import { type SuggestionId } from "../../generateId.js";
|
|
5
|
-
interface SerializedJoinNode {
|
|
6
|
-
type: string;
|
|
7
|
-
attrs: object;
|
|
8
|
-
marks: object[];
|
|
9
|
-
}
|
|
10
|
-
interface JoinMarkAttrs {
|
|
11
|
-
type: "join";
|
|
12
|
-
data: {
|
|
13
|
-
leftNode?: SerializedJoinNode;
|
|
14
|
-
rightNode?: SerializedJoinNode;
|
|
15
|
-
leftNodes?: SerializedJoinNode[];
|
|
16
|
-
rightNodes?: SerializedJoinNode[];
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
export declare function isJoinMarkAttrs(attrs: Attrs): attrs is JoinMarkAttrs;
|
|
20
5
|
export declare function maybeRevertJoinMark(tr: Transform, from: number, to: number, node: Node, markType: MarkType): boolean;
|
|
21
6
|
/**
|
|
22
7
|
* Remove ZWSP text nodes marked as deletions (except for type=join) from the given range
|
|
@@ -33,4 +18,3 @@ export declare function joinNodesAndMarkJoinPoints(trackedTransaction: Transacti
|
|
|
33
18
|
*/
|
|
34
19
|
export declare function collapseZWSPNodes(trackedTransaction: Transaction, from: number, to: number): Transform;
|
|
35
20
|
export declare function findJoinMark(pos: ResolvedPos): Mark | null;
|
|
36
|
-
export {};
|
|
@@ -2,40 +2,10 @@ import { Mark } from "prosemirror-model";
|
|
|
2
2
|
import { canJoin, Transform } from "prosemirror-transform";
|
|
3
3
|
import { ZWSP } from "../../constants.js";
|
|
4
4
|
import { getSuggestionMarks } from "../../utils.js";
|
|
5
|
+
import { areEquivalentStructureMarks } from "../wrapUnwrap/areEquivalentStructureMarks.js";
|
|
5
6
|
import { guardStructureMarkAttrs } from "../wrapUnwrap/types.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const MAX_BLOCK_JOIN_DEPTH = 2;
|
|
9
|
-
function isSerializedJoinNode(node) {
|
|
10
|
-
if (node === null || typeof node !== "object") return false;
|
|
11
|
-
const data = node;
|
|
12
|
-
return typeof data.type === "string" && typeof data.attrs === "object" && data.attrs !== null && Array.isArray(data.marks);
|
|
13
|
-
}
|
|
14
|
-
function normalizeJoinNodes(attrs) {
|
|
15
|
-
if (attrs["type"] !== "join") return false;
|
|
16
|
-
if (attrs["data"] == null) return false;
|
|
17
|
-
const data = attrs["data"];
|
|
18
|
-
// Normalize legacy metadata so revert can use the same array path.
|
|
19
|
-
const leftNodes = data.leftNodes ?? (data.leftNode ? [
|
|
20
|
-
data.leftNode
|
|
21
|
-
] : null);
|
|
22
|
-
const rightNodes = data.rightNodes ?? (data.rightNode ? [
|
|
23
|
-
data.rightNode
|
|
24
|
-
] : null);
|
|
25
|
-
if (!Array.isArray(leftNodes) || !Array.isArray(rightNodes)) return false;
|
|
26
|
-
if (leftNodes.length === 0 || leftNodes.length !== rightNodes.length) return false;
|
|
27
|
-
// Reject unsupported depths instead of partially reverting unknown structure.
|
|
28
|
-
if (leftNodes.length > MAX_BLOCK_JOIN_DEPTH) return false;
|
|
29
|
-
if (!leftNodes.every(isSerializedJoinNode)) return false;
|
|
30
|
-
if (!rightNodes.every(isSerializedJoinNode)) return false;
|
|
31
|
-
return {
|
|
32
|
-
leftNodes,
|
|
33
|
-
rightNodes
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
export function isJoinMarkAttrs(attrs) {
|
|
37
|
-
return normalizeJoinNodes(attrs) !== false;
|
|
38
|
-
}
|
|
7
|
+
import { MAX_BLOCK_JOIN_DEPTH, normalizeJoinNodesMetadata } from "./normalizeJoinNodesMetadata.js";
|
|
8
|
+
import { isJoinMark } from "./types.js";
|
|
39
9
|
function serializeJoinNode(node) {
|
|
40
10
|
return {
|
|
41
11
|
type: node.type.name,
|
|
@@ -46,16 +16,35 @@ function serializeJoinNode(node) {
|
|
|
46
16
|
function marksFromJSON(schema, markData) {
|
|
47
17
|
return markData.map((markData)=>Mark.fromJSON(schema, markData));
|
|
48
18
|
}
|
|
19
|
+
// when we revert a block join deletion mark, we restore nodes on both sides of the mark using leftNodes rightNodes metadata
|
|
20
|
+
// but existing left and right nodes may have structure marks that we need to preserve
|
|
21
|
+
function mergeSerializedMarksWithCurrentStructureMarks(tr, pos, serializedMarks) {
|
|
22
|
+
const currentNode = tr.doc.nodeAt(pos);
|
|
23
|
+
if (!currentNode) return serializedMarks;
|
|
24
|
+
const { structure } = getSuggestionMarks(tr.doc.type.schema);
|
|
25
|
+
const mergedMarks = [
|
|
26
|
+
...serializedMarks
|
|
27
|
+
];
|
|
28
|
+
for (const currentMark of currentNode.marks){
|
|
29
|
+
if (currentMark.type !== structure) continue;
|
|
30
|
+
const alreadyIncluded = mergedMarks.some((mark)=>mark.type === structure && areEquivalentStructureMarks(mark, currentMark));
|
|
31
|
+
if (!alreadyIncluded) {
|
|
32
|
+
mergedMarks.push(currentMark);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return mergedMarks;
|
|
36
|
+
}
|
|
49
37
|
function restoreNodeMarkup(tr, pos, node) {
|
|
50
38
|
const nodeType = tr.doc.type.schema.nodes[node.type];
|
|
51
39
|
if (!nodeType) return false;
|
|
52
|
-
tr
|
|
40
|
+
const marks = mergeSerializedMarksWithCurrentStructureMarks(tr, pos, marksFromJSON(tr.doc.type.schema, node.marks));
|
|
41
|
+
tr.setNodeMarkup(pos, nodeType, node.attrs, marks);
|
|
53
42
|
return true;
|
|
54
43
|
}
|
|
55
44
|
export function maybeRevertJoinMark(tr, from, to, node, markType) {
|
|
56
45
|
const mark = node.marks.find((mark)=>mark.type === markType);
|
|
57
|
-
if (!mark || mark
|
|
58
|
-
const joinNodes =
|
|
46
|
+
if (!mark || !isJoinMark(mark) || node.text !== ZWSP) return false;
|
|
47
|
+
const joinNodes = normalizeJoinNodesMetadata(mark.attrs);
|
|
59
48
|
if (!joinNodes) return false;
|
|
60
49
|
for (const node of [
|
|
61
50
|
...joinNodes.leftNodes,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type JoinMarkAttrs } from "./types.js";
|
|
2
|
+
export declare const MAX_BLOCK_JOIN_DEPTH = 2;
|
|
3
|
+
export declare function normalizeJoinNodesMetadata(attrs: JoinMarkAttrs): false | {
|
|
4
|
+
leftNodes: import("./types.js").SerializedJoinNode[];
|
|
5
|
+
rightNodes: import("./types.js").SerializedJoinNode[];
|
|
6
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isSerializedJoinNode } from "./types.js";
|
|
2
|
+
// Block join suggestion metadata/revert currently supports the depth TipTap uses
|
|
3
|
+
// when Backspace joins both list-item paragraphs and their parent list items.
|
|
4
|
+
export const MAX_BLOCK_JOIN_DEPTH = 2;
|
|
5
|
+
export function normalizeJoinNodesMetadata(attrs) {
|
|
6
|
+
const data = attrs.data;
|
|
7
|
+
// Normalize legacy metadata so revert can use the same array path.
|
|
8
|
+
const leftNodes = data.leftNodes ?? (data.leftNode ? [
|
|
9
|
+
data.leftNode
|
|
10
|
+
] : null);
|
|
11
|
+
const rightNodes = data.rightNodes ?? (data.rightNode ? [
|
|
12
|
+
data.rightNode
|
|
13
|
+
] : null);
|
|
14
|
+
if (!Array.isArray(leftNodes) || !Array.isArray(rightNodes)) return false;
|
|
15
|
+
if (leftNodes.length === 0 || leftNodes.length !== rightNodes.length) return false;
|
|
16
|
+
// Reject unsupported depths instead of partially reverting unknown structure.
|
|
17
|
+
if (leftNodes.length > MAX_BLOCK_JOIN_DEPTH) return false;
|
|
18
|
+
if (!leftNodes.every(isSerializedJoinNode)) return false;
|
|
19
|
+
if (!rightNodes.every(isSerializedJoinNode)) return false;
|
|
20
|
+
return {
|
|
21
|
+
leftNodes,
|
|
22
|
+
rightNodes
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Mark, type Attrs, type Node } from "prosemirror-model";
|
|
2
|
+
export interface SerializedJoinNode {
|
|
3
|
+
type: string;
|
|
4
|
+
attrs: object;
|
|
5
|
+
marks: {
|
|
6
|
+
attrs: Record<string, unknown>;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
export interface JoinMarkAttrs {
|
|
10
|
+
type: "join";
|
|
11
|
+
data: {
|
|
12
|
+
leftNode?: SerializedJoinNode;
|
|
13
|
+
rightNode?: SerializedJoinNode;
|
|
14
|
+
leftNodes?: SerializedJoinNode[];
|
|
15
|
+
rightNodes?: SerializedJoinNode[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface JoinPair {
|
|
19
|
+
leftNode: Node;
|
|
20
|
+
rightNode: Node;
|
|
21
|
+
}
|
|
22
|
+
export interface JoinCandidate {
|
|
23
|
+
joinPos: number;
|
|
24
|
+
leftNodes: Node[];
|
|
25
|
+
rightNodes: Node[];
|
|
26
|
+
}
|
|
27
|
+
export declare function isSerializedJoinNode(node: unknown): node is SerializedJoinNode;
|
|
28
|
+
export declare function isJoinMarkAttrs(attrs: Attrs): attrs is JoinMarkAttrs;
|
|
29
|
+
export declare function isJoinMarkObject(mark: unknown): mark is Omit<Mark, "attrs"> & {
|
|
30
|
+
attrs: JoinMarkAttrs;
|
|
31
|
+
};
|
|
32
|
+
export declare function isJoinMark(mark: Mark): mark is Omit<Mark, "attrs"> & {
|
|
33
|
+
attrs: JoinMarkAttrs;
|
|
34
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getSuggestionMarks } from "../../utils.js";
|
|
2
|
+
export function isSerializedJoinNode(node) {
|
|
3
|
+
if (node === null || typeof node !== "object") return false;
|
|
4
|
+
const data = node;
|
|
5
|
+
return typeof data.type === "string" && typeof data.attrs === "object" && data.attrs !== null && Array.isArray(data.marks);
|
|
6
|
+
}
|
|
7
|
+
export function isJoinMarkAttrs(attrs) {
|
|
8
|
+
if (attrs["type"] !== "join") return false;
|
|
9
|
+
if (attrs["data"] == null) return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
export function isJoinMarkObject(mark) {
|
|
13
|
+
if (mark === null || typeof mark !== "object") return false;
|
|
14
|
+
if (!("attrs" in mark)) return false;
|
|
15
|
+
return isJoinMarkAttrs(mark.attrs);
|
|
16
|
+
}
|
|
17
|
+
export function isJoinMark(mark) {
|
|
18
|
+
const { deletion } = getSuggestionMarks(mark.type.schema);
|
|
19
|
+
return mark.type === deletion && isJoinMarkAttrs(mark.attrs);
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { handleTipTapParagraphIntoListJoin } from "./tipTapParagraphIntoListJoin/index.js";
|
|
2
|
+
const specialTransactionShapeHandlers = [
|
|
3
|
+
handleTipTapParagraphIntoListJoin
|
|
4
|
+
];
|
|
5
|
+
export function handleSpecialTransactionShape(args) {
|
|
6
|
+
for (const handler of specialTransactionShapeHandlers){
|
|
7
|
+
const transaction = handler(args);
|
|
8
|
+
if (transaction) return transaction;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ReplaceStep, Transform } from "prosemirror-transform";
|
|
2
|
+
import { getNodeId } from "../../wrapUnwrap/getNodeId.js";
|
|
3
|
+
// detect a very specific TipTap pattern where joining a paragraph into a list above it
|
|
4
|
+
// causes a 3-step transaction, which is different from normal prosemirror where you just get 1 step
|
|
5
|
+
// see unit test for details
|
|
6
|
+
export function detectTipTapParagraphIntoListJoin(transaction) {
|
|
7
|
+
if (transaction.steps.length !== 3) return null;
|
|
8
|
+
const [deleteStep, insertStep, joinStep] = transaction.steps;
|
|
9
|
+
if (!(deleteStep instanceof ReplaceStep) || !(insertStep instanceof ReplaceStep) || !(joinStep instanceof ReplaceStep)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (deleteStep.slice.content.size !== 0) return null;
|
|
13
|
+
if (insertStep.from !== insertStep.to) return null;
|
|
14
|
+
if (insertStep.slice.openStart !== 0 || insertStep.slice.openEnd !== 0) return null;
|
|
15
|
+
if (insertStep.slice.content.childCount !== 1) return null;
|
|
16
|
+
if (joinStep.slice.content.size !== 0) return null;
|
|
17
|
+
if (joinStep.structure !== true) return null;
|
|
18
|
+
const docBefore = transaction.docs[0];
|
|
19
|
+
if (!docBefore) return null;
|
|
20
|
+
const movedNode = docBefore.nodeAt(deleteStep.from);
|
|
21
|
+
if (!movedNode || !movedNode.isTextblock || movedNode.nodeSize !== deleteStep.to - deleteStep.from) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const insertedNode = insertStep.slice.content.firstChild;
|
|
25
|
+
if (!insertedNode?.isTextblock) return null;
|
|
26
|
+
if (insertedNode.type !== movedNode.type) return null;
|
|
27
|
+
if (insertedNode.textContent !== movedNode.textContent) return null;
|
|
28
|
+
const movedNodeId = getNodeId(movedNode);
|
|
29
|
+
if (!movedNodeId || getNodeId(insertedNode) !== movedNodeId) return null;
|
|
30
|
+
const previousSibling = docBefore.resolve(deleteStep.from).nodeBefore;
|
|
31
|
+
if (!previousSibling || previousSibling.isInline) return null;
|
|
32
|
+
try {
|
|
33
|
+
const preview = new Transform(docBefore);
|
|
34
|
+
preview.step(deleteStep);
|
|
35
|
+
preview.step(insertStep);
|
|
36
|
+
new Transform(preview.doc).step(joinStep);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
console.warn("[prosemirror-suggest-changes]", "detected TipTap paragraph into list join shape");
|
|
41
|
+
return {
|
|
42
|
+
type: "tipTapParagraphIntoListJoin",
|
|
43
|
+
deleteStep,
|
|
44
|
+
insertStep,
|
|
45
|
+
joinStep,
|
|
46
|
+
movedNode
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { EditorState } from "prosemirror-state";
|
|
2
|
+
import { Step } from "prosemirror-transform";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createSchema } from "../../../testing/e2eTestSchema.js";
|
|
5
|
+
import { detectTipTapParagraphIntoListJoin } from "./detectTipTapParagraphIntoListJoin.js";
|
|
6
|
+
const schema = createSchema();
|
|
7
|
+
// when joining a paragraph into a list above it,
|
|
8
|
+
// TipTap List extension overrides the default ProseMirror behavior
|
|
9
|
+
// by default, ProseMirror puts the paragraph to the end of list as a separate list item
|
|
10
|
+
// TipTap instead joins the paragraph with the paragraph of the last list item
|
|
11
|
+
const TIPTAP_PARAGRAPH_INTO_LIST_STEPS = [
|
|
12
|
+
// first step deletes the paragraph that we're joining
|
|
13
|
+
{
|
|
14
|
+
stepType: "replace",
|
|
15
|
+
from: 42,
|
|
16
|
+
to: 60
|
|
17
|
+
},
|
|
18
|
+
// second step inserts that paragraph into the last list item
|
|
19
|
+
{
|
|
20
|
+
stepType: "replace",
|
|
21
|
+
from: 40,
|
|
22
|
+
to: 40,
|
|
23
|
+
slice: {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "paragraph",
|
|
27
|
+
attrs: {
|
|
28
|
+
id: "node-9",
|
|
29
|
+
textAlign: null
|
|
30
|
+
},
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: "sample paragraph"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
// third step joins the two paragraphs in the list item
|
|
42
|
+
{
|
|
43
|
+
stepType: "replace",
|
|
44
|
+
from: 39,
|
|
45
|
+
to: 41,
|
|
46
|
+
structure: true
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
const TIPTAP_PARAGRAPH_INTO_LIST_DOC = {
|
|
50
|
+
type: "doc",
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "orderedList",
|
|
54
|
+
attrs: {
|
|
55
|
+
order: 1,
|
|
56
|
+
id: "node-0"
|
|
57
|
+
},
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "listItem",
|
|
61
|
+
attrs: {
|
|
62
|
+
id: "node-1"
|
|
63
|
+
},
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "paragraph",
|
|
67
|
+
attrs: {
|
|
68
|
+
id: "node-2"
|
|
69
|
+
},
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: "Item 1"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "listItem",
|
|
81
|
+
attrs: {
|
|
82
|
+
id: "node-3"
|
|
83
|
+
},
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "paragraph",
|
|
87
|
+
attrs: {
|
|
88
|
+
id: "node-4"
|
|
89
|
+
},
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: "Item 2"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: "listItem",
|
|
101
|
+
attrs: {
|
|
102
|
+
id: "node-5"
|
|
103
|
+
},
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "paragraph",
|
|
107
|
+
attrs: {
|
|
108
|
+
id: "node-6"
|
|
109
|
+
},
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: "Item 3"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: "listItem",
|
|
121
|
+
attrs: {
|
|
122
|
+
id: "node-7"
|
|
123
|
+
},
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "paragraph",
|
|
127
|
+
attrs: {
|
|
128
|
+
id: "node-8"
|
|
129
|
+
},
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: "Item 4"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: "paragraph",
|
|
143
|
+
attrs: {
|
|
144
|
+
id: "node-9"
|
|
145
|
+
},
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: "sample paragraph"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
describe("detectTipTapParagraphIntoListJoin", ()=>{
|
|
156
|
+
it("detects TipTap's three-step paragraph-into-list join shape", ()=>{
|
|
157
|
+
const doc = schema.nodeFromJSON(TIPTAP_PARAGRAPH_INTO_LIST_DOC);
|
|
158
|
+
const state = EditorState.create({
|
|
159
|
+
doc
|
|
160
|
+
});
|
|
161
|
+
const transaction = state.tr;
|
|
162
|
+
TIPTAP_PARAGRAPH_INTO_LIST_STEPS.forEach((stepJSON)=>{
|
|
163
|
+
transaction.step(Step.fromJSON(schema, stepJSON));
|
|
164
|
+
});
|
|
165
|
+
const shape = detectTipTapParagraphIntoListJoin(transaction);
|
|
166
|
+
expect(shape).toMatchObject({
|
|
167
|
+
type: "tipTapParagraphIntoListJoin",
|
|
168
|
+
deleteStep: {
|
|
169
|
+
from: 42,
|
|
170
|
+
to: 60
|
|
171
|
+
},
|
|
172
|
+
insertStep: {
|
|
173
|
+
from: 40,
|
|
174
|
+
to: 40
|
|
175
|
+
},
|
|
176
|
+
joinStep: {
|
|
177
|
+
from: 39,
|
|
178
|
+
to: 41
|
|
179
|
+
},
|
|
180
|
+
movedNode: {
|
|
181
|
+
attrs: {
|
|
182
|
+
id: "node-9"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
expect(shape?.movedNode.textContent).toBe("sample paragraph");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { EditorState } from "prosemirror-state";
|
|
2
|
+
import { preserveTransactionData, transformToSuggestionTransaction } from "../../../transformToSuggestionTransaction.js";
|
|
3
|
+
import { getSuggestionMarks } from "../../../utils.js";
|
|
4
|
+
import { getNodeId } from "../../wrapUnwrap/getNodeId.js";
|
|
5
|
+
import { suggestStructureChanges } from "../../wrapUnwrap/structureChangesPlugin.js";
|
|
6
|
+
import { guardStructureMarkAttrs } from "../../wrapUnwrap/types.js";
|
|
7
|
+
import { detectSpecialTransactionShape } from "../detectSpecialTransactionShape.js";
|
|
8
|
+
// handle the specific TipTap pattern when backspacing from a paragraph into a list above
|
|
9
|
+
// causes a transaction with 3 steps
|
|
10
|
+
// "slice" the transaction into two pieces, first piece is the first 2 steps, second piece is the last step
|
|
11
|
+
// first piece "moves" the paragraph - handled by structure change tracking - a structure "move" suggestion
|
|
12
|
+
// second piece joins two paragraphs - handled by the main plugin join-on-delete feature - deletion type="join" mark
|
|
13
|
+
export function handleTipTapParagraphIntoListJoin(args) {
|
|
14
|
+
const shape = detectSpecialTransactionShape(args.transaction);
|
|
15
|
+
if (!isTipTapParagraphIntoListJoinShape(shape)) return null;
|
|
16
|
+
if (!args.structuralContextPaths || !args.ensureUniqueNodeIds) return null;
|
|
17
|
+
const docBefore = args.transaction.docs[0];
|
|
18
|
+
if (!docBefore) return null;
|
|
19
|
+
const movedNodeId = getNodeId(shape.movedNode);
|
|
20
|
+
if (!movedNodeId) return null;
|
|
21
|
+
const trackedTransaction = args.state.tr;
|
|
22
|
+
try {
|
|
23
|
+
trackedTransaction.step(shape.deleteStep);
|
|
24
|
+
trackedTransaction.step(shape.insertStep);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const uniqueNodeIdsTransform = args.ensureUniqueNodeIds([
|
|
29
|
+
args.transaction
|
|
30
|
+
], docBefore, trackedTransaction.doc);
|
|
31
|
+
uniqueNodeIdsTransform.steps.forEach((step)=>{
|
|
32
|
+
trackedTransaction.step(step);
|
|
33
|
+
});
|
|
34
|
+
const structureChangesResult = suggestStructureChanges(docBefore, uniqueNodeIdsTransform.doc, args.structuralContextPaths, args.generateId);
|
|
35
|
+
if (!structureChangesResult.handled || !hasMoveStructureMark(structureChangesResult, movedNodeId)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
structureChangesResult.transform.steps.forEach((step)=>{
|
|
39
|
+
trackedTransaction.step(step);
|
|
40
|
+
});
|
|
41
|
+
const intermediateState = EditorState.create({
|
|
42
|
+
schema: args.state.schema,
|
|
43
|
+
doc: trackedTransaction.doc
|
|
44
|
+
});
|
|
45
|
+
const joinTransaction = intermediateState.tr;
|
|
46
|
+
try {
|
|
47
|
+
joinTransaction.step(shape.joinStep);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const trackedJoinTransaction = transformToSuggestionTransaction(joinTransaction, intermediateState, args.generateId);
|
|
52
|
+
trackedJoinTransaction.steps.forEach((step)=>{
|
|
53
|
+
trackedTransaction.step(step);
|
|
54
|
+
});
|
|
55
|
+
preserveTransactionData(trackedTransaction, trackedJoinTransaction, {
|
|
56
|
+
selection: "currentDocument",
|
|
57
|
+
preserveScroll: false,
|
|
58
|
+
preserveStoredMarks: false,
|
|
59
|
+
preserveMeta: false
|
|
60
|
+
});
|
|
61
|
+
preserveTransactionData(trackedTransaction, args.transaction);
|
|
62
|
+
return trackedTransaction;
|
|
63
|
+
}
|
|
64
|
+
function isTipTapParagraphIntoListJoinShape(shape) {
|
|
65
|
+
return shape?.type === "tipTapParagraphIntoListJoin";
|
|
66
|
+
}
|
|
67
|
+
function hasMoveStructureMark(structureChangesResult, movedNodeId) {
|
|
68
|
+
const { structure } = getSuggestionMarks(structureChangesResult.transform.doc.type.schema);
|
|
69
|
+
let found = false;
|
|
70
|
+
structureChangesResult.transform.doc.descendants((node)=>{
|
|
71
|
+
if (getNodeId(node) !== movedNodeId) return true;
|
|
72
|
+
found = node.marks.some((mark)=>{
|
|
73
|
+
if (mark.type !== structure) return false;
|
|
74
|
+
if (!guardStructureMarkAttrs(mark.attrs)) return false;
|
|
75
|
+
return mark.attrs.data.op.op === "move";
|
|
76
|
+
});
|
|
77
|
+
return !found;
|
|
78
|
+
});
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Node, type Schema } from "prosemirror-model";
|
|
2
|
+
import { type EditorState, type Transaction } from "prosemirror-state";
|
|
3
|
+
import { type ReplaceStep, type Transform } from "prosemirror-transform";
|
|
4
|
+
import { type SuggestionId } from "../../generateId.js";
|
|
5
|
+
import { type StructuralContextPath } from "../wrapUnwrap/types.js";
|
|
6
|
+
export interface TipTapParagraphIntoListJoinShape {
|
|
7
|
+
type: "tipTapParagraphIntoListJoin";
|
|
8
|
+
deleteStep: ReplaceStep;
|
|
9
|
+
insertStep: ReplaceStep;
|
|
10
|
+
joinStep: ReplaceStep;
|
|
11
|
+
movedNode: Node;
|
|
12
|
+
}
|
|
13
|
+
export type SpecialTransactionShape = TipTapParagraphIntoListJoinShape;
|
|
14
|
+
export interface HandleSpecialTransactionShapeArgs {
|
|
15
|
+
transaction: Transaction;
|
|
16
|
+
state: EditorState;
|
|
17
|
+
generateId: ((schema: Schema, doc?: Node) => SuggestionId) | undefined;
|
|
18
|
+
structuralContextPaths: StructuralContextPath[] | null;
|
|
19
|
+
ensureUniqueNodeIds: ((transactions: Transaction[], oldDoc: Node, newDoc: Node) => Transform) | undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sameParentChain } from "./sameParentChain.js";
|
|
2
|
+
import { guardStructureMarkAttrs } from "./types.js";
|
|
3
|
+
export function areEquivalentStructureMarks(a, b) {
|
|
4
|
+
if (a.type !== b.type) return false;
|
|
5
|
+
if (!guardStructureMarkAttrs(a.attrs)) return false;
|
|
6
|
+
if (!guardStructureMarkAttrs(b.attrs)) return false;
|
|
7
|
+
const aOp = a.attrs.data.op;
|
|
8
|
+
const bOp = b.attrs.data.op;
|
|
9
|
+
if (aOp.op !== bOp.op) return false;
|
|
10
|
+
if (aOp.op === "add" && bOp.op === "add") {
|
|
11
|
+
return a.attrs.id === b.attrs.id;
|
|
12
|
+
}
|
|
13
|
+
if (aOp.op === "move" && bOp.op === "move") {
|
|
14
|
+
return sameParentChain(aOp.from, bOp.from) && sameParentChain(aOp.to, bOp.to);
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
@@ -37,4 +37,9 @@ export type MaterializedPaths = Map<string, {
|
|
|
37
37
|
}>;
|
|
38
38
|
export declare function guardDocParent(parent: Parent | undefined): parent is DocParent;
|
|
39
39
|
export declare function guardStructureMarkAttrs(attrs: Attrs): attrs is StructureMarkAttrs;
|
|
40
|
+
export interface StructureMarkObject {
|
|
41
|
+
type: "structure";
|
|
42
|
+
attrs: StructureMarkAttrs;
|
|
43
|
+
}
|
|
44
|
+
export declare function isStructureMarkObject(mark: unknown): mark is StructureMarkObject;
|
|
40
45
|
export {};
|
|
@@ -9,3 +9,9 @@ export function guardStructureMarkAttrs(attrs) {
|
|
|
9
9
|
if (!("op" in data)) return false;
|
|
10
10
|
return true;
|
|
11
11
|
}
|
|
12
|
+
export function isStructureMarkObject(mark) {
|
|
13
|
+
if (mark === null || typeof mark !== "object") return false;
|
|
14
|
+
if (!("type" in mark) || mark.type !== "structure") return false;
|
|
15
|
+
if (!("attrs" in mark)) return false;
|
|
16
|
+
return guardStructureMarkAttrs(mark.attrs);
|
|
17
|
+
}
|
package/dist/generateId.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getSuggestionMarks } from "./utils.js";
|
|
2
|
+
import { isJoinMark } from "./features/joinOnDelete/types.js";
|
|
3
|
+
import { normalizeJoinNodesMetadata } from "./features/joinOnDelete/normalizeJoinNodesMetadata.js";
|
|
2
4
|
export const suggestionIdValidate = "number|string";
|
|
3
5
|
export function parseSuggestionId(id) {
|
|
4
6
|
const parsed = parseInt(id, 10);
|
|
@@ -13,9 +15,34 @@ export function generateNextNumberId(schema, doc) {
|
|
|
13
15
|
// and use that as the starting point for new changes
|
|
14
16
|
let suggestionId = 0;
|
|
15
17
|
doc?.descendants((node)=>{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// find max suggestion id across all suggestion marks and across all suggestion marks that are serialized into metadata of the join marks
|
|
19
|
+
const marks = node.marks.filter((mark)=>mark.type === insertion || mark.type === deletion || mark.type === modification || mark.type === structure);
|
|
20
|
+
const markIds = marks.map((mark)=>mark.attrs["id"]);
|
|
21
|
+
// collect suggestion ids of marks that are serialized into join marks metadata
|
|
22
|
+
const joinMarks = marks.filter((mark)=>isJoinMark(mark));
|
|
23
|
+
const joinMetadataMarkIds = [];
|
|
24
|
+
joinMarks.forEach((mark)=>{
|
|
25
|
+
const joinMetadata = normalizeJoinNodesMetadata(mark.attrs);
|
|
26
|
+
if (!joinMetadata) return;
|
|
27
|
+
joinMetadata.leftNodes.forEach((node)=>{
|
|
28
|
+
node.marks.forEach((mark)=>{
|
|
29
|
+
if (!mark.attrs["id"]) return;
|
|
30
|
+
joinMetadataMarkIds.push(mark.attrs["id"]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
joinMetadata.rightNodes.forEach((node)=>{
|
|
34
|
+
node.marks.forEach((mark)=>{
|
|
35
|
+
if (!mark.attrs["id"]) return;
|
|
36
|
+
joinMetadataMarkIds.push(mark.attrs["id"]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const allMarkIds = [
|
|
41
|
+
...markIds,
|
|
42
|
+
...joinMetadataMarkIds
|
|
43
|
+
];
|
|
44
|
+
if (allMarkIds.length > 0) {
|
|
45
|
+
suggestionId = Math.max(suggestionId, ...allMarkIds);
|
|
19
46
|
return false;
|
|
20
47
|
}
|
|
21
48
|
return true;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Node, type Schema } from "prosemirror-model";
|
|
2
|
+
import { type EditorState, type Transaction } from "prosemirror-state";
|
|
3
|
+
import { type SuggestionId } from "./generateId.js";
|
|
4
|
+
interface PreserveTransactionDataOptions {
|
|
5
|
+
selection?: "mapFromOriginalTransaction" | "currentDocument" | false;
|
|
6
|
+
preserveScroll?: boolean;
|
|
7
|
+
preserveStoredMarks?: boolean;
|
|
8
|
+
preserveMeta?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function preserveTransactionData(transaction: Transaction, originalTransaction: Transaction, options?: PreserveTransactionDataOptions): void;
|
|
11
|
+
/**
|
|
12
|
+
* Given a standard transaction from ProseMirror, produce
|
|
13
|
+
* a new transaction that tracks the changes from the original,
|
|
14
|
+
* rather than applying them.
|
|
15
|
+
*
|
|
16
|
+
* For each type of step, we implement custom behavior to prevent
|
|
17
|
+
* deletions from being removed from the document, instead adding
|
|
18
|
+
* deletion marks, and ensuring that all insertions have insertion
|
|
19
|
+
* marks.
|
|
20
|
+
*/
|
|
21
|
+
export declare function transformToSuggestionTransaction(originalTransaction: Transaction, state: EditorState, generateId?: (schema: Schema, doc?: Node) => SuggestionId): Transaction;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AddMarkStep, AddNodeMarkStep, AttrStep, Mapping, RemoveMarkStep, RemoveNodeMarkStep, ReplaceAroundStep, ReplaceStep } from "prosemirror-transform";
|
|
2
|
+
import { trackAddMarkStep } from "./addMarkStep.js";
|
|
3
|
+
import { trackAddNodeMarkStep } from "./addNodeMarkStep.js";
|
|
4
|
+
import { trackAttrStep } from "./attrStep.js";
|
|
5
|
+
import { generateNextNumberId } from "./generateId.js";
|
|
6
|
+
import { suggestRemoveMarkStep } from "./removeMarkStep.js";
|
|
7
|
+
import { suggestRemoveNodeMarkStep } from "./removeNodeMarkStep.js";
|
|
8
|
+
import { suggestReplaceAroundStep } from "./replaceAroundStep.js";
|
|
9
|
+
import { suggestReplaceStep } from "./replaceStep.js";
|
|
10
|
+
import { getSuggestionMarks } from "./utils.js";
|
|
11
|
+
function getStepHandler(step) {
|
|
12
|
+
if (step instanceof ReplaceStep) {
|
|
13
|
+
return suggestReplaceStep;
|
|
14
|
+
}
|
|
15
|
+
if (step instanceof ReplaceAroundStep) {
|
|
16
|
+
return suggestReplaceAroundStep;
|
|
17
|
+
}
|
|
18
|
+
if (step instanceof AddMarkStep) {
|
|
19
|
+
return trackAddMarkStep;
|
|
20
|
+
}
|
|
21
|
+
if (step instanceof RemoveMarkStep) {
|
|
22
|
+
return suggestRemoveMarkStep;
|
|
23
|
+
}
|
|
24
|
+
if (step instanceof AddNodeMarkStep) {
|
|
25
|
+
return trackAddNodeMarkStep;
|
|
26
|
+
}
|
|
27
|
+
if (step instanceof RemoveNodeMarkStep) {
|
|
28
|
+
return suggestRemoveNodeMarkStep;
|
|
29
|
+
}
|
|
30
|
+
if (step instanceof AttrStep) {
|
|
31
|
+
return trackAttrStep;
|
|
32
|
+
}
|
|
33
|
+
// Default handler — simply rebase the step onto the
|
|
34
|
+
// tracked transaction and apply it.
|
|
35
|
+
return (trackedTransaction, _state, _doc, step, prevSteps)=>{
|
|
36
|
+
const reset = prevSteps.slice().reverse().reduce((acc, step)=>acc?.map(step.getMap().invert()) ?? null, step);
|
|
37
|
+
const rebased = trackedTransaction.steps.reduce((acc, step)=>acc?.map(step.getMap()) ?? null, reset);
|
|
38
|
+
if (rebased) {
|
|
39
|
+
trackedTransaction.step(rebased);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function preserveTransactionData(transaction, originalTransaction, options = {}) {
|
|
45
|
+
const { selection = "mapFromOriginalTransaction", preserveScroll = true, preserveStoredMarks = true, preserveMeta = true } = options;
|
|
46
|
+
if (selection && originalTransaction.selectionSet && !transaction.selectionSet) {
|
|
47
|
+
if (selection === "currentDocument") {
|
|
48
|
+
transaction.setSelection(originalTransaction.selection.map(transaction.doc, new Mapping()));
|
|
49
|
+
} else {
|
|
50
|
+
const originalBaseDoc = originalTransaction.docs[0];
|
|
51
|
+
const base = originalBaseDoc ? originalTransaction.selection.map(originalBaseDoc, originalTransaction.mapping.invert()) : originalTransaction.selection;
|
|
52
|
+
transaction.setSelection(base.map(transaction.doc, transaction.mapping));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (preserveScroll && originalTransaction.scrolledIntoView) {
|
|
56
|
+
transaction.scrollIntoView();
|
|
57
|
+
}
|
|
58
|
+
if (preserveStoredMarks && originalTransaction.storedMarksSet) {
|
|
59
|
+
transaction.setStoredMarks(originalTransaction.storedMarks);
|
|
60
|
+
}
|
|
61
|
+
if (preserveMeta) {
|
|
62
|
+
// @ts-expect-error Preserve original transaction meta exactly as-is
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
64
|
+
transaction.meta = originalTransaction.meta;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Given a standard transaction from ProseMirror, produce
|
|
69
|
+
* a new transaction that tracks the changes from the original,
|
|
70
|
+
* rather than applying them.
|
|
71
|
+
*
|
|
72
|
+
* For each type of step, we implement custom behavior to prevent
|
|
73
|
+
* deletions from being removed from the document, instead adding
|
|
74
|
+
* deletion marks, and ensuring that all insertions have insertion
|
|
75
|
+
* marks.
|
|
76
|
+
*/ export function transformToSuggestionTransaction(originalTransaction, state, generateId) {
|
|
77
|
+
getSuggestionMarks(state.schema);
|
|
78
|
+
let suggestionId = generateId ? generateId(state.schema, originalTransaction.docs[0]) : generateNextNumberId(state.schema, originalTransaction.docs[0]);
|
|
79
|
+
// Create a new transaction from scratch. The original transaction
|
|
80
|
+
// is going to be dropped in favor of this one.
|
|
81
|
+
const trackedTransaction = state.tr;
|
|
82
|
+
for(let i = 0; i < originalTransaction.steps.length; i++){
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
84
|
+
const step = originalTransaction.steps[i];
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
86
|
+
const doc = originalTransaction.docs[i];
|
|
87
|
+
const stepTracker = getStepHandler(step);
|
|
88
|
+
if (stepTracker(trackedTransaction, state, doc, step, originalTransaction.steps.slice(0, i), suggestionId) && i < originalTransaction.steps.length - 1) {
|
|
89
|
+
// If the suggestionId was used by one of the step handlers,
|
|
90
|
+
// increment it so that it's not reused.
|
|
91
|
+
if (generateId) {
|
|
92
|
+
suggestionId = generateId(state.schema, trackedTransaction.doc);
|
|
93
|
+
} else if (typeof suggestionId === "number") {
|
|
94
|
+
suggestionId = suggestionId + 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
preserveTransactionData(trackedTransaction, originalTransaction);
|
|
100
|
+
return trackedTransaction;
|
|
101
|
+
}
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { type Node, type Schema } from "prosemirror-model";
|
|
2
|
+
import { type Transaction } from "prosemirror-state";
|
|
3
3
|
import { type Transform } from "prosemirror-transform";
|
|
4
4
|
import { type EditorView } from "prosemirror-view";
|
|
5
5
|
import { type SuggestionId } from "./generateId.js";
|
|
6
6
|
import { type StructuralContextPath } from "./features/wrapUnwrap/types.js";
|
|
7
|
-
|
|
8
|
-
* Given a standard transaction from ProseMirror, produce
|
|
9
|
-
* a new transaction that tracks the changes from the original,
|
|
10
|
-
* rather than applying them.
|
|
11
|
-
*
|
|
12
|
-
* For each type of step, we implement custom behavior to prevent
|
|
13
|
-
* deletions from being removed from the document, instead adding
|
|
14
|
-
* deletion marks, and ensuring that all insertions have insertion
|
|
15
|
-
* marks.
|
|
16
|
-
*/
|
|
17
|
-
export declare function transformToSuggestionTransaction(originalTransaction: Transaction, state: EditorState, generateId?: (schema: Schema, doc?: Node) => SuggestionId): Transaction;
|
|
7
|
+
export { transformToSuggestionTransaction } from "./transformToSuggestionTransaction.js";
|
|
18
8
|
/**
|
|
19
9
|
* A `dispatchTransaction` decorator. Wrap your existing `dispatchTransaction`
|
|
20
10
|
* function with `withSuggestChanges`, or pass no arguments to use the default
|
|
@@ -1,105 +1,15 @@
|
|
|
1
|
-
import { AddMarkStep, AddNodeMarkStep, AttrStep, RemoveMarkStep, RemoveNodeMarkStep, ReplaceAroundStep, ReplaceStep } from "prosemirror-transform";
|
|
2
|
-
import { trackAddMarkStep } from "./addMarkStep.js";
|
|
3
|
-
import { trackAddNodeMarkStep } from "./addNodeMarkStep.js";
|
|
4
|
-
import { trackAttrStep } from "./attrStep.js";
|
|
5
|
-
import { suggestRemoveMarkStep } from "./removeMarkStep.js";
|
|
6
|
-
import { suggestRemoveNodeMarkStep } from "./removeNodeMarkStep.js";
|
|
7
|
-
import { suggestReplaceAroundStep } from "./replaceAroundStep.js";
|
|
8
|
-
import { suggestReplaceStep } from "./replaceStep.js";
|
|
9
1
|
import { isSuggestChangesEnabled, suggestChangesKey } from "./plugin.js";
|
|
10
|
-
import { generateNextNumberId } from "./generateId.js";
|
|
11
|
-
import { getSuggestionMarks } from "./utils.js";
|
|
12
2
|
import { prependDeletionsWithZWSP } from "./prependDeletionsWithZWSP.js";
|
|
13
3
|
import { getRequiredStructuralContextPaths, suggestStructureChanges } from "./features/wrapUnwrap/structureChangesPlugin.js";
|
|
4
|
+
import { handleSpecialTransactionShape } from "./features/transactionShaping/index.js";
|
|
5
|
+
import { transformToSuggestionTransaction } from "./transformToSuggestionTransaction.js";
|
|
6
|
+
export { transformToSuggestionTransaction } from "./transformToSuggestionTransaction.js";
|
|
14
7
|
const TRACE_ENABLED = true;
|
|
15
8
|
function trace(...args) {
|
|
16
9
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
17
10
|
if (!TRACE_ENABLED) return;
|
|
18
11
|
console.log("[withSuggestChanges]", ...args);
|
|
19
12
|
}
|
|
20
|
-
function getStepHandler(step) {
|
|
21
|
-
if (step instanceof ReplaceStep) {
|
|
22
|
-
return suggestReplaceStep;
|
|
23
|
-
}
|
|
24
|
-
if (step instanceof ReplaceAroundStep) {
|
|
25
|
-
return suggestReplaceAroundStep;
|
|
26
|
-
}
|
|
27
|
-
if (step instanceof AddMarkStep) {
|
|
28
|
-
return trackAddMarkStep;
|
|
29
|
-
}
|
|
30
|
-
if (step instanceof RemoveMarkStep) {
|
|
31
|
-
return suggestRemoveMarkStep;
|
|
32
|
-
}
|
|
33
|
-
if (step instanceof AddNodeMarkStep) {
|
|
34
|
-
return trackAddNodeMarkStep;
|
|
35
|
-
}
|
|
36
|
-
if (step instanceof RemoveNodeMarkStep) {
|
|
37
|
-
return suggestRemoveNodeMarkStep;
|
|
38
|
-
}
|
|
39
|
-
if (step instanceof AttrStep) {
|
|
40
|
-
return trackAttrStep;
|
|
41
|
-
}
|
|
42
|
-
// Default handler — simply rebase the step onto the
|
|
43
|
-
// tracked transaction and apply it.
|
|
44
|
-
return (trackedTransaction, _state, _doc, step, prevSteps)=>{
|
|
45
|
-
const reset = prevSteps.slice().reverse().reduce((acc, step)=>acc?.map(step.getMap().invert()) ?? null, step);
|
|
46
|
-
const rebased = trackedTransaction.steps.reduce((acc, step)=>acc?.map(step.getMap()) ?? null, reset);
|
|
47
|
-
if (rebased) {
|
|
48
|
-
trackedTransaction.step(rebased);
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Given a standard transaction from ProseMirror, produce
|
|
55
|
-
* a new transaction that tracks the changes from the original,
|
|
56
|
-
* rather than applying them.
|
|
57
|
-
*
|
|
58
|
-
* For each type of step, we implement custom behavior to prevent
|
|
59
|
-
* deletions from being removed from the document, instead adding
|
|
60
|
-
* deletion marks, and ensuring that all insertions have insertion
|
|
61
|
-
* marks.
|
|
62
|
-
*/ export function transformToSuggestionTransaction(originalTransaction, state, generateId) {
|
|
63
|
-
getSuggestionMarks(state.schema);
|
|
64
|
-
let suggestionId = generateId ? generateId(state.schema, originalTransaction.docs[0]) : generateNextNumberId(state.schema, originalTransaction.docs[0]);
|
|
65
|
-
// Create a new transaction from scratch. The original transaction
|
|
66
|
-
// is going to be dropped in favor of this one.
|
|
67
|
-
const trackedTransaction = state.tr;
|
|
68
|
-
for(let i = 0; i < originalTransaction.steps.length; i++){
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
70
|
-
const step = originalTransaction.steps[i];
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
72
|
-
const doc = originalTransaction.docs[i];
|
|
73
|
-
const stepTracker = getStepHandler(step);
|
|
74
|
-
if (stepTracker(trackedTransaction, state, doc, step, originalTransaction.steps.slice(0, i), suggestionId) && i < originalTransaction.steps.length - 1) {
|
|
75
|
-
// If the suggestionId was used by one of the step handlers,
|
|
76
|
-
// increment it so that it's not reused.
|
|
77
|
-
if (generateId) {
|
|
78
|
-
suggestionId = generateId(state.schema, trackedTransaction.doc);
|
|
79
|
-
} else if (typeof suggestionId === "number") {
|
|
80
|
-
suggestionId = suggestionId + 1;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (originalTransaction.selectionSet && !trackedTransaction.selectionSet) {
|
|
86
|
-
// Map the original selection backwards through the original transaction,
|
|
87
|
-
// and then forwards through the new one.
|
|
88
|
-
const originalBaseDoc = originalTransaction.docs[0];
|
|
89
|
-
const base = originalBaseDoc ? originalTransaction.selection.map(originalBaseDoc, originalTransaction.mapping.invert()) : originalTransaction.selection;
|
|
90
|
-
trackedTransaction.setSelection(base.map(trackedTransaction.doc, trackedTransaction.mapping));
|
|
91
|
-
}
|
|
92
|
-
if (originalTransaction.scrolledIntoView) {
|
|
93
|
-
trackedTransaction.scrollIntoView();
|
|
94
|
-
}
|
|
95
|
-
if (originalTransaction.storedMarksSet) {
|
|
96
|
-
trackedTransaction.setStoredMarks(originalTransaction.storedMarks);
|
|
97
|
-
}
|
|
98
|
-
// @ts-expect-error Preserve original transaction meta exactly as-is
|
|
99
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
100
|
-
trackedTransaction.meta = originalTransaction.meta;
|
|
101
|
-
return trackedTransaction;
|
|
102
|
-
}
|
|
103
13
|
/**
|
|
104
14
|
* A `dispatchTransaction` decorator. Wrap your existing `dispatchTransaction`
|
|
105
15
|
* function with `withSuggestChanges`, or pass no arguments to use the default
|
|
@@ -123,7 +33,14 @@ function getStepHandler(step) {
|
|
|
123
33
|
const docBefore = transaction.docs[0];
|
|
124
34
|
const structuralContextPaths = opts?.experimental_trackStructureChanges ? getRequiredStructuralContextPaths(opts.experimental_trackStructures) : null;
|
|
125
35
|
const ensureUniqueNodeIds = opts?.experimental_ensureUniqueNodeIds;
|
|
126
|
-
|
|
36
|
+
const shapedTransaction = transaction.docChanged ? handleSpecialTransactionShape({
|
|
37
|
+
transaction,
|
|
38
|
+
state: this.state,
|
|
39
|
+
generateId,
|
|
40
|
+
structuralContextPaths,
|
|
41
|
+
ensureUniqueNodeIds
|
|
42
|
+
}) : null;
|
|
43
|
+
if (!shapedTransaction && transaction.docChanged && docBefore && structuralContextPaths && typeof ensureUniqueNodeIds === "function") {
|
|
127
44
|
trace("trying to track structure changes first...");
|
|
128
45
|
// after a transaction, some nodes may not yet have unique ids (they were just added, and the unique id plugin has not yet run)
|
|
129
46
|
// this hook allows to "post-process" the transaction and add the missing ids
|
|
@@ -152,7 +69,9 @@ function getStepHandler(step) {
|
|
|
152
69
|
trace("applied unique id transform and structure changes transform to the transaction", transaction);
|
|
153
70
|
}
|
|
154
71
|
}
|
|
155
|
-
if (
|
|
72
|
+
if (shapedTransaction) {
|
|
73
|
+
transaction = shapedTransaction;
|
|
74
|
+
} else if (transaction.docChanged && structureChangesResult?.handled !== true) {
|
|
156
75
|
trace("running the main suggestions plugin...");
|
|
157
76
|
const perfSuggestions = performance.now();
|
|
158
77
|
transaction = transformToSuggestionTransaction(tr, this.state, generateId);
|