@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
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React, { forwardRef, useImperativeHandle, useRef
|
|
1
|
+
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
|
2
2
|
import { domIndex } from "../dom.js";
|
|
3
3
|
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
4
4
|
export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(param, ref) {
|
|
5
5
|
let { widget, getPos, ...props } = param;
|
|
6
|
-
const [shouldRender, setShouldRender] = useState(true);
|
|
7
6
|
const innerRef = useRef(null);
|
|
8
7
|
useImperativeHandle(ref, ()=>{
|
|
9
8
|
return innerRef.current;
|
|
@@ -14,19 +13,19 @@ export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(par
|
|
|
14
13
|
view.domObserver.disconnectSelection();
|
|
15
14
|
// @ts-expect-error Internal property - domSelection
|
|
16
15
|
const domSel = view.domSelection();
|
|
16
|
+
if (!domSel.isCollapsed) return;
|
|
17
17
|
const node = innerRef.current;
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
19
19
|
domSel.collapse(node.parentNode, domIndex(node) + 1);
|
|
20
20
|
// @ts-expect-error Internal property - domObserver
|
|
21
21
|
view.domObserver.connectSelection();
|
|
22
|
-
setShouldRender(false);
|
|
23
22
|
}, []);
|
|
24
|
-
return
|
|
23
|
+
return /*#__PURE__*/ React.createElement("img", {
|
|
25
24
|
ref: innerRef,
|
|
26
25
|
className: "ProseMirror-separator",
|
|
27
26
|
// eslint-disable-next-line react/no-unknown-property
|
|
28
27
|
"mark-placeholder": "true",
|
|
29
28
|
alt: "",
|
|
30
29
|
...props
|
|
31
|
-
})
|
|
30
|
+
});
|
|
32
31
|
});
|
|
@@ -17,12 +17,14 @@ const rootChildDescriptionsContextValue = {
|
|
|
17
17
|
},
|
|
18
18
|
siblingsRef: {
|
|
19
19
|
current: []
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
22
|
+
findCompositionDOM: ()=>{}
|
|
21
23
|
};
|
|
22
24
|
function ProseMirrorInner(param) {
|
|
23
25
|
let { children, nodeViewComponents, markViewComponents, ...props } = param;
|
|
24
26
|
const [mount, setMount] = useState(null);
|
|
25
|
-
const { editor,
|
|
27
|
+
const { editor, state } = useEditor(mount, props);
|
|
26
28
|
const nodeViewConstructors = editor.view.nodeViews;
|
|
27
29
|
const nodeViewContextValue = useMemo(()=>{
|
|
28
30
|
return {
|
|
@@ -39,7 +41,7 @@ function ProseMirrorInner(param) {
|
|
|
39
41
|
]);
|
|
40
42
|
const node = state.doc;
|
|
41
43
|
const decorations = computeDocDeco(editor.view);
|
|
42
|
-
const innerDecorations = viewDecorations(editor.view
|
|
44
|
+
const innerDecorations = viewDecorations(editor.view);
|
|
43
45
|
const docNodeViewContextValue = useMemo(()=>({
|
|
44
46
|
setMount,
|
|
45
47
|
node,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { TextSelection } from "prosemirror-state";
|
|
2
2
|
import { DecorationSet } from "prosemirror-view";
|
|
3
|
-
import { Component } from "react";
|
|
3
|
+
import React, { Component, createRef, useReducer } from "react";
|
|
4
4
|
import { ReactEditorView } from "../ReactEditorView.js";
|
|
5
5
|
import { findDOMNode } from "../findDOMNode.js";
|
|
6
|
-
import { CompositionViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
6
|
+
import { CompositionViewDesc, TextViewDesc, findTextInFragment, sortViewDescs } from "../viewdesc.js";
|
|
7
7
|
import { wrapInDeco } from "./ChildNodeViews.js";
|
|
8
8
|
function shallowEqual(objA, objB) {
|
|
9
9
|
if (objA === objB) {
|
|
@@ -28,10 +28,10 @@ function shallowEqual(objA, objB) {
|
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
30
|
export class TextNodeView extends Component {
|
|
31
|
-
viewDescRef =
|
|
32
|
-
renderRef =
|
|
33
|
-
wasProtecting =
|
|
34
|
-
containsCompositionNodeText =
|
|
31
|
+
viewDescRef = createMutRef();
|
|
32
|
+
renderRef = createMutRef();
|
|
33
|
+
wasProtecting = createMutRef();
|
|
34
|
+
containsCompositionNodeText = createMutRef();
|
|
35
35
|
// This is basically NodeViewDesc.localCompositionInfo
|
|
36
36
|
// from prosemirror-view. It's been slightly adjusted so that
|
|
37
37
|
// it can be used accurately during render, before we've
|
|
@@ -43,17 +43,42 @@ export class TextNodeView extends Component {
|
|
|
43
43
|
if (!view.composing) {
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const viewDesc = this.viewDescRef.current;
|
|
47
|
+
// If our DOM text node IS the IME's composition node, protect regardless
|
|
48
|
+
// of where the PM selection currently is. The IME may have replaced a
|
|
49
|
+
// selection that included us — moving the PM selection past us — but our
|
|
50
|
+
// DOM is still part of the in-progress composition. Until another
|
|
51
|
+
// TextNodeView's findCompositionDOM displaces us into a comp desc, only
|
|
52
|
+
// our own protect/no-update is preventing React from rewriting the IME's
|
|
53
|
+
// text. (When we *are* displaced, viewDesc is already a CompositionViewDesc
|
|
54
|
+
// and the existing position-based logic doesn't apply anyway.)
|
|
55
|
+
const ownsCompositionNode = viewDesc instanceof TextViewDesc && viewDesc.nodeDOM === view.input.compositionNode;
|
|
56
|
+
if (!ownsCompositionNode) {
|
|
57
|
+
const pos = getPos();
|
|
58
|
+
const { from, to } = view.state.selection;
|
|
59
|
+
if (!(view.state.selection instanceof TextSelection) || from <= pos || to > pos + node.nodeSize) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
50
62
|
}
|
|
51
|
-
return this.containsCompositionNodeText;
|
|
63
|
+
return !!this.containsCompositionNodeText.current;
|
|
52
64
|
}
|
|
53
65
|
handleCompositionEnd = ()=>{
|
|
54
|
-
if (!this.wasProtecting) return;
|
|
55
|
-
this.
|
|
56
|
-
return;
|
|
66
|
+
if (!this.wasProtecting.current) return;
|
|
67
|
+
const { view } = this.props;
|
|
68
|
+
if (!(view instanceof ReactEditorView)) return;
|
|
69
|
+
// If the IME detached our DOM during composition, React's fiber is now
|
|
70
|
+
// wired to a detached node and will silently send all subsequent updates
|
|
71
|
+
// into the void. Re-attach the orphan (so the upcoming unmount's
|
|
72
|
+
// removeChild has something to remove), then ask our wrapper to mint a
|
|
73
|
+
// new key — that forces React to drop this fiber and mount a fresh one
|
|
74
|
+
// whose stateNode it creates from the current render output.
|
|
75
|
+
const dom = findDOMNode(this);
|
|
76
|
+
if (dom instanceof HTMLElement && !view.dom.contains(dom)) {
|
|
77
|
+
this.reattachAtCorrectPosition(dom);
|
|
78
|
+
this.props.forceRemount();
|
|
79
|
+
} else {
|
|
80
|
+
this.forceUpdate();
|
|
81
|
+
}
|
|
57
82
|
};
|
|
58
83
|
create() {
|
|
59
84
|
const { view, decorations, siblingsRef, parentRef, getPos, node } = this.props;
|
|
@@ -81,14 +106,25 @@ export class TextNodeView extends Component {
|
|
|
81
106
|
}
|
|
82
107
|
siblingsRef.current.push(viewDesc);
|
|
83
108
|
siblingsRef.current.sort(sortViewDescs);
|
|
109
|
+
if (viewDesc instanceof CompositionViewDesc) {
|
|
110
|
+
this.props.findCompositionDOM(viewDesc);
|
|
111
|
+
}
|
|
84
112
|
return viewDesc;
|
|
85
113
|
}
|
|
86
114
|
update() {
|
|
87
115
|
const { view, node, decorations } = this.props;
|
|
88
116
|
if (!(view instanceof ReactEditorView)) return false;
|
|
89
|
-
const viewDesc = this.viewDescRef;
|
|
117
|
+
const viewDesc = this.viewDescRef.current;
|
|
90
118
|
if (!viewDesc) return false;
|
|
91
|
-
|
|
119
|
+
// Don't force destroy/recreate just because we transitioned into protect
|
|
120
|
+
// mode. If our DOM text node is the IME's composition node, we want to
|
|
121
|
+
// keep the TextViewDesc alive so the new composition-text TextNodeView's
|
|
122
|
+
// findCompositionDOM second pass can find us, validate the size mismatch,
|
|
123
|
+
// and displace us into a properly-sized CompositionViewDesc. If we
|
|
124
|
+
// destroyed here, create() would put a wrong-size CompositionViewDesc on
|
|
125
|
+
// T and pre-empt that displacement.
|
|
126
|
+
const ownsCompositionNode = viewDesc instanceof TextViewDesc && viewDesc.nodeDOM === view.input.compositionNode;
|
|
127
|
+
if (!ownsCompositionNode && this.shouldProtect(this.props) !== viewDesc instanceof CompositionViewDesc) {
|
|
92
128
|
return false;
|
|
93
129
|
}
|
|
94
130
|
if (viewDesc instanceof CompositionViewDesc) return false;
|
|
@@ -98,7 +134,7 @@ export class TextNodeView extends Component {
|
|
|
98
134
|
return viewDesc.matchesNode(node, decorations, DecorationSet.empty) || viewDesc.update(node, decorations, DecorationSet.empty, view);
|
|
99
135
|
}
|
|
100
136
|
destroy() {
|
|
101
|
-
const viewDesc = this.viewDescRef;
|
|
137
|
+
const viewDesc = this.viewDescRef.current;
|
|
102
138
|
if (!viewDesc) return;
|
|
103
139
|
viewDesc.destroy();
|
|
104
140
|
const siblings = this.props.siblingsRef.current;
|
|
@@ -110,43 +146,85 @@ export class TextNodeView extends Component {
|
|
|
110
146
|
updateEffect() {
|
|
111
147
|
if (!this.update()) {
|
|
112
148
|
this.destroy();
|
|
113
|
-
this.viewDescRef = this.create();
|
|
149
|
+
this.viewDescRef.current = this.create();
|
|
114
150
|
}
|
|
115
|
-
const { view
|
|
151
|
+
const { view } = this.props;
|
|
116
152
|
if (!(view instanceof ReactEditorView)) {
|
|
117
|
-
this.containsCompositionNodeText = true;
|
|
153
|
+
this.containsCompositionNodeText.current = true;
|
|
118
154
|
return;
|
|
119
155
|
}
|
|
120
156
|
const textNode = view.input.compositionNode;
|
|
121
157
|
if (!textNode) {
|
|
122
|
-
this.containsCompositionNodeText = true;
|
|
158
|
+
this.containsCompositionNodeText.current = true;
|
|
123
159
|
return;
|
|
124
160
|
}
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
161
|
+
// Resolve the parent textblock containing this text node and ask
|
|
162
|
+
// findTextInFragment whether the IME text node's *current* content can be
|
|
163
|
+
// placed somewhere in the textblock's PM content overlapping the
|
|
164
|
+
// selection. If it can, the composition is still consistent with PM state
|
|
165
|
+
// and we should protect. If it can't (e.g. a remote change overwrote the
|
|
166
|
+
// composing region), PM and the DOM have diverged — abandon protection
|
|
167
|
+
// so the re-render can rewrite the DOM and cancel the composition.
|
|
168
|
+
const $pos = view.state.doc.resolve(this.props.getPos());
|
|
169
|
+
const parent = $pos.parent;
|
|
170
|
+
if (!parent.inlineContent) {
|
|
171
|
+
this.containsCompositionNodeText.current = false;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const parentStart = $pos.start();
|
|
175
|
+
const { from, to } = view.state.selection;
|
|
176
|
+
const textPos = findTextInFragment(parent.content, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
177
|
+
textNode.nodeValue, from - parentStart, to - parentStart);
|
|
178
|
+
this.containsCompositionNodeText.current = textPos >= 0;
|
|
129
179
|
}
|
|
130
180
|
shouldComponentUpdate(nextProps) {
|
|
131
181
|
// When leaving the protected state, force a re-render so React's
|
|
132
182
|
// virtual DOM resyncs with whatever the IME wrote into the real DOM
|
|
133
183
|
// while we were returning a stale renderRef.
|
|
134
|
-
if (this.wasProtecting && !this.shouldProtect(nextProps)) {
|
|
184
|
+
if (this.wasProtecting.current && !this.shouldProtect(nextProps)) {
|
|
135
185
|
return true;
|
|
136
186
|
}
|
|
137
187
|
return !shallowEqual(this.props, nextProps);
|
|
138
188
|
}
|
|
189
|
+
constructor(props){
|
|
190
|
+
super(props);
|
|
191
|
+
this.viewDescRef.current = null;
|
|
192
|
+
this.renderRef.current = null;
|
|
193
|
+
this.wasProtecting.current = false;
|
|
194
|
+
this.containsCompositionNodeText.current = true;
|
|
195
|
+
}
|
|
139
196
|
componentDidMount() {
|
|
140
|
-
this.
|
|
197
|
+
this.containsCompositionNodeText.current = true;
|
|
141
198
|
// After a composition, force an update so that we re-check whether we need
|
|
142
199
|
// to be protecting our rendered content and allow React to re-sync with the
|
|
143
200
|
// DOM.
|
|
144
201
|
const { registerEventListener } = this.props;
|
|
145
202
|
registerEventListener("compositionend", this.handleCompositionEnd);
|
|
203
|
+
this.viewDescRef.current = this.create();
|
|
146
204
|
this.updateEffect();
|
|
147
205
|
}
|
|
148
206
|
componentDidUpdate() {
|
|
149
207
|
this.updateEffect();
|
|
208
|
+
const { view } = this.props;
|
|
209
|
+
if (!(view instanceof ReactEditorView)) return;
|
|
210
|
+
}
|
|
211
|
+
reattachAtCorrectPosition(dom) {
|
|
212
|
+
const viewDesc = this.viewDescRef.current;
|
|
213
|
+
if (!viewDesc) return;
|
|
214
|
+
let host = viewDesc.parent;
|
|
215
|
+
while(host && !host.contentDOM)host = host.parent;
|
|
216
|
+
if (!host?.contentDOM) return;
|
|
217
|
+
const siblings = viewDesc.parent?.children ?? [];
|
|
218
|
+
const idx = siblings.indexOf(viewDesc);
|
|
219
|
+
let nextDom = null;
|
|
220
|
+
for(let i = idx + 1; i < siblings.length; i++){
|
|
221
|
+
const sib = siblings[i];
|
|
222
|
+
if (sib?.dom && sib.dom.parentNode === host.contentDOM) {
|
|
223
|
+
nextDom = sib.dom;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
host.contentDOM.insertBefore(dom, nextDom);
|
|
150
228
|
}
|
|
151
229
|
componentWillUnmount() {
|
|
152
230
|
const { unregisterEventListener } = this.props;
|
|
@@ -161,11 +239,26 @@ export class TextNodeView extends Component {
|
|
|
161
239
|
// we freeze the DOM of this element so that it doesn't
|
|
162
240
|
// interrupt the composition
|
|
163
241
|
if (this.shouldProtect(this.props)) {
|
|
164
|
-
this.wasProtecting = true;
|
|
165
|
-
return this.renderRef;
|
|
242
|
+
this.wasProtecting.current = true;
|
|
243
|
+
return this.renderRef.current;
|
|
166
244
|
}
|
|
167
|
-
this.wasProtecting = false;
|
|
168
|
-
this.renderRef = decorations.reduce(wrapInDeco, node.text);
|
|
169
|
-
return this.renderRef;
|
|
245
|
+
this.wasProtecting.current = false;
|
|
246
|
+
this.renderRef.current = decorations.reduce(wrapInDeco, node.text);
|
|
247
|
+
return this.renderRef.current;
|
|
170
248
|
}
|
|
171
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* createRef returns a RefObject, even though the docs
|
|
252
|
+
* say that it's acceptible to manage the ref's value
|
|
253
|
+
* yourself.
|
|
254
|
+
*/ function createMutRef() {
|
|
255
|
+
return /*#__PURE__*/ createRef();
|
|
256
|
+
}
|
|
257
|
+
export function RemountableTextNodeView(props) {
|
|
258
|
+
const [key, forceRemount] = useReducer((x)=>x + 1, 0);
|
|
259
|
+
return /*#__PURE__*/ React.createElement(TextNodeView, {
|
|
260
|
+
key: key,
|
|
261
|
+
forceRemount: forceRemount,
|
|
262
|
+
...props
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useContext, useRef, useState } from "react";
|
|
2
|
+
import { ReactEditorView } from "../ReactEditorView.js";
|
|
2
3
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
3
4
|
import { useClientLayoutEffect } from "../hooks/useClientLayoutEffect.js";
|
|
4
5
|
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
@@ -7,9 +8,14 @@ import { TrailingHackViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
|
7
8
|
export function TrailingHackView(param) {
|
|
8
9
|
let { getPos } = param;
|
|
9
10
|
const [shouldRender, setShouldRender] = useState(true);
|
|
11
|
+
const [shouldReinsert, setShouldReinsert] = useState(false);
|
|
10
12
|
const { siblingsRef, parentRef } = useContext(ChildDescriptionsContext);
|
|
11
13
|
const viewDescRef = useRef(null);
|
|
12
14
|
const ref = useRef(null);
|
|
15
|
+
const preservedRef = useRef(ref.current);
|
|
16
|
+
if (ref.current) {
|
|
17
|
+
preservedRef.current = ref.current;
|
|
18
|
+
}
|
|
13
19
|
useClientLayoutEffect(()=>{
|
|
14
20
|
const siblings = siblingsRef.current;
|
|
15
21
|
return ()=>{
|
|
@@ -43,13 +49,47 @@ export function TrailingHackView(param) {
|
|
|
43
49
|
const { from } = view.state.selection;
|
|
44
50
|
if (from === getPos()) {
|
|
45
51
|
setShouldRender(false);
|
|
52
|
+
setShouldReinsert(true);
|
|
46
53
|
}
|
|
47
54
|
});
|
|
55
|
+
// Chrome and Safari will cancel/mangle the composition if the br element isn't
|
|
56
|
+
// still in the DOM after the compositionstart event. We manually add it
|
|
57
|
+
// back to the DOM, without React managing it, so that it can be removed
|
|
58
|
+
// again by the browser when it starts the composition.
|
|
59
|
+
useClientLayoutEffect(()=>{
|
|
60
|
+
if (!shouldReinsert) return;
|
|
61
|
+
const preservedHack = preservedRef.current;
|
|
62
|
+
if (!preservedHack) return;
|
|
63
|
+
if (!viewDescRef.current) return;
|
|
64
|
+
const { parent } = viewDescRef.current;
|
|
65
|
+
if (!parent) return;
|
|
66
|
+
const dom = parent.contentDOM;
|
|
67
|
+
if (!dom) return;
|
|
68
|
+
preservedHack.pmViewDesc = undefined;
|
|
69
|
+
const index = parent.children.indexOf(viewDescRef.current);
|
|
70
|
+
if (index === 0) {
|
|
71
|
+
dom.appendChild(preservedHack);
|
|
72
|
+
} else {
|
|
73
|
+
dom.insertBefore(preservedHack, dom.childNodes.item(index));
|
|
74
|
+
}
|
|
75
|
+
return ()=>{
|
|
76
|
+
try {
|
|
77
|
+
dom.removeChild(preservedHack);
|
|
78
|
+
} catch {
|
|
79
|
+
// It may have already been removed by the browser during
|
|
80
|
+
// the composition, but if we get unmounted before that happens,
|
|
81
|
+
// we need to remove it ourselves
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}, [
|
|
85
|
+
shouldReinsert
|
|
86
|
+
]);
|
|
48
87
|
// We need to run the same composition check when we first get mounted,
|
|
49
88
|
// in case we got mounted in the same render batch as the beginning of
|
|
50
89
|
// a composition
|
|
51
90
|
useEditorEffect((view)=>{
|
|
52
|
-
if (!view
|
|
91
|
+
if (!(view instanceof ReactEditorView)) return;
|
|
92
|
+
if (!view.compositionStarting) return;
|
|
53
93
|
const { from } = view.state.selection;
|
|
54
94
|
if (from === getPos()) {
|
|
55
95
|
setShouldRender(false);
|
|
@@ -59,6 +99,7 @@ export function TrailingHackView(param) {
|
|
|
59
99
|
]);
|
|
60
100
|
useEditorEventListener("compositionend", ()=>{
|
|
61
101
|
setShouldRender(true);
|
|
102
|
+
setShouldReinsert(false);
|
|
62
103
|
});
|
|
63
104
|
if (!shouldRender) return null;
|
|
64
105
|
return /*#__PURE__*/ React.createElement("br", {
|
|
@@ -27,6 +27,7 @@ export function WidgetView(param) {
|
|
|
27
27
|
viewDescRef.current.parent = parentRef.current;
|
|
28
28
|
viewDescRef.current.widget = widget;
|
|
29
29
|
viewDescRef.current.dom = domRef.current;
|
|
30
|
+
viewDescRef.current.dom.pmViewDesc = viewDescRef.current;
|
|
30
31
|
}
|
|
31
32
|
if (!siblingsRef.current.includes(viewDescRef.current)) {
|
|
32
33
|
siblingsRef.current.push(viewDescRef.current);
|
|
@@ -38,6 +39,8 @@ export function WidgetView(param) {
|
|
|
38
39
|
ref: domRef,
|
|
39
40
|
widget: widget,
|
|
40
41
|
getPos: getPos,
|
|
41
|
-
|
|
42
|
+
...!widget.type.spec.raw && {
|
|
43
|
+
contentEditable: false
|
|
44
|
+
}
|
|
42
45
|
});
|
|
43
46
|
}
|
|
@@ -123,17 +123,12 @@ const ViewDecorationsCache = new WeakMap();
|
|
|
123
123
|
*
|
|
124
124
|
* This makes it safe to call in a React render function, even
|
|
125
125
|
* if its result is used in a dependencies array for a hook.
|
|
126
|
-
*/ export function viewDecorations(view
|
|
126
|
+
*/ export function viewDecorations(view) {
|
|
127
127
|
const found = [];
|
|
128
128
|
view.someProp("decorations", (f)=>{
|
|
129
129
|
const result = f(view.state);
|
|
130
130
|
if (result && result != empty) found.push(result);
|
|
131
131
|
});
|
|
132
|
-
if (cursorWrapper) {
|
|
133
|
-
found.push(DecorationSet.create(view.state.doc, [
|
|
134
|
-
cursorWrapper
|
|
135
|
-
]));
|
|
136
|
-
}
|
|
137
132
|
const previous = ViewDecorationsCache.get(view);
|
|
138
133
|
if (!previous) {
|
|
139
134
|
const result = DecorationGroup.from(found);
|
|
@@ -25,16 +25,8 @@ import { unstable_batchedUpdates as batch } from "react-dom";
|
|
|
25
25
|
*
|
|
26
26
|
* To accomplish this, we shallowly clone the registry whenever a new event
|
|
27
27
|
* type is registered.
|
|
28
|
-
*/ export function useComponentEventListeners(
|
|
29
|
-
const [registry, setRegistry] = useState(new Map(
|
|
30
|
-
let [eventName, handler] = param;
|
|
31
|
-
return [
|
|
32
|
-
eventName,
|
|
33
|
-
handler ? [
|
|
34
|
-
handler
|
|
35
|
-
] : []
|
|
36
|
-
];
|
|
37
|
-
})));
|
|
28
|
+
*/ export function useComponentEventListeners(handleDOMEventsProp) {
|
|
29
|
+
const [registry, setRegistry] = useState(new Map());
|
|
38
30
|
const registerEventListener = useCallback((eventType, handler)=>{
|
|
39
31
|
const handlers = registry.get(eventType) ?? [];
|
|
40
32
|
handlers.unshift(handler);
|
|
@@ -52,19 +44,19 @@ import { unstable_batchedUpdates as batch } from "react-dom";
|
|
|
52
44
|
registry
|
|
53
45
|
]);
|
|
54
46
|
useLayoutEffect(()=>{
|
|
55
|
-
if (!
|
|
56
|
-
for (const [eventType, handler] of Object.entries(
|
|
47
|
+
if (!handleDOMEventsProp) return;
|
|
48
|
+
for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
|
|
57
49
|
if (!handler) return;
|
|
58
50
|
registerEventListener(eventType, handler);
|
|
59
51
|
}
|
|
60
52
|
return ()=>{
|
|
61
|
-
for (const [eventType, handler] of Object.entries(
|
|
53
|
+
for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
|
|
62
54
|
if (!handler) return;
|
|
63
55
|
unregisterEventListener(eventType, handler);
|
|
64
56
|
}
|
|
65
57
|
};
|
|
66
58
|
}, [
|
|
67
|
-
|
|
59
|
+
handleDOMEventsProp,
|
|
68
60
|
registerEventListener,
|
|
69
61
|
unregisterEventListener
|
|
70
62
|
]);
|
|
@@ -25,23 +25,16 @@ let didWarnValueDefaultValue = false;
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
const flushSyncRef = useRef(true);
|
|
28
|
-
const [cursorWrapper, _setCursorWrapper] = useState(null);
|
|
29
28
|
const forceUpdate = useForceUpdate();
|
|
30
29
|
const defaultState = options.defaultState ?? EMPTY_STATE;
|
|
31
30
|
const [_state, setState] = useState(defaultState);
|
|
32
31
|
const state = options.state ?? _state;
|
|
33
32
|
const { handleDOMEvents, registerEventListener, unregisterEventListener } = useComponentEventListeners(options.handleDOMEvents);
|
|
34
|
-
const setCursorWrapper = useCallback((deco)=>{
|
|
35
|
-
flushSync(()=>{
|
|
36
|
-
_setCursorWrapper(deco);
|
|
37
|
-
});
|
|
38
|
-
}, []);
|
|
39
33
|
const plugins = useMemo(()=>[
|
|
40
34
|
...options.plugins ?? [],
|
|
41
|
-
beforeInputPlugin(
|
|
35
|
+
beforeInputPlugin()
|
|
42
36
|
], [
|
|
43
|
-
options.plugins
|
|
44
|
-
setCursorWrapper
|
|
37
|
+
options.plugins
|
|
45
38
|
]);
|
|
46
39
|
const dispatchTransaction = useCallback(function dispatchTransaction(tr) {
|
|
47
40
|
if (flushSyncRef.current) {
|
|
@@ -120,7 +113,6 @@ let didWarnValueDefaultValue = false;
|
|
|
120
113
|
]);
|
|
121
114
|
return {
|
|
122
115
|
editor,
|
|
123
|
-
cursorWrapper,
|
|
124
116
|
state
|
|
125
117
|
};
|
|
126
118
|
}
|
|
@@ -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 { ReactMarkViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
5
|
+
import { ReactMarkViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
6
6
|
import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
|
|
7
7
|
import { useEffectEvent } from "./useEffectEvent.js";
|
|
8
8
|
export function useMarkViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
@@ -110,12 +110,70 @@ export function useMarkViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
110
110
|
child.parent = viewDesc;
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
|
+
const findCompositionDOM = useCallback((compositionViewDesc)=>{
|
|
114
|
+
const children = childrenRef.current;
|
|
115
|
+
// Because TextNodeViews can't locate the DOM nodes
|
|
116
|
+
// for compositions, we need to override them here
|
|
117
|
+
if (!viewDescRef.current?.contentDOM) return;
|
|
118
|
+
let compositionTopDOM = null;
|
|
119
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
120
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
121
|
+
compositionTopDOM = childNode;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!compositionTopDOM) {
|
|
126
|
+
// Otherwise the IME extended an existing tracked text node. Take it over.
|
|
127
|
+
const reactView = view;
|
|
128
|
+
const imeTextNode = reactView.input.compositionNode;
|
|
129
|
+
if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const claimedDesc = imeTextNode.pmViewDesc;
|
|
133
|
+
if (!(claimedDesc instanceof TextViewDesc)) return;
|
|
134
|
+
if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
|
|
135
|
+
// Walk up to the direct child of contentDOM that contains the IME text node
|
|
136
|
+
// (could be the text node itself, could be wrapped in a mark span).
|
|
137
|
+
let topDOM = imeTextNode;
|
|
138
|
+
while(topDOM.parentNode !== viewDescRef.current.contentDOM){
|
|
139
|
+
const next = topDOM.parentNode;
|
|
140
|
+
if (!next) return;
|
|
141
|
+
topDOM = next;
|
|
142
|
+
}
|
|
143
|
+
// Detach the displaced TextViewDesc from the sibling list so sibling-size
|
|
144
|
+
// accounting (used by posBeforeChild) doesn't double-count this text node.
|
|
145
|
+
const displacedIdx = children.indexOf(claimedDesc);
|
|
146
|
+
if (displacedIdx >= 0) children.splice(displacedIdx, 1);
|
|
147
|
+
compositionViewDesc.dom = topDOM;
|
|
148
|
+
compositionViewDesc.textDOM = imeTextNode;
|
|
149
|
+
compositionViewDesc.text = imeTextNode.data;
|
|
150
|
+
imeTextNode.pmViewDesc = compositionViewDesc;
|
|
151
|
+
compositionViewDesc._displacedDesc = claimedDesc;
|
|
152
|
+
reactView.input.compositionNodes.push(compositionViewDesc);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let textDOM = compositionTopDOM;
|
|
156
|
+
while(textDOM.firstChild){
|
|
157
|
+
textDOM = textDOM.firstChild;
|
|
158
|
+
}
|
|
159
|
+
if (!textDOM || !(textDOM instanceof Text)) {
|
|
160
|
+
console.error(compositionTopDOM, textDOM);
|
|
161
|
+
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
162
|
+
}
|
|
163
|
+
compositionViewDesc.dom = compositionTopDOM;
|
|
164
|
+
compositionViewDesc.textDOM = textDOM;
|
|
165
|
+
compositionViewDesc.text = textDOM.data;
|
|
166
|
+
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
167
|
+
view.input.compositionNodes.push(compositionViewDesc);
|
|
168
|
+
}, [
|
|
169
|
+
view
|
|
170
|
+
]);
|
|
113
171
|
const childContextValue = useMemo(()=>({
|
|
114
172
|
parentRef: viewDescRef,
|
|
115
|
-
siblingsRef: childrenRef
|
|
173
|
+
siblingsRef: childrenRef,
|
|
174
|
+
findCompositionDOM
|
|
116
175
|
}), [
|
|
117
|
-
|
|
118
|
-
viewDescRef
|
|
176
|
+
findCompositionDOM
|
|
119
177
|
]);
|
|
120
178
|
return {
|
|
121
179
|
childContextValue,
|