@handlewithcare/react-prosemirror 3.1.0-tiptap.52 → 3.1.0-tiptap.53
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/cjs/ReactEditorView.js +6 -0
- package/dist/cjs/components/ChildNodeViews.js +16 -10
- package/dist/cjs/components/CursorWrapper.js +3 -7
- package/dist/cjs/components/ProseMirror.js +5 -3
- package/dist/cjs/components/TextNodeView.js +260 -47
- package/dist/cjs/components/TrailingHackView.js +70 -0
- package/dist/cjs/components/WidgetView.js +3 -1
- package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
- package/dist/cjs/decorations/viewDecorations.js +1 -6
- package/dist/cjs/hooks/useEditor.js +2 -10
- package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
- package/dist/cjs/hooks/useNodeViewDescription.js +64 -21
- package/dist/cjs/plugins/beforeInputPlugin.js +147 -45
- package/dist/cjs/plugins/reactKeys.js +21 -14
- package/dist/cjs/viewdesc.js +52 -4
- package/dist/esm/ReactEditorView.js +6 -0
- package/dist/esm/components/ChildNodeViews.js +17 -11
- package/dist/esm/components/CursorWrapper.js +3 -7
- package/dist/esm/components/ProseMirror.js +5 -3
- package/dist/esm/components/TextNodeView.js +209 -45
- package/dist/esm/components/TrailingHackView.js +71 -1
- package/dist/esm/components/WidgetView.js +3 -1
- package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
- package/dist/esm/decorations/viewDecorations.js +1 -6
- package/dist/esm/hooks/useEditor.js +2 -10
- package/dist/esm/hooks/useMarkViewDescription.js +62 -4
- package/dist/esm/hooks/useNodeViewDescription.js +65 -22
- package/dist/esm/plugins/beforeInputPlugin.js +147 -45
- package/dist/esm/plugins/reactKeys.js +21 -14
- package/dist/esm/viewdesc.js +51 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +6 -1
- package/dist/types/components/TextNodeView.d.ts +20 -5
- package/dist/types/components/TrailingHackView.d.ts +1 -1
- package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
- package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
- package/dist/types/decorations/viewDecorations.d.ts +2 -2
- package/dist/types/hooks/useEditor.d.ts +1 -2
- package/dist/types/hooks/useMarkViewDescription.d.ts +2 -1
- package/dist/types/hooks/useNodeViewDescription.d.ts +2 -1
- package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
- package/dist/types/plugins/reactKeys.d.ts +9 -8
- package/dist/types/viewdesc.d.ts +3 -2
- package/package.json +3 -1
|
@@ -2,7 +2,7 @@ import { useCallback, useContext, useMemo, useRef } from "react";
|
|
|
2
2
|
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
3
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
4
4
|
import { EditorContext } from "../contexts/EditorContext.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ReactNodeViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
6
6
|
import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
|
|
7
7
|
import { useEffectEvent } from "./useEffectEvent.js";
|
|
8
8
|
export function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
@@ -131,32 +131,75 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
131
131
|
children.sort(sortViewDescs);
|
|
132
132
|
for (const child of children){
|
|
133
133
|
child.parent = viewDesc;
|
|
134
|
-
// Because TextNodeViews can't locate the DOM nodes
|
|
135
|
-
// for compositions, we need to override them here
|
|
136
|
-
if (child instanceof CompositionViewDesc) {
|
|
137
|
-
const compositionTopDOM = viewDesc?.contentDOM?.firstChild;
|
|
138
|
-
if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
139
|
-
let textDOM = compositionTopDOM;
|
|
140
|
-
while(textDOM.firstChild){
|
|
141
|
-
textDOM = textDOM.firstChild;
|
|
142
|
-
}
|
|
143
|
-
if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
144
|
-
child.dom = compositionTopDOM;
|
|
145
|
-
child.textDOM = textDOM;
|
|
146
|
-
child.text = textDOM.data;
|
|
147
|
-
child.textDOM.pmViewDesc = child;
|
|
148
|
-
// It should not be possible to be in a composition because one could
|
|
149
|
-
// not start between the renders that switch the view type.
|
|
150
|
-
view.input.compositionNodes.push(child);
|
|
151
|
-
}
|
|
152
134
|
}
|
|
153
135
|
});
|
|
136
|
+
const findCompositionDOM = useCallback((compositionViewDesc)=>{
|
|
137
|
+
if (!props.node.isTextblock) return;
|
|
138
|
+
const children = childrenRef.current;
|
|
139
|
+
// Because TextNodeViews can't locate the DOM nodes
|
|
140
|
+
// for compositions, we need to override them here
|
|
141
|
+
if (!viewDescRef.current?.contentDOM) return;
|
|
142
|
+
let compositionTopDOM = null;
|
|
143
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
144
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
145
|
+
compositionTopDOM = childNode;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!compositionTopDOM) {
|
|
150
|
+
// Otherwise the IME extended an existing tracked text node. Take it over.
|
|
151
|
+
const reactView = view;
|
|
152
|
+
const imeTextNode = reactView.input.compositionNode;
|
|
153
|
+
if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const claimedDesc = imeTextNode.pmViewDesc;
|
|
157
|
+
if (!(claimedDesc instanceof TextViewDesc)) return;
|
|
158
|
+
if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
|
|
159
|
+
// Walk up to the direct child of contentDOM that contains the IME text node
|
|
160
|
+
// (could be the text node itself, could be wrapped in a mark span).
|
|
161
|
+
let topDOM = imeTextNode;
|
|
162
|
+
while(topDOM.parentNode !== viewDescRef.current.contentDOM){
|
|
163
|
+
const next = topDOM.parentNode;
|
|
164
|
+
if (!next) return;
|
|
165
|
+
topDOM = next;
|
|
166
|
+
}
|
|
167
|
+
// Detach the displaced TextViewDesc from the sibling list so sibling-size
|
|
168
|
+
// accounting (used by posBeforeChild) doesn't double-count this text node.
|
|
169
|
+
const displacedIdx = children.indexOf(claimedDesc);
|
|
170
|
+
if (displacedIdx >= 0) children.splice(displacedIdx, 1);
|
|
171
|
+
reactView.displacedNodes.push(claimedDesc);
|
|
172
|
+
compositionViewDesc.dom = topDOM;
|
|
173
|
+
compositionViewDesc.textDOM = imeTextNode;
|
|
174
|
+
compositionViewDesc.text = imeTextNode.data;
|
|
175
|
+
imeTextNode.pmViewDesc = compositionViewDesc;
|
|
176
|
+
compositionViewDesc._displacedDesc = claimedDesc;
|
|
177
|
+
reactView.input.compositionNodes.push(compositionViewDesc);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let textDOM = compositionTopDOM;
|
|
181
|
+
while(textDOM.firstChild){
|
|
182
|
+
textDOM = textDOM.firstChild;
|
|
183
|
+
}
|
|
184
|
+
if (!textDOM || !(textDOM instanceof Text)) {
|
|
185
|
+
console.error(compositionTopDOM, textDOM);
|
|
186
|
+
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
187
|
+
}
|
|
188
|
+
compositionViewDesc.dom = compositionTopDOM;
|
|
189
|
+
compositionViewDesc.textDOM = textDOM;
|
|
190
|
+
compositionViewDesc.text = textDOM.data;
|
|
191
|
+
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
192
|
+
view.input.compositionNodes.push(compositionViewDesc);
|
|
193
|
+
}, [
|
|
194
|
+
props.node.isTextblock,
|
|
195
|
+
view
|
|
196
|
+
]);
|
|
154
197
|
const childContextValue = useMemo(()=>({
|
|
155
198
|
parentRef: viewDescRef,
|
|
156
|
-
siblingsRef: childrenRef
|
|
199
|
+
siblingsRef: childrenRef,
|
|
200
|
+
findCompositionDOM
|
|
157
201
|
}), [
|
|
158
|
-
|
|
159
|
-
viewDescRef
|
|
202
|
+
findCompositionDOM
|
|
160
203
|
]);
|
|
161
204
|
return {
|
|
162
205
|
childContextValue,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Fragment, Slice } from "prosemirror-model";
|
|
2
2
|
import { Plugin, TextSelection } from "prosemirror-state";
|
|
3
|
+
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
4
|
import { CursorWrapper } from "../components/CursorWrapper.js";
|
|
4
5
|
import { widget } from "../decorations/ReactWidgetType.js";
|
|
6
|
+
import { TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
7
|
+
import { reactKeysPluginKey } from "./reactKeys.js";
|
|
5
8
|
function insertText(view, eventData) {
|
|
6
9
|
let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
7
10
|
if (eventData === null) return false;
|
|
@@ -44,33 +47,63 @@ function handleGapCursorComposition(view) {
|
|
|
44
47
|
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
45
48
|
view.dispatch(tr);
|
|
46
49
|
}
|
|
47
|
-
export function beforeInputPlugin(
|
|
50
|
+
export function beforeInputPlugin() {
|
|
48
51
|
let compositionMarks = null;
|
|
49
|
-
let precompositionSnapshot = null;
|
|
50
52
|
return new Plugin({
|
|
51
53
|
props: {
|
|
52
54
|
handleDOMEvents: {
|
|
53
55
|
compositionstart (view) {
|
|
54
|
-
|
|
55
|
-
view.
|
|
56
|
-
handleGapCursorComposition(view);
|
|
56
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
57
|
+
view.compositionStarting = true;
|
|
57
58
|
const { state } = view;
|
|
58
|
-
const
|
|
59
|
+
const { selection } = state;
|
|
60
|
+
const isEmptyTr = state.tr.delete(selection.from, selection.to);
|
|
61
|
+
const $from = isEmptyTr.doc.resolve(isEmptyTr.mapping.map(selection.from));
|
|
62
|
+
const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
|
|
63
|
+
compositionMarks = view.state.storedMarks;
|
|
64
|
+
// Render a CursorWrapper with empty marks if starting a composition in an
|
|
65
|
+
// empty textblock with no marks. This prevents the browser from adding a
|
|
66
|
+
// <br> to the text block when it becomes empty (either via canceling the
|
|
67
|
+
// composition with the escape key or deleting all composition text when
|
|
68
|
+
// the composition node is the only text node in the text block)
|
|
69
|
+
if (compositionMarks === null && isEmptyTextblock) {
|
|
70
|
+
compositionMarks = [];
|
|
71
|
+
}
|
|
72
|
+
const tr = view.state.tr.setStoredMarks(null);
|
|
73
|
+
view.dispatch(tr);
|
|
74
|
+
handleGapCursorComposition(view);
|
|
59
75
|
if (compositionMarks) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
77
|
+
cursorWrapper: widget(state.selection.from, CursorWrapper, {
|
|
78
|
+
key: "cursor-wrapper",
|
|
79
|
+
marks: compositionMarks,
|
|
80
|
+
side: 0,
|
|
81
|
+
raw: true
|
|
82
|
+
})
|
|
63
83
|
}));
|
|
84
|
+
// Pin the DOM cursor to PM's canonical position before the IME
|
|
85
|
+
// captures wherever the browser happened to leave it. Without this,
|
|
86
|
+
// a cursor at a mark boundary lands in either the left or right text
|
|
87
|
+
// node depending on the user's last navigation direction, and the
|
|
88
|
+
// IME composes into whichever one it found.
|
|
89
|
+
} else if (view.state.selection.empty) {
|
|
90
|
+
// @ts-expect-error internal method
|
|
91
|
+
view.domObserver.disconnectSelection();
|
|
92
|
+
try {
|
|
93
|
+
view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
|
|
94
|
+
);
|
|
95
|
+
} finally{
|
|
96
|
+
// @ts-expect-error internal method
|
|
97
|
+
view.domObserver.setCurSelection();
|
|
98
|
+
// @ts-expect-error internal method
|
|
99
|
+
view.domObserver.connectSelection();
|
|
100
|
+
}
|
|
64
101
|
}
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for (const node of parent.childNodes){
|
|
71
|
-
precompositionSnapshot.push(node);
|
|
72
|
-
}
|
|
73
|
-
// @ts-expect-error Internal property - input
|
|
102
|
+
view.compositionStarting = false;
|
|
103
|
+
// We set composing to true after creating the cursor wrapper
|
|
104
|
+
// so that no existing text nodes try to protect themselves
|
|
105
|
+
// while we're creating the cursor wrapper, which may need
|
|
106
|
+
// to split a text node.
|
|
74
107
|
view.input.composing = true;
|
|
75
108
|
return true;
|
|
76
109
|
},
|
|
@@ -78,40 +111,44 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
78
111
|
return true;
|
|
79
112
|
},
|
|
80
113
|
compositionend (view, event) {
|
|
81
|
-
|
|
114
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
115
|
+
if (!view.composing) return false;
|
|
82
116
|
view.input.composing = false;
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
117
|
+
compositionMarks = null;
|
|
118
|
+
for (const displaced of view.displacedNodes){
|
|
119
|
+
// Put the displaced TextViewDesc back into its parent's child list.
|
|
120
|
+
const parent = displaced.parent;
|
|
121
|
+
if (parent && !parent.children.includes(displaced)) {
|
|
122
|
+
parent.children.push(displaced);
|
|
123
|
+
parent.children.sort(sortViewDescs);
|
|
124
|
+
}
|
|
125
|
+
// Restore pmViewDesc claim on the text node.
|
|
126
|
+
displaced.dom.pmViewDesc = displaced;
|
|
127
|
+
// Truncate the IME text node back to what the displaced PM node says it
|
|
128
|
+
// is. The composed content lives in PM state; the next React render will
|
|
129
|
+
// mount a sibling TextNodeView that inserts its own DOM (e.g.
|
|
130
|
+
// `<span class="word">k</span>`) right after this node.
|
|
131
|
+
const claimedText = displaced.node.text ?? "";
|
|
132
|
+
if (displaced.nodeDOM.nodeValue !== claimedText) {
|
|
133
|
+
displaced.nodeDOM.nodeValue = claimedText;
|
|
101
134
|
}
|
|
102
135
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
137
|
+
cursorWrapper: null
|
|
138
|
+
}));
|
|
139
|
+
if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
|
|
140
|
+
view.input.compositionNode.remove();
|
|
107
141
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
view.input.compositionEndedAt = event.timeStamp;
|
|
143
|
+
view.input.compositionNode = null;
|
|
144
|
+
view.input.compositionNodes = [];
|
|
145
|
+
view.input.compositionID++;
|
|
111
146
|
return true;
|
|
112
147
|
},
|
|
113
148
|
beforeinput (view, event) {
|
|
114
|
-
event.
|
|
149
|
+
if (event.inputType !== "insertFromComposition") {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
}
|
|
115
152
|
switch(event.inputType){
|
|
116
153
|
case "insertParagraph":
|
|
117
154
|
case "insertLineBreak":
|
|
@@ -156,6 +193,63 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
156
193
|
insertText(view, event.data);
|
|
157
194
|
break;
|
|
158
195
|
}
|
|
196
|
+
case "insertCompositionText":
|
|
197
|
+
case "deleteCompositionText":
|
|
198
|
+
case "insertFromComposition":
|
|
199
|
+
{
|
|
200
|
+
if (!(view instanceof ReactEditorView)) break;
|
|
201
|
+
const { tr } = view.state;
|
|
202
|
+
// There's always a range on insertCompositionText beforeinput events
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
204
|
+
const range = event.getTargetRanges()[0];
|
|
205
|
+
const start = view.posAtDOM(range.startContainer, range.startOffset);
|
|
206
|
+
const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
|
|
207
|
+
if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (event.data) {
|
|
211
|
+
if (compositionMarks) tr.ensureMarks(compositionMarks);
|
|
212
|
+
tr.insertText(event.data, start, end);
|
|
213
|
+
} else {
|
|
214
|
+
tr.delete(start, end);
|
|
215
|
+
}
|
|
216
|
+
// When updating a composition within an existing text node,
|
|
217
|
+
// we need to avoid remounting it. If the composition is at
|
|
218
|
+
// the very beginning of the text node, the start position of
|
|
219
|
+
// that node will either be mapped forward (if inserting new
|
|
220
|
+
// content) or deleted (if replacing existing content).
|
|
221
|
+
//
|
|
222
|
+
// This will cause the reactKeys plugin to mint a new key for
|
|
223
|
+
// that node, which triggers a remount. So we check to see whether
|
|
224
|
+
// we're working on a composition at the very beginning of a text
|
|
225
|
+
// node, and if so, tell the react keys plugin not to change the
|
|
226
|
+
// key for that node.
|
|
227
|
+
//
|
|
228
|
+
// We need to check that the marks are the same — if they're not,
|
|
229
|
+
// then we're inserting text _before_ this text node, not at the
|
|
230
|
+
// start of it, so we actually _do_ want to map the exsting node
|
|
231
|
+
// forward.
|
|
232
|
+
const $start = view.state.doc.resolve(start);
|
|
233
|
+
const $end = view.state.doc.resolve(end);
|
|
234
|
+
const marks = compositionMarks ?? $start.marksAcross($end) ?? [];
|
|
235
|
+
if ($start.textOffset === 0 && $end.nodeAfter?.marks.every((m)=>m.isInSet(marks))) {
|
|
236
|
+
tr.setMeta(reactKeysPluginKey, {
|
|
237
|
+
overrides: {
|
|
238
|
+
[start]: start
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
view.dom.addEventListener("input", ()=>{
|
|
243
|
+
const sel = view.domSelectionRange();
|
|
244
|
+
if (sel.focusNode && sel.focusNode.nodeType === 3) {
|
|
245
|
+
view.input.compositionNode = sel.focusNode;
|
|
246
|
+
}
|
|
247
|
+
view.dispatch(tr);
|
|
248
|
+
}, {
|
|
249
|
+
once: true
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
159
253
|
case "deleteWordBackward":
|
|
160
254
|
case "deleteHardLineBackward":
|
|
161
255
|
case "deleteSoftLineBackward":
|
|
@@ -189,3 +283,11 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
189
283
|
}
|
|
190
284
|
});
|
|
191
285
|
}
|
|
286
|
+
function isCompositionNodeOrphaned(tn) {
|
|
287
|
+
if (tn.pmViewDesc) return false;
|
|
288
|
+
for(let parent = tn.parentNode; parent; parent = parent.parentNode){
|
|
289
|
+
const desc = parent.pmViewDesc;
|
|
290
|
+
if (desc instanceof TextViewDesc && desc.nodeDOM === tn) return false;
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
|
+
import { DecorationSet } from "prosemirror-view";
|
|
3
|
+
import { widget } from "../decorations/ReactWidgetType.js";
|
|
2
4
|
export function createNodeKey() {
|
|
3
5
|
const key = Math.floor(Math.random() * 0xffffffffffff).toString(16);
|
|
4
6
|
return key;
|
|
@@ -11,14 +13,14 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
11
13
|
* key for a given node can be accessed by that node's
|
|
12
14
|
* current position in the document, and vice versa.
|
|
13
15
|
*/ export function reactKeys() {
|
|
14
|
-
let composing = false;
|
|
15
16
|
return new Plugin({
|
|
16
17
|
key: reactKeysPluginKey,
|
|
17
18
|
state: {
|
|
18
19
|
init (_, state) {
|
|
19
20
|
const next = {
|
|
20
21
|
posToKey: new Map(),
|
|
21
|
-
keyToPos: new Map()
|
|
22
|
+
keyToPos: new Map(),
|
|
23
|
+
cursorWrapper: null
|
|
22
24
|
};
|
|
23
25
|
state.doc.descendants((_, pos)=>{
|
|
24
26
|
const key = createNodeKey();
|
|
@@ -36,14 +38,20 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
36
38
|
* and assign its key to that new position, dropping it if the
|
|
37
39
|
* node was deleted.
|
|
38
40
|
*/ apply (tr, value, _, newState) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const overrides = tr.getMeta(reactKeysPluginKey)?.overrides;
|
|
41
|
+
const meta = tr.getMeta(reactKeysPluginKey);
|
|
42
|
+
const overrides = meta && "overrides" in meta ? meta.overrides : {};
|
|
43
|
+
const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
|
|
43
44
|
const next = {
|
|
44
45
|
posToKey: new Map(),
|
|
45
|
-
keyToPos: new Map()
|
|
46
|
+
keyToPos: new Map(),
|
|
47
|
+
cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? widget(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper
|
|
46
48
|
};
|
|
49
|
+
if (!tr.docChanged) {
|
|
50
|
+
return {
|
|
51
|
+
...value,
|
|
52
|
+
cursorWrapper: next.cursorWrapper
|
|
53
|
+
};
|
|
54
|
+
}
|
|
47
55
|
const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
|
|
48
56
|
let [a] = param, [b] = param1;
|
|
49
57
|
return a - b;
|
|
@@ -69,13 +77,12 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
69
77
|
}
|
|
70
78
|
},
|
|
71
79
|
props: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
80
|
+
decorations (state) {
|
|
81
|
+
const deco = reactKeysPluginKey.getState(state)?.cursorWrapper;
|
|
82
|
+
if (!deco) return DecorationSet.empty;
|
|
83
|
+
return DecorationSet.create(state.doc, [
|
|
84
|
+
deco
|
|
85
|
+
]);
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
});
|
package/dist/esm/viewdesc.js
CHANGED
|
@@ -16,7 +16,17 @@ import { domIndex, isEquivalentPosition } from "./dom.js";
|
|
|
16
16
|
export function sortViewDescs(a, b) {
|
|
17
17
|
if (a instanceof TrailingHackViewDesc) return 1;
|
|
18
18
|
if (b instanceof TrailingHackViewDesc) return -1;
|
|
19
|
-
|
|
19
|
+
const posDiff = a.getPos() - b.getPos();
|
|
20
|
+
if (posDiff !== 0) return posDiff;
|
|
21
|
+
// When two descs share the same PM position (e.g. a zero-width widget
|
|
22
|
+
// and a text node that starts at the same position), fall back to DOM
|
|
23
|
+
// order so that the viewdesc children match the actual DOM layout.
|
|
24
|
+
// Without this, position computations like `posBeforeChild` can return
|
|
25
|
+
// the wrong PM position for the widget's container.
|
|
26
|
+
const cmp = a.dom.compareDocumentPosition(b.dom);
|
|
27
|
+
if (cmp & 4 /* DOCUMENT_POSITION_FOLLOWING */ ) return -1;
|
|
28
|
+
if (cmp & 2 /* DOCUMENT_POSITION_PRECEDING */ ) return 1;
|
|
29
|
+
return 0;
|
|
20
30
|
}
|
|
21
31
|
const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
|
|
22
32
|
// Superclass for the various kinds of descriptions. Defines their
|
|
@@ -235,7 +245,9 @@ export class ViewDesc {
|
|
|
235
245
|
prev = i ? this.children[i - 1] : null;
|
|
236
246
|
if (!prev || prev.dom.parentNode == this.contentDOM) break;
|
|
237
247
|
}
|
|
238
|
-
if (prev && side && enter && !prev.border && !prev.domAtom)
|
|
248
|
+
if (prev && side && enter && !prev.border && !prev.domAtom) {
|
|
249
|
+
return prev.domFromPos(prev.size, side);
|
|
250
|
+
}
|
|
239
251
|
return {
|
|
240
252
|
node: this.contentDOM,
|
|
241
253
|
offset: prev ? domIndex(prev.dom) + 1 : 0
|
|
@@ -370,7 +382,9 @@ export class ViewDesc {
|
|
|
370
382
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
371
383
|
if (after && after.contentEditable == "false") force = true;
|
|
372
384
|
}
|
|
373
|
-
if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset))
|
|
385
|
+
if (view.composing || !(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
374
388
|
// Selection.extend can be used to create an 'inverted' selection
|
|
375
389
|
// (one where the focus is before the anchor), but not all
|
|
376
390
|
// browsers support it yet.
|
|
@@ -645,7 +659,10 @@ export class TextViewDesc extends NodeViewDesc {
|
|
|
645
659
|
skip: skip || true
|
|
646
660
|
};
|
|
647
661
|
}
|
|
648
|
-
update(
|
|
662
|
+
update(node, outerDeco, _innerDeco, _view) {
|
|
663
|
+
if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
|
|
664
|
+
this.updateOuterDeco(outerDeco);
|
|
665
|
+
this.node = node;
|
|
649
666
|
this.dirty = NOT_DIRTY;
|
|
650
667
|
return true;
|
|
651
668
|
}
|
|
@@ -763,3 +780,33 @@ export function sameOuterDeco(a, b) {
|
|
|
763
780
|
for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
|
|
764
781
|
return true;
|
|
765
782
|
}
|
|
783
|
+
// Find a piece of text in an inline fragment, overlapping from-to.
|
|
784
|
+
// Ported from prosemirror-view's findTextInFragment.
|
|
785
|
+
export function findTextInFragment(frag, text, from, to) {
|
|
786
|
+
for(let i = 0, pos = 0; i < frag.childCount && pos <= to;){
|
|
787
|
+
const child = frag.child(i++);
|
|
788
|
+
const childStart = pos;
|
|
789
|
+
pos += child.nodeSize;
|
|
790
|
+
if (!child.isText) continue;
|
|
791
|
+
let str = child.text;
|
|
792
|
+
while(i < frag.childCount){
|
|
793
|
+
const next = frag.child(i++);
|
|
794
|
+
pos += next.nodeSize;
|
|
795
|
+
if (!next.isText) break;
|
|
796
|
+
str += next.text;
|
|
797
|
+
}
|
|
798
|
+
if (pos >= from) {
|
|
799
|
+
if (pos >= to && str.slice(to - text.length - childStart, to - childStart) === text) {
|
|
800
|
+
return to - text.length;
|
|
801
|
+
}
|
|
802
|
+
const found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
|
|
803
|
+
if (found >= 0 && found + text.length + childStart >= from) {
|
|
804
|
+
return childStart + found;
|
|
805
|
+
}
|
|
806
|
+
if (from === to && str.length >= to + text.length - childStart && str.slice(to - childStart, to - childStart + text.length) === text) {
|
|
807
|
+
return to;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return -1;
|
|
812
|
+
}
|