@handlewithcare/react-prosemirror 3.1.0-tiptap.51 → 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 -2
- package/dist/cjs/components/ChildNodeViews.js +3 -2
- package/dist/cjs/components/CursorWrapper.js +3 -4
- package/dist/cjs/components/ProseMirror.js +5 -3
- package/dist/cjs/components/TextNodeView.js +176 -34
- package/dist/cjs/components/TrailingHackView.js +42 -1
- package/dist/cjs/components/WidgetView.js +4 -1
- package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
- package/dist/cjs/decorations/viewDecorations.js +1 -6
- package/dist/cjs/hooks/useComponentEventListeners.js +6 -14
- package/dist/cjs/hooks/useEditor.js +2 -10
- package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
- package/dist/cjs/hooks/useNodeViewDescription.js +45 -23
- package/dist/cjs/plugins/beforeInputPlugin.js +116 -12
- package/dist/cjs/plugins/componentEventListeners.js +2 -9
- package/dist/cjs/plugins/reactKeys.js +21 -14
- package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +59 -0
- package/dist/cjs/viewdesc.js +43 -2
- package/dist/esm/ReactEditorView.js +6 -2
- package/dist/esm/components/ChildNodeViews.js +4 -3
- package/dist/esm/components/CursorWrapper.js +4 -5
- package/dist/esm/components/ProseMirror.js +5 -3
- package/dist/esm/components/TextNodeView.js +125 -32
- package/dist/esm/components/TrailingHackView.js +42 -1
- package/dist/esm/components/WidgetView.js +4 -1
- package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
- package/dist/esm/decorations/viewDecorations.js +1 -6
- package/dist/esm/hooks/useComponentEventListeners.js +6 -14
- package/dist/esm/hooks/useEditor.js +2 -10
- package/dist/esm/hooks/useMarkViewDescription.js +62 -4
- package/dist/esm/hooks/useNodeViewDescription.js +46 -24
- package/dist/esm/plugins/beforeInputPlugin.js +116 -12
- package/dist/esm/plugins/componentEventListeners.js +2 -9
- package/dist/esm/plugins/reactKeys.js +21 -14
- package/dist/esm/tiptap/hooks/useTiptapEditor.js +7 -1
- package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +56 -0
- package/dist/esm/viewdesc.js +42 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +3 -2
- package/dist/types/components/CursorWrapper.d.ts +2 -4
- package/dist/types/components/TextNodeView.d.ts +11 -6
- package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
- package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
- package/dist/types/decorations/viewDecorations.d.ts +2 -2
- package/dist/types/hooks/useComponentEventListeners.d.ts +1 -1
- 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/componentEventListeners.d.ts +2 -3
- package/dist/types/plugins/reactKeys.d.ts +9 -8
- package/dist/types/props.d.ts +26 -26
- package/dist/types/tiptap/hooks/useTiptapEditor.d.ts +7 -0
- package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +1 -0
- package/dist/types/viewdesc.d.ts +3 -2
- package/package.json +2 -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,
|
|
@@ -10,10 +10,8 @@ Object.defineProperty(exports, "useNodeViewDescription", {
|
|
|
10
10
|
});
|
|
11
11
|
const _react = require("react");
|
|
12
12
|
const _ReactEditorView = require("../ReactEditorView.js");
|
|
13
|
-
const _CursorWrapper = require("../components/CursorWrapper.js");
|
|
14
13
|
const _ChildDescriptionsContext = require("../contexts/ChildDescriptionsContext.js");
|
|
15
14
|
const _EditorContext = require("../contexts/EditorContext.js");
|
|
16
|
-
const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
|
|
17
15
|
const _viewdesc = require("../viewdesc.js");
|
|
18
16
|
const _useClientLayoutEffect = require("./useClientLayoutEffect.js");
|
|
19
17
|
const _useEffectEvent = require("./useEffectEvent.js");
|
|
@@ -144,30 +142,51 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
|
144
142
|
for (const child of children){
|
|
145
143
|
child.parent = viewDesc;
|
|
146
144
|
}
|
|
145
|
+
});
|
|
146
|
+
const findCompositionDOM = (0, _react.useCallback)((compositionViewDesc)=>{
|
|
147
147
|
if (!props.node.isTextblock) return;
|
|
148
|
+
const children = childrenRef.current;
|
|
148
149
|
// Because TextNodeViews can't locate the DOM nodes
|
|
149
150
|
// for compositions, we need to override them here
|
|
150
151
|
if (!viewDescRef.current?.contentDOM) return;
|
|
151
|
-
const compositionChildIndex = children.findIndex((child)=>child instanceof _viewdesc.CompositionViewDesc);
|
|
152
|
-
if (compositionChildIndex === -1) return;
|
|
153
|
-
const compositionViewDesc = children[compositionChildIndex];
|
|
154
|
-
if (!(compositionViewDesc instanceof _viewdesc.CompositionViewDesc)) return;
|
|
155
152
|
let compositionTopDOM = null;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (search instanceof _viewdesc.WidgetViewDesc && search.widget.type instanceof _ReactWidgetType.ReactWidgetType && search.widget.type.Component === _CursorWrapper.CursorWrapper) {
|
|
161
|
-
compositionTopDOM = search.dom.nextSibling;
|
|
162
|
-
} else {
|
|
163
|
-
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
164
|
-
if (children.every((child)=>child.dom !== childNode)) {
|
|
165
|
-
compositionTopDOM = childNode;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
153
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
154
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
155
|
+
compositionTopDOM = childNode;
|
|
156
|
+
break;
|
|
168
157
|
}
|
|
169
158
|
}
|
|
170
|
-
if (!compositionTopDOM)
|
|
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
|
+
}
|
|
171
190
|
let textDOM = compositionTopDOM;
|
|
172
191
|
while(textDOM.firstChild){
|
|
173
192
|
textDOM = textDOM.firstChild;
|
|
@@ -181,13 +200,16 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
|
181
200
|
compositionViewDesc.text = textDOM.data;
|
|
182
201
|
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
183
202
|
view.input.compositionNodes.push(compositionViewDesc);
|
|
184
|
-
}
|
|
203
|
+
}, [
|
|
204
|
+
props.node.isTextblock,
|
|
205
|
+
view
|
|
206
|
+
]);
|
|
185
207
|
const childContextValue = (0, _react.useMemo)(()=>({
|
|
186
208
|
parentRef: viewDescRef,
|
|
187
|
-
siblingsRef: childrenRef
|
|
209
|
+
siblingsRef: childrenRef,
|
|
210
|
+
findCompositionDOM
|
|
188
211
|
}), [
|
|
189
|
-
|
|
190
|
-
viewDescRef
|
|
212
|
+
findCompositionDOM
|
|
191
213
|
]);
|
|
192
214
|
return {
|
|
193
215
|
childContextValue,
|
|
@@ -13,6 +13,8 @@ const _prosemirrorstate = require("prosemirror-state");
|
|
|
13
13
|
const _ReactEditorView = require("../ReactEditorView.js");
|
|
14
14
|
const _CursorWrapper = require("../components/CursorWrapper.js");
|
|
15
15
|
const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
|
|
16
|
+
const _viewdesc = require("../viewdesc.js");
|
|
17
|
+
const _reactKeys = require("./reactKeys.js");
|
|
16
18
|
function insertText(view, eventData) {
|
|
17
19
|
let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
18
20
|
if (eventData === null) return false;
|
|
@@ -55,26 +57,64 @@ function handleGapCursorComposition(view) {
|
|
|
55
57
|
tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
56
58
|
view.dispatch(tr);
|
|
57
59
|
}
|
|
58
|
-
function beforeInputPlugin(
|
|
60
|
+
function beforeInputPlugin() {
|
|
59
61
|
let compositionMarks = null;
|
|
60
62
|
return new _prosemirrorstate.Plugin({
|
|
61
63
|
props: {
|
|
62
64
|
handleDOMEvents: {
|
|
63
65
|
compositionstart (view) {
|
|
64
66
|
if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
|
|
65
|
-
view.
|
|
67
|
+
view.compositionStarting = true;
|
|
68
|
+
const { state } = view;
|
|
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;
|
|
66
73
|
compositionMarks = view.state.storedMarks;
|
|
67
|
-
|
|
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);
|
|
68
83
|
view.dispatch(tr);
|
|
69
84
|
handleGapCursorComposition(view);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
if (compositionMarks) {
|
|
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
|
+
})
|
|
76
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
|
+
}
|
|
77
111
|
}
|
|
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.
|
|
117
|
+
view.input.composing = true;
|
|
78
118
|
return true;
|
|
79
119
|
},
|
|
80
120
|
compositionupdate () {
|
|
@@ -85,17 +125,40 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
85
125
|
if (!view.composing) return false;
|
|
86
126
|
view.input.composing = false;
|
|
87
127
|
compositionMarks = null;
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
|
|
147
|
+
cursorWrapper: null
|
|
148
|
+
}));
|
|
149
|
+
if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
|
|
90
150
|
view.input.compositionNode.remove();
|
|
91
151
|
}
|
|
92
152
|
view.input.compositionEndedAt = event.timeStamp;
|
|
93
153
|
view.input.compositionNode = null;
|
|
154
|
+
view.input.compositionNodes = [];
|
|
94
155
|
view.input.compositionID++;
|
|
95
156
|
return true;
|
|
96
157
|
},
|
|
97
158
|
beforeinput (view, event) {
|
|
98
|
-
event.
|
|
159
|
+
if (event.inputType !== "insertFromComposition") {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
}
|
|
99
162
|
switch(event.inputType){
|
|
100
163
|
case "insertParagraph":
|
|
101
164
|
case "insertLineBreak":
|
|
@@ -141,7 +204,10 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
141
204
|
break;
|
|
142
205
|
}
|
|
143
206
|
case "insertCompositionText":
|
|
207
|
+
case "deleteCompositionText":
|
|
208
|
+
case "insertFromComposition":
|
|
144
209
|
{
|
|
210
|
+
if (!(view instanceof _ReactEditorView.ReactEditorView)) break;
|
|
145
211
|
const { tr } = view.state;
|
|
146
212
|
// There's always a range on insertCompositionText beforeinput events
|
|
147
213
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -157,7 +223,37 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
157
223
|
} else {
|
|
158
224
|
tr.delete(start, end);
|
|
159
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
|
+
}
|
|
160
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
|
+
}
|
|
161
257
|
view.dispatch(tr);
|
|
162
258
|
}, {
|
|
163
259
|
once: true
|
|
@@ -197,3 +293,11 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
197
293
|
}
|
|
198
294
|
});
|
|
199
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
|
+
}
|
|
@@ -8,7 +8,6 @@ Object.defineProperty(exports, "componentEventListeners", {
|
|
|
8
8
|
return componentEventListeners;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
const _prosemirrorstate = require("prosemirror-state");
|
|
12
11
|
const _reactdom = require("react-dom");
|
|
13
12
|
function componentEventListeners(eventHandlerRegistry) {
|
|
14
13
|
const domEventHandlers = {};
|
|
@@ -17,7 +16,7 @@ function componentEventListeners(eventHandlerRegistry) {
|
|
|
17
16
|
for (const handler of handlers){
|
|
18
17
|
let handled = false;
|
|
19
18
|
(0, _reactdom.unstable_batchedUpdates)(()=>{
|
|
20
|
-
handled = !!handler
|
|
19
|
+
handled = !!handler(view, event);
|
|
21
20
|
});
|
|
22
21
|
if (handled || event.defaultPrevented) return true;
|
|
23
22
|
}
|
|
@@ -25,11 +24,5 @@ function componentEventListeners(eventHandlerRegistry) {
|
|
|
25
24
|
}
|
|
26
25
|
domEventHandlers[eventType] = handleEvent;
|
|
27
26
|
}
|
|
28
|
-
|
|
29
|
-
key: new _prosemirrorstate.PluginKey("@handlewithcare/react-prosemirror/componentEventListeners"),
|
|
30
|
-
props: {
|
|
31
|
-
handleDOMEvents: domEventHandlers
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return plugin;
|
|
27
|
+
return domEventHandlers;
|
|
35
28
|
}
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is used to patch global DOM variables in a NodeJS environment.
|
|
3
|
+
* This is needed for ProseMirror to work in a NodeJS environment.
|
|
4
|
+
*/ "use strict";
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
// Make sure to import JSDOM only in a NodeJS environment.
|
|
10
|
+
// The magic comments prevent bundlers from statically analyzing this require:
|
|
11
|
+
// - webpackIgnore: true → webpack / Next.js
|
|
12
|
+
// - @vite-ignore → Vite
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
+
const jsdom = require(/* webpackIgnore: true */ /* @vite-ignore */ "jsdom");
|
|
15
|
+
const html = `
|
|
16
|
+
<!DOCTYPE html>
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<title>Testing</title>
|
|
20
|
+
</head>
|
|
21
|
+
<body></body>
|
|
22
|
+
</html>
|
|
23
|
+
`;
|
|
24
|
+
const { window: window1 } = new jsdom.JSDOM(html);
|
|
25
|
+
global.window = window1;
|
|
26
|
+
global.document = window1.document;
|
|
27
|
+
// Use Object.defineProperty for navigator since it's read-only in Node.js 22+
|
|
28
|
+
Object.defineProperty(global, "navigator", {
|
|
29
|
+
value: window1.navigator,
|
|
30
|
+
writable: true,
|
|
31
|
+
configurable: true
|
|
32
|
+
});
|
|
33
|
+
global.innerHeight = 0;
|
|
34
|
+
global.SVGElement = window1.SVGElement;
|
|
35
|
+
// @ts-expect-error stub getSelection for SSR
|
|
36
|
+
document.getSelection = ()=>({});
|
|
37
|
+
document.createRange = ()=>({
|
|
38
|
+
setStart () {},
|
|
39
|
+
setEnd () {},
|
|
40
|
+
// @ts-expect-error stub getBoundingClientRect for SSR
|
|
41
|
+
getClientRects () {
|
|
42
|
+
return {
|
|
43
|
+
left: 0,
|
|
44
|
+
top: 0,
|
|
45
|
+
right: 0,
|
|
46
|
+
bottom: 0
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
// @ts-expect-error stub getBoundingClientRect for SSR
|
|
50
|
+
getBoundingClientRect () {
|
|
51
|
+
return {
|
|
52
|
+
left: 0,
|
|
53
|
+
top: 0,
|
|
54
|
+
right: 0,
|
|
55
|
+
bottom: 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
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 {
|
|
@@ -403,7 +416,7 @@ let ViewDesc = class ViewDesc {
|
|
|
403
416
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
404
417
|
if (after && after.contentEditable == "false") force = true;
|
|
405
418
|
}
|
|
406
|
-
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)) {
|
|
407
420
|
return;
|
|
408
421
|
}
|
|
409
422
|
// Selection.extend can be used to create an 'inverted' selection
|
|
@@ -789,3 +802,31 @@ function sameOuterDeco(a, b) {
|
|
|
789
802
|
for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
|
|
790
803
|
return true;
|
|
791
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,7 +33,10 @@ function changedNodeViews(a, b) {
|
|
|
33
33
|
nextProps;
|
|
34
34
|
prevState;
|
|
35
35
|
_destroyed;
|
|
36
|
-
|
|
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;
|
|
37
40
|
constructor(place, props){
|
|
38
41
|
// Prevent the base class from destroying the React-managed nodes.
|
|
39
42
|
// Restore them below after invoking the base class constructor.
|
|
@@ -86,7 +89,8 @@ function changedNodeViews(a, b) {
|
|
|
86
89
|
// @ts-expect-error this violates the typing but class does it, too.
|
|
87
90
|
this.docView = null;
|
|
88
91
|
this._destroyed = false;
|
|
89
|
-
this.
|
|
92
|
+
this.compositionStarting = false;
|
|
93
|
+
this.displacedNodes = [];
|
|
90
94
|
}
|
|
91
95
|
get props() {
|
|
92
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,15 +50,16 @@ 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;
|
|
53
|
+
let { siblingsRef, parentRef, findCompositionDOM } = param;
|
|
54
54
|
return /*#__PURE__*/ React.createElement(EditorContext.Consumer, null, (param)=>{
|
|
55
55
|
let { registerEventListener, unregisterEventListener } = param;
|
|
56
|
-
return /*#__PURE__*/ React.createElement(
|
|
56
|
+
return /*#__PURE__*/ React.createElement(RemountableTextNodeView, {
|
|
57
57
|
view: view,
|
|
58
58
|
node: child.node,
|
|
59
59
|
getPos: getPos,
|
|
60
60
|
siblingsRef: siblingsRef,
|
|
61
61
|
parentRef: parentRef,
|
|
62
|
+
findCompositionDOM: findCompositionDOM,
|
|
62
63
|
decorations: child.outerDeco,
|
|
63
64
|
registerEventListener: registerEventListener,
|
|
64
65
|
unregisterEventListener: unregisterEventListener
|