@handlewithcare/react-prosemirror 3.1.0-tiptap.49 → 3.1.0-tiptap.50
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 +12 -7
- package/dist/cjs/components/CursorWrapper.js +8 -9
- package/dist/cjs/components/TextNodeView.js +112 -38
- package/dist/cjs/hooks/useComponentEventListeners.js +46 -5
- package/dist/cjs/hooks/useEditor.js +16 -8
- package/dist/cjs/hooks/useNodeViewDescription.js +38 -15
- package/dist/cjs/plugins/beforeInputPlugin.js +47 -41
- package/dist/cjs/viewdesc.js +10 -3
- package/dist/esm/ReactEditorView.js +2 -0
- package/dist/esm/components/ChildNodeViews.js +12 -7
- package/dist/esm/components/CursorWrapper.js +9 -10
- package/dist/esm/components/TextNodeView.js +112 -38
- package/dist/esm/hooks/useComponentEventListeners.js +53 -12
- package/dist/esm/hooks/useEditor.js +16 -8
- package/dist/esm/hooks/useNodeViewDescription.js +39 -16
- package/dist/esm/plugins/beforeInputPlugin.js +47 -41
- 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/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
|
@@ -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,
|
|
@@ -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,20 @@ 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
|
+
setTimeout(()=>{
|
|
23
|
+
setShouldRender(false);
|
|
24
|
+
});
|
|
26
25
|
}, []);
|
|
27
|
-
return /*#__PURE__*/ React.createElement("img", {
|
|
26
|
+
return shouldRender ? /*#__PURE__*/ React.createElement("img", {
|
|
28
27
|
ref: innerRef,
|
|
29
28
|
className: "ProseMirror-separator",
|
|
30
29
|
// eslint-disable-next-line react/no-unknown-property
|
|
31
30
|
"mark-placeholder": "true",
|
|
32
31
|
alt: "",
|
|
33
32
|
...props
|
|
34
|
-
});
|
|
33
|
+
}) : null;
|
|
35
34
|
});
|
|
@@ -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,144 @@ 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 || // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
49
|
+
to > pos + node.nodeSize) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return this.containsCompositionNodeText;
|
|
53
|
+
}
|
|
54
|
+
handleCompositionEnd = ()=>{
|
|
55
|
+
if (!this.wasProtecting) return;
|
|
56
|
+
this.forceUpdate();
|
|
57
|
+
return;
|
|
58
|
+
};
|
|
59
|
+
create() {
|
|
32
60
|
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
61
|
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
|
-
}
|
|
62
|
+
if (!dom && !view.composing) return null;
|
|
49
63
|
let textNode = dom;
|
|
50
|
-
while(textNode
|
|
64
|
+
while(textNode?.firstChild){
|
|
51
65
|
textNode = textNode.firstChild;
|
|
52
66
|
}
|
|
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;
|
|
67
|
+
if (!(textNode instanceof Text)) {
|
|
68
|
+
textNode = null;
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
let viewDesc;
|
|
71
|
+
if (this.shouldProtect(this.props)) {
|
|
72
|
+
viewDesc = new CompositionViewDesc(parentRef.current, getPos, // If we can't
|
|
73
|
+
// actually find the correct DOM nodes from here (
|
|
74
|
+
// which is the case in a composition in a newly
|
|
75
|
+
// created text node), we let our parent do it.
|
|
76
|
+
// Passing a valid element here just so that the
|
|
77
|
+
// ViewDesc constructor doesn't blow up.
|
|
78
|
+
dom ?? document.createElement("div"), textNode ?? document.createTextNode(node.text ?? ""), node.text ?? "");
|
|
79
|
+
} else {
|
|
80
|
+
if (!dom || !textNode) return null;
|
|
81
|
+
viewDesc = new TextViewDesc(parentRef.current, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
|
|
67
82
|
}
|
|
83
|
+
siblingsRef.current.push(viewDesc);
|
|
68
84
|
siblingsRef.current.sort(sortViewDescs);
|
|
85
|
+
return viewDesc;
|
|
86
|
+
}
|
|
87
|
+
update() {
|
|
88
|
+
const { view, node, decorations } = this.props;
|
|
89
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
90
|
+
const viewDesc = this.viewDescRef;
|
|
91
|
+
if (!viewDesc) return false;
|
|
92
|
+
if (this.shouldProtect(this.props) !== viewDesc instanceof CompositionViewDesc) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (viewDesc instanceof CompositionViewDesc) return false;
|
|
96
|
+
const dom = findDOMNode(this);
|
|
97
|
+
if (!dom || dom !== viewDesc.dom) return false;
|
|
98
|
+
if (!dom.contains(viewDesc.nodeDOM)) return false;
|
|
99
|
+
return viewDesc.matchesNode(node, decorations, DecorationSet.empty) || viewDesc.update(node, decorations, DecorationSet.empty, view);
|
|
100
|
+
}
|
|
101
|
+
destroy() {
|
|
102
|
+
const viewDesc = this.viewDescRef;
|
|
103
|
+
if (!viewDesc) return;
|
|
104
|
+
viewDesc.destroy();
|
|
105
|
+
const siblings = this.props.siblingsRef.current;
|
|
106
|
+
if (siblings.includes(viewDesc)) {
|
|
107
|
+
const index = siblings.indexOf(viewDesc);
|
|
108
|
+
siblings.splice(index, 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
updateEffect() {
|
|
112
|
+
if (!this.update()) {
|
|
113
|
+
this.destroy();
|
|
114
|
+
this.viewDescRef = this.create();
|
|
115
|
+
}
|
|
116
|
+
setTimeout(()=>{
|
|
117
|
+
const { view, node } = this.props;
|
|
118
|
+
if (!(view instanceof ReactEditorView)) {
|
|
119
|
+
this.containsCompositionNodeText = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const textNode = view.input.compositionNode;
|
|
123
|
+
if (!textNode) {
|
|
124
|
+
this.containsCompositionNodeText = true;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
128
|
+
const text = textNode.nodeValue;
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
130
|
+
this.containsCompositionNodeText = node.text === text;
|
|
131
|
+
});
|
|
69
132
|
}
|
|
70
133
|
shouldComponentUpdate(nextProps) {
|
|
134
|
+
// When leaving the protected state, force a re-render so React's
|
|
135
|
+
// virtual DOM resyncs with whatever the IME wrote into the real DOM
|
|
136
|
+
// while we were returning a stale renderRef.
|
|
137
|
+
if (this.wasProtecting && !this.shouldProtect(nextProps)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
71
140
|
return !shallowEqual(this.props, nextProps);
|
|
72
141
|
}
|
|
73
142
|
componentDidMount() {
|
|
143
|
+
this.viewDescRef = null;
|
|
144
|
+
// After a composition, force an update so that we re-check whether we need
|
|
145
|
+
// to be protecting our rendered content and allow React to re-sync with the
|
|
146
|
+
// DOM.
|
|
147
|
+
const { registerEventListener } = this.props;
|
|
148
|
+
registerEventListener("compositionend", this.handleCompositionEnd);
|
|
74
149
|
this.updateEffect();
|
|
75
150
|
}
|
|
76
151
|
componentDidUpdate() {
|
|
77
152
|
this.updateEffect();
|
|
78
153
|
}
|
|
79
154
|
componentWillUnmount() {
|
|
80
|
-
const {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const index = siblingsRef.current.indexOf(this.viewDescRef);
|
|
84
|
-
siblingsRef.current.splice(index, 1);
|
|
85
|
-
}
|
|
155
|
+
const { unregisterEventListener } = this.props;
|
|
156
|
+
unregisterEventListener("compositionend", this.handleCompositionEnd);
|
|
157
|
+
this.destroy();
|
|
86
158
|
}
|
|
87
159
|
render() {
|
|
88
|
-
const {
|
|
160
|
+
const { node, decorations } = this.props;
|
|
89
161
|
// During a composition, it's crucial that we don't try to
|
|
90
162
|
// update the DOM that the user is working in. If there's
|
|
91
163
|
// an active composition and the selection is in this node,
|
|
92
164
|
// we freeze the DOM of this element so that it doesn't
|
|
93
165
|
// interrupt the composition
|
|
94
|
-
if (
|
|
166
|
+
if (this.shouldProtect(this.props)) {
|
|
167
|
+
this.wasProtecting = true;
|
|
95
168
|
return this.renderRef;
|
|
96
169
|
}
|
|
170
|
+
this.wasProtecting = false;
|
|
97
171
|
this.renderRef = decorations.reduce(wrapInDeco, node.text);
|
|
98
172
|
return this.renderRef;
|
|
99
173
|
}
|
|
@@ -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,11 +100,20 @@ 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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
if (view.deferPendingEffects) {
|
|
104
|
+
setTimeout(()=>{
|
|
105
|
+
// Plugins might dispatch transactions from their
|
|
106
|
+
// view update lifecycle hooks
|
|
107
|
+
flushSyncRef.current = false;
|
|
108
|
+
view.commitPendingEffects();
|
|
109
|
+
flushSyncRef.current = true;
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
flushSyncRef.current = false;
|
|
113
|
+
view.commitPendingEffects();
|
|
114
|
+
flushSyncRef.current = true;
|
|
115
|
+
}
|
|
107
116
|
view.commitPendingEffects();
|
|
108
|
-
flushSyncRef.current = true;
|
|
109
117
|
}
|
|
110
118
|
});
|
|
111
119
|
view.update(directEditorProps);
|
|
@@ -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,46 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
131
133
|
children.sort(sortViewDescs);
|
|
132
134
|
for (const child of children){
|
|
133
135
|
child.parent = viewDesc;
|
|
136
|
+
}
|
|
137
|
+
setTimeout(()=>{
|
|
134
138
|
// Because TextNodeViews can't locate the DOM nodes
|
|
135
139
|
// for compositions, we need to override them here
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
if (!viewDescRef.current?.contentDOM) return;
|
|
141
|
+
const children = viewDescRef.current?.children;
|
|
142
|
+
const compositionChildIndex = children.findIndex((child)=>child instanceof CompositionViewDesc);
|
|
143
|
+
if (compositionChildIndex === -1) return;
|
|
144
|
+
const compositionViewDesc = children[compositionChildIndex];
|
|
145
|
+
if (!(compositionViewDesc instanceof CompositionViewDesc)) return;
|
|
146
|
+
let compositionTopDOM = null;
|
|
147
|
+
let search = children[compositionChildIndex - 1];
|
|
148
|
+
while(search instanceof MarkViewDesc){
|
|
149
|
+
search = search.children[0];
|
|
150
|
+
}
|
|
151
|
+
if (search instanceof WidgetViewDesc && search.widget.type instanceof ReactWidgetType && search.widget.type.Component === CursorWrapper) {
|
|
152
|
+
compositionTopDOM = search.dom.nextSibling;
|
|
153
|
+
} else {
|
|
154
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
155
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
156
|
+
compositionTopDOM = childNode;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
142
159
|
}
|
|
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
160
|
}
|
|
152
|
-
|
|
161
|
+
if (!compositionTopDOM) return;
|
|
162
|
+
let textDOM = compositionTopDOM;
|
|
163
|
+
while(textDOM.firstChild){
|
|
164
|
+
textDOM = textDOM.firstChild;
|
|
165
|
+
}
|
|
166
|
+
if (!textDOM || !(textDOM instanceof Text)) {
|
|
167
|
+
console.error(compositionTopDOM, textDOM);
|
|
168
|
+
throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
169
|
+
}
|
|
170
|
+
compositionViewDesc.dom = compositionTopDOM;
|
|
171
|
+
compositionViewDesc.textDOM = textDOM;
|
|
172
|
+
compositionViewDesc.text = textDOM.data;
|
|
173
|
+
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
174
|
+
view.input.compositionNodes.push(compositionViewDesc);
|
|
175
|
+
});
|
|
153
176
|
});
|
|
154
177
|
const childContextValue = useMemo(()=>({
|
|
155
178
|
parentRef: viewDescRef,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment, Slice } from "prosemirror-model";
|
|
2
2
|
import { Plugin, TextSelection } from "prosemirror-state";
|
|
3
|
+
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
4
|
import { CursorWrapper } from "../components/CursorWrapper.js";
|
|
4
5
|
import { widget } from "../decorations/ReactWidgetType.js";
|
|
5
6
|
function insertText(view, eventData) {
|
|
@@ -46,31 +47,23 @@ function handleGapCursorComposition(view) {
|
|
|
46
47
|
}
|
|
47
48
|
export function beforeInputPlugin(setCursorWrapper) {
|
|
48
49
|
let compositionMarks = null;
|
|
49
|
-
let precompositionSnapshot = null;
|
|
50
50
|
return new Plugin({
|
|
51
51
|
props: {
|
|
52
52
|
handleDOMEvents: {
|
|
53
53
|
compositionstart (view) {
|
|
54
|
-
|
|
54
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
55
|
+
compositionMarks = view.state.storedMarks;
|
|
55
56
|
view.dispatch(view.state.tr.deleteSelection());
|
|
56
57
|
handleGapCursorComposition(view);
|
|
57
58
|
const { state } = view;
|
|
58
|
-
const $pos = state.selection.$from;
|
|
59
|
-
if (compositionMarks) {
|
|
59
|
+
// const $pos = state.selection.$from;
|
|
60
|
+
if (compositionMarks?.length) {
|
|
60
61
|
setCursorWrapper(widget(state.selection.from, CursorWrapper, {
|
|
61
62
|
key: "cursor-wrapper",
|
|
62
|
-
marks: compositionMarks
|
|
63
|
+
marks: compositionMarks,
|
|
64
|
+
side: 1
|
|
63
65
|
}));
|
|
64
66
|
}
|
|
65
|
-
// Snapshot the siblings of the node that contains the
|
|
66
|
-
// current cursor. We'll restore this later, so that React
|
|
67
|
-
// doesn't panic about unknown DOM nodes.
|
|
68
|
-
const { node: parent } = view.domAtPos($pos.pos);
|
|
69
|
-
precompositionSnapshot = [];
|
|
70
|
-
for (const node of parent.childNodes){
|
|
71
|
-
precompositionSnapshot.push(node);
|
|
72
|
-
}
|
|
73
|
-
// @ts-expect-error Internal property - input
|
|
74
67
|
view.input.composing = true;
|
|
75
68
|
return true;
|
|
76
69
|
},
|
|
@@ -78,36 +71,17 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
78
71
|
return true;
|
|
79
72
|
},
|
|
80
73
|
compositionend (view, event) {
|
|
81
|
-
|
|
74
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
75
|
+
if (!view.composing) return false;
|
|
82
76
|
view.input.composing = false;
|
|
83
|
-
const { state } = view;
|
|
84
|
-
const { node: parent } = view.domAtPos(state.selection.from);
|
|
85
|
-
if (precompositionSnapshot) {
|
|
86
|
-
// Restore the snapshot of the parent node's children
|
|
87
|
-
// from before the composition started. This gives us a
|
|
88
|
-
// clean slate from which to dispatch our transaction
|
|
89
|
-
// and trigger a React update.
|
|
90
|
-
precompositionSnapshot.forEach((prevNode, i)=>{
|
|
91
|
-
if (parent.childNodes.length <= i) {
|
|
92
|
-
parent.appendChild(prevNode);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
parent.replaceChild(prevNode, parent.childNodes.item(i));
|
|
96
|
-
});
|
|
97
|
-
if (parent.childNodes.length > precompositionSnapshot.length) {
|
|
98
|
-
for(let i = precompositionSnapshot.length; i < parent.childNodes.length; i++){
|
|
99
|
-
parent.removeChild(parent.childNodes.item(i));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (event.data) {
|
|
104
|
-
insertText(view, event.data, {
|
|
105
|
-
marks: compositionMarks
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
77
|
compositionMarks = null;
|
|
109
|
-
precompositionSnapshot = null;
|
|
110
78
|
setCursorWrapper(null);
|
|
79
|
+
if (view.input.compositionNode && !view.input.compositionNode.pmViewDesc && (view.input.compositionNode instanceof Text || view.input.compositionNode instanceof Element)) {
|
|
80
|
+
view.input.compositionNode.remove();
|
|
81
|
+
}
|
|
82
|
+
view.input.compositionEndedAt = event.timeStamp;
|
|
83
|
+
view.input.compositionNode = null;
|
|
84
|
+
view.input.compositionID++;
|
|
111
85
|
return true;
|
|
112
86
|
},
|
|
113
87
|
beforeinput (view, event) {
|
|
@@ -156,6 +130,38 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
156
130
|
insertText(view, event.data);
|
|
157
131
|
break;
|
|
158
132
|
}
|
|
133
|
+
case "insertCompositionText":
|
|
134
|
+
{
|
|
135
|
+
const { tr } = view.state;
|
|
136
|
+
// There's always a range on insertCompositionText beforeinput events
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
138
|
+
const range = event.getTargetRanges()[0];
|
|
139
|
+
const start = view.posAtDOM(range.startContainer, range.startOffset);
|
|
140
|
+
const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
|
|
141
|
+
if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (event.data) {
|
|
145
|
+
if (compositionMarks) tr.ensureMarks(compositionMarks);
|
|
146
|
+
tr.insertText(event.data, start, end);
|
|
147
|
+
} else {
|
|
148
|
+
tr.delete(start, end);
|
|
149
|
+
}
|
|
150
|
+
// When we insert the text that corresponds to an ongoing composition,
|
|
151
|
+
// the relevant TextNodeView will pause re-rendering so that React doesn't
|
|
152
|
+
// clobber the composition in the DOM. This means that we have to wait for
|
|
153
|
+
// the browser to update the DOM itself before attempting to reconcile
|
|
154
|
+
// the selection, so we specifically defer pending effects to the next
|
|
155
|
+
// macro task
|
|
156
|
+
if (view instanceof ReactEditorView) {
|
|
157
|
+
view.deferPendingEffects = true;
|
|
158
|
+
}
|
|
159
|
+
view.dispatch(tr);
|
|
160
|
+
if (view instanceof ReactEditorView) {
|
|
161
|
+
view.deferPendingEffects = false;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
159
165
|
case "deleteWordBackward":
|
|
160
166
|
case "deleteHardLineBackward":
|
|
161
167
|
case "deleteSoftLineBackward":
|
package/dist/esm/viewdesc.js
CHANGED
|
@@ -235,7 +235,9 @@ export class ViewDesc {
|
|
|
235
235
|
prev = i ? this.children[i - 1] : null;
|
|
236
236
|
if (!prev || prev.dom.parentNode == this.contentDOM) break;
|
|
237
237
|
}
|
|
238
|
-
if (prev && side && enter && !prev.border && !prev.domAtom)
|
|
238
|
+
if (prev && side && enter && !prev.border && !prev.domAtom) {
|
|
239
|
+
return prev.domFromPos(prev.size, side);
|
|
240
|
+
}
|
|
239
241
|
return {
|
|
240
242
|
node: this.contentDOM,
|
|
241
243
|
offset: prev ? domIndex(prev.dom) + 1 : 0
|
|
@@ -370,7 +372,9 @@ export class ViewDesc {
|
|
|
370
372
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
371
373
|
if (after && after.contentEditable == "false") force = true;
|
|
372
374
|
}
|
|
373
|
-
if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset))
|
|
375
|
+
if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
374
378
|
// Selection.extend can be used to create an 'inverted' selection
|
|
375
379
|
// (one where the focus is before the anchor), but not all
|
|
376
380
|
// browsers support it yet.
|
|
@@ -645,7 +649,10 @@ export class TextViewDesc extends NodeViewDesc {
|
|
|
645
649
|
skip: skip || true
|
|
646
650
|
};
|
|
647
651
|
}
|
|
648
|
-
update(
|
|
652
|
+
update(node, outerDeco, _innerDeco, _view) {
|
|
653
|
+
if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
|
|
654
|
+
this.updateOuterDeco(outerDeco);
|
|
655
|
+
this.node = node;
|
|
649
656
|
this.dirty = NOT_DIRTY;
|
|
650
657
|
return true;
|
|
651
658
|
}
|