@handlewithcare/react-prosemirror 3.1.0-tiptap.49 → 3.1.0-tiptap.51
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 +2 -0
- package/dist/cjs/components/ChildNodeViews.js +14 -9
- package/dist/cjs/components/CursorWrapper.js +6 -9
- package/dist/cjs/components/TextNodeView.js +109 -38
- package/dist/cjs/components/TrailingHackView.js +29 -0
- package/dist/cjs/hooks/useComponentEventListeners.js +46 -5
- package/dist/cjs/hooks/useEditor.js +3 -6
- package/dist/cjs/hooks/useNodeViewDescription.js +37 -16
- package/dist/cjs/plugins/beforeInputPlugin.js +41 -43
- package/dist/cjs/viewdesc.js +10 -3
- package/dist/esm/ReactEditorView.js +2 -0
- package/dist/esm/components/ChildNodeViews.js +14 -9
- package/dist/esm/components/CursorWrapper.js +7 -10
- package/dist/esm/components/TextNodeView.js +109 -38
- package/dist/esm/components/TrailingHackView.js +30 -1
- package/dist/esm/hooks/useComponentEventListeners.js +53 -12
- package/dist/esm/hooks/useEditor.js +3 -6
- package/dist/esm/hooks/useNodeViewDescription.js +38 -17
- package/dist/esm/plugins/beforeInputPlugin.js +41 -43
- package/dist/esm/viewdesc.js +10 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +4 -0
- package/dist/types/components/TextNodeView.d.ts +14 -4
- package/dist/types/components/TrailingHackView.d.ts +1 -1
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/contexts/EditorContext.d.ts +1 -1
- package/dist/types/hooks/useComponentEventListeners.d.ts +11 -10
- package/dist/types/hooks/useEditor.d.ts +2 -2
- package/dist/types/hooks/useEditorEventListener.d.ts +1 -1
- package/dist/types/props.d.ts +26 -26
- package/dist/types/viewdesc.d.ts +2 -2
- package/package.json +2 -1
- package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +0 -59
- package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +0 -56
- package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +0 -1
package/dist/cjs/viewdesc.js
CHANGED
|
@@ -266,7 +266,9 @@ let ViewDesc = class ViewDesc {
|
|
|
266
266
|
prev = i ? this.children[i - 1] : null;
|
|
267
267
|
if (!prev || prev.dom.parentNode == this.contentDOM) break;
|
|
268
268
|
}
|
|
269
|
-
if (prev && side && enter && !prev.border && !prev.domAtom)
|
|
269
|
+
if (prev && side && enter && !prev.border && !prev.domAtom) {
|
|
270
|
+
return prev.domFromPos(prev.size, side);
|
|
271
|
+
}
|
|
270
272
|
return {
|
|
271
273
|
node: this.contentDOM,
|
|
272
274
|
offset: prev ? (0, _dom.domIndex)(prev.dom) + 1 : 0
|
|
@@ -401,7 +403,9 @@ let ViewDesc = class ViewDesc {
|
|
|
401
403
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
402
404
|
if (after && after.contentEditable == "false") force = true;
|
|
403
405
|
}
|
|
404
|
-
if (!(force || brKludge && _browser.browser.safari) && (0, _dom.isEquivalentPosition)(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && (0, _dom.isEquivalentPosition)(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset))
|
|
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)) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
405
409
|
// Selection.extend can be used to create an 'inverted' selection
|
|
406
410
|
// (one where the focus is before the anchor), but not all
|
|
407
411
|
// browsers support it yet.
|
|
@@ -666,7 +670,10 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
|
|
|
666
670
|
skip: skip || true
|
|
667
671
|
};
|
|
668
672
|
}
|
|
669
|
-
update(
|
|
673
|
+
update(node, outerDeco, _innerDeco, _view) {
|
|
674
|
+
if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
|
|
675
|
+
this.updateOuterDeco(outerDeco);
|
|
676
|
+
this.node = node;
|
|
670
677
|
this.dirty = NOT_DIRTY;
|
|
671
678
|
return true;
|
|
672
679
|
}
|
|
@@ -33,6 +33,7 @@ function changedNodeViews(a, b) {
|
|
|
33
33
|
nextProps;
|
|
34
34
|
prevState;
|
|
35
35
|
_destroyed;
|
|
36
|
+
deferPendingEffects;
|
|
36
37
|
constructor(place, props){
|
|
37
38
|
// Prevent the base class from destroying the React-managed nodes.
|
|
38
39
|
// Restore them below after invoking the base class constructor.
|
|
@@ -85,6 +86,7 @@ function changedNodeViews(a, b) {
|
|
|
85
86
|
// @ts-expect-error this violates the typing but class does it, too.
|
|
86
87
|
this.docView = null;
|
|
87
88
|
this._destroyed = false;
|
|
89
|
+
this.deferPendingEffects = false;
|
|
88
90
|
}
|
|
89
91
|
get props() {
|
|
90
92
|
return this.nextProps;
|
|
@@ -51,13 +51,18 @@ const ChildView = /*#__PURE__*/ memo(function ChildView(param) {
|
|
|
51
51
|
key: child.key
|
|
52
52
|
}, (param)=>{
|
|
53
53
|
let { siblingsRef, parentRef } = param;
|
|
54
|
-
return /*#__PURE__*/ React.createElement(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
return /*#__PURE__*/ React.createElement(EditorContext.Consumer, null, (param)=>{
|
|
55
|
+
let { registerEventListener, unregisterEventListener } = param;
|
|
56
|
+
return /*#__PURE__*/ React.createElement(TextNodeView, {
|
|
57
|
+
view: view,
|
|
58
|
+
node: child.node,
|
|
59
|
+
getPos: getPos,
|
|
60
|
+
siblingsRef: siblingsRef,
|
|
61
|
+
parentRef: parentRef,
|
|
62
|
+
decorations: child.outerDeco,
|
|
63
|
+
registerEventListener: registerEventListener,
|
|
64
|
+
unregisterEventListener: unregisterEventListener
|
|
65
|
+
});
|
|
61
66
|
});
|
|
62
67
|
}) : /*#__PURE__*/ React.createElement(NodeView, {
|
|
63
68
|
key: child.key,
|
|
@@ -340,14 +345,14 @@ export const ChildNodeViews = /*#__PURE__*/ memo(function ChildNodeViews(param)
|
|
|
340
345
|
component: SeparatorHackView,
|
|
341
346
|
marks: [],
|
|
342
347
|
offset: lastChild?.offset ?? 0,
|
|
343
|
-
index: (lastChild?.index ?? 0) +
|
|
348
|
+
index: (lastChild?.index ?? 0) + 1,
|
|
344
349
|
key: "trailing-hack-img"
|
|
345
350
|
}, {
|
|
346
351
|
type: "hack",
|
|
347
352
|
component: TrailingHackView,
|
|
348
353
|
marks: [],
|
|
349
354
|
offset: lastChild?.offset ?? 0,
|
|
350
|
-
index: (lastChild?.index ?? 0) +
|
|
355
|
+
index: (lastChild?.index ?? 0) + 2,
|
|
351
356
|
key: "trailing-hack-br"
|
|
352
357
|
});
|
|
353
358
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
|
1
|
+
import React, { forwardRef, useImperativeHandle, useRef, useState } 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);
|
|
6
7
|
const innerRef = useRef(null);
|
|
7
8
|
useImperativeHandle(ref, ()=>{
|
|
8
9
|
return innerRef.current;
|
|
@@ -14,22 +15,18 @@ export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(par
|
|
|
14
15
|
// @ts-expect-error Internal property - domSelection
|
|
15
16
|
const domSel = view.domSelection();
|
|
16
17
|
const node = innerRef.current;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
20
|
-
domSel.collapse(node.parentNode, domIndex(node) + 1);
|
|
21
|
-
} else {
|
|
22
|
-
domSel.collapse(node, 0);
|
|
23
|
-
}
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
19
|
+
domSel.collapse(node.parentNode, domIndex(node) + 1);
|
|
24
20
|
// @ts-expect-error Internal property - domObserver
|
|
25
21
|
view.domObserver.connectSelection();
|
|
22
|
+
setShouldRender(false);
|
|
26
23
|
}, []);
|
|
27
|
-
return /*#__PURE__*/ React.createElement("img", {
|
|
24
|
+
return shouldRender ? /*#__PURE__*/ React.createElement("img", {
|
|
28
25
|
ref: innerRef,
|
|
29
26
|
className: "ProseMirror-separator",
|
|
30
27
|
// eslint-disable-next-line react/no-unknown-property
|
|
31
28
|
"mark-placeholder": "true",
|
|
32
29
|
alt: "",
|
|
33
30
|
...props
|
|
34
|
-
});
|
|
31
|
+
}) : null;
|
|
35
32
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { TextSelection } from "prosemirror-state";
|
|
1
2
|
import { DecorationSet } from "prosemirror-view";
|
|
2
3
|
import { Component } from "react";
|
|
4
|
+
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
5
|
import { findDOMNode } from "../findDOMNode.js";
|
|
4
6
|
import { CompositionViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
5
7
|
import { wrapInDeco } from "./ChildNodeViews.js";
|
|
@@ -28,72 +30,141 @@ function shallowEqual(objA, objB) {
|
|
|
28
30
|
export class TextNodeView extends Component {
|
|
29
31
|
viewDescRef = null;
|
|
30
32
|
renderRef = null;
|
|
31
|
-
|
|
33
|
+
wasProtecting = false;
|
|
34
|
+
containsCompositionNodeText = true;
|
|
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 pos = getPos();
|
|
47
|
+
const { from, to } = view.state.selection;
|
|
48
|
+
if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + node.nodeSize) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return this.containsCompositionNodeText;
|
|
52
|
+
}
|
|
53
|
+
handleCompositionEnd = ()=>{
|
|
54
|
+
if (!this.wasProtecting) return;
|
|
55
|
+
this.forceUpdate();
|
|
56
|
+
return;
|
|
57
|
+
};
|
|
58
|
+
create() {
|
|
32
59
|
const { view, decorations, siblingsRef, parentRef, getPos, node } = this.props;
|
|
33
|
-
// There simply is no other way to ref a text node
|
|
34
|
-
// eslint-disable-next-line react/no-find-dom-node
|
|
35
60
|
const dom = findDOMNode(this);
|
|
36
|
-
|
|
37
|
-
// when a composition was started that produces a new text node.
|
|
38
|
-
// Otherwise we just rely on re-rendering the renderRef
|
|
39
|
-
if (!dom) {
|
|
40
|
-
if (!view.composing) return;
|
|
41
|
-
this.viewDescRef = new CompositionViewDesc(parentRef.current, getPos, // These are just placeholders/dummies. We can't
|
|
42
|
-
// actually find the correct DOM nodes from here,
|
|
43
|
-
// so we let our parent do it.
|
|
44
|
-
// Passing a valid element here just so that the
|
|
45
|
-
// ViewDesc constructor doesn't blow up.
|
|
46
|
-
document.createElement("div"), document.createTextNode(node.text ?? ""), node.text ?? "");
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
61
|
+
if (!dom && !view.composing) return null;
|
|
49
62
|
let textNode = dom;
|
|
50
|
-
while(textNode
|
|
63
|
+
while(textNode?.firstChild){
|
|
51
64
|
textNode = textNode.firstChild;
|
|
52
65
|
}
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
} else {
|
|
56
|
-
this.viewDescRef.parent = parentRef.current;
|
|
57
|
-
this.viewDescRef.children = [];
|
|
58
|
-
this.viewDescRef.node = node;
|
|
59
|
-
this.viewDescRef.outerDeco = decorations;
|
|
60
|
-
this.viewDescRef.innerDeco = DecorationSet.empty;
|
|
61
|
-
this.viewDescRef.dom = dom;
|
|
62
|
-
this.viewDescRef.dom.pmViewDesc = this.viewDescRef;
|
|
63
|
-
this.viewDescRef.nodeDOM = textNode;
|
|
66
|
+
if (!(textNode instanceof Text)) {
|
|
67
|
+
textNode = null;
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
let viewDesc;
|
|
70
|
+
if (this.shouldProtect(this.props)) {
|
|
71
|
+
viewDesc = new CompositionViewDesc(parentRef.current, getPos, // If we can't
|
|
72
|
+
// actually find the correct DOM nodes from here (
|
|
73
|
+
// which is the case in a composition in a newly
|
|
74
|
+
// created text node), we let our parent do it.
|
|
75
|
+
// Passing a valid element here just so that the
|
|
76
|
+
// ViewDesc constructor doesn't blow up.
|
|
77
|
+
dom ?? document.createElement("div"), textNode ?? document.createTextNode(node.text ?? ""), node.text ?? "");
|
|
78
|
+
} else {
|
|
79
|
+
if (!dom || !textNode) return null;
|
|
80
|
+
viewDesc = new TextViewDesc(parentRef.current, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
|
|
67
81
|
}
|
|
82
|
+
siblingsRef.current.push(viewDesc);
|
|
68
83
|
siblingsRef.current.sort(sortViewDescs);
|
|
84
|
+
return viewDesc;
|
|
85
|
+
}
|
|
86
|
+
update() {
|
|
87
|
+
const { view, node, decorations } = this.props;
|
|
88
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
89
|
+
const viewDesc = this.viewDescRef;
|
|
90
|
+
if (!viewDesc) return false;
|
|
91
|
+
if (this.shouldProtect(this.props) !== viewDesc instanceof CompositionViewDesc) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (viewDesc instanceof CompositionViewDesc) return false;
|
|
95
|
+
const dom = findDOMNode(this);
|
|
96
|
+
if (!dom || dom !== viewDesc.dom) return false;
|
|
97
|
+
if (!dom.contains(viewDesc.nodeDOM)) return false;
|
|
98
|
+
return viewDesc.matchesNode(node, decorations, DecorationSet.empty) || viewDesc.update(node, decorations, DecorationSet.empty, view);
|
|
99
|
+
}
|
|
100
|
+
destroy() {
|
|
101
|
+
const viewDesc = this.viewDescRef;
|
|
102
|
+
if (!viewDesc) return;
|
|
103
|
+
viewDesc.destroy();
|
|
104
|
+
const siblings = this.props.siblingsRef.current;
|
|
105
|
+
if (siblings.includes(viewDesc)) {
|
|
106
|
+
const index = siblings.indexOf(viewDesc);
|
|
107
|
+
siblings.splice(index, 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
updateEffect() {
|
|
111
|
+
if (!this.update()) {
|
|
112
|
+
this.destroy();
|
|
113
|
+
this.viewDescRef = this.create();
|
|
114
|
+
}
|
|
115
|
+
const { view, node } = this.props;
|
|
116
|
+
if (!(view instanceof ReactEditorView)) {
|
|
117
|
+
this.containsCompositionNodeText = true;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const textNode = view.input.compositionNode;
|
|
121
|
+
if (!textNode) {
|
|
122
|
+
this.containsCompositionNodeText = true;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
126
|
+
const text = textNode.nodeValue;
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
128
|
+
this.containsCompositionNodeText = node.text === text;
|
|
69
129
|
}
|
|
70
130
|
shouldComponentUpdate(nextProps) {
|
|
131
|
+
// When leaving the protected state, force a re-render so React's
|
|
132
|
+
// virtual DOM resyncs with whatever the IME wrote into the real DOM
|
|
133
|
+
// while we were returning a stale renderRef.
|
|
134
|
+
if (this.wasProtecting && !this.shouldProtect(nextProps)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
71
137
|
return !shallowEqual(this.props, nextProps);
|
|
72
138
|
}
|
|
73
139
|
componentDidMount() {
|
|
140
|
+
this.viewDescRef = null;
|
|
141
|
+
// After a composition, force an update so that we re-check whether we need
|
|
142
|
+
// to be protecting our rendered content and allow React to re-sync with the
|
|
143
|
+
// DOM.
|
|
144
|
+
const { registerEventListener } = this.props;
|
|
145
|
+
registerEventListener("compositionend", this.handleCompositionEnd);
|
|
74
146
|
this.updateEffect();
|
|
75
147
|
}
|
|
76
148
|
componentDidUpdate() {
|
|
77
149
|
this.updateEffect();
|
|
78
150
|
}
|
|
79
151
|
componentWillUnmount() {
|
|
80
|
-
const {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const index = siblingsRef.current.indexOf(this.viewDescRef);
|
|
84
|
-
siblingsRef.current.splice(index, 1);
|
|
85
|
-
}
|
|
152
|
+
const { unregisterEventListener } = this.props;
|
|
153
|
+
unregisterEventListener("compositionend", this.handleCompositionEnd);
|
|
154
|
+
this.destroy();
|
|
86
155
|
}
|
|
87
156
|
render() {
|
|
88
|
-
const {
|
|
157
|
+
const { node, decorations } = this.props;
|
|
89
158
|
// During a composition, it's crucial that we don't try to
|
|
90
159
|
// update the DOM that the user is working in. If there's
|
|
91
160
|
// an active composition and the selection is in this node,
|
|
92
161
|
// we freeze the DOM of this element so that it doesn't
|
|
93
162
|
// interrupt the composition
|
|
94
|
-
if (
|
|
163
|
+
if (this.shouldProtect(this.props)) {
|
|
164
|
+
this.wasProtecting = true;
|
|
95
165
|
return this.renderRef;
|
|
96
166
|
}
|
|
167
|
+
this.wasProtecting = false;
|
|
97
168
|
this.renderRef = decorations.reduce(wrapInDeco, node.text);
|
|
98
169
|
return this.renderRef;
|
|
99
170
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import React, { useContext, useRef } from "react";
|
|
1
|
+
import React, { useContext, useRef, useState } from "react";
|
|
2
2
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
3
3
|
import { useClientLayoutEffect } from "../hooks/useClientLayoutEffect.js";
|
|
4
|
+
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
5
|
+
import { useEditorEventListener } from "../hooks/useEditorEventListener.js";
|
|
4
6
|
import { TrailingHackViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
5
7
|
export function TrailingHackView(param) {
|
|
6
8
|
let { getPos } = param;
|
|
9
|
+
const [shouldRender, setShouldRender] = useState(true);
|
|
7
10
|
const { siblingsRef, parentRef } = useContext(ChildDescriptionsContext);
|
|
8
11
|
const viewDescRef = useRef(null);
|
|
9
12
|
const ref = useRef(null);
|
|
@@ -32,6 +35,32 @@ export function TrailingHackView(param) {
|
|
|
32
35
|
}
|
|
33
36
|
siblingsRef.current.sort(sortViewDescs);
|
|
34
37
|
});
|
|
38
|
+
// At the start of a composition, the browser will automatically delete
|
|
39
|
+
// the trailing hack br element. We need to unmount ourselves _before_
|
|
40
|
+
// that happens, so that React doesn't try to remove the already-removed
|
|
41
|
+
// br node when this component gets unmounted
|
|
42
|
+
useEditorEventListener("compositionstart", (view)=>{
|
|
43
|
+
const { from } = view.state.selection;
|
|
44
|
+
if (from === getPos()) {
|
|
45
|
+
setShouldRender(false);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// We need to run the same composition check when we first get mounted,
|
|
49
|
+
// in case we got mounted in the same render batch as the beginning of
|
|
50
|
+
// a composition
|
|
51
|
+
useEditorEffect((view)=>{
|
|
52
|
+
if (!view.composing) return;
|
|
53
|
+
const { from } = view.state.selection;
|
|
54
|
+
if (from === getPos()) {
|
|
55
|
+
setShouldRender(false);
|
|
56
|
+
}
|
|
57
|
+
}, [
|
|
58
|
+
getPos
|
|
59
|
+
]);
|
|
60
|
+
useEditorEventListener("compositionend", ()=>{
|
|
61
|
+
setShouldRender(true);
|
|
62
|
+
});
|
|
63
|
+
if (!shouldRender) return null;
|
|
35
64
|
return /*#__PURE__*/ React.createElement("br", {
|
|
36
65
|
ref: ref,
|
|
37
66
|
className: "ProseMirror-trailingBreak"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* Copyright (c) The New York Times Company */ import { useCallback, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
/* Copyright (c) The New York Times Company */ import { useCallback, useLayoutEffect, useMemo, useState } from "react";
|
|
2
|
+
import { unstable_batchedUpdates as batch } from "react-dom";
|
|
3
3
|
/**
|
|
4
4
|
* Produces a plugin that can be used with ProseMirror to handle DOM
|
|
5
5
|
* events at the EditorView.dom element.
|
|
@@ -14,19 +14,27 @@ import { componentEventListeners } from "../plugins/componentEventListeners.js";
|
|
|
14
14
|
* @privateRemarks
|
|
15
15
|
*
|
|
16
16
|
* This hook uses a combination of mutable and immutable updates to give
|
|
17
|
-
* us precise control over when we re-create the
|
|
17
|
+
* us precise control over when we re-create the event listeners.
|
|
18
18
|
*
|
|
19
|
-
* The
|
|
19
|
+
* The hook has a mutable reference to the set of handlers for each
|
|
20
20
|
* event type, but the set of event types is static. This means that we
|
|
21
|
-
* need to produce a new
|
|
22
|
-
* registered. We avoid producing a new
|
|
23
|
-
* scenario to avoid the performance overhead of
|
|
24
|
-
* in the EditorView.
|
|
21
|
+
* need to produce a new handleDOMEVents record whenever a new event type is
|
|
22
|
+
* registered. We avoid producing a new record in any other
|
|
23
|
+
* scenario to avoid the performance overhead of re-registering the event
|
|
24
|
+
* listeners in the EditorView.
|
|
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())
|
|
28
|
+
*/ export function useComponentEventListeners(existingHandlers) {
|
|
29
|
+
const [registry, setRegistry] = useState(new Map(Object.entries(existingHandlers ?? {}).map((param)=>{
|
|
30
|
+
let [eventName, handler] = param;
|
|
31
|
+
return [
|
|
32
|
+
eventName,
|
|
33
|
+
handler ? [
|
|
34
|
+
handler
|
|
35
|
+
] : []
|
|
36
|
+
];
|
|
37
|
+
})));
|
|
30
38
|
const registerEventListener = useCallback((eventType, handler)=>{
|
|
31
39
|
const handlers = registry.get(eventType) ?? [];
|
|
32
40
|
handlers.unshift(handler);
|
|
@@ -43,12 +51,45 @@ import { componentEventListeners } from "../plugins/componentEventListeners.js";
|
|
|
43
51
|
}, [
|
|
44
52
|
registry
|
|
45
53
|
]);
|
|
46
|
-
|
|
54
|
+
useLayoutEffect(()=>{
|
|
55
|
+
if (!existingHandlers) return;
|
|
56
|
+
for (const [eventType, handler] of Object.entries(existingHandlers)){
|
|
57
|
+
if (!handler) return;
|
|
58
|
+
registerEventListener(eventType, handler);
|
|
59
|
+
}
|
|
60
|
+
return ()=>{
|
|
61
|
+
for (const [eventType, handler] of Object.entries(existingHandlers)){
|
|
62
|
+
if (!handler) return;
|
|
63
|
+
unregisterEventListener(eventType, handler);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [
|
|
67
|
+
existingHandlers,
|
|
68
|
+
registerEventListener,
|
|
69
|
+
unregisterEventListener
|
|
70
|
+
]);
|
|
71
|
+
const handleDOMEvents = useMemo(()=>{
|
|
72
|
+
const domEventHandlers = {};
|
|
73
|
+
for (const [eventType, handlers] of registry.entries()){
|
|
74
|
+
function handleEvent(view, event) {
|
|
75
|
+
for (const handler of handlers){
|
|
76
|
+
let handled = false;
|
|
77
|
+
batch(()=>{
|
|
78
|
+
handled = !!handler(view, event);
|
|
79
|
+
});
|
|
80
|
+
if (handled || event.defaultPrevented) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
domEventHandlers[eventType] = handleEvent;
|
|
85
|
+
}
|
|
86
|
+
return domEventHandlers;
|
|
87
|
+
}, [
|
|
47
88
|
registry
|
|
48
89
|
]);
|
|
49
90
|
return {
|
|
50
91
|
registerEventListener,
|
|
51
92
|
unregisterEventListener,
|
|
52
|
-
|
|
93
|
+
handleDOMEvents
|
|
53
94
|
};
|
|
54
95
|
}
|
|
@@ -30,7 +30,7 @@ let didWarnValueDefaultValue = false;
|
|
|
30
30
|
const defaultState = options.defaultState ?? EMPTY_STATE;
|
|
31
31
|
const [_state, setState] = useState(defaultState);
|
|
32
32
|
const state = options.state ?? _state;
|
|
33
|
-
const {
|
|
33
|
+
const { handleDOMEvents, registerEventListener, unregisterEventListener } = useComponentEventListeners(options.handleDOMEvents);
|
|
34
34
|
const setCursorWrapper = useCallback((deco)=>{
|
|
35
35
|
flushSync(()=>{
|
|
36
36
|
_setCursorWrapper(deco);
|
|
@@ -38,11 +38,9 @@ let didWarnValueDefaultValue = false;
|
|
|
38
38
|
}, []);
|
|
39
39
|
const plugins = useMemo(()=>[
|
|
40
40
|
...options.plugins ?? [],
|
|
41
|
-
componentEventListenersPlugin,
|
|
42
41
|
beforeInputPlugin(setCursorWrapper)
|
|
43
42
|
], [
|
|
44
43
|
options.plugins,
|
|
45
|
-
componentEventListenersPlugin,
|
|
46
44
|
setCursorWrapper
|
|
47
45
|
]);
|
|
48
46
|
const dispatchTransaction = useCallback(function dispatchTransaction(tr) {
|
|
@@ -71,7 +69,8 @@ let didWarnValueDefaultValue = false;
|
|
|
71
69
|
...options,
|
|
72
70
|
state,
|
|
73
71
|
plugins,
|
|
74
|
-
dispatchTransaction
|
|
72
|
+
dispatchTransaction,
|
|
73
|
+
handleDOMEvents
|
|
75
74
|
};
|
|
76
75
|
const [view, setView] = useState(()=>{
|
|
77
76
|
return new StaticEditorView(directEditorProps);
|
|
@@ -101,8 +100,6 @@ let didWarnValueDefaultValue = false;
|
|
|
101
100
|
// running effects. Running effects will reattach selection
|
|
102
101
|
// change listeners if the EditorView has been destroyed.
|
|
103
102
|
if (view instanceof ReactEditorView && !view.isDestroyed) {
|
|
104
|
-
// Plugins might dispatch transactions from their
|
|
105
|
-
// view update lifecycle hooks
|
|
106
103
|
flushSyncRef.current = false;
|
|
107
104
|
view.commitPendingEffects();
|
|
108
105
|
flushSyncRef.current = true;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useCallback, useContext, useMemo, useRef } from "react";
|
|
2
2
|
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
|
+
import { CursorWrapper } from "../components/CursorWrapper.js";
|
|
3
4
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
4
5
|
import { EditorContext } from "../contexts/EditorContext.js";
|
|
5
|
-
import {
|
|
6
|
+
import { ReactWidgetType } from "../decorations/ReactWidgetType.js";
|
|
7
|
+
import { CompositionViewDesc, MarkViewDesc, ReactNodeViewDesc, WidgetViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
6
8
|
import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
|
|
7
9
|
import { useEffectEvent } from "./useEffectEvent.js";
|
|
8
10
|
export function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
@@ -131,25 +133,44 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
131
133
|
children.sort(sortViewDescs);
|
|
132
134
|
for (const child of children){
|
|
133
135
|
child.parent = viewDesc;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
}
|
|
137
|
+
if (!props.node.isTextblock) return;
|
|
138
|
+
// Because TextNodeViews can't locate the DOM nodes
|
|
139
|
+
// for compositions, we need to override them here
|
|
140
|
+
if (!viewDescRef.current?.contentDOM) return;
|
|
141
|
+
const compositionChildIndex = children.findIndex((child)=>child instanceof CompositionViewDesc);
|
|
142
|
+
if (compositionChildIndex === -1) return;
|
|
143
|
+
const compositionViewDesc = children[compositionChildIndex];
|
|
144
|
+
if (!(compositionViewDesc instanceof CompositionViewDesc)) return;
|
|
145
|
+
let compositionTopDOM = null;
|
|
146
|
+
let search = children[compositionChildIndex - 1];
|
|
147
|
+
while(search instanceof MarkViewDesc){
|
|
148
|
+
search = search.children[0];
|
|
149
|
+
}
|
|
150
|
+
if (search instanceof WidgetViewDesc && search.widget.type instanceof ReactWidgetType && search.widget.type.Component === CursorWrapper) {
|
|
151
|
+
compositionTopDOM = search.dom.nextSibling;
|
|
152
|
+
} else {
|
|
153
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
154
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
155
|
+
compositionTopDOM = childNode;
|
|
156
|
+
break;
|
|
142
157
|
}
|
|
143
|
-
if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
144
|
-
child.dom = compositionTopDOM;
|
|
145
|
-
child.textDOM = textDOM;
|
|
146
|
-
child.text = textDOM.data;
|
|
147
|
-
child.textDOM.pmViewDesc = child;
|
|
148
|
-
// It should not be possible to be in a composition because one could
|
|
149
|
-
// not start between the renders that switch the view type.
|
|
150
|
-
view.input.compositionNodes.push(child);
|
|
151
158
|
}
|
|
152
159
|
}
|
|
160
|
+
if (!compositionTopDOM) return;
|
|
161
|
+
let textDOM = compositionTopDOM;
|
|
162
|
+
while(textDOM.firstChild){
|
|
163
|
+
textDOM = textDOM.firstChild;
|
|
164
|
+
}
|
|
165
|
+
if (!textDOM || !(textDOM instanceof Text)) {
|
|
166
|
+
console.error(compositionTopDOM, textDOM);
|
|
167
|
+
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
168
|
+
}
|
|
169
|
+
compositionViewDesc.dom = compositionTopDOM;
|
|
170
|
+
compositionViewDesc.textDOM = textDOM;
|
|
171
|
+
compositionViewDesc.text = textDOM.data;
|
|
172
|
+
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
173
|
+
view.input.compositionNodes.push(compositionViewDesc);
|
|
153
174
|
});
|
|
154
175
|
const childContextValue = useMemo(()=>({
|
|
155
176
|
parentRef: viewDescRef,
|