@magic-marker/prosemirror-suggest-changes 0.2.0 → 0.2.1-block-join.0
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/{features/joinBlocks/__tests__ → __tests__}/playwrightHelpers.d.ts +13 -0
- package/dist/commands.js +4 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/features/joinOnDelete/__tests__/joinOnDelete.playwright.test.d.ts +1 -0
- package/dist/features/joinOnDelete/__tests__/joinOnDelete.test.d.ts +1 -0
- package/dist/features/joinOnDelete/index.d.ts +37 -0
- package/dist/features/joinOnDelete/index.js +157 -0
- package/dist/replaceStep.d.ts +1 -1
- package/dist/replaceStep.js +60 -22
- package/dist/schema.js +13 -2
- package/package.json +2 -2
- package/src/features/joinBlocks/README.md +7 -0
- package/src/features/joinOnDelete/README.md +8 -0
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* These helpers reduce test duplication while keeping actual keyboard events.
|
|
4
4
|
*/
|
|
5
5
|
import type { Page } from "@playwright/test";
|
|
6
|
+
import { type Mark } from "prosemirror-model";
|
|
6
7
|
export interface EditorState {
|
|
7
8
|
paragraphCount: number;
|
|
8
9
|
textContent: string;
|
|
9
10
|
cursorFrom: number;
|
|
10
11
|
cursorTo: number;
|
|
11
12
|
blockCount?: number;
|
|
13
|
+
marks: Mark[];
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Common test pattern: Enter then Backspace
|
|
@@ -20,6 +22,7 @@ export declare function testEnterThenBackspace(page: Page): Promise<{
|
|
|
20
22
|
textContent: string;
|
|
21
23
|
cursorFrom: number;
|
|
22
24
|
cursorTo: number;
|
|
25
|
+
marks: Mark[];
|
|
23
26
|
};
|
|
24
27
|
afterEnterState: {
|
|
25
28
|
paragraphCount: number;
|
|
@@ -27,6 +30,7 @@ export declare function testEnterThenBackspace(page: Page): Promise<{
|
|
|
27
30
|
textContent: string;
|
|
28
31
|
cursorFrom: number;
|
|
29
32
|
cursorTo: number;
|
|
33
|
+
marks: Mark[];
|
|
30
34
|
};
|
|
31
35
|
finalState: {
|
|
32
36
|
paragraphCount: number;
|
|
@@ -34,6 +38,7 @@ export declare function testEnterThenBackspace(page: Page): Promise<{
|
|
|
34
38
|
textContent: string;
|
|
35
39
|
cursorFrom: number;
|
|
36
40
|
cursorTo: number;
|
|
41
|
+
marks: Mark[];
|
|
37
42
|
};
|
|
38
43
|
}>;
|
|
39
44
|
/**
|
|
@@ -46,6 +51,7 @@ export declare function testDoubleEnterDoubleBackspace(page: Page): Promise<{
|
|
|
46
51
|
textContent: string;
|
|
47
52
|
cursorFrom: number;
|
|
48
53
|
cursorTo: number;
|
|
54
|
+
marks: Mark[];
|
|
49
55
|
};
|
|
50
56
|
afterFirstEnter: {
|
|
51
57
|
paragraphCount: number;
|
|
@@ -53,6 +59,7 @@ export declare function testDoubleEnterDoubleBackspace(page: Page): Promise<{
|
|
|
53
59
|
textContent: string;
|
|
54
60
|
cursorFrom: number;
|
|
55
61
|
cursorTo: number;
|
|
62
|
+
marks: Mark[];
|
|
56
63
|
};
|
|
57
64
|
afterSecondEnter: {
|
|
58
65
|
paragraphCount: number;
|
|
@@ -60,6 +67,7 @@ export declare function testDoubleEnterDoubleBackspace(page: Page): Promise<{
|
|
|
60
67
|
textContent: string;
|
|
61
68
|
cursorFrom: number;
|
|
62
69
|
cursorTo: number;
|
|
70
|
+
marks: Mark[];
|
|
63
71
|
};
|
|
64
72
|
afterFirstBackspace: {
|
|
65
73
|
paragraphCount: number;
|
|
@@ -67,6 +75,7 @@ export declare function testDoubleEnterDoubleBackspace(page: Page): Promise<{
|
|
|
67
75
|
textContent: string;
|
|
68
76
|
cursorFrom: number;
|
|
69
77
|
cursorTo: number;
|
|
78
|
+
marks: Mark[];
|
|
70
79
|
};
|
|
71
80
|
finalState: {
|
|
72
81
|
paragraphCount: number;
|
|
@@ -74,6 +83,7 @@ export declare function testDoubleEnterDoubleBackspace(page: Page): Promise<{
|
|
|
74
83
|
textContent: string;
|
|
75
84
|
cursorFrom: number;
|
|
76
85
|
cursorTo: number;
|
|
86
|
+
marks: Mark[];
|
|
77
87
|
};
|
|
78
88
|
}>;
|
|
79
89
|
/**
|
|
@@ -86,6 +96,7 @@ export declare function testEnterThenDeleteFromFirst(page: Page): Promise<{
|
|
|
86
96
|
textContent: string;
|
|
87
97
|
cursorFrom: number;
|
|
88
98
|
cursorTo: number;
|
|
99
|
+
marks: Mark[];
|
|
89
100
|
};
|
|
90
101
|
afterEnter: {
|
|
91
102
|
paragraphCount: number;
|
|
@@ -93,6 +104,7 @@ export declare function testEnterThenDeleteFromFirst(page: Page): Promise<{
|
|
|
93
104
|
textContent: string;
|
|
94
105
|
cursorFrom: number;
|
|
95
106
|
cursorTo: number;
|
|
107
|
+
marks: Mark[];
|
|
96
108
|
};
|
|
97
109
|
finalState: {
|
|
98
110
|
paragraphCount: number;
|
|
@@ -100,6 +112,7 @@ export declare function testEnterThenDeleteFromFirst(page: Page): Promise<{
|
|
|
100
112
|
textContent: string;
|
|
101
113
|
cursorFrom: number;
|
|
102
114
|
cursorTo: number;
|
|
115
|
+
marks: Mark[];
|
|
103
116
|
};
|
|
104
117
|
}>;
|
|
105
118
|
/**
|
package/dist/commands.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Transform } from "prosemirror-transform";
|
|
|
3
3
|
import { findSuggestionMarkEnd } from "./findSuggestionMarkEnd.js";
|
|
4
4
|
import { suggestChangesKey } from "./plugin.js";
|
|
5
5
|
import { getSuggestionMarks } from "./utils.js";
|
|
6
|
+
import { ZWSP } from "./constants.js";
|
|
7
|
+
import { maybeRevertJoinMark } from "./features/joinOnDelete/index.js";
|
|
6
8
|
/**
|
|
7
9
|
* Given a node and a transform, add a set of steps to the
|
|
8
10
|
* transform that applies all marks of type markTypeToApply
|
|
@@ -58,7 +60,8 @@ import { getSuggestionMarks } from "./utils.js";
|
|
|
58
60
|
const insertionTo = insertionFrom + child.nodeSize;
|
|
59
61
|
if (child.isInline) {
|
|
60
62
|
tr.removeMark(insertionFrom, insertionTo, markTypeToApply);
|
|
61
|
-
|
|
63
|
+
const reverted = maybeRevertJoinMark(tr, insertionFrom, insertionTo, child, markTypeToApply);
|
|
64
|
+
if (!reverted && child.text === ZWSP) {
|
|
62
65
|
tr.delete(insertionFrom, insertionTo);
|
|
63
66
|
}
|
|
64
67
|
} else {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ZWSP = "\u200B";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ZWSP = "\u200B";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Mark, type Node, type Attrs, type MarkType, type ResolvedPos } from "prosemirror-model";
|
|
2
|
+
import { Transform } from "prosemirror-transform";
|
|
3
|
+
import { type Transaction } from "prosemirror-state";
|
|
4
|
+
import { type SuggestionId } from "../../generateId.js";
|
|
5
|
+
interface JoinMarkAttrs {
|
|
6
|
+
type: "join";
|
|
7
|
+
data: {
|
|
8
|
+
leftNode: {
|
|
9
|
+
type: string;
|
|
10
|
+
attrs: object;
|
|
11
|
+
marks: object[];
|
|
12
|
+
};
|
|
13
|
+
rightNode: {
|
|
14
|
+
type: string;
|
|
15
|
+
attrs: object;
|
|
16
|
+
marks: object[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function isJoinMarkAttrs(attrs: Attrs): attrs is JoinMarkAttrs;
|
|
21
|
+
export declare function maybeRevertJoinMark(tr: Transform, from: number, to: number, node: Node, markType: MarkType): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Remove ZWSP text nodes marked as deletions (except for type=join) from the given range
|
|
24
|
+
*/
|
|
25
|
+
export declare function removeZWSPDeletions(trackedTransaction: Transaction, from: number, to: number): Transform;
|
|
26
|
+
/**
|
|
27
|
+
* Join nodes in the given range,
|
|
28
|
+
* add deletion marks of type="join" at the join points
|
|
29
|
+
*/
|
|
30
|
+
export declare function joinNodesAndMarkJoinPoints(trackedTransaction: Transaction, from: number, to: number, markId: SuggestionId): Transform;
|
|
31
|
+
/**
|
|
32
|
+
* Find ZWSP nodes marked as insertions and deletions with the same mark id
|
|
33
|
+
* Delete them from the given range
|
|
34
|
+
*/
|
|
35
|
+
export declare function collapseZWSPNodes(trackedTransaction: Transaction, from: number, to: number): Transform;
|
|
36
|
+
export declare function findJoinMark(pos: ResolvedPos): Mark | null;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Mark } from "prosemirror-model";
|
|
2
|
+
import { canJoin, Transform } from "prosemirror-transform";
|
|
3
|
+
import { ZWSP } from "../../constants.js";
|
|
4
|
+
import { getSuggestionMarks } from "../../utils.js";
|
|
5
|
+
export function isJoinMarkAttrs(attrs) {
|
|
6
|
+
if (attrs["type"] !== "join") return false;
|
|
7
|
+
if (attrs["data"] == null) return false;
|
|
8
|
+
const data = attrs["data"];
|
|
9
|
+
if (data.leftNode == null || data.rightNode == null) return false;
|
|
10
|
+
if (typeof data.leftNode.type !== "string" || typeof data.rightNode.type !== "string") return false;
|
|
11
|
+
if (typeof data.leftNode.attrs !== "object" || typeof data.rightNode.attrs !== "object") return false;
|
|
12
|
+
if (!Array.isArray(data.leftNode.marks) || !Array.isArray(data.rightNode.marks)) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
export function maybeRevertJoinMark(tr, from, to, node, markType) {
|
|
16
|
+
const mark = node.marks.find((mark)=>mark.type === markType);
|
|
17
|
+
if (!mark || mark.attrs["type"] !== "join" || node.text !== ZWSP) return false;
|
|
18
|
+
// this is a mark of type join
|
|
19
|
+
// split the current node at this mark position
|
|
20
|
+
// delete this mark together with its zwsp content
|
|
21
|
+
// assign left and right node (after the split) properties from the mark's data
|
|
22
|
+
tr.delete(from, to);
|
|
23
|
+
tr.split(from);
|
|
24
|
+
// insertionFrom is now at the end of the left node (but before the closing token)
|
|
25
|
+
// after() will give us the position after the closing token (but before the next opening token) - exactly the split pos
|
|
26
|
+
const $insertionFrom = tr.doc.resolve(from);
|
|
27
|
+
const $splitPos = tr.doc.resolve($insertionFrom.after());
|
|
28
|
+
const { attrs } = mark;
|
|
29
|
+
if (!isJoinMarkAttrs(attrs) || !$splitPos.nodeBefore || !$splitPos.nodeAfter) return false;
|
|
30
|
+
// restore left and right node type, attrs and marks, as they were before the join
|
|
31
|
+
const { leftNode, rightNode } = attrs.data;
|
|
32
|
+
const leftNodeType = tr.doc.type.schema.nodes[leftNode.type];
|
|
33
|
+
const rightNodeType = tr.doc.type.schema.nodes[rightNode.type];
|
|
34
|
+
const leftNodeMarks = leftNode.marks.map((markData)=>Mark.fromJSON(tr.doc.type.schema, markData));
|
|
35
|
+
const rightNodeMarks = rightNode.marks.map((markData)=>Mark.fromJSON(tr.doc.type.schema, markData));
|
|
36
|
+
if (!leftNodeType || !rightNodeType) return false;
|
|
37
|
+
tr.setNodeMarkup($splitPos.pos - $splitPos.nodeBefore.nodeSize, leftNodeType, leftNode.attrs, leftNodeMarks);
|
|
38
|
+
tr.setNodeMarkup($splitPos.pos, rightNodeType, rightNode.attrs, rightNodeMarks);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Remove ZWSP text nodes marked as deletions (except for type=join) from the given range
|
|
43
|
+
*/ export function removeZWSPDeletions(trackedTransaction, from, to) {
|
|
44
|
+
const transform = new Transform(trackedTransaction.doc);
|
|
45
|
+
const $from = transform.doc.resolve(from);
|
|
46
|
+
const $to = transform.doc.resolve(to);
|
|
47
|
+
const blockRange = $from.blockRange($to);
|
|
48
|
+
const doc = transform.doc;
|
|
49
|
+
if (!blockRange) return transform;
|
|
50
|
+
const { deletion } = getSuggestionMarks(transform.doc.type.schema);
|
|
51
|
+
doc.nodesBetween(blockRange.start, blockRange.end, (node, pos)=>{
|
|
52
|
+
const joinMark = node.marks.find((mark)=>mark.type === deletion && mark.attrs["type"] === "join");
|
|
53
|
+
const isZWSPNode = node.isText && node.text === ZWSP && deletion.isInSet(node.marks) && joinMark == null;
|
|
54
|
+
if (!isZWSPNode) return true;
|
|
55
|
+
const mappedPos = transform.mapping.map(pos);
|
|
56
|
+
transform.delete(mappedPos, mappedPos + node.nodeSize);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
return transform;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Join nodes in the given range,
|
|
63
|
+
* add deletion marks of type="join" at the join points
|
|
64
|
+
*/ export function joinNodesAndMarkJoinPoints(trackedTransaction, from, to, markId) {
|
|
65
|
+
const transform = new Transform(trackedTransaction.doc);
|
|
66
|
+
const $from = transform.doc.resolve(from);
|
|
67
|
+
const $to = transform.doc.resolve(to);
|
|
68
|
+
const blockRange = $from.blockRange($to);
|
|
69
|
+
const doc = transform.doc;
|
|
70
|
+
if (!blockRange) return transform;
|
|
71
|
+
const { deletion } = getSuggestionMarks(transform.doc.type.schema);
|
|
72
|
+
doc.nodesBetween(blockRange.start, blockRange.end, (node, pos)=>{
|
|
73
|
+
if (node.isInline) return false;
|
|
74
|
+
const endOfNode = pos + node.nodeSize;
|
|
75
|
+
// make sure the node ends within the range
|
|
76
|
+
if (endOfNode >= blockRange.$to.pos) return true;
|
|
77
|
+
const $endOfNode = doc.resolve(endOfNode);
|
|
78
|
+
// make sure we are between two nodes
|
|
79
|
+
if (!$endOfNode.nodeBefore || !$endOfNode.nodeAfter) return false;
|
|
80
|
+
// we cannot insert zwsp text nodes into non-textblock nodes
|
|
81
|
+
if (!$endOfNode.nodeBefore.isTextblock || !$endOfNode.nodeAfter.isTextblock) return true;
|
|
82
|
+
const mappedEndOfNode = transform.mapping.map(endOfNode);
|
|
83
|
+
const $mappedEndOfNode = transform.doc.resolve(mappedEndOfNode);
|
|
84
|
+
if (!canJoin(transform.doc, mappedEndOfNode) || !$mappedEndOfNode.nodeBefore || !$mappedEndOfNode.nodeAfter) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const leftNode = {
|
|
88
|
+
type: $endOfNode.nodeBefore.type.name,
|
|
89
|
+
attrs: $endOfNode.nodeBefore.attrs,
|
|
90
|
+
marks: $endOfNode.nodeBefore.marks.map((mark)=>mark.toJSON())
|
|
91
|
+
};
|
|
92
|
+
const rightNode = {
|
|
93
|
+
type: $endOfNode.nodeAfter.type.name,
|
|
94
|
+
attrs: $endOfNode.nodeAfter.attrs,
|
|
95
|
+
marks: $endOfNode.nodeAfter.marks.map((mark)=>mark.toJSON())
|
|
96
|
+
};
|
|
97
|
+
transform.join(mappedEndOfNode);
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
99
|
+
const joinStep = transform.steps[transform.steps.length - 1];
|
|
100
|
+
const joinPos = joinStep.getMap().map(mappedEndOfNode);
|
|
101
|
+
transform.insert(joinPos, transform.doc.type.schema.text(ZWSP));
|
|
102
|
+
transform.addMark(joinPos, joinPos + 1, deletion.create({
|
|
103
|
+
id: markId,
|
|
104
|
+
type: "join",
|
|
105
|
+
data: {
|
|
106
|
+
leftNode,
|
|
107
|
+
rightNode
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
110
|
+
return false;
|
|
111
|
+
});
|
|
112
|
+
return transform;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Find ZWSP nodes marked as insertions and deletions with the same mark id
|
|
116
|
+
* Delete them from the given range
|
|
117
|
+
*/ export function collapseZWSPNodes(trackedTransaction, from, to) {
|
|
118
|
+
const transform = new Transform(trackedTransaction.doc);
|
|
119
|
+
const $from = transform.doc.resolve(from);
|
|
120
|
+
const $to = transform.doc.resolve(to);
|
|
121
|
+
const blockRange = $from.blockRange($to);
|
|
122
|
+
if (!blockRange) return transform;
|
|
123
|
+
const { insertion } = getSuggestionMarks(transform.doc.type.schema);
|
|
124
|
+
let joinPos = null;
|
|
125
|
+
let insertionPos = null;
|
|
126
|
+
let insertionPairPos = null;
|
|
127
|
+
transform.doc.nodesBetween(blockRange.start, blockRange.end, (node, pos)=>{
|
|
128
|
+
const { deletion } = getSuggestionMarks(transform.doc.type.schema);
|
|
129
|
+
const joinZWSP = node.marks.find((mark)=>mark.type === deletion && mark.attrs["type"] === "join");
|
|
130
|
+
if (joinZWSP && joinPos === null) joinPos = pos;
|
|
131
|
+
const insertionZWSP = node.marks.find((mark)=>mark.type === insertion);
|
|
132
|
+
if (insertionZWSP && insertionPos !== null && insertionPairPos === null) insertionPairPos = pos;
|
|
133
|
+
if (insertionZWSP && insertionPos === null) insertionPos = pos;
|
|
134
|
+
return joinPos == null || insertionPos == null || insertionPairPos == null;
|
|
135
|
+
});
|
|
136
|
+
if (joinPos !== null && insertionPos !== null && insertionPairPos !== null) {
|
|
137
|
+
const between = transform.doc.textBetween(joinPos, insertionPairPos + 1, "__BLOCK__", "__LEAF__");
|
|
138
|
+
if (between === `${ZWSP}${ZWSP}__BLOCK__${ZWSP}`) {
|
|
139
|
+
// delete only if there is no real content in between the join and insertion zwsp marks
|
|
140
|
+
const toDelete = [
|
|
141
|
+
joinPos,
|
|
142
|
+
insertionPos,
|
|
143
|
+
insertionPairPos
|
|
144
|
+
];
|
|
145
|
+
for (const pos of toDelete){
|
|
146
|
+
const mappedPos = transform.mapping.map(pos);
|
|
147
|
+
transform.delete(mappedPos, mappedPos + 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return transform;
|
|
152
|
+
}
|
|
153
|
+
export function findJoinMark(pos) {
|
|
154
|
+
if (!pos.nodeAfter) return null;
|
|
155
|
+
const { deletion } = getSuggestionMarks(pos.doc.type.schema);
|
|
156
|
+
return pos.nodeAfter.marks.find((mark)=>mark.type === deletion && mark.attrs["type"] === "join") ?? null;
|
|
157
|
+
}
|
package/dist/replaceStep.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Node } from "prosemirror-model";
|
|
2
2
|
import { type EditorState, type Transaction } from "prosemirror-state";
|
|
3
|
-
import { type
|
|
3
|
+
import { type Step, type ReplaceStep } from "prosemirror-transform";
|
|
4
4
|
import { type SuggestionId } from "./generateId.js";
|
|
5
5
|
/**
|
|
6
6
|
* Transform a replace step into its equivalent tracked steps.
|
package/dist/replaceStep.js
CHANGED
|
@@ -3,6 +3,7 @@ import { findSuggestionMarkEnd } from "./findSuggestionMarkEnd.js";
|
|
|
3
3
|
import { rebasePos } from "./rebasePos.js";
|
|
4
4
|
import { getSuggestionMarks } from "./utils.js";
|
|
5
5
|
import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
6
|
+
import { collapseZWSPNodes, findJoinMark, joinNodesAndMarkJoinPoints, removeZWSPDeletions } from "./features/joinOnDelete/index.js";
|
|
6
7
|
/**
|
|
7
8
|
* Transform a replace step into its equivalent tracked steps.
|
|
8
9
|
*
|
|
@@ -74,13 +75,13 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
74
75
|
markId = suggestionId;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
let $stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
79
|
+
let $stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
77
80
|
// If there's a deletion, we need to check for and handle
|
|
78
81
|
// the case where it crosses a block boundary, so that we
|
|
79
82
|
// can leave zero-width spaces as markers if there's no other
|
|
80
83
|
// content to anchor the deletion to.
|
|
81
84
|
if (stepFrom !== stepTo) {
|
|
82
|
-
let $stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
83
|
-
let $stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
84
85
|
// When there are no characters to mark with deletions before
|
|
85
86
|
// the end of a block, we add zero-width, non-printable
|
|
86
87
|
// characters as markers to indicate that a deletion exists
|
|
@@ -97,21 +98,6 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
97
98
|
stepTo++;
|
|
98
99
|
$stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
99
100
|
}
|
|
100
|
-
// When we produce a deletion mark that directly abuts
|
|
101
|
-
// an existing mark with a zero-width space, we delete
|
|
102
|
-
// that space. We'll join the marks later, and can use
|
|
103
|
-
// the joined marks to find deletions across the block
|
|
104
|
-
// boundary
|
|
105
|
-
if ($stepFrom.nodeBefore?.text?.endsWith("\u200B") && !$stepTo.nodeAfter?.text?.startsWith("\u200B")) {
|
|
106
|
-
trackedTransaction.delete(stepFrom - 1, stepFrom);
|
|
107
|
-
stepFrom--;
|
|
108
|
-
stepTo--;
|
|
109
|
-
$stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
110
|
-
$stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
111
|
-
}
|
|
112
|
-
if ($stepTo.nodeAfter?.text?.startsWith("\u200B") && !$stepFrom.nodeBefore?.text?.endsWith("\u200B")) {
|
|
113
|
-
trackedTransaction.delete(stepTo, stepTo + 1);
|
|
114
|
-
}
|
|
115
101
|
// If the user is deleting exactly a zero-width space,
|
|
116
102
|
// delete the space and also shift the range back by one,
|
|
117
103
|
// so that they actually mark the character before the
|
|
@@ -119,7 +105,10 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
119
105
|
// the zero-width space is there, so deleting it would
|
|
120
106
|
// appear to do nothing
|
|
121
107
|
if ($stepFrom.nodeBefore && stepTo - stepFrom === 1 && trackedTransaction.doc.textBetween(stepFrom, stepTo) === "\u200B") {
|
|
122
|
-
|
|
108
|
+
const joinMark = findJoinMark($stepFrom);
|
|
109
|
+
if (joinMark === null) {
|
|
110
|
+
trackedTransaction.delete(stepFrom, stepTo);
|
|
111
|
+
}
|
|
123
112
|
stepFrom--;
|
|
124
113
|
stepTo--;
|
|
125
114
|
$stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
@@ -127,6 +116,8 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
127
116
|
trackedTransaction.setSelection(TextSelection.near($stepFrom));
|
|
128
117
|
}
|
|
129
118
|
}
|
|
119
|
+
$stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
120
|
+
$stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
130
121
|
// TODO: Even if the range doesn't map to a block
|
|
131
122
|
// range, check whether it contains any whole
|
|
132
123
|
// blocks, so that we can use node marks on those.
|
|
@@ -137,9 +128,20 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
137
128
|
// on the content.
|
|
138
129
|
const blockRange = trackedTransaction.doc.resolve(stepFrom).blockRange(trackedTransaction.doc.resolve(stepTo));
|
|
139
130
|
if (!blockRange || blockRange.start !== stepFrom || blockRange.end !== stepTo) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
// add marks on inline nodes in [stepFrom, stepTo] range
|
|
132
|
+
// preserve their attributes
|
|
133
|
+
// this is needed for the join mark to be preserved in case a new deletion goes over it
|
|
134
|
+
trackedTransaction.doc.nodesBetween(stepFrom, stepTo, (node, pos)=>{
|
|
135
|
+
if (!node.isInline) return true;
|
|
136
|
+
const rangeFrom = Math.max(stepFrom, pos);
|
|
137
|
+
const rangeTo = Math.min(stepTo, pos + node.nodeSize);
|
|
138
|
+
const mark = deletion.isInSet(node.marks);
|
|
139
|
+
trackedTransaction.addMark(rangeFrom, rangeTo, deletion.create({
|
|
140
|
+
...mark?.attrs ?? {},
|
|
141
|
+
id: markId
|
|
142
|
+
}));
|
|
143
|
+
return false;
|
|
144
|
+
});
|
|
143
145
|
} else {
|
|
144
146
|
trackedTransaction.doc.nodesBetween(blockRange.start, blockRange.end, (_, pos)=>{
|
|
145
147
|
if (pos < blockRange.start) return true;
|
|
@@ -172,6 +174,31 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
}
|
|
177
|
+
// remove zwsp deletion marks that we don't need
|
|
178
|
+
// then actually join nodes inside the range
|
|
179
|
+
// mark join points with zwsp deletion marks of type join
|
|
180
|
+
// save left/right node data as they were before the join,
|
|
181
|
+
// so we can restore the node markup on revert (when we split them back)
|
|
182
|
+
const removeDeletionsTransform = removeZWSPDeletions(trackedTransaction, stepFrom, stepTo);
|
|
183
|
+
removeDeletionsTransform.steps.forEach((step)=>{
|
|
184
|
+
trackedTransaction.step(step);
|
|
185
|
+
});
|
|
186
|
+
stepFrom = removeDeletionsTransform.mapping.map(stepFrom);
|
|
187
|
+
stepTo = removeDeletionsTransform.mapping.map(stepTo);
|
|
188
|
+
$stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
189
|
+
$stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
190
|
+
const joinNodesTransform = joinNodesAndMarkJoinPoints(trackedTransaction, stepFrom, stepTo, markId);
|
|
191
|
+
joinNodesTransform.steps.forEach((step)=>{
|
|
192
|
+
trackedTransaction.step(step);
|
|
193
|
+
});
|
|
194
|
+
stepFrom = joinNodesTransform.mapping.map(stepFrom);
|
|
195
|
+
stepTo = joinNodesTransform.mapping.map(stepTo);
|
|
196
|
+
$stepFrom = trackedTransaction.doc.resolve(stepFrom);
|
|
197
|
+
$stepTo = trackedTransaction.doc.resolve(stepTo);
|
|
198
|
+
// make sure that cursor does not end up "in between" nodes when the replacement is structural
|
|
199
|
+
if (removeDeletionsTransform.steps.length > 0 && joinNodesTransform.steps.length > 0 && step.structure) {
|
|
200
|
+
trackedTransaction.setSelection(TextSelection.near(trackedTransaction.doc.resolve($stepFrom.pos - 1)));
|
|
201
|
+
}
|
|
175
202
|
// Handle insertions
|
|
176
203
|
// When didBlockJoin is true, only process insertions if the slice contains
|
|
177
204
|
// actual new content (closed slice) rather than just structural info for the join (open slice).
|
|
@@ -187,7 +214,7 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
187
214
|
// Don't allow inserting content within an existing deletion
|
|
188
215
|
// mark. Instead, shift the proposed insertion to the end
|
|
189
216
|
// of the deletion.
|
|
190
|
-
|
|
217
|
+
let insertFrom = findSuggestionMarkEnd($to, deletion);
|
|
191
218
|
// We execute the insertion normally, on top of all of the existing
|
|
192
219
|
// tracked changes.
|
|
193
220
|
trackedTransaction.replace(insertFrom, insertFrom, step.slice);
|
|
@@ -239,9 +266,20 @@ import { joinBlocks } from "./features/joinBlocks/index.js";
|
|
|
239
266
|
insertedTo++;
|
|
240
267
|
$insertedTo = trackedTransaction.doc.resolve(insertedTo);
|
|
241
268
|
}
|
|
269
|
+
const collapseZWSPNodesTransform = collapseZWSPNodes(trackedTransaction, stepFrom, insertedTo);
|
|
270
|
+
collapseZWSPNodesTransform.steps.forEach((step)=>{
|
|
271
|
+
trackedTransaction.step(step);
|
|
272
|
+
});
|
|
273
|
+
insertFrom = collapseZWSPNodesTransform.mapping.map(insertFrom);
|
|
274
|
+
insertedTo = collapseZWSPNodesTransform.mapping.map(insertedTo);
|
|
242
275
|
if (insertFrom !== $to.pos) {
|
|
243
276
|
trackedTransaction.setSelection(TextSelection.near(trackedTransaction.doc.resolve(insertFrom + step.slice.size)));
|
|
244
277
|
}
|
|
278
|
+
// when insertion zwsp and a join mark cancel each other out, what happens is a normal editing "split" of a node
|
|
279
|
+
// so we need to make sure the cursor is set at the start of the split node on the right
|
|
280
|
+
if (collapseZWSPNodesTransform.steps.length > 0) {
|
|
281
|
+
trackedTransaction.setSelection(TextSelection.near(trackedTransaction.doc.resolve(insertedTo)));
|
|
282
|
+
}
|
|
245
283
|
}
|
|
246
284
|
return markId === suggestionId;
|
|
247
285
|
}
|
package/dist/schema.js
CHANGED
|
@@ -5,6 +5,13 @@ export const deletion = {
|
|
|
5
5
|
attrs: {
|
|
6
6
|
id: {
|
|
7
7
|
validate: suggestionIdValidate
|
|
8
|
+
},
|
|
9
|
+
type: {
|
|
10
|
+
default: null,
|
|
11
|
+
validate: "string|null"
|
|
12
|
+
},
|
|
13
|
+
data: {
|
|
14
|
+
default: null
|
|
8
15
|
}
|
|
9
16
|
},
|
|
10
17
|
toDOM (mark, inline) {
|
|
@@ -15,7 +22,9 @@ export const deletion = {
|
|
|
15
22
|
"data-inline": String(inline),
|
|
16
23
|
...!inline && {
|
|
17
24
|
style: "display: block"
|
|
18
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"data-type": JSON.stringify(mark.attrs["type"]),
|
|
27
|
+
"data-data": JSON.stringify(mark.attrs["data"])
|
|
19
28
|
},
|
|
20
29
|
0
|
|
21
30
|
];
|
|
@@ -26,7 +35,9 @@ export const deletion = {
|
|
|
26
35
|
getAttrs (node) {
|
|
27
36
|
if (!node.dataset["id"]) return false;
|
|
28
37
|
return {
|
|
29
|
-
id: JSON.parse(node.dataset["id"])
|
|
38
|
+
id: JSON.parse(node.dataset["id"]),
|
|
39
|
+
type: JSON.parse(node.dataset["type"] ?? "null"),
|
|
40
|
+
data: JSON.parse(node.dataset["data"] ?? "null")
|
|
30
41
|
};
|
|
31
42
|
}
|
|
32
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magic-marker/prosemirror-suggest-changes",
|
|
3
|
-
"version": "0.2.0",
|
|
3
|
+
"version": "0.2.1-block-join.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -80,4 +80,4 @@
|
|
|
80
80
|
"prosemirror-transform": "^1.0.0",
|
|
81
81
|
"prosemirror-view": "^1.0.0"
|
|
82
82
|
}
|
|
83
|
-
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Inside the replaced range, between stepFrom and stepTo in replaceStep.ts we will
|
|
2
|
+
join blocks if on the two sides of the block we find matching ZWSP pairs. For
|
|
3
|
+
that we will need a function to find ZWSP in the range and on the boundaries (
|
|
4
|
+
the next content outside our range ) Then we have to make pairs out of them.
|
|
5
|
+
Then find the positions between those pairs. Then try to do block joins between
|
|
6
|
+
those pairs If the block joins were successful then remove the ZWSPs too. We
|
|
7
|
+
might have to do multiple block joins for non-root level zwsp pairs.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
When nodes are joined using backspace or delete, let the join happen normally,
|
|
2
|
+
but mark former node boundaries with deletion mark of type="join".
|
|
3
|
+
|
|
4
|
+
Save information about what nodes were at each side of the boundary before the
|
|
5
|
+
join.
|
|
6
|
+
|
|
7
|
+
Then on restore, use marks at join points to split the node and restore the node
|
|
8
|
+
markup at each side of the split.
|