@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
|
@@ -120,12 +120,70 @@ function useMarkViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
|
120
120
|
child.parent = viewDesc;
|
|
121
121
|
}
|
|
122
122
|
});
|
|
123
|
+
const findCompositionDOM = (0, _react.useCallback)((compositionViewDesc)=>{
|
|
124
|
+
const children = childrenRef.current;
|
|
125
|
+
// Because TextNodeViews can't locate the DOM nodes
|
|
126
|
+
// for compositions, we need to override them here
|
|
127
|
+
if (!viewDescRef.current?.contentDOM) return;
|
|
128
|
+
let compositionTopDOM = null;
|
|
129
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
130
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
131
|
+
compositionTopDOM = childNode;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!compositionTopDOM) {
|
|
136
|
+
// Otherwise the IME extended an existing tracked text node. Take it over.
|
|
137
|
+
const reactView = view;
|
|
138
|
+
const imeTextNode = reactView.input.compositionNode;
|
|
139
|
+
if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const claimedDesc = imeTextNode.pmViewDesc;
|
|
143
|
+
if (!(claimedDesc instanceof _viewdesc.TextViewDesc)) return;
|
|
144
|
+
if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
|
|
145
|
+
// Walk up to the direct child of contentDOM that contains the IME text node
|
|
146
|
+
// (could be the text node itself, could be wrapped in a mark span).
|
|
147
|
+
let topDOM = imeTextNode;
|
|
148
|
+
while(topDOM.parentNode !== viewDescRef.current.contentDOM){
|
|
149
|
+
const next = topDOM.parentNode;
|
|
150
|
+
if (!next) return;
|
|
151
|
+
topDOM = next;
|
|
152
|
+
}
|
|
153
|
+
// Detach the displaced TextViewDesc from the sibling list so sibling-size
|
|
154
|
+
// accounting (used by posBeforeChild) doesn't double-count this text node.
|
|
155
|
+
const displacedIdx = children.indexOf(claimedDesc);
|
|
156
|
+
if (displacedIdx >= 0) children.splice(displacedIdx, 1);
|
|
157
|
+
compositionViewDesc.dom = topDOM;
|
|
158
|
+
compositionViewDesc.textDOM = imeTextNode;
|
|
159
|
+
compositionViewDesc.text = imeTextNode.data;
|
|
160
|
+
imeTextNode.pmViewDesc = compositionViewDesc;
|
|
161
|
+
compositionViewDesc._displacedDesc = claimedDesc;
|
|
162
|
+
reactView.input.compositionNodes.push(compositionViewDesc);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let textDOM = compositionTopDOM;
|
|
166
|
+
while(textDOM.firstChild){
|
|
167
|
+
textDOM = textDOM.firstChild;
|
|
168
|
+
}
|
|
169
|
+
if (!textDOM || !(textDOM instanceof Text)) {
|
|
170
|
+
console.error(compositionTopDOM, textDOM);
|
|
171
|
+
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
172
|
+
}
|
|
173
|
+
compositionViewDesc.dom = compositionTopDOM;
|
|
174
|
+
compositionViewDesc.textDOM = textDOM;
|
|
175
|
+
compositionViewDesc.text = textDOM.data;
|
|
176
|
+
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
177
|
+
view.input.compositionNodes.push(compositionViewDesc);
|
|
178
|
+
}, [
|
|
179
|
+
view
|
|
180
|
+
]);
|
|
123
181
|
const childContextValue = (0, _react.useMemo)(()=>({
|
|
124
182
|
parentRef: viewDescRef,
|
|
125
|
-
siblingsRef: childrenRef
|
|
183
|
+
siblingsRef: childrenRef,
|
|
184
|
+
findCompositionDOM
|
|
126
185
|
}), [
|
|
127
|
-
|
|
128
|
-
viewDescRef
|
|
186
|
+
findCompositionDOM
|
|
129
187
|
]);
|
|
130
188
|
return {
|
|
131
189
|
childContextValue,
|
|
@@ -141,32 +141,75 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
|
141
141
|
children.sort(_viewdesc.sortViewDescs);
|
|
142
142
|
for (const child of children){
|
|
143
143
|
child.parent = viewDesc;
|
|
144
|
-
// Because TextNodeViews can't locate the DOM nodes
|
|
145
|
-
// for compositions, we need to override them here
|
|
146
|
-
if (child instanceof _viewdesc.CompositionViewDesc) {
|
|
147
|
-
const compositionTopDOM = viewDesc?.contentDOM?.firstChild;
|
|
148
|
-
if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
149
|
-
let textDOM = compositionTopDOM;
|
|
150
|
-
while(textDOM.firstChild){
|
|
151
|
-
textDOM = textDOM.firstChild;
|
|
152
|
-
}
|
|
153
|
-
if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
154
|
-
child.dom = compositionTopDOM;
|
|
155
|
-
child.textDOM = textDOM;
|
|
156
|
-
child.text = textDOM.data;
|
|
157
|
-
child.textDOM.pmViewDesc = child;
|
|
158
|
-
// It should not be possible to be in a composition because one could
|
|
159
|
-
// not start between the renders that switch the view type.
|
|
160
|
-
view.input.compositionNodes.push(child);
|
|
161
|
-
}
|
|
162
144
|
}
|
|
163
145
|
});
|
|
146
|
+
const findCompositionDOM = (0, _react.useCallback)((compositionViewDesc)=>{
|
|
147
|
+
if (!props.node.isTextblock) return;
|
|
148
|
+
const children = childrenRef.current;
|
|
149
|
+
// Because TextNodeViews can't locate the DOM nodes
|
|
150
|
+
// for compositions, we need to override them here
|
|
151
|
+
if (!viewDescRef.current?.contentDOM) return;
|
|
152
|
+
let compositionTopDOM = null;
|
|
153
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
154
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
155
|
+
compositionTopDOM = childNode;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!compositionTopDOM) {
|
|
160
|
+
// Otherwise the IME extended an existing tracked text node. Take it over.
|
|
161
|
+
const reactView = view;
|
|
162
|
+
const imeTextNode = reactView.input.compositionNode;
|
|
163
|
+
if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const claimedDesc = imeTextNode.pmViewDesc;
|
|
167
|
+
if (!(claimedDesc instanceof _viewdesc.TextViewDesc)) return;
|
|
168
|
+
if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
|
|
169
|
+
// Walk up to the direct child of contentDOM that contains the IME text node
|
|
170
|
+
// (could be the text node itself, could be wrapped in a mark span).
|
|
171
|
+
let topDOM = imeTextNode;
|
|
172
|
+
while(topDOM.parentNode !== viewDescRef.current.contentDOM){
|
|
173
|
+
const next = topDOM.parentNode;
|
|
174
|
+
if (!next) return;
|
|
175
|
+
topDOM = next;
|
|
176
|
+
}
|
|
177
|
+
// Detach the displaced TextViewDesc from the sibling list so sibling-size
|
|
178
|
+
// accounting (used by posBeforeChild) doesn't double-count this text node.
|
|
179
|
+
const displacedIdx = children.indexOf(claimedDesc);
|
|
180
|
+
if (displacedIdx >= 0) children.splice(displacedIdx, 1);
|
|
181
|
+
reactView.displacedNodes.push(claimedDesc);
|
|
182
|
+
compositionViewDesc.dom = topDOM;
|
|
183
|
+
compositionViewDesc.textDOM = imeTextNode;
|
|
184
|
+
compositionViewDesc.text = imeTextNode.data;
|
|
185
|
+
imeTextNode.pmViewDesc = compositionViewDesc;
|
|
186
|
+
compositionViewDesc._displacedDesc = claimedDesc;
|
|
187
|
+
reactView.input.compositionNodes.push(compositionViewDesc);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let textDOM = compositionTopDOM;
|
|
191
|
+
while(textDOM.firstChild){
|
|
192
|
+
textDOM = textDOM.firstChild;
|
|
193
|
+
}
|
|
194
|
+
if (!textDOM || !(textDOM instanceof Text)) {
|
|
195
|
+
console.error(compositionTopDOM, textDOM);
|
|
196
|
+
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
197
|
+
}
|
|
198
|
+
compositionViewDesc.dom = compositionTopDOM;
|
|
199
|
+
compositionViewDesc.textDOM = textDOM;
|
|
200
|
+
compositionViewDesc.text = textDOM.data;
|
|
201
|
+
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
202
|
+
view.input.compositionNodes.push(compositionViewDesc);
|
|
203
|
+
}, [
|
|
204
|
+
props.node.isTextblock,
|
|
205
|
+
view
|
|
206
|
+
]);
|
|
164
207
|
const childContextValue = (0, _react.useMemo)(()=>({
|
|
165
208
|
parentRef: viewDescRef,
|
|
166
|
-
siblingsRef: childrenRef
|
|
209
|
+
siblingsRef: childrenRef,
|
|
210
|
+
findCompositionDOM
|
|
167
211
|
}), [
|
|
168
|
-
|
|
169
|
-
viewDescRef
|
|
212
|
+
findCompositionDOM
|
|
170
213
|
]);
|
|
171
214
|
return {
|
|
172
215
|
childContextValue,
|
|
@@ -10,8 +10,11 @@ Object.defineProperty(exports, "beforeInputPlugin", {
|
|
|
10
10
|
});
|
|
11
11
|
const _prosemirrormodel = require("prosemirror-model");
|
|
12
12
|
const _prosemirrorstate = require("prosemirror-state");
|
|
13
|
+
const _ReactEditorView = require("../ReactEditorView.js");
|
|
13
14
|
const _CursorWrapper = require("../components/CursorWrapper.js");
|
|
14
15
|
const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
|
|
16
|
+
const _viewdesc = require("../viewdesc.js");
|
|
17
|
+
const _reactKeys = require("./reactKeys.js");
|
|
15
18
|
function insertText(view, eventData) {
|
|
16
19
|
let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
17
20
|
if (eventData === null) return false;
|
|
@@ -54,33 +57,63 @@ function handleGapCursorComposition(view) {
|
|
|
54
57
|
tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
55
58
|
view.dispatch(tr);
|
|
56
59
|
}
|
|
57
|
-
function beforeInputPlugin(
|
|
60
|
+
function beforeInputPlugin() {
|
|
58
61
|
let compositionMarks = null;
|
|
59
|
-
let precompositionSnapshot = null;
|
|
60
62
|
return new _prosemirrorstate.Plugin({
|
|
61
63
|
props: {
|
|
62
64
|
handleDOMEvents: {
|
|
63
65
|
compositionstart (view) {
|
|
64
|
-
|
|
65
|
-
view.
|
|
66
|
-
handleGapCursorComposition(view);
|
|
66
|
+
if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
|
|
67
|
+
view.compositionStarting = true;
|
|
67
68
|
const { state } = view;
|
|
68
|
-
const
|
|
69
|
+
const { selection } = state;
|
|
70
|
+
const isEmptyTr = state.tr.delete(selection.from, selection.to);
|
|
71
|
+
const $from = isEmptyTr.doc.resolve(isEmptyTr.mapping.map(selection.from));
|
|
72
|
+
const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
|
|
73
|
+
compositionMarks = view.state.storedMarks;
|
|
74
|
+
// Render a CursorWrapper with empty marks if starting a composition in an
|
|
75
|
+
// empty textblock with no marks. This prevents the browser from adding a
|
|
76
|
+
// <br> to the text block when it becomes empty (either via canceling the
|
|
77
|
+
// composition with the escape key or deleting all composition text when
|
|
78
|
+
// the composition node is the only text node in the text block)
|
|
79
|
+
if (compositionMarks === null && isEmptyTextblock) {
|
|
80
|
+
compositionMarks = [];
|
|
81
|
+
}
|
|
82
|
+
const tr = view.state.tr.setStoredMarks(null);
|
|
83
|
+
view.dispatch(tr);
|
|
84
|
+
handleGapCursorComposition(view);
|
|
69
85
|
if (compositionMarks) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
|
|
87
|
+
cursorWrapper: (0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
|
|
88
|
+
key: "cursor-wrapper",
|
|
89
|
+
marks: compositionMarks,
|
|
90
|
+
side: 0,
|
|
91
|
+
raw: true
|
|
92
|
+
})
|
|
73
93
|
}));
|
|
94
|
+
// Pin the DOM cursor to PM's canonical position before the IME
|
|
95
|
+
// captures wherever the browser happened to leave it. Without this,
|
|
96
|
+
// a cursor at a mark boundary lands in either the left or right text
|
|
97
|
+
// node depending on the user's last navigation direction, and the
|
|
98
|
+
// IME composes into whichever one it found.
|
|
99
|
+
} else if (view.state.selection.empty) {
|
|
100
|
+
// @ts-expect-error internal method
|
|
101
|
+
view.domObserver.disconnectSelection();
|
|
102
|
+
try {
|
|
103
|
+
view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
|
|
104
|
+
);
|
|
105
|
+
} finally{
|
|
106
|
+
// @ts-expect-error internal method
|
|
107
|
+
view.domObserver.setCurSelection();
|
|
108
|
+
// @ts-expect-error internal method
|
|
109
|
+
view.domObserver.connectSelection();
|
|
110
|
+
}
|
|
74
111
|
}
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const node of parent.childNodes){
|
|
81
|
-
precompositionSnapshot.push(node);
|
|
82
|
-
}
|
|
83
|
-
// @ts-expect-error Internal property - input
|
|
112
|
+
view.compositionStarting = false;
|
|
113
|
+
// We set composing to true after creating the cursor wrapper
|
|
114
|
+
// so that no existing text nodes try to protect themselves
|
|
115
|
+
// while we're creating the cursor wrapper, which may need
|
|
116
|
+
// to split a text node.
|
|
84
117
|
view.input.composing = true;
|
|
85
118
|
return true;
|
|
86
119
|
},
|
|
@@ -88,40 +121,44 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
88
121
|
return true;
|
|
89
122
|
},
|
|
90
123
|
compositionend (view, event) {
|
|
91
|
-
|
|
124
|
+
if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
|
|
125
|
+
if (!view.composing) return false;
|
|
92
126
|
view.input.composing = false;
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
127
|
+
compositionMarks = null;
|
|
128
|
+
for (const displaced of view.displacedNodes){
|
|
129
|
+
// Put the displaced TextViewDesc back into its parent's child list.
|
|
130
|
+
const parent = displaced.parent;
|
|
131
|
+
if (parent && !parent.children.includes(displaced)) {
|
|
132
|
+
parent.children.push(displaced);
|
|
133
|
+
parent.children.sort(_viewdesc.sortViewDescs);
|
|
134
|
+
}
|
|
135
|
+
// Restore pmViewDesc claim on the text node.
|
|
136
|
+
displaced.dom.pmViewDesc = displaced;
|
|
137
|
+
// Truncate the IME text node back to what the displaced PM node says it
|
|
138
|
+
// is. The composed content lives in PM state; the next React render will
|
|
139
|
+
// mount a sibling TextNodeView that inserts its own DOM (e.g.
|
|
140
|
+
// `<span class="word">k</span>`) right after this node.
|
|
141
|
+
const claimedText = displaced.node.text ?? "";
|
|
142
|
+
if (displaced.nodeDOM.nodeValue !== claimedText) {
|
|
143
|
+
displaced.nodeDOM.nodeValue = claimedText;
|
|
111
144
|
}
|
|
112
145
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
146
|
+
view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
|
|
147
|
+
cursorWrapper: null
|
|
148
|
+
}));
|
|
149
|
+
if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
|
|
150
|
+
view.input.compositionNode.remove();
|
|
117
151
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
view.input.compositionEndedAt = event.timeStamp;
|
|
153
|
+
view.input.compositionNode = null;
|
|
154
|
+
view.input.compositionNodes = [];
|
|
155
|
+
view.input.compositionID++;
|
|
121
156
|
return true;
|
|
122
157
|
},
|
|
123
158
|
beforeinput (view, event) {
|
|
124
|
-
event.
|
|
159
|
+
if (event.inputType !== "insertFromComposition") {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
}
|
|
125
162
|
switch(event.inputType){
|
|
126
163
|
case "insertParagraph":
|
|
127
164
|
case "insertLineBreak":
|
|
@@ -166,6 +203,63 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
166
203
|
insertText(view, event.data);
|
|
167
204
|
break;
|
|
168
205
|
}
|
|
206
|
+
case "insertCompositionText":
|
|
207
|
+
case "deleteCompositionText":
|
|
208
|
+
case "insertFromComposition":
|
|
209
|
+
{
|
|
210
|
+
if (!(view instanceof _ReactEditorView.ReactEditorView)) break;
|
|
211
|
+
const { tr } = view.state;
|
|
212
|
+
// There's always a range on insertCompositionText beforeinput events
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
214
|
+
const range = event.getTargetRanges()[0];
|
|
215
|
+
const start = view.posAtDOM(range.startContainer, range.startOffset);
|
|
216
|
+
const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
|
|
217
|
+
if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (event.data) {
|
|
221
|
+
if (compositionMarks) tr.ensureMarks(compositionMarks);
|
|
222
|
+
tr.insertText(event.data, start, end);
|
|
223
|
+
} else {
|
|
224
|
+
tr.delete(start, end);
|
|
225
|
+
}
|
|
226
|
+
// When updating a composition within an existing text node,
|
|
227
|
+
// we need to avoid remounting it. If the composition is at
|
|
228
|
+
// the very beginning of the text node, the start position of
|
|
229
|
+
// that node will either be mapped forward (if inserting new
|
|
230
|
+
// content) or deleted (if replacing existing content).
|
|
231
|
+
//
|
|
232
|
+
// This will cause the reactKeys plugin to mint a new key for
|
|
233
|
+
// that node, which triggers a remount. So we check to see whether
|
|
234
|
+
// we're working on a composition at the very beginning of a text
|
|
235
|
+
// node, and if so, tell the react keys plugin not to change the
|
|
236
|
+
// key for that node.
|
|
237
|
+
//
|
|
238
|
+
// We need to check that the marks are the same — if they're not,
|
|
239
|
+
// then we're inserting text _before_ this text node, not at the
|
|
240
|
+
// start of it, so we actually _do_ want to map the exsting node
|
|
241
|
+
// forward.
|
|
242
|
+
const $start = view.state.doc.resolve(start);
|
|
243
|
+
const $end = view.state.doc.resolve(end);
|
|
244
|
+
const marks = compositionMarks ?? $start.marksAcross($end) ?? [];
|
|
245
|
+
if ($start.textOffset === 0 && $end.nodeAfter?.marks.every((m)=>m.isInSet(marks))) {
|
|
246
|
+
tr.setMeta(_reactKeys.reactKeysPluginKey, {
|
|
247
|
+
overrides: {
|
|
248
|
+
[start]: start
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
view.dom.addEventListener("input", ()=>{
|
|
253
|
+
const sel = view.domSelectionRange();
|
|
254
|
+
if (sel.focusNode && sel.focusNode.nodeType === 3) {
|
|
255
|
+
view.input.compositionNode = sel.focusNode;
|
|
256
|
+
}
|
|
257
|
+
view.dispatch(tr);
|
|
258
|
+
}, {
|
|
259
|
+
once: true
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
169
263
|
case "deleteWordBackward":
|
|
170
264
|
case "deleteHardLineBackward":
|
|
171
265
|
case "deleteSoftLineBackward":
|
|
@@ -199,3 +293,11 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
199
293
|
}
|
|
200
294
|
});
|
|
201
295
|
}
|
|
296
|
+
function isCompositionNodeOrphaned(tn) {
|
|
297
|
+
if (tn.pmViewDesc) return false;
|
|
298
|
+
for(let parent = tn.parentNode; parent; parent = parent.parentNode){
|
|
299
|
+
const desc = parent.pmViewDesc;
|
|
300
|
+
if (desc instanceof _viewdesc.TextViewDesc && desc.nodeDOM === tn) return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
@@ -20,20 +20,22 @@ _export(exports, {
|
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
const _prosemirrorstate = require("prosemirror-state");
|
|
23
|
+
const _prosemirrorview = require("prosemirror-view");
|
|
24
|
+
const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
|
|
23
25
|
function createNodeKey() {
|
|
24
26
|
const key = Math.floor(Math.random() * 0xffffffffffff).toString(16);
|
|
25
27
|
return key;
|
|
26
28
|
}
|
|
27
29
|
const reactKeysPluginKey = new _prosemirrorstate.PluginKey("@handlewithcare/react-prosemirror/reactKeys");
|
|
28
30
|
function reactKeys() {
|
|
29
|
-
let composing = false;
|
|
30
31
|
return new _prosemirrorstate.Plugin({
|
|
31
32
|
key: reactKeysPluginKey,
|
|
32
33
|
state: {
|
|
33
34
|
init (_, state) {
|
|
34
35
|
const next = {
|
|
35
36
|
posToKey: new Map(),
|
|
36
|
-
keyToPos: new Map()
|
|
37
|
+
keyToPos: new Map(),
|
|
38
|
+
cursorWrapper: null
|
|
37
39
|
};
|
|
38
40
|
state.doc.descendants((_, pos)=>{
|
|
39
41
|
const key = createNodeKey();
|
|
@@ -51,14 +53,20 @@ function reactKeys() {
|
|
|
51
53
|
* and assign its key to that new position, dropping it if the
|
|
52
54
|
* node was deleted.
|
|
53
55
|
*/ apply (tr, value, _, newState) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const overrides = tr.getMeta(reactKeysPluginKey)?.overrides;
|
|
56
|
+
const meta = tr.getMeta(reactKeysPluginKey);
|
|
57
|
+
const overrides = meta && "overrides" in meta ? meta.overrides : {};
|
|
58
|
+
const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
|
|
58
59
|
const next = {
|
|
59
60
|
posToKey: new Map(),
|
|
60
|
-
keyToPos: new Map()
|
|
61
|
+
keyToPos: new Map(),
|
|
62
|
+
cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? (0, _ReactWidgetType.widget)(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper
|
|
61
63
|
};
|
|
64
|
+
if (!tr.docChanged) {
|
|
65
|
+
return {
|
|
66
|
+
...value,
|
|
67
|
+
cursorWrapper: next.cursorWrapper
|
|
68
|
+
};
|
|
69
|
+
}
|
|
62
70
|
const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
|
|
63
71
|
let [a] = param, [b] = param1;
|
|
64
72
|
return a - b;
|
|
@@ -84,13 +92,12 @@ function reactKeys() {
|
|
|
84
92
|
}
|
|
85
93
|
},
|
|
86
94
|
props: {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
95
|
+
decorations (state) {
|
|
96
|
+
const deco = reactKeysPluginKey.getState(state)?.cursorWrapper;
|
|
97
|
+
if (!deco) return _prosemirrorview.DecorationSet.empty;
|
|
98
|
+
return _prosemirrorview.DecorationSet.create(state.doc, [
|
|
99
|
+
deco
|
|
100
|
+
]);
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
});
|
package/dist/cjs/viewdesc.js
CHANGED
|
@@ -36,6 +36,9 @@ _export(exports, {
|
|
|
36
36
|
WidgetViewDesc: function() {
|
|
37
37
|
return WidgetViewDesc;
|
|
38
38
|
},
|
|
39
|
+
findTextInFragment: function() {
|
|
40
|
+
return findTextInFragment;
|
|
41
|
+
},
|
|
39
42
|
sameOuterDeco: function() {
|
|
40
43
|
return sameOuterDeco;
|
|
41
44
|
},
|
|
@@ -49,7 +52,17 @@ const _dom = require("./dom.js");
|
|
|
49
52
|
function sortViewDescs(a, b) {
|
|
50
53
|
if (a instanceof TrailingHackViewDesc) return 1;
|
|
51
54
|
if (b instanceof TrailingHackViewDesc) return -1;
|
|
52
|
-
|
|
55
|
+
const posDiff = a.getPos() - b.getPos();
|
|
56
|
+
if (posDiff !== 0) return posDiff;
|
|
57
|
+
// When two descs share the same PM position (e.g. a zero-width widget
|
|
58
|
+
// and a text node that starts at the same position), fall back to DOM
|
|
59
|
+
// order so that the viewdesc children match the actual DOM layout.
|
|
60
|
+
// Without this, position computations like `posBeforeChild` can return
|
|
61
|
+
// the wrong PM position for the widget's container.
|
|
62
|
+
const cmp = a.dom.compareDocumentPosition(b.dom);
|
|
63
|
+
if (cmp & 4 /* DOCUMENT_POSITION_FOLLOWING */ ) return -1;
|
|
64
|
+
if (cmp & 2 /* DOCUMENT_POSITION_PRECEDING */ ) return 1;
|
|
65
|
+
return 0;
|
|
53
66
|
}
|
|
54
67
|
const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
|
|
55
68
|
let ViewDesc = class ViewDesc {
|
|
@@ -266,7 +279,9 @@ let ViewDesc = class ViewDesc {
|
|
|
266
279
|
prev = i ? this.children[i - 1] : null;
|
|
267
280
|
if (!prev || prev.dom.parentNode == this.contentDOM) break;
|
|
268
281
|
}
|
|
269
|
-
if (prev && side && enter && !prev.border && !prev.domAtom)
|
|
282
|
+
if (prev && side && enter && !prev.border && !prev.domAtom) {
|
|
283
|
+
return prev.domFromPos(prev.size, side);
|
|
284
|
+
}
|
|
270
285
|
return {
|
|
271
286
|
node: this.contentDOM,
|
|
272
287
|
offset: prev ? (0, _dom.domIndex)(prev.dom) + 1 : 0
|
|
@@ -401,7 +416,9 @@ let ViewDesc = class ViewDesc {
|
|
|
401
416
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
402
417
|
if (after && after.contentEditable == "false") force = true;
|
|
403
418
|
}
|
|
404
|
-
if (!(force || brKludge && _browser.browser.safari) && (0, _dom.isEquivalentPosition)(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && (0, _dom.isEquivalentPosition)(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset))
|
|
419
|
+
if (view.composing || !(force || brKludge && _browser.browser.safari) && (0, _dom.isEquivalentPosition)(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && (0, _dom.isEquivalentPosition)(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
405
422
|
// Selection.extend can be used to create an 'inverted' selection
|
|
406
423
|
// (one where the focus is before the anchor), but not all
|
|
407
424
|
// browsers support it yet.
|
|
@@ -666,7 +683,10 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
|
|
|
666
683
|
skip: skip || true
|
|
667
684
|
};
|
|
668
685
|
}
|
|
669
|
-
update(
|
|
686
|
+
update(node, outerDeco, _innerDeco, _view) {
|
|
687
|
+
if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
|
|
688
|
+
this.updateOuterDeco(outerDeco);
|
|
689
|
+
this.node = node;
|
|
670
690
|
this.dirty = NOT_DIRTY;
|
|
671
691
|
return true;
|
|
672
692
|
}
|
|
@@ -782,3 +802,31 @@ function sameOuterDeco(a, b) {
|
|
|
782
802
|
for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
|
|
783
803
|
return true;
|
|
784
804
|
}
|
|
805
|
+
function findTextInFragment(frag, text, from, to) {
|
|
806
|
+
for(let i = 0, pos = 0; i < frag.childCount && pos <= to;){
|
|
807
|
+
const child = frag.child(i++);
|
|
808
|
+
const childStart = pos;
|
|
809
|
+
pos += child.nodeSize;
|
|
810
|
+
if (!child.isText) continue;
|
|
811
|
+
let str = child.text;
|
|
812
|
+
while(i < frag.childCount){
|
|
813
|
+
const next = frag.child(i++);
|
|
814
|
+
pos += next.nodeSize;
|
|
815
|
+
if (!next.isText) break;
|
|
816
|
+
str += next.text;
|
|
817
|
+
}
|
|
818
|
+
if (pos >= from) {
|
|
819
|
+
if (pos >= to && str.slice(to - text.length - childStart, to - childStart) === text) {
|
|
820
|
+
return to - text.length;
|
|
821
|
+
}
|
|
822
|
+
const found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
|
|
823
|
+
if (found >= 0 && found + text.length + childStart >= from) {
|
|
824
|
+
return childStart + found;
|
|
825
|
+
}
|
|
826
|
+
if (from === to && str.length >= to + text.length - childStart && str.slice(to - childStart, to - childStart + text.length) === text) {
|
|
827
|
+
return to;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return -1;
|
|
832
|
+
}
|
|
@@ -33,6 +33,10 @@ function changedNodeViews(a, b) {
|
|
|
33
33
|
nextProps;
|
|
34
34
|
prevState;
|
|
35
35
|
_destroyed;
|
|
36
|
+
// TODO: Probably refactor? It's used in TrailingHackView to detect
|
|
37
|
+
// whether it was mounted during a compositionstart event handler
|
|
38
|
+
compositionStarting;
|
|
39
|
+
displacedNodes;
|
|
36
40
|
constructor(place, props){
|
|
37
41
|
// Prevent the base class from destroying the React-managed nodes.
|
|
38
42
|
// Restore them below after invoking the base class constructor.
|
|
@@ -85,6 +89,8 @@ function changedNodeViews(a, b) {
|
|
|
85
89
|
// @ts-expect-error this violates the typing but class does it, too.
|
|
86
90
|
this.docView = null;
|
|
87
91
|
this._destroyed = false;
|
|
92
|
+
this.compositionStarting = false;
|
|
93
|
+
this.displacedNodes = [];
|
|
88
94
|
}
|
|
89
95
|
get props() {
|
|
90
96
|
return this.nextProps;
|
|
@@ -7,7 +7,7 @@ import { htmlAttrsToReactProps, mergeReactProps } from "../props.js";
|
|
|
7
7
|
import { sameOuterDeco } from "../viewdesc.js";
|
|
8
8
|
import { NativeWidgetView } from "./NativeWidgetView.js";
|
|
9
9
|
import { SeparatorHackView } from "./SeparatorHackView.js";
|
|
10
|
-
import {
|
|
10
|
+
import { RemountableTextNodeView } from "./TextNodeView.js";
|
|
11
11
|
import { TrailingHackView } from "./TrailingHackView.js";
|
|
12
12
|
import { WidgetView } from "./WidgetView.js";
|
|
13
13
|
import { MarkView } from "./marks/MarkView.js";
|
|
@@ -50,14 +50,20 @@ const ChildView = /*#__PURE__*/ memo(function ChildView(param) {
|
|
|
50
50
|
}) : child.node.isText ? /*#__PURE__*/ React.createElement(ChildDescriptionsContext.Consumer, {
|
|
51
51
|
key: child.key
|
|
52
52
|
}, (param)=>{
|
|
53
|
-
let { siblingsRef, parentRef } = param;
|
|
54
|
-
return /*#__PURE__*/ React.createElement(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
let { siblingsRef, parentRef, findCompositionDOM } = param;
|
|
54
|
+
return /*#__PURE__*/ React.createElement(EditorContext.Consumer, null, (param)=>{
|
|
55
|
+
let { registerEventListener, unregisterEventListener } = param;
|
|
56
|
+
return /*#__PURE__*/ React.createElement(RemountableTextNodeView, {
|
|
57
|
+
view: view,
|
|
58
|
+
node: child.node,
|
|
59
|
+
getPos: getPos,
|
|
60
|
+
siblingsRef: siblingsRef,
|
|
61
|
+
parentRef: parentRef,
|
|
62
|
+
findCompositionDOM: findCompositionDOM,
|
|
63
|
+
decorations: child.outerDeco,
|
|
64
|
+
registerEventListener: registerEventListener,
|
|
65
|
+
unregisterEventListener: unregisterEventListener
|
|
66
|
+
});
|
|
61
67
|
});
|
|
62
68
|
}) : /*#__PURE__*/ React.createElement(NodeView, {
|
|
63
69
|
key: child.key,
|
|
@@ -340,14 +346,14 @@ export const ChildNodeViews = /*#__PURE__*/ memo(function ChildNodeViews(param)
|
|
|
340
346
|
component: SeparatorHackView,
|
|
341
347
|
marks: [],
|
|
342
348
|
offset: lastChild?.offset ?? 0,
|
|
343
|
-
index: (lastChild?.index ?? 0) +
|
|
349
|
+
index: (lastChild?.index ?? 0) + 1,
|
|
344
350
|
key: "trailing-hack-img"
|
|
345
351
|
}, {
|
|
346
352
|
type: "hack",
|
|
347
353
|
component: TrailingHackView,
|
|
348
354
|
marks: [],
|
|
349
355
|
offset: lastChild?.offset ?? 0,
|
|
350
|
-
index: (lastChild?.index ?? 0) +
|
|
356
|
+
index: (lastChild?.index ?? 0) + 2,
|
|
351
357
|
key: "trailing-hack-br"
|
|
352
358
|
});
|
|
353
359
|
}
|