@magic-marker/prosemirror-suggest-changes 0.2.1-block-join.3 → 0.2.1-block-join.5
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/ensureSelectionPlugin.js +197 -140
- package/dist/prependDeletionsWithZWSP.js +17 -8
- package/dist/replaceStep.js +3 -2
- package/dist/schema.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
|
2
2
|
import { getSuggestionMarks } from "./utils.js";
|
|
3
|
+
import { ZWSP } from "./constants.js";
|
|
4
|
+
// import { ZWSP } from "./constants.js";
|
|
5
|
+
const TRACE_ENABLED = true;
|
|
6
|
+
function trace(...args) {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
8
|
+
if (!TRACE_ENABLED) return;
|
|
9
|
+
console.log("[ensureSelectionPlugin]", ...args);
|
|
10
|
+
}
|
|
3
11
|
export const ensureSelectionKey = new PluginKey("@handlewithcare/prosemirror-suggest-changes-ensure-selection");
|
|
4
12
|
export function ensureSelection() {
|
|
5
13
|
return new Plugin({
|
|
@@ -35,91 +43,145 @@ export function ensureSelection() {
|
|
|
35
43
|
newState.handleKeyDown.arrowLeft = event.key === "ArrowLeft";
|
|
36
44
|
newState.handleKeyDown.arrowRight = event.key === "ArrowRight";
|
|
37
45
|
if (newState.handleKeyDown.backspace !== state.handleKeyDown.backspace || newState.handleKeyDown.delete !== state.handleKeyDown.delete || newState.handleKeyDown.arrowLeft !== state.handleKeyDown.arrowLeft || newState.handleKeyDown.arrowRight !== state.handleKeyDown.arrowRight) {
|
|
38
|
-
|
|
46
|
+
trace("handleKeyDown newState =", newState);
|
|
39
47
|
view.dispatch(view.state.tr.setMeta(ensureSelectionKey, newState));
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
51
|
appendTransaction (_transactions, oldState, newState) {
|
|
44
|
-
const state = newState;
|
|
45
|
-
if (!(state.selection instanceof TextSelection)) return null;
|
|
46
|
-
if (!(oldState.selection instanceof TextSelection)) return null;
|
|
47
|
-
const { $cursor } = state.selection;
|
|
48
|
-
if ($cursor == null) return null;
|
|
49
|
-
const $oldCursor = oldState.selection.$cursor;
|
|
50
|
-
let dir;
|
|
51
|
-
if ($oldCursor != null) {
|
|
52
|
-
dir = $cursor.pos > $oldCursor.pos ? 1 : -1;
|
|
53
|
-
} else {
|
|
54
|
-
const { $from, $to } = oldState.selection;
|
|
55
|
-
const distToFrom = $cursor.pos - $from.pos;
|
|
56
|
-
const distToTo = $to.pos - $cursor.pos;
|
|
57
|
-
// if cursor ended up closer to the right side of the selection (to),
|
|
58
|
-
// consider direction as 1 - "to the right"
|
|
59
|
-
dir = distToTo <= distToFrom ? 1 : -1;
|
|
60
|
-
}
|
|
61
52
|
const pluginState = ensureSelectionKey.getState(newState);
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
} else if (pluginState?.handleKeyDown.delete || pluginState?.handleKeyDown.arrowRight) {
|
|
65
|
-
dir = 1;
|
|
53
|
+
if (isPosValid(newState.selection.$anchor) && isPosValid(newState.selection.$head)) {
|
|
54
|
+
return null;
|
|
66
55
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!isValidPos($pos, dir)) {
|
|
88
|
-
console.warn("failed to find valid $cursor after all attempts", $pos.pos, "keeping the original $cursor", $cursor.pos, {
|
|
89
|
-
$cursor,
|
|
90
|
-
$pos
|
|
91
|
-
});
|
|
92
|
-
console.groupEnd();
|
|
93
|
-
console.log("final $cursor (unchanged)", $cursor);
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
console.info("found new valid $cursor", $pos.pos, {
|
|
97
|
-
$pos
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
57
|
+
if (TRACE_ENABLED) console.groupCollapsed("[ensureSelectionPlugin]", "appendTransaction");
|
|
58
|
+
trace("appendTransaction", "search for new valid $anchor...");
|
|
59
|
+
let $newAnchor = getNewValidPos(newState.selection.$anchor, getDirection(oldState.selection.$anchor, newState.selection.$anchor, pluginState));
|
|
60
|
+
trace("appendTransaction", "new valid $anchor", $newAnchor?.pos, {
|
|
61
|
+
$newAnchor
|
|
62
|
+
});
|
|
63
|
+
trace("appendTransaction", "search for new valid $head...");
|
|
64
|
+
let $newHead = getNewValidPos(newState.selection.$head, getDirection(oldState.selection.$head, newState.selection.$head, pluginState));
|
|
65
|
+
trace("appendTransaction", "new valid $head", $newHead?.pos, {
|
|
66
|
+
$newHead
|
|
67
|
+
});
|
|
68
|
+
$newAnchor = $newAnchor ?? newState.selection.$anchor;
|
|
69
|
+
$newHead = $newHead ?? newState.selection.$head;
|
|
70
|
+
const newSelection = new TextSelection($newAnchor, $newHead);
|
|
71
|
+
if (newSelection.anchor === newState.selection.anchor && newSelection.head === newState.selection.head) {
|
|
72
|
+
trace("appendTransaction", "new selection is the same as old selection, skipping", {
|
|
73
|
+
$newAnchor,
|
|
74
|
+
$newHead,
|
|
75
|
+
selection: newSelection
|
|
98
76
|
});
|
|
99
|
-
|
|
100
|
-
console.
|
|
101
|
-
|
|
102
|
-
});
|
|
103
|
-
return newState.tr.setSelection(TextSelection.create(newState.doc, $pos.pos, $pos.pos));
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
78
|
+
if (TRACE_ENABLED) console.groupEnd();
|
|
79
|
+
return null;
|
|
104
80
|
}
|
|
105
|
-
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
82
|
+
if (TRACE_ENABLED) console.groupEnd();
|
|
83
|
+
trace("appendTransaction", "setting new selection", $newAnchor.pos, $newHead.pos, {
|
|
84
|
+
$newAnchor,
|
|
85
|
+
$newHead,
|
|
86
|
+
selection: newSelection
|
|
87
|
+
});
|
|
88
|
+
return newState.tr.setSelection(newSelection);
|
|
106
89
|
}
|
|
107
90
|
});
|
|
108
91
|
}
|
|
109
92
|
export function isEnsureSelectionEnabled() {
|
|
110
93
|
return true;
|
|
111
94
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
95
|
+
function isPosValid($pos) {
|
|
96
|
+
// text selection is only valid in nodes that allow inline content
|
|
97
|
+
// https://github.com/ProseMirror/prosemirror-state/blob/1.4.4/src/selection.ts#L219
|
|
98
|
+
if (!$pos.parent.inlineContent) {
|
|
99
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: not in inlineContent node", {
|
|
100
|
+
$pos
|
|
101
|
+
});
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const { deletion, insertion } = getSuggestionMarks($pos.doc.type.schema);
|
|
105
|
+
const deletionBefore = deletion.isInSet($pos.nodeBefore?.marks ?? []);
|
|
106
|
+
const deletionAfter = deletion.isInSet($pos.nodeAfter?.marks ?? []);
|
|
107
|
+
const isAnchorBefore = deletionBefore && deletionBefore.attrs["type"] === "anchor";
|
|
108
|
+
const isAnchorAfter = deletionAfter && deletionAfter.attrs["type"] === "anchor";
|
|
109
|
+
if (isAnchorBefore && deletionAfter && !isAnchorAfter) {
|
|
110
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between deletion anchor and non-anchor deletion", {
|
|
111
|
+
$pos
|
|
112
|
+
});
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (deletionBefore && deletionAfter && !isAnchorBefore && !isAnchorAfter) {
|
|
116
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between two non-anchor deletions", {
|
|
117
|
+
$pos
|
|
118
|
+
});
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if ($pos.nodeBefore == null && deletionAfter && !isAnchorAfter) {
|
|
122
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between node boundary and non-anchor deletion", {
|
|
123
|
+
$pos
|
|
124
|
+
});
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (deletionBefore && $pos.nodeAfter == null && !isAnchorBefore) {
|
|
128
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between non-anchor deletion and node boundary", {
|
|
129
|
+
$pos
|
|
130
|
+
});
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (deletionBefore && !isAnchorBefore && $pos.nodeAfter == null) {
|
|
134
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between non-anchor deletion and node boundary", {
|
|
135
|
+
$pos
|
|
136
|
+
});
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (deletionBefore && !isAnchorBefore) {
|
|
140
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between non-anchor deletion and anything", {
|
|
141
|
+
$pos
|
|
142
|
+
});
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const insertionBefore = insertion.isInSet($pos.nodeBefore?.marks ?? []);
|
|
146
|
+
const insertionAfter = insertion.isInSet($pos.nodeAfter?.marks ?? []);
|
|
147
|
+
const ZWSP_REGEXP = new RegExp(ZWSP, "g");
|
|
148
|
+
const isZWSPBefore = $pos.nodeBefore && $pos.nodeBefore.textContent.replace(ZWSP_REGEXP, "") === "";
|
|
149
|
+
const isZWSPAfter = $pos.nodeAfter && $pos.nodeAfter.textContent.replace(ZWSP_REGEXP, "") === "";
|
|
150
|
+
if (insertionBefore && insertionAfter && isZWSPBefore && isZWSPAfter) {
|
|
151
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between two ZWSP insertions", {
|
|
152
|
+
$pos
|
|
153
|
+
});
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if (insertionBefore && isZWSPBefore && $pos.nodeAfter == null && // a position like this:
|
|
157
|
+
// <p><insertion>ZWSP</insertion>|</p>
|
|
158
|
+
// because it means this paragraph was just created and it's empty
|
|
159
|
+
$pos.parent.textContent.replace(ZWSP_REGEXP, "") !== "") {
|
|
160
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between ZWSP insertion and right node boundary", {
|
|
161
|
+
$pos
|
|
162
|
+
});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (insertionAfter && isZWSPAfter && $pos.nodeBefore == null) {
|
|
166
|
+
trace("isPosValid", $pos.pos, "pos invalid", "reason: between ZWSP insertion and left node boundary", {
|
|
167
|
+
$pos
|
|
168
|
+
});
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function findNextValidPos($initialPos) {
|
|
114
174
|
let $pos = $initialPos;
|
|
115
|
-
|
|
175
|
+
// to keep searching for the next valid pos we need non-null nodeAfter so we can go right or non-root depth so we can go up
|
|
176
|
+
while(!isPosValid($pos) && ($pos.nodeAfter != null || $pos.depth > 0)){
|
|
177
|
+
// first check if we can go into nodeAfter
|
|
116
178
|
if ($pos.nodeAfter != null) {
|
|
179
|
+
// if nodeAfter is inline, we can step into it and search for the valid pos in it
|
|
117
180
|
if ($pos.nodeAfter.isInline) {
|
|
118
181
|
// nodeAfter is inline - move in by one
|
|
119
182
|
$pos = $pos.doc.resolve($pos.pos + 1);
|
|
120
|
-
console.log("nodeAfter is inline, move to end of it", $pos);
|
|
121
183
|
} else {
|
|
122
|
-
// nodeAfter is not inline - find first inline descendant in nodeAfter
|
|
184
|
+
// nodeAfter is not inline - find starting position of the first inline descendant in nodeAfter
|
|
123
185
|
let localStartPos = null;
|
|
124
186
|
$pos.nodeAfter.descendants((child, pos)=>{
|
|
125
187
|
if (!child.isInline) return true;
|
|
@@ -128,41 +190,33 @@ function findNextPos($initialPos, dir) {
|
|
|
128
190
|
return false;
|
|
129
191
|
});
|
|
130
192
|
if (localStartPos !== null) {
|
|
131
|
-
// we have a local position of
|
|
193
|
+
// we have a local starting position of the first inline descendant - convert it to global position
|
|
132
194
|
// +1 to "enter" the node, and add local pos
|
|
133
195
|
$pos = $pos.doc.resolve($pos.pos + 1 + localStartPos);
|
|
134
|
-
console.log("found first inline descendant, move to start of it", $pos);
|
|
135
196
|
} else {
|
|
136
|
-
// unable to find first inline descendant of nodeAfter - just skip nodeAfter
|
|
197
|
+
// unable to find first inline descendant of nodeAfter - just skip nodeAfter altogether
|
|
137
198
|
$pos = $pos.doc.resolve($pos.pos + $pos.nodeAfter.nodeSize);
|
|
138
|
-
console.log("unable to find first inline descendant, move to end of nodeAfter", $pos);
|
|
139
199
|
}
|
|
140
200
|
}
|
|
141
201
|
} else if ($pos.depth > 0) {
|
|
142
202
|
// nodeAfter is null - go up
|
|
143
203
|
$pos = $pos.doc.resolve($pos.after());
|
|
144
|
-
console.log("nodeAfter is null, go up", $pos);
|
|
145
204
|
}
|
|
146
205
|
}
|
|
147
|
-
|
|
148
|
-
console.log("found next valid $pos", $pos);
|
|
149
|
-
} else {
|
|
150
|
-
console.warn("failed to find next valid $pos", $pos, "keep initial pos", $initialPos);
|
|
151
|
-
}
|
|
152
|
-
console.groupEnd();
|
|
153
|
-
return isValidPos($pos, dir) ? $pos : $initialPos;
|
|
206
|
+
return isPosValid($pos) ? $pos : null;
|
|
154
207
|
}
|
|
155
|
-
function
|
|
156
|
-
console.groupCollapsed("finding prev valid pos from $initialPos =", $initialPos);
|
|
208
|
+
function findPreviousValidPos($initialPos) {
|
|
157
209
|
let $pos = $initialPos;
|
|
158
|
-
|
|
210
|
+
// in order to be able to keep searching, we need either nodeBefore so we can go left, or non-root depth so we can go up
|
|
211
|
+
while(!isPosValid($pos) && ($pos.nodeBefore != null || $pos.depth > 0)){
|
|
212
|
+
// first check if we can go into nodeBefore
|
|
159
213
|
if ($pos.nodeBefore != null) {
|
|
214
|
+
// if nodeBefore is inline, we can step into it and search for the valid pos in it
|
|
160
215
|
if ($pos.nodeBefore.isInline) {
|
|
161
216
|
// nodeBefore is inline - move in by one
|
|
162
217
|
$pos = $pos.doc.resolve($pos.pos - 1);
|
|
163
|
-
console.log("nodeBefore is inline, move to start of it", $pos);
|
|
164
218
|
} else {
|
|
165
|
-
// nodeBefore is not inline - find last inline descendant in nodeBefore
|
|
219
|
+
// nodeBefore is not inline - find ending position of the last inline descendant in nodeBefore
|
|
166
220
|
let localEndPos = null;
|
|
167
221
|
$pos.nodeBefore.descendants((child, pos)=>{
|
|
168
222
|
if (!child.isInline) return true;
|
|
@@ -170,78 +224,81 @@ function findPrevPos($initialPos, dir) {
|
|
|
170
224
|
return false;
|
|
171
225
|
});
|
|
172
226
|
if (localEndPos !== null) {
|
|
173
|
-
// we have a local position of
|
|
227
|
+
// we have a local ending position of the last inline descendant - convert it to global position
|
|
174
228
|
// move pos to start of node before, add 1 to "enter" nodeBefore, then add local pos
|
|
175
229
|
$pos = $pos.doc.resolve($pos.pos - $pos.nodeBefore.nodeSize + 1 + localEndPos);
|
|
176
|
-
console.log("found last inline descendant, move to end of it", $pos);
|
|
177
230
|
} else {
|
|
178
|
-
// unable to find last inline descendant of nodeBefore - just skip nodeBefore
|
|
231
|
+
// unable to find last inline descendant of nodeBefore - just skip nodeBefore altogether
|
|
179
232
|
$pos = $pos.doc.resolve($pos.pos - $pos.nodeBefore.nodeSize);
|
|
180
|
-
console.log("unable to find last inline descendant, move to start of nodeBefore", $pos);
|
|
181
233
|
}
|
|
182
234
|
}
|
|
183
235
|
} else if ($pos.depth > 0) {
|
|
184
236
|
// nodeBefore is null - go up
|
|
185
237
|
$pos = $pos.doc.resolve($pos.before());
|
|
186
|
-
console.log("nodeBefore is null, go up", $pos);
|
|
187
238
|
}
|
|
188
239
|
}
|
|
189
|
-
|
|
190
|
-
console.log("found prev valid $pos", $pos);
|
|
191
|
-
} else {
|
|
192
|
-
console.warn("failed to find prev valid $pos", $pos, "keep initial pos", $initialPos);
|
|
193
|
-
}
|
|
194
|
-
console.groupEnd();
|
|
195
|
-
return isValidPos($pos, dir) ? $pos : $initialPos;
|
|
240
|
+
return isPosValid($pos) ? $pos : null;
|
|
196
241
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// "moving left - zwsp is on the left - skip backward",
|
|
220
|
-
// $pos,
|
|
221
|
-
// );
|
|
222
|
-
// return false;
|
|
223
|
-
// }
|
|
224
|
-
const deletionBefore = deletion.isInSet($pos.nodeBefore?.marks ?? []);
|
|
225
|
-
const deletionAfter = deletion.isInSet($pos.nodeAfter?.marks ?? []);
|
|
226
|
-
// between two deletions
|
|
227
|
-
if (deletionBefore && deletionAfter) {
|
|
228
|
-
console.warn("cursor invalid", "between two deletions", $pos);
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
// between a deletion and a node boundary
|
|
232
|
-
if (deletionBefore && $pos.nodeAfter == null) {
|
|
233
|
-
console.warn("cursor invalid", "between a deletion and a node boundary", $pos);
|
|
234
|
-
return false;
|
|
242
|
+
function getNewValidPos($pos, dir) {
|
|
243
|
+
if (isPosValid($pos)) return $pos;
|
|
244
|
+
trace("getNewValidPos for", $pos.pos, {
|
|
245
|
+
$pos,
|
|
246
|
+
dir
|
|
247
|
+
});
|
|
248
|
+
if (dir === "right") {
|
|
249
|
+
const $nextValidPos = findNextValidPos($pos);
|
|
250
|
+
trace("getNewValidPos", "$nextValidPos", $nextValidPos?.pos, {
|
|
251
|
+
dir,
|
|
252
|
+
$pos,
|
|
253
|
+
$nextValidPos
|
|
254
|
+
});
|
|
255
|
+
if ($nextValidPos != null) return $nextValidPos;
|
|
256
|
+
const $prevValidPos = findPreviousValidPos($pos);
|
|
257
|
+
trace("getNewValidPos", "$prevValidPos", $prevValidPos?.pos, {
|
|
258
|
+
dir,
|
|
259
|
+
$pos,
|
|
260
|
+
$prevValidPos
|
|
261
|
+
});
|
|
262
|
+
if ($prevValidPos != null) return $prevValidPos;
|
|
263
|
+
return null;
|
|
235
264
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
265
|
+
if (dir === "left") {
|
|
266
|
+
const $prevValidPos = findPreviousValidPos($pos);
|
|
267
|
+
trace("getNewValidPos", "$prevValidPos", $prevValidPos?.pos, {
|
|
268
|
+
dir,
|
|
269
|
+
$pos,
|
|
270
|
+
$prevValidPos
|
|
271
|
+
});
|
|
272
|
+
if ($prevValidPos != null) return $prevValidPos;
|
|
273
|
+
const $nextValidPos = findNextValidPos($pos);
|
|
274
|
+
trace("getNewValidPos", "$nextValidPos", $nextValidPos?.pos, {
|
|
275
|
+
dir,
|
|
276
|
+
$pos,
|
|
277
|
+
$nextValidPos
|
|
278
|
+
});
|
|
279
|
+
if ($nextValidPos != null) return $nextValidPos;
|
|
280
|
+
return null;
|
|
240
281
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
const $nextValidPos = findNextValidPos($pos);
|
|
283
|
+
const $prevValidPos = findPreviousValidPos($pos);
|
|
284
|
+
trace("getNewValidPos", "$nextValidPos", $nextValidPos?.pos, "$prevValidPos", $prevValidPos?.pos, {
|
|
285
|
+
dir,
|
|
286
|
+
$pos,
|
|
287
|
+
$nextValidPos,
|
|
288
|
+
$prevValidPos
|
|
289
|
+
});
|
|
290
|
+
if ($nextValidPos == null && $prevValidPos == null) {
|
|
291
|
+
return null;
|
|
245
292
|
}
|
|
246
|
-
return
|
|
293
|
+
if ($nextValidPos == null) return $prevValidPos;
|
|
294
|
+
if ($prevValidPos == null) return $nextValidPos;
|
|
295
|
+
const nextDist = Math.abs($pos.pos - $nextValidPos.pos);
|
|
296
|
+
const prevDist = Math.abs($pos.pos - $prevValidPos.pos);
|
|
297
|
+
return nextDist <= prevDist ? $nextValidPos : $prevValidPos;
|
|
298
|
+
}
|
|
299
|
+
function getDirection($oldPos, $newPos, pluginState) {
|
|
300
|
+
if (pluginState?.handleKeyDown.backspace) return "left";
|
|
301
|
+
if ($newPos.pos > $oldPos.pos) return "right";
|
|
302
|
+
if ($newPos.pos < $oldPos.pos) return "left";
|
|
303
|
+
return null;
|
|
247
304
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { getSuggestionMarks } from "./utils.js";
|
|
2
2
|
import { Transform } from "prosemirror-transform";
|
|
3
3
|
import { ZWSP } from "./constants.js";
|
|
4
|
+
const TRACE_ENABLED = true;
|
|
5
|
+
function trace(...args) {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
7
|
+
if (!TRACE_ENABLED) return;
|
|
8
|
+
console.log("[prependDeletionsWithZWSP]", ...args);
|
|
9
|
+
}
|
|
4
10
|
export function prependDeletionsWithZWSP(transaction, opts) {
|
|
5
|
-
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
12
|
+
if (TRACE_ENABLED) console.groupCollapsed("prepend deletions with zwsp");
|
|
6
13
|
const { deletion } = getSuggestionMarks(transaction.doc.type.schema);
|
|
7
14
|
let transform = new Transform(transaction.doc);
|
|
8
15
|
transform.doc.descendants((node, pos)=>{
|
|
@@ -10,7 +17,7 @@ export function prependDeletionsWithZWSP(transaction, opts) {
|
|
|
10
17
|
if (!node.isText || mark == null) return true;
|
|
11
18
|
const mappedPos = transform.mapping.map(pos);
|
|
12
19
|
transform.delete(mappedPos, mappedPos + node.nodeSize);
|
|
13
|
-
|
|
20
|
+
trace("found zwsp, deleted", {
|
|
14
21
|
from: mappedPos,
|
|
15
22
|
to: mappedPos + node.nodeSize
|
|
16
23
|
});
|
|
@@ -19,10 +26,11 @@ export function prependDeletionsWithZWSP(transaction, opts) {
|
|
|
19
26
|
transform.steps.forEach((step)=>{
|
|
20
27
|
transaction.step(step);
|
|
21
28
|
});
|
|
22
|
-
|
|
29
|
+
trace(`added ${String(transform.steps.length)} remove zwsp steps to tr`);
|
|
23
30
|
if (opts?.experimental_deletions !== "hidden") {
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
trace("deletions are visible, skipping prepend zwsp");
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
33
|
+
if (TRACE_ENABLED) console.groupEnd();
|
|
26
34
|
return transaction;
|
|
27
35
|
}
|
|
28
36
|
transform = new Transform(transaction.doc);
|
|
@@ -39,7 +47,7 @@ export function prependDeletionsWithZWSP(transaction, opts) {
|
|
|
39
47
|
]);
|
|
40
48
|
const mappedPos = transform.mapping.map(pos);
|
|
41
49
|
transform.insert(mappedPos + 1, zwspNode);
|
|
42
|
-
|
|
50
|
+
trace("found first-child-deletion inline content node, inserted zwsp at", {
|
|
43
51
|
pos,
|
|
44
52
|
mappedPos
|
|
45
53
|
});
|
|
@@ -48,7 +56,8 @@ export function prependDeletionsWithZWSP(transaction, opts) {
|
|
|
48
56
|
transform.steps.forEach((step)=>{
|
|
49
57
|
transaction.step(step);
|
|
50
58
|
});
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
trace(`added ${String(transform.steps.length)} add zwsp steps to tr`);
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
61
|
+
if (TRACE_ENABLED) console.groupEnd();
|
|
53
62
|
return transaction;
|
|
54
63
|
}
|
package/dist/replaceStep.js
CHANGED
|
@@ -200,15 +200,16 @@ import { collapseZWSPNodes, findJoinMark, joinNodesAndMarkJoinPoints, removeZWSP
|
|
|
200
200
|
trackedTransaction.setSelection(TextSelection.near(trackedTransaction.doc.resolve($stepFrom.pos - 1)));
|
|
201
201
|
}
|
|
202
202
|
// Handle insertions
|
|
203
|
-
// When didBlockJoin is true, only process insertions if the slice contains
|
|
203
|
+
// When didBlockJoin is true, or nodes were joined, only process insertions if the slice contains
|
|
204
204
|
// actual new content (closed slice) rather than just structural info for the join (open slice).
|
|
205
205
|
// Open slices have openStart > 0 or openEnd > 0 and represent block structure.
|
|
206
206
|
// Closed slices have openStart = 0 and openEnd = 0 and contain new user content.
|
|
207
207
|
// TODO: Done with AI, not 100% sure about the argument but it works. Kind of.
|
|
208
208
|
// The replaced content is not equivalent to what would happen without suggestions, just with insertions
|
|
209
209
|
// but it's workable. Only issues are with deleting between different depths ( for ex. between list and root level paragraph )
|
|
210
|
+
const didJoinNodes = didBlockJoin || joinNodesTransform.steps.length > 0;
|
|
210
211
|
const sliceHasNewContent = step.slice.openStart === 0 && step.slice.openEnd === 0;
|
|
211
|
-
const shouldProcessInsertion = step.slice.content.size && (!
|
|
212
|
+
const shouldProcessInsertion = step.slice.content.size && (!didJoinNodes || sliceHasNewContent);
|
|
212
213
|
if (shouldProcessInsertion) {
|
|
213
214
|
const $to = trackedTransaction.doc.resolve(stepTo);
|
|
214
215
|
// Don't allow inserting content within an existing deletion
|
package/dist/schema.js
CHANGED
|
@@ -49,7 +49,7 @@ export const hiddenDeletion = {
|
|
|
49
49
|
const isAnchor = mark.attrs["type"] === "anchor";
|
|
50
50
|
const blockStyle = `display: block;`;
|
|
51
51
|
const inlineStyle = `display: inline;`;
|
|
52
|
-
const hiddenStyle = `display: inline; font-size: 1px; line-height: 0px; color: transparent;`;
|
|
52
|
+
const hiddenStyle = `display: inline; font-size: 1px; line-height: 0px; color: transparent; letter-spacing: -1px;`;
|
|
53
53
|
return [
|
|
54
54
|
"del",
|
|
55
55
|
{
|