@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.
@@ -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
- if (child.text === "\u200B") {
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,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
+ }
@@ -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 ReplaceStep, type Step } from "prosemirror-transform";
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.
@@ -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
- trackedTransaction.delete(stepFrom, stepTo);
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
- trackedTransaction.addMark(stepFrom, stepTo, deletion.create({
141
- id: markId
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
- const insertFrom = findSuggestionMarkEnd($to, deletion);
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.