@handlewithcare/react-prosemirror 3.1.0-tiptap.53 → 3.1.0
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/README.md +3 -0
- package/dist/cjs/ReactEditorView.js +18 -7
- package/dist/cjs/components/ChildNodeViews.js +10 -16
- package/dist/cjs/components/CursorWrapper.js +6 -4
- package/dist/cjs/components/ProseMirror.js +12 -4
- package/dist/cjs/components/TextNodeView.js +7 -216
- package/dist/cjs/components/TrailingHackView.js +0 -70
- package/dist/cjs/components/nodes/NodeView.js +40 -4
- package/dist/cjs/contexts/ChildDescriptionsContext.js +1 -3
- package/dist/cjs/contexts/CompositionContext.js +14 -0
- package/dist/cjs/hooks/useMarkViewDescription.js +2 -63
- package/dist/cjs/hooks/useNodeViewDescription.js +2 -66
- package/dist/cjs/plugins/beforeInputPlugin.js +130 -120
- package/dist/cjs/plugins/reactKeys.js +16 -4
- package/dist/cjs/tiptap/tiptapNodeView.js +10 -9
- package/dist/cjs/viewdesc.js +4 -1
- package/dist/esm/ReactEditorView.js +18 -7
- package/dist/esm/components/ChildNodeViews.js +12 -18
- package/dist/esm/components/CursorWrapper.js +6 -4
- package/dist/esm/components/ProseMirror.js +12 -4
- package/dist/esm/components/TextNodeView.js +5 -165
- package/dist/esm/components/TrailingHackView.js +1 -71
- package/dist/esm/components/nodes/NodeView.js +38 -5
- package/dist/esm/contexts/ChildDescriptionsContext.js +1 -3
- package/dist/esm/contexts/CompositionContext.js +4 -0
- package/dist/esm/hooks/useIsEditorStatic.js +4 -1
- package/dist/esm/hooks/useMarkViewDescription.js +3 -64
- package/dist/esm/hooks/useNodeViewDescription.js +3 -67
- package/dist/esm/plugins/beforeInputPlugin.js +131 -121
- package/dist/esm/plugins/reactKeys.js +16 -4
- package/dist/esm/tiptap/ReactProseMirrorNodeView.js +1 -1
- package/dist/esm/tiptap/TiptapEditorContent.js +8 -1
- package/dist/esm/tiptap/hooks/useIsInReactProseMirror.js +5 -1
- package/dist/esm/tiptap/tiptapNodeView.js +13 -14
- package/dist/esm/viewdesc.js +4 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +8 -4
- package/dist/types/components/ChildNodeViews.d.ts +2 -2
- package/dist/types/components/CursorWrapper.d.ts +1 -1
- package/dist/types/components/TextNodeView.d.ts +6 -18
- package/dist/types/components/TrailingHackView.d.ts +1 -1
- package/dist/types/components/marks/DefaultMarkView.d.ts +1 -1
- package/dist/types/components/marks/MarkView.d.ts +1 -1
- package/dist/types/components/marks/MarkViewConstructorView.d.ts +1 -1
- package/dist/types/components/marks/ReactMarkView.d.ts +1 -1
- package/dist/types/components/nodes/DefaultNodeView.d.ts +1 -1
- package/dist/types/components/nodes/NodeView.d.ts +3 -1
- package/dist/types/components/nodes/NodeViewConstructorView.d.ts +1 -1
- package/dist/types/components/nodes/ReactNodeView.d.ts +1 -1
- package/dist/types/contexts/ChildDescriptionsContext.d.ts +1 -2
- package/dist/types/contexts/CompositionContext.d.ts +4 -0
- package/dist/types/hooks/useEditor.d.ts +2 -2
- package/dist/types/hooks/useIsEditorStatic.d.ts +4 -0
- package/dist/types/hooks/useMarkViewDescription.d.ts +1 -2
- package/dist/types/hooks/useNodeViewDescription.d.ts +1 -2
- package/dist/types/hooks/useReactKeys.d.ts +2 -5
- package/dist/types/plugins/reactKeys.d.ts +5 -5
- package/dist/types/props.d.ts +225 -225
- package/dist/types/tiptap/ReactProseMirrorNodeView.d.ts +1 -1
- package/dist/types/tiptap/TiptapEditorContent.d.ts +10 -1
- package/dist/types/tiptap/hooks/useIsInReactProseMirror.d.ts +5 -0
- package/dist/types/tiptap/tiptapNodeView.d.ts +5 -6
- package/dist/types/viewdesc.d.ts +2 -1
- package/package.json +20 -6
- package/dist/cjs/plugins/componentEventListeners.js +0 -28
- package/dist/cjs/plugins/componentEventListenersPlugin.js +0 -35
- package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +0 -59
- package/dist/esm/plugins/componentEventListeners.js +0 -18
- package/dist/esm/plugins/componentEventListenersPlugin.js +0 -25
- package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +0 -56
- package/dist/types/plugins/componentEventListeners.d.ts +0 -3
- package/dist/types/plugins/componentEventListenersPlugin.d.ts +0 -4
- package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
|
2
|
+
import { ReactEditorView } from "../ReactEditorView.js";
|
|
2
3
|
import { domIndex } from "../dom.js";
|
|
3
4
|
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
4
5
|
export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(param, ref) {
|
|
@@ -8,17 +9,18 @@ export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(par
|
|
|
8
9
|
return innerRef.current;
|
|
9
10
|
}, []);
|
|
10
11
|
useEditorEffect((view)=>{
|
|
11
|
-
if (!view || !innerRef.current) return;
|
|
12
|
-
// @ts-expect-error Internal property - domObserver
|
|
12
|
+
if (!(view instanceof ReactEditorView) || !innerRef.current) return;
|
|
13
13
|
view.domObserver.disconnectSelection();
|
|
14
|
-
// @ts-expect-error Internal property - domSelection
|
|
15
14
|
const domSel = view.domSelection();
|
|
16
15
|
if (!domSel.isCollapsed) return;
|
|
17
16
|
const node = innerRef.current;
|
|
18
17
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
19
18
|
domSel.collapse(node.parentNode, domIndex(node) + 1);
|
|
20
|
-
|
|
19
|
+
view.cursorWrapped = true;
|
|
21
20
|
view.domObserver.connectSelection();
|
|
21
|
+
return ()=>{
|
|
22
|
+
view.cursorWrapped = false;
|
|
23
|
+
};
|
|
22
24
|
}, []);
|
|
23
25
|
return /*#__PURE__*/ React.createElement("img", {
|
|
24
26
|
ref: innerRef,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React, { useMemo, useState } from "react";
|
|
2
2
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
3
|
+
import { CompositionContext } from "../contexts/CompositionContext.js";
|
|
3
4
|
import { EditorContext } from "../contexts/EditorContext.js";
|
|
4
5
|
import { EditorStateContext } from "../contexts/EditorStateContext.js";
|
|
5
6
|
import { NodeViewContext } from "../contexts/NodeViewContext.js";
|
|
6
7
|
import { computeDocDeco } from "../decorations/computeDocDeco.js";
|
|
7
8
|
import { viewDecorations } from "../decorations/viewDecorations.js";
|
|
8
9
|
import { useEditor } from "../hooks/useEditor.js";
|
|
10
|
+
import { reactKeysPluginKey } from "../plugins/reactKeys.js";
|
|
9
11
|
import { LayoutGroup } from "./LayoutGroup.js";
|
|
10
12
|
import { DocNodeViewContext } from "./ProseMirrorDoc.js";
|
|
11
13
|
function getPos() {
|
|
@@ -17,9 +19,7 @@ const rootChildDescriptionsContextValue = {
|
|
|
17
19
|
},
|
|
18
20
|
siblingsRef: {
|
|
19
21
|
current: []
|
|
20
|
-
}
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
22
|
-
findCompositionDOM: ()=>{}
|
|
22
|
+
}
|
|
23
23
|
};
|
|
24
24
|
function ProseMirrorInner(param) {
|
|
25
25
|
let { children, nodeViewComponents, markViewComponents, ...props } = param;
|
|
@@ -53,6 +53,12 @@ function ProseMirrorInner(param) {
|
|
|
53
53
|
decorations,
|
|
54
54
|
innerDecorations
|
|
55
55
|
]);
|
|
56
|
+
const freezeFrom = reactKeysPluginKey.getState(state)?.freezeFrom ?? null;
|
|
57
|
+
const compositionContextValue = useMemo(()=>({
|
|
58
|
+
freezeFrom
|
|
59
|
+
}), [
|
|
60
|
+
freezeFrom
|
|
61
|
+
]);
|
|
56
62
|
return /*#__PURE__*/ React.createElement(EditorContext.Provider, {
|
|
57
63
|
value: editor
|
|
58
64
|
}, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
|
|
@@ -61,9 +67,11 @@ function ProseMirrorInner(param) {
|
|
|
61
67
|
value: nodeViewContextValue
|
|
62
68
|
}, /*#__PURE__*/ React.createElement(ChildDescriptionsContext.Provider, {
|
|
63
69
|
value: rootChildDescriptionsContextValue
|
|
70
|
+
}, /*#__PURE__*/ React.createElement(CompositionContext.Provider, {
|
|
71
|
+
value: compositionContextValue
|
|
64
72
|
}, /*#__PURE__*/ React.createElement(DocNodeViewContext.Provider, {
|
|
65
73
|
value: docNodeViewContextValue
|
|
66
|
-
}, children)))));
|
|
74
|
+
}, children))))));
|
|
67
75
|
}
|
|
68
76
|
export function ProseMirror(props) {
|
|
69
77
|
return /*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(ProseMirrorInner, props));
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { TextSelection } from "prosemirror-state";
|
|
2
1
|
import { DecorationSet } from "prosemirror-view";
|
|
3
|
-
import
|
|
2
|
+
import { Component, createRef } from "react";
|
|
4
3
|
import { ReactEditorView } from "../ReactEditorView.js";
|
|
5
4
|
import { findDOMNode } from "../findDOMNode.js";
|
|
6
|
-
import {
|
|
5
|
+
import { TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
7
6
|
import { wrapInDeco } from "./ChildNodeViews.js";
|
|
8
7
|
function shallowEqual(objA, objB) {
|
|
9
8
|
if (objA === objB) {
|
|
@@ -29,57 +28,6 @@ function shallowEqual(objA, objB) {
|
|
|
29
28
|
}
|
|
30
29
|
export class TextNodeView extends Component {
|
|
31
30
|
viewDescRef = createMutRef();
|
|
32
|
-
renderRef = createMutRef();
|
|
33
|
-
wasProtecting = createMutRef();
|
|
34
|
-
containsCompositionNodeText = createMutRef();
|
|
35
|
-
// This is basically NodeViewDesc.localCompositionInfo
|
|
36
|
-
// from prosemirror-view. It's been slightly adjusted so that
|
|
37
|
-
// it can be used accurately during render, before we've
|
|
38
|
-
// necessarily found (or even let the browser create)
|
|
39
|
-
// view.input.compositionNode
|
|
40
|
-
shouldProtect(props) {
|
|
41
|
-
const { view, getPos, node } = props;
|
|
42
|
-
if (!(view instanceof ReactEditorView)) return false;
|
|
43
|
-
if (!view.composing) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
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
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return !!this.containsCompositionNodeText.current;
|
|
64
|
-
}
|
|
65
|
-
handleCompositionEnd = ()=>{
|
|
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
|
-
}
|
|
82
|
-
};
|
|
83
31
|
create() {
|
|
84
32
|
const { view, decorations, siblingsRef, parentRef, getPos, node } = this.props;
|
|
85
33
|
const dom = findDOMNode(this);
|
|
@@ -91,24 +39,10 @@ export class TextNodeView extends Component {
|
|
|
91
39
|
if (!(textNode instanceof Text)) {
|
|
92
40
|
textNode = null;
|
|
93
41
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
viewDesc = new CompositionViewDesc(parentRef.current, getPos, // If we can't
|
|
97
|
-
// actually find the correct DOM nodes from here (
|
|
98
|
-
// which is the case in a composition in a newly
|
|
99
|
-
// created text node), we let our parent do it.
|
|
100
|
-
// Passing a valid element here just so that the
|
|
101
|
-
// ViewDesc constructor doesn't blow up.
|
|
102
|
-
dom ?? document.createElement("div"), textNode ?? document.createTextNode(node.text ?? ""), node.text ?? "");
|
|
103
|
-
} else {
|
|
104
|
-
if (!dom || !textNode) return null;
|
|
105
|
-
viewDesc = new TextViewDesc(parentRef.current, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
|
|
106
|
-
}
|
|
42
|
+
if (!dom || !textNode) return null;
|
|
43
|
+
const viewDesc = new TextViewDesc(parentRef.current, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
|
|
107
44
|
siblingsRef.current.push(viewDesc);
|
|
108
45
|
siblingsRef.current.sort(sortViewDescs);
|
|
109
|
-
if (viewDesc instanceof CompositionViewDesc) {
|
|
110
|
-
this.props.findCompositionDOM(viewDesc);
|
|
111
|
-
}
|
|
112
46
|
return viewDesc;
|
|
113
47
|
}
|
|
114
48
|
update() {
|
|
@@ -116,18 +50,6 @@ export class TextNodeView extends Component {
|
|
|
116
50
|
if (!(view instanceof ReactEditorView)) return false;
|
|
117
51
|
const viewDesc = this.viewDescRef.current;
|
|
118
52
|
if (!viewDesc) return false;
|
|
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) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
if (viewDesc instanceof CompositionViewDesc) return false;
|
|
131
53
|
const dom = findDOMNode(this);
|
|
132
54
|
if (!dom || dom !== viewDesc.dom) return false;
|
|
133
55
|
if (!dom.contains(viewDesc.nodeDOM)) return false;
|
|
@@ -148,58 +70,15 @@ export class TextNodeView extends Component {
|
|
|
148
70
|
this.destroy();
|
|
149
71
|
this.viewDescRef.current = this.create();
|
|
150
72
|
}
|
|
151
|
-
const { view } = this.props;
|
|
152
|
-
if (!(view instanceof ReactEditorView)) {
|
|
153
|
-
this.containsCompositionNodeText.current = true;
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const textNode = view.input.compositionNode;
|
|
157
|
-
if (!textNode) {
|
|
158
|
-
this.containsCompositionNodeText.current = true;
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
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;
|
|
179
73
|
}
|
|
180
74
|
shouldComponentUpdate(nextProps) {
|
|
181
|
-
// When leaving the protected state, force a re-render so React's
|
|
182
|
-
// virtual DOM resyncs with whatever the IME wrote into the real DOM
|
|
183
|
-
// while we were returning a stale renderRef.
|
|
184
|
-
if (this.wasProtecting.current && !this.shouldProtect(nextProps)) {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
75
|
return !shallowEqual(this.props, nextProps);
|
|
188
76
|
}
|
|
189
77
|
constructor(props){
|
|
190
78
|
super(props);
|
|
191
79
|
this.viewDescRef.current = null;
|
|
192
|
-
this.renderRef.current = null;
|
|
193
|
-
this.wasProtecting.current = false;
|
|
194
|
-
this.containsCompositionNodeText.current = true;
|
|
195
80
|
}
|
|
196
81
|
componentDidMount() {
|
|
197
|
-
this.containsCompositionNodeText.current = true;
|
|
198
|
-
// After a composition, force an update so that we re-check whether we need
|
|
199
|
-
// to be protecting our rendered content and allow React to re-sync with the
|
|
200
|
-
// DOM.
|
|
201
|
-
const { registerEventListener } = this.props;
|
|
202
|
-
registerEventListener("compositionend", this.handleCompositionEnd);
|
|
203
82
|
this.viewDescRef.current = this.create();
|
|
204
83
|
this.updateEffect();
|
|
205
84
|
}
|
|
@@ -208,43 +87,12 @@ export class TextNodeView extends Component {
|
|
|
208
87
|
const { view } = this.props;
|
|
209
88
|
if (!(view instanceof ReactEditorView)) return;
|
|
210
89
|
}
|
|
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);
|
|
228
|
-
}
|
|
229
90
|
componentWillUnmount() {
|
|
230
|
-
const { unregisterEventListener } = this.props;
|
|
231
|
-
unregisterEventListener("compositionend", this.handleCompositionEnd);
|
|
232
91
|
this.destroy();
|
|
233
92
|
}
|
|
234
93
|
render() {
|
|
235
94
|
const { node, decorations } = this.props;
|
|
236
|
-
|
|
237
|
-
// update the DOM that the user is working in. If there's
|
|
238
|
-
// an active composition and the selection is in this node,
|
|
239
|
-
// we freeze the DOM of this element so that it doesn't
|
|
240
|
-
// interrupt the composition
|
|
241
|
-
if (this.shouldProtect(this.props)) {
|
|
242
|
-
this.wasProtecting.current = true;
|
|
243
|
-
return this.renderRef.current;
|
|
244
|
-
}
|
|
245
|
-
this.wasProtecting.current = false;
|
|
246
|
-
this.renderRef.current = decorations.reduce(wrapInDeco, node.text);
|
|
247
|
-
return this.renderRef.current;
|
|
95
|
+
return decorations.reduce(wrapInDeco, node.text);
|
|
248
96
|
}
|
|
249
97
|
}
|
|
250
98
|
/**
|
|
@@ -254,11 +102,3 @@ export class TextNodeView extends Component {
|
|
|
254
102
|
*/ function createMutRef() {
|
|
255
103
|
return /*#__PURE__*/ createRef();
|
|
256
104
|
}
|
|
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,21 +1,12 @@
|
|
|
1
|
-
import React, { useContext, useRef
|
|
2
|
-
import { ReactEditorView } from "../ReactEditorView.js";
|
|
1
|
+
import React, { useContext, useRef } from "react";
|
|
3
2
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
4
3
|
import { useClientLayoutEffect } from "../hooks/useClientLayoutEffect.js";
|
|
5
|
-
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
6
|
-
import { useEditorEventListener } from "../hooks/useEditorEventListener.js";
|
|
7
4
|
import { TrailingHackViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
8
5
|
export function TrailingHackView(param) {
|
|
9
6
|
let { getPos } = param;
|
|
10
|
-
const [shouldRender, setShouldRender] = useState(true);
|
|
11
|
-
const [shouldReinsert, setShouldReinsert] = useState(false);
|
|
12
7
|
const { siblingsRef, parentRef } = useContext(ChildDescriptionsContext);
|
|
13
8
|
const viewDescRef = useRef(null);
|
|
14
9
|
const ref = useRef(null);
|
|
15
|
-
const preservedRef = useRef(ref.current);
|
|
16
|
-
if (ref.current) {
|
|
17
|
-
preservedRef.current = ref.current;
|
|
18
|
-
}
|
|
19
10
|
useClientLayoutEffect(()=>{
|
|
20
11
|
const siblings = siblingsRef.current;
|
|
21
12
|
return ()=>{
|
|
@@ -41,67 +32,6 @@ export function TrailingHackView(param) {
|
|
|
41
32
|
}
|
|
42
33
|
siblingsRef.current.sort(sortViewDescs);
|
|
43
34
|
});
|
|
44
|
-
// At the start of a composition, the browser will automatically delete
|
|
45
|
-
// the trailing hack br element. We need to unmount ourselves _before_
|
|
46
|
-
// that happens, so that React doesn't try to remove the already-removed
|
|
47
|
-
// br node when this component gets unmounted
|
|
48
|
-
useEditorEventListener("compositionstart", (view)=>{
|
|
49
|
-
const { from } = view.state.selection;
|
|
50
|
-
if (from === getPos()) {
|
|
51
|
-
setShouldRender(false);
|
|
52
|
-
setShouldReinsert(true);
|
|
53
|
-
}
|
|
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
|
-
]);
|
|
87
|
-
// We need to run the same composition check when we first get mounted,
|
|
88
|
-
// in case we got mounted in the same render batch as the beginning of
|
|
89
|
-
// a composition
|
|
90
|
-
useEditorEffect((view)=>{
|
|
91
|
-
if (!(view instanceof ReactEditorView)) return;
|
|
92
|
-
if (!view.compositionStarting) return;
|
|
93
|
-
const { from } = view.state.selection;
|
|
94
|
-
if (from === getPos()) {
|
|
95
|
-
setShouldRender(false);
|
|
96
|
-
}
|
|
97
|
-
}, [
|
|
98
|
-
getPos
|
|
99
|
-
]);
|
|
100
|
-
useEditorEventListener("compositionend", ()=>{
|
|
101
|
-
setShouldRender(true);
|
|
102
|
-
setShouldReinsert(false);
|
|
103
|
-
});
|
|
104
|
-
if (!shouldRender) return null;
|
|
105
35
|
return /*#__PURE__*/ React.createElement("br", {
|
|
106
36
|
ref: ref,
|
|
107
37
|
className: "ProseMirror-trailingBreak"
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import React, { createContext, memo, useContext, useMemo } from "react";
|
|
1
|
+
import React, { createContext, memo, useContext, useLayoutEffect, useMemo, useReducer, useRef } from "react";
|
|
2
|
+
import { CompositionContext } from "../../contexts/CompositionContext.js";
|
|
2
3
|
import { NodeViewContext } from "../../contexts/NodeViewContext.js";
|
|
3
4
|
import { DefaultNodeView } from "./DefaultNodeView.js";
|
|
4
5
|
import { NodeViewConstructorView } from "./NodeViewConstructorView.js";
|
|
5
6
|
import { ReactNodeView } from "./ReactNodeView.js";
|
|
6
|
-
export const NodeView = /*#__PURE__*/ memo(function NodeView(
|
|
7
|
+
export const NodeView = /*#__PURE__*/ memo(function NodeView(param) {
|
|
8
|
+
let { forceRemount, ...props } = param;
|
|
9
|
+
const renderRef = useRef(null);
|
|
10
|
+
const { freezeFrom } = useContext(CompositionContext);
|
|
7
11
|
const { components, constructors } = useContext(NodeViewContext);
|
|
12
|
+
const committedFrozenRef = useRef(false);
|
|
8
13
|
const component = components[props.node.type.name] ?? DefaultNodeView;
|
|
9
14
|
const constructor = constructors[props.node.type.name];
|
|
10
15
|
// Construct a wrapper component so that the node view remounts when either
|
|
@@ -31,8 +36,36 @@ export const NodeView = /*#__PURE__*/ memo(function NodeView(props) {
|
|
|
31
36
|
constructor,
|
|
32
37
|
component
|
|
33
38
|
]);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
// It's not generally safe to access getPos during render, because the
|
|
40
|
+
// component may not re-render when its return value would change. Here it's
|
|
41
|
+
// safe because we only use it to _suppress_ commits that would otherwise
|
|
42
|
+
// have happened.
|
|
43
|
+
const frozen = props.getPos() === freezeFrom;
|
|
44
|
+
// Protect content while frozen, and also through the single render where we
|
|
45
|
+
// leave the frozen state: `committedFrozenRef` still reflects the previous
|
|
46
|
+
// commit, so we keep returning the exact same cached element reference.
|
|
47
|
+
const protecting = (frozen || committedFrozenRef.current) && renderRef.current != null;
|
|
48
|
+
if (!protecting) {
|
|
49
|
+
renderRef.current = /*#__PURE__*/ React.createElement(GetPosContext.Provider, {
|
|
50
|
+
value: props.getPos
|
|
51
|
+
}, /*#__PURE__*/ React.createElement(Component, props));
|
|
52
|
+
}
|
|
53
|
+
useLayoutEffect(()=>{
|
|
54
|
+
const wasFrozen = committedFrozenRef.current;
|
|
55
|
+
committedFrozenRef.current = frozen;
|
|
56
|
+
if (wasFrozen && !frozen) forceRemount();
|
|
57
|
+
}, [
|
|
58
|
+
frozen,
|
|
59
|
+
forceRemount
|
|
60
|
+
]);
|
|
61
|
+
return renderRef.current;
|
|
37
62
|
});
|
|
38
63
|
export const GetPosContext = /*#__PURE__*/ createContext(null);
|
|
64
|
+
export function RemountableNodeView(props) {
|
|
65
|
+
const [key, forceRemount] = useReducer((x)=>x + 1, 0);
|
|
66
|
+
return /*#__PURE__*/ React.createElement(NodeView, {
|
|
67
|
+
key: key.toString(),
|
|
68
|
+
...props,
|
|
69
|
+
forceRemount: forceRemount
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useContext } from "react";
|
|
2
2
|
import { EditorContext } from "../contexts/EditorContext.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if the nearest ProseMirror component
|
|
5
|
+
* is rendered with the `static` prop set to `true`.
|
|
6
|
+
*/ export function useIsEditorStatic() {
|
|
4
7
|
return useContext(EditorContext)?.isStatic ?? false;
|
|
5
8
|
}
|
|
@@ -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,
|
|
5
|
+
import { ReactMarkViewDesc, 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,71 +110,10 @@ 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
|
-
]);
|
|
171
113
|
const childContextValue = useMemo(()=>({
|
|
172
114
|
parentRef: viewDescRef,
|
|
173
|
-
siblingsRef: childrenRef
|
|
174
|
-
|
|
175
|
-
}), [
|
|
176
|
-
findCompositionDOM
|
|
177
|
-
]);
|
|
115
|
+
siblingsRef: childrenRef
|
|
116
|
+
}), []);
|
|
178
117
|
return {
|
|
179
118
|
childContextValue,
|
|
180
119
|
contentDOM: contentDOMRef.current ?? viewDescRef.current?.dom,
|
|
@@ -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 { ReactNodeViewDesc,
|
|
5
|
+
import { ReactNodeViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
6
6
|
import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
|
|
7
7
|
import { useEffectEvent } from "./useEffectEvent.js";
|
|
8
8
|
export function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
@@ -133,74 +133,10 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
133
133
|
child.parent = viewDesc;
|
|
134
134
|
}
|
|
135
135
|
});
|
|
136
|
-
const findCompositionDOM = useCallback((compositionViewDesc)=>{
|
|
137
|
-
if (!props.node.isTextblock) return;
|
|
138
|
-
const children = childrenRef.current;
|
|
139
|
-
// Because TextNodeViews can't locate the DOM nodes
|
|
140
|
-
// for compositions, we need to override them here
|
|
141
|
-
if (!viewDescRef.current?.contentDOM) return;
|
|
142
|
-
let compositionTopDOM = null;
|
|
143
|
-
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
144
|
-
if (children.every((child)=>child.dom !== childNode)) {
|
|
145
|
-
compositionTopDOM = childNode;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (!compositionTopDOM) {
|
|
150
|
-
// Otherwise the IME extended an existing tracked text node. Take it over.
|
|
151
|
-
const reactView = view;
|
|
152
|
-
const imeTextNode = reactView.input.compositionNode;
|
|
153
|
-
if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const claimedDesc = imeTextNode.pmViewDesc;
|
|
157
|
-
if (!(claimedDesc instanceof TextViewDesc)) return;
|
|
158
|
-
if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
|
|
159
|
-
// Walk up to the direct child of contentDOM that contains the IME text node
|
|
160
|
-
// (could be the text node itself, could be wrapped in a mark span).
|
|
161
|
-
let topDOM = imeTextNode;
|
|
162
|
-
while(topDOM.parentNode !== viewDescRef.current.contentDOM){
|
|
163
|
-
const next = topDOM.parentNode;
|
|
164
|
-
if (!next) return;
|
|
165
|
-
topDOM = next;
|
|
166
|
-
}
|
|
167
|
-
// Detach the displaced TextViewDesc from the sibling list so sibling-size
|
|
168
|
-
// accounting (used by posBeforeChild) doesn't double-count this text node.
|
|
169
|
-
const displacedIdx = children.indexOf(claimedDesc);
|
|
170
|
-
if (displacedIdx >= 0) children.splice(displacedIdx, 1);
|
|
171
|
-
reactView.displacedNodes.push(claimedDesc);
|
|
172
|
-
compositionViewDesc.dom = topDOM;
|
|
173
|
-
compositionViewDesc.textDOM = imeTextNode;
|
|
174
|
-
compositionViewDesc.text = imeTextNode.data;
|
|
175
|
-
imeTextNode.pmViewDesc = compositionViewDesc;
|
|
176
|
-
compositionViewDesc._displacedDesc = claimedDesc;
|
|
177
|
-
reactView.input.compositionNodes.push(compositionViewDesc);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
let textDOM = compositionTopDOM;
|
|
181
|
-
while(textDOM.firstChild){
|
|
182
|
-
textDOM = textDOM.firstChild;
|
|
183
|
-
}
|
|
184
|
-
if (!textDOM || !(textDOM instanceof Text)) {
|
|
185
|
-
console.error(compositionTopDOM, textDOM);
|
|
186
|
-
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
187
|
-
}
|
|
188
|
-
compositionViewDesc.dom = compositionTopDOM;
|
|
189
|
-
compositionViewDesc.textDOM = textDOM;
|
|
190
|
-
compositionViewDesc.text = textDOM.data;
|
|
191
|
-
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
192
|
-
view.input.compositionNodes.push(compositionViewDesc);
|
|
193
|
-
}, [
|
|
194
|
-
props.node.isTextblock,
|
|
195
|
-
view
|
|
196
|
-
]);
|
|
197
136
|
const childContextValue = useMemo(()=>({
|
|
198
137
|
parentRef: viewDescRef,
|
|
199
|
-
siblingsRef: childrenRef
|
|
200
|
-
|
|
201
|
-
}), [
|
|
202
|
-
findCompositionDOM
|
|
203
|
-
]);
|
|
138
|
+
siblingsRef: childrenRef
|
|
139
|
+
}), []);
|
|
204
140
|
return {
|
|
205
141
|
childContextValue,
|
|
206
142
|
contentDOM: contentDOMRef.current,
|