@handlewithcare/react-prosemirror 3.1.0-tiptap.52 → 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 -1
- package/dist/cjs/components/ChildNodeViews.js +4 -4
- package/dist/cjs/components/CursorWrapper.js +9 -11
- package/dist/cjs/components/ProseMirror.js +13 -3
- package/dist/cjs/components/TextNodeView.js +54 -50
- package/dist/cjs/components/WidgetView.js +3 -1
- package/dist/cjs/components/nodes/NodeView.js +40 -4
- package/dist/cjs/contexts/CompositionContext.js +14 -0
- package/dist/cjs/decorations/viewDecorations.js +1 -6
- package/dist/cjs/hooks/useEditor.js +2 -10
- package/dist/cjs/hooks/useMarkViewDescription.js +1 -4
- package/dist/cjs/hooks/useNodeViewDescription.js +1 -22
- package/dist/cjs/plugins/beforeInputPlugin.js +162 -50
- package/dist/cjs/plugins/reactKeys.js +34 -15
- package/dist/cjs/tiptap/tiptapNodeView.js +10 -9
- package/dist/cjs/viewdesc.js +55 -4
- package/dist/esm/ReactEditorView.js +18 -1
- package/dist/esm/components/ChildNodeViews.js +5 -5
- package/dist/esm/components/CursorWrapper.js +9 -11
- package/dist/esm/components/ProseMirror.js +13 -3
- package/dist/esm/components/TextNodeView.js +56 -52
- package/dist/esm/components/WidgetView.js +3 -1
- package/dist/esm/components/nodes/NodeView.js +38 -5
- package/dist/esm/contexts/CompositionContext.js +4 -0
- package/dist/esm/decorations/viewDecorations.js +1 -6
- package/dist/esm/hooks/useEditor.js +2 -10
- package/dist/esm/hooks/useIsEditorStatic.js +4 -1
- package/dist/esm/hooks/useMarkViewDescription.js +1 -4
- package/dist/esm/hooks/useNodeViewDescription.js +2 -23
- package/dist/esm/plugins/beforeInputPlugin.js +162 -50
- package/dist/esm/plugins/reactKeys.js +34 -15
- 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 +54 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +10 -1
- 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 +7 -4
- package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -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 +3 -2
- package/dist/types/contexts/CompositionContext.d.ts +4 -0
- package/dist/types/decorations/viewDecorations.d.ts +2 -2
- package/dist/types/hooks/useEditor.d.ts +3 -4
- package/dist/types/hooks/useIsEditorStatic.d.ts +4 -0
- package/dist/types/hooks/useReactKeys.d.ts +2 -5
- package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
- package/dist/types/plugins/reactKeys.d.ts +10 -9
- 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 +5 -3
- package/package.json +22 -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,7 +1,8 @@
|
|
|
1
1
|
import { DecorationSet } from "prosemirror-view";
|
|
2
|
-
import { Component } from "react";
|
|
2
|
+
import { Component, createRef } from "react";
|
|
3
|
+
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
4
|
import { findDOMNode } from "../findDOMNode.js";
|
|
4
|
-
import {
|
|
5
|
+
import { TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
5
6
|
import { wrapInDeco } from "./ChildNodeViews.js";
|
|
6
7
|
function shallowEqual(objA, objB) {
|
|
7
8
|
if (objA === objB) {
|
|
@@ -26,75 +27,78 @@ function shallowEqual(objA, objB) {
|
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
export class TextNodeView extends Component {
|
|
29
|
-
viewDescRef =
|
|
30
|
-
|
|
31
|
-
updateEffect() {
|
|
30
|
+
viewDescRef = createMutRef();
|
|
31
|
+
create() {
|
|
32
32
|
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
33
|
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
|
-
}
|
|
34
|
+
if (!dom && !view.composing) return null;
|
|
49
35
|
let textNode = dom;
|
|
50
|
-
while(textNode
|
|
36
|
+
while(textNode?.firstChild){
|
|
51
37
|
textNode = textNode.firstChild;
|
|
52
38
|
}
|
|
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;
|
|
64
|
-
}
|
|
65
|
-
if (!siblingsRef.current.includes(this.viewDescRef)) {
|
|
66
|
-
siblingsRef.current.push(this.viewDescRef);
|
|
39
|
+
if (!(textNode instanceof Text)) {
|
|
40
|
+
textNode = null;
|
|
67
41
|
}
|
|
42
|
+
if (!dom || !textNode) return null;
|
|
43
|
+
const viewDesc = new TextViewDesc(parentRef.current, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
|
|
44
|
+
siblingsRef.current.push(viewDesc);
|
|
68
45
|
siblingsRef.current.sort(sortViewDescs);
|
|
46
|
+
return viewDesc;
|
|
47
|
+
}
|
|
48
|
+
update() {
|
|
49
|
+
const { view, node, decorations } = this.props;
|
|
50
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
51
|
+
const viewDesc = this.viewDescRef.current;
|
|
52
|
+
if (!viewDesc) return false;
|
|
53
|
+
const dom = findDOMNode(this);
|
|
54
|
+
if (!dom || dom !== viewDesc.dom) return false;
|
|
55
|
+
if (!dom.contains(viewDesc.nodeDOM)) return false;
|
|
56
|
+
return viewDesc.matchesNode(node, decorations, DecorationSet.empty) || viewDesc.update(node, decorations, DecorationSet.empty, view);
|
|
57
|
+
}
|
|
58
|
+
destroy() {
|
|
59
|
+
const viewDesc = this.viewDescRef.current;
|
|
60
|
+
if (!viewDesc) return;
|
|
61
|
+
viewDesc.destroy();
|
|
62
|
+
const siblings = this.props.siblingsRef.current;
|
|
63
|
+
if (siblings.includes(viewDesc)) {
|
|
64
|
+
const index = siblings.indexOf(viewDesc);
|
|
65
|
+
siblings.splice(index, 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
updateEffect() {
|
|
69
|
+
if (!this.update()) {
|
|
70
|
+
this.destroy();
|
|
71
|
+
this.viewDescRef.current = this.create();
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
shouldComponentUpdate(nextProps) {
|
|
71
75
|
return !shallowEqual(this.props, nextProps);
|
|
72
76
|
}
|
|
77
|
+
constructor(props){
|
|
78
|
+
super(props);
|
|
79
|
+
this.viewDescRef.current = null;
|
|
80
|
+
}
|
|
73
81
|
componentDidMount() {
|
|
82
|
+
this.viewDescRef.current = this.create();
|
|
74
83
|
this.updateEffect();
|
|
75
84
|
}
|
|
76
85
|
componentDidUpdate() {
|
|
77
86
|
this.updateEffect();
|
|
87
|
+
const { view } = this.props;
|
|
88
|
+
if (!(view instanceof ReactEditorView)) return;
|
|
78
89
|
}
|
|
79
90
|
componentWillUnmount() {
|
|
80
|
-
|
|
81
|
-
if (!this.viewDescRef) return;
|
|
82
|
-
if (siblingsRef.current.includes(this.viewDescRef)) {
|
|
83
|
-
const index = siblingsRef.current.indexOf(this.viewDescRef);
|
|
84
|
-
siblingsRef.current.splice(index, 1);
|
|
85
|
-
}
|
|
91
|
+
this.destroy();
|
|
86
92
|
}
|
|
87
93
|
render() {
|
|
88
|
-
const {
|
|
89
|
-
|
|
90
|
-
// update the DOM that the user is working in. If there's
|
|
91
|
-
// an active composition and the selection is in this node,
|
|
92
|
-
// we freeze the DOM of this element so that it doesn't
|
|
93
|
-
// interrupt the composition
|
|
94
|
-
if (view.composing && view.state.selection.from >= getPos() && view.state.selection.from <= getPos() + node.nodeSize) {
|
|
95
|
-
return this.renderRef;
|
|
96
|
-
}
|
|
97
|
-
this.renderRef = decorations.reduce(wrapInDeco, node.text);
|
|
98
|
-
return this.renderRef;
|
|
94
|
+
const { node, decorations } = this.props;
|
|
95
|
+
return decorations.reduce(wrapInDeco, node.text);
|
|
99
96
|
}
|
|
100
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* createRef returns a RefObject, even though the docs
|
|
100
|
+
* say that it's acceptible to manage the ref's value
|
|
101
|
+
* yourself.
|
|
102
|
+
*/ function createMutRef() {
|
|
103
|
+
return /*#__PURE__*/ createRef();
|
|
104
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -123,17 +123,12 @@ const ViewDecorationsCache = new WeakMap();
|
|
|
123
123
|
*
|
|
124
124
|
* This makes it safe to call in a React render function, even
|
|
125
125
|
* if its result is used in a dependencies array for a hook.
|
|
126
|
-
*/ export function viewDecorations(view
|
|
126
|
+
*/ export function viewDecorations(view) {
|
|
127
127
|
const found = [];
|
|
128
128
|
view.someProp("decorations", (f)=>{
|
|
129
129
|
const result = f(view.state);
|
|
130
130
|
if (result && result != empty) found.push(result);
|
|
131
131
|
});
|
|
132
|
-
if (cursorWrapper) {
|
|
133
|
-
found.push(DecorationSet.create(view.state.doc, [
|
|
134
|
-
cursorWrapper
|
|
135
|
-
]));
|
|
136
|
-
}
|
|
137
132
|
const previous = ViewDecorationsCache.get(view);
|
|
138
133
|
if (!previous) {
|
|
139
134
|
const result = DecorationGroup.from(found);
|
|
@@ -25,23 +25,16 @@ let didWarnValueDefaultValue = false;
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
const flushSyncRef = useRef(true);
|
|
28
|
-
const [cursorWrapper, _setCursorWrapper] = useState(null);
|
|
29
28
|
const forceUpdate = useForceUpdate();
|
|
30
29
|
const defaultState = options.defaultState ?? EMPTY_STATE;
|
|
31
30
|
const [_state, setState] = useState(defaultState);
|
|
32
31
|
const state = options.state ?? _state;
|
|
33
32
|
const { handleDOMEvents, registerEventListener, unregisterEventListener } = useComponentEventListeners(options.handleDOMEvents);
|
|
34
|
-
const setCursorWrapper = useCallback((deco)=>{
|
|
35
|
-
flushSync(()=>{
|
|
36
|
-
_setCursorWrapper(deco);
|
|
37
|
-
});
|
|
38
|
-
}, []);
|
|
39
33
|
const plugins = useMemo(()=>[
|
|
40
34
|
...options.plugins ?? [],
|
|
41
|
-
beforeInputPlugin(
|
|
35
|
+
beforeInputPlugin()
|
|
42
36
|
], [
|
|
43
|
-
options.plugins
|
|
44
|
-
setCursorWrapper
|
|
37
|
+
options.plugins
|
|
45
38
|
]);
|
|
46
39
|
const dispatchTransaction = useCallback(function dispatchTransaction(tr) {
|
|
47
40
|
if (flushSyncRef.current) {
|
|
@@ -120,7 +113,6 @@ let didWarnValueDefaultValue = false;
|
|
|
120
113
|
]);
|
|
121
114
|
return {
|
|
122
115
|
editor,
|
|
123
|
-
cursorWrapper,
|
|
124
116
|
state
|
|
125
117
|
};
|
|
126
118
|
}
|
|
@@ -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
|
}
|
|
@@ -113,10 +113,7 @@ export function useMarkViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
113
113
|
const childContextValue = useMemo(()=>({
|
|
114
114
|
parentRef: viewDescRef,
|
|
115
115
|
siblingsRef: childrenRef
|
|
116
|
-
}), [
|
|
117
|
-
childrenRef,
|
|
118
|
-
viewDescRef
|
|
119
|
-
]);
|
|
116
|
+
}), []);
|
|
120
117
|
return {
|
|
121
118
|
childContextValue,
|
|
122
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 {
|
|
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) {
|
|
@@ -131,33 +131,12 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
131
131
|
children.sort(sortViewDescs);
|
|
132
132
|
for (const child of children){
|
|
133
133
|
child.parent = viewDesc;
|
|
134
|
-
// Because TextNodeViews can't locate the DOM nodes
|
|
135
|
-
// for compositions, we need to override them here
|
|
136
|
-
if (child instanceof CompositionViewDesc) {
|
|
137
|
-
const compositionTopDOM = viewDesc?.contentDOM?.firstChild;
|
|
138
|
-
if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
|
|
139
|
-
let textDOM = compositionTopDOM;
|
|
140
|
-
while(textDOM.firstChild){
|
|
141
|
-
textDOM = textDOM.firstChild;
|
|
142
|
-
}
|
|
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
|
-
}
|
|
152
134
|
}
|
|
153
135
|
});
|
|
154
136
|
const childContextValue = useMemo(()=>({
|
|
155
137
|
parentRef: viewDescRef,
|
|
156
138
|
siblingsRef: childrenRef
|
|
157
|
-
}), [
|
|
158
|
-
childrenRef,
|
|
159
|
-
viewDescRef
|
|
160
|
-
]);
|
|
139
|
+
}), []);
|
|
161
140
|
return {
|
|
162
141
|
childContextValue,
|
|
163
142
|
contentDOM: contentDOMRef.current,
|
|
@@ -1,7 +1,10 @@
|
|
|
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";
|
|
6
|
+
import { CompositionViewDesc, TextViewDesc, findTextInFragment } from "../viewdesc.js";
|
|
7
|
+
import { reactKeysPluginKey } from "./reactKeys.js";
|
|
5
8
|
function insertText(view, eventData) {
|
|
6
9
|
let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
7
10
|
if (eventData === null) return false;
|
|
@@ -44,74 +47,127 @@ function handleGapCursorComposition(view) {
|
|
|
44
47
|
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
45
48
|
view.dispatch(tr);
|
|
46
49
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const observeOptions = {
|
|
51
|
+
childList: true,
|
|
52
|
+
characterData: true,
|
|
53
|
+
characterDataOldValue: true,
|
|
54
|
+
attributes: true,
|
|
55
|
+
attributeOldValue: true,
|
|
56
|
+
subtree: true
|
|
57
|
+
};
|
|
58
|
+
export function beforeInputPlugin() {
|
|
59
|
+
let observer = null;
|
|
60
|
+
let preCompositionSnapshot = null;
|
|
61
|
+
function teardownComposition(view, endedAt) {
|
|
62
|
+
view.input.composing = false;
|
|
63
|
+
if (observer) {
|
|
64
|
+
if (view.input.compositionNode && view.dom.contains(view.input.compositionNode)) {
|
|
65
|
+
view.domObserver.queue.push(...observer.takeRecords());
|
|
66
|
+
view.domObserver.flush();
|
|
67
|
+
} else {
|
|
68
|
+
const freezeFrom = reactKeysPluginKey.getState(view.state)?.freezeFrom;
|
|
69
|
+
const frozenNode = freezeFrom == null ? null : view.state.doc.nodeAt(freezeFrom);
|
|
70
|
+
if (freezeFrom != null && frozenNode != null && preCompositionSnapshot) {
|
|
71
|
+
// This is a little hacky — it only works because we always abort
|
|
72
|
+
// compositions if the node after freezeFrom changes, so we can
|
|
73
|
+
// be sure that if a composition was canceled by the user/browser,
|
|
74
|
+
// the content hasn't changed since the composition started
|
|
75
|
+
view.dispatch(view.state.tr.replaceWith(freezeFrom + 1, freezeFrom + 1 + frozenNode.content.size, preCompositionSnapshot));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
observer.disconnect();
|
|
79
|
+
observer = null;
|
|
80
|
+
}
|
|
81
|
+
view.input.compositionEndedAt = endedAt;
|
|
82
|
+
view.input.compositionNode = null;
|
|
83
|
+
view.input.compositionNodes = [];
|
|
84
|
+
view.input.compositionID++;
|
|
85
|
+
}
|
|
50
86
|
return new Plugin({
|
|
87
|
+
view () {
|
|
88
|
+
return {
|
|
89
|
+
update (view) {
|
|
90
|
+
if (!(view instanceof ReactEditorView)) return;
|
|
91
|
+
const frozen = reactKeysPluginKey.getState(view.state)?.freezeFrom != null;
|
|
92
|
+
if (observer && view.composing && !frozen) {
|
|
93
|
+
teardownComposition(view, Date.now());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
},
|
|
51
98
|
props: {
|
|
52
99
|
handleDOMEvents: {
|
|
53
100
|
compositionstart (view) {
|
|
54
|
-
|
|
55
|
-
view.
|
|
101
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
102
|
+
const storedMarks = view.state.selection.empty ? view.state.storedMarks : view.state.storedMarks ?? (view.state.selection instanceof TextSelection ? view.state.selection.$from.marksAcross(view.state.selection.$to) : null);
|
|
103
|
+
view.dispatch(view.state.tr.deleteSelection().setStoredMarks(storedMarks));
|
|
56
104
|
handleGapCursorComposition(view);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
if (storedMarks) {
|
|
106
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
107
|
+
cursorWrapper: widget(view.state.selection.from, CursorWrapper, {
|
|
108
|
+
key: "cursor-wrapper",
|
|
109
|
+
marks: storedMarks,
|
|
110
|
+
side: 0,
|
|
111
|
+
raw: true
|
|
112
|
+
})
|
|
63
113
|
}));
|
|
114
|
+
// Pin the DOM cursor to PM's canonical position before the IME
|
|
115
|
+
// captures wherever the browser happened to leave it. Without this,
|
|
116
|
+
// a cursor at a mark boundary lands in either the left or right text
|
|
117
|
+
// node depending on the user's last navigation direction, and the
|
|
118
|
+
// IME composes into whichever one it found.
|
|
119
|
+
} else if (view.state.selection.empty) {
|
|
120
|
+
view.domObserver.disconnectSelection();
|
|
121
|
+
try {
|
|
122
|
+
view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
|
|
123
|
+
);
|
|
124
|
+
} finally{
|
|
125
|
+
view.domObserver.setCurSelection();
|
|
126
|
+
view.domObserver.connectSelection();
|
|
127
|
+
}
|
|
64
128
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
129
|
+
const freezeFrom = view.state.selection.$from.before();
|
|
130
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
131
|
+
freezeFrom
|
|
132
|
+
}));
|
|
133
|
+
const frozenDom = view.nodeDOM(freezeFrom);
|
|
134
|
+
if (!frozenDom) {
|
|
135
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
136
|
+
cursorWrapper: null,
|
|
137
|
+
freezeFrom: null
|
|
138
|
+
}));
|
|
139
|
+
return false;
|
|
72
140
|
}
|
|
73
|
-
|
|
141
|
+
preCompositionSnapshot = view.state.doc.nodeAt(freezeFrom)?.content ?? null;
|
|
74
142
|
view.input.composing = true;
|
|
143
|
+
observer = new MutationObserver((records)=>{
|
|
144
|
+
if (reactKeysPluginKey.getState(view.state)?.freezeFrom == null) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
view.domObserver.queue.push(...records);
|
|
148
|
+
view.domObserver.flush();
|
|
149
|
+
syncCompositionViewDescs(view);
|
|
150
|
+
});
|
|
151
|
+
observer.observe(frozenDom, observeOptions);
|
|
75
152
|
return true;
|
|
76
153
|
},
|
|
77
154
|
compositionupdate () {
|
|
78
155
|
return true;
|
|
79
156
|
},
|
|
80
157
|
compositionend (view, event) {
|
|
81
|
-
|
|
82
|
-
view.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
compositionMarks = null;
|
|
109
|
-
precompositionSnapshot = null;
|
|
110
|
-
setCursorWrapper(null);
|
|
158
|
+
if (!(view instanceof ReactEditorView)) return false;
|
|
159
|
+
if (!view.composing) return false;
|
|
160
|
+
teardownComposition(view, event.timeStamp);
|
|
161
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
162
|
+
cursorWrapper: null,
|
|
163
|
+
freezeFrom: null
|
|
164
|
+
}));
|
|
111
165
|
return true;
|
|
112
166
|
},
|
|
113
167
|
beforeinput (view, event) {
|
|
114
|
-
event.
|
|
168
|
+
if (event.inputType !== "insertFromComposition") {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
}
|
|
115
171
|
switch(event.inputType){
|
|
116
172
|
case "insertParagraph":
|
|
117
173
|
case "insertLineBreak":
|
|
@@ -189,3 +245,59 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
189
245
|
}
|
|
190
246
|
});
|
|
191
247
|
}
|
|
248
|
+
function syncCompositionViewDescs(view) {
|
|
249
|
+
const compositionNode = view.domObserver.lastChangedTextNode;
|
|
250
|
+
if (!compositionNode) return;
|
|
251
|
+
const freezeFrom = reactKeysPluginKey.getState(view.state)?.freezeFrom;
|
|
252
|
+
if (freezeFrom == null) return;
|
|
253
|
+
const compositionBlock = view.state.doc.nodeAt(freezeFrom);
|
|
254
|
+
if (!compositionBlock) return;
|
|
255
|
+
const compositionBlockDesc = view.docView.descAt(freezeFrom);
|
|
256
|
+
if (!compositionBlockDesc) return;
|
|
257
|
+
const desc = view.docView.nearestDesc(compositionNode);
|
|
258
|
+
compositionBlockDesc.node = compositionBlock;
|
|
259
|
+
if (desc instanceof TextViewDesc) {
|
|
260
|
+
if (compositionNode.nodeValue && desc.node.text !== compositionNode.nodeValue) {
|
|
261
|
+
desc.node = view.state.schema.text(compositionNode.nodeValue, desc.node.marks);
|
|
262
|
+
desc.nodeDOM = compositionNode;
|
|
263
|
+
compositionNode.pmViewDesc = desc;
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (desc instanceof CompositionViewDesc) {
|
|
268
|
+
if (compositionNode.nodeValue != null && desc.text !== compositionNode.nodeValue) {
|
|
269
|
+
desc.dom = compositionNode;
|
|
270
|
+
desc.textDOM = compositionNode;
|
|
271
|
+
desc.text = compositionNode.nodeValue;
|
|
272
|
+
compositionNode.pmViewDesc = desc;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const parentDesc = desc?.contentDOM ? desc : compositionBlockDesc;
|
|
277
|
+
const children = parentDesc.children;
|
|
278
|
+
// Drop any text or composition desc in this container whose DOM the
|
|
279
|
+
// IME has detached. This covers two cases: a TextViewDesc the IME subsumed
|
|
280
|
+
// into the composition node, and (on Safari, which replaces the whole text
|
|
281
|
+
// node on each composition update) any orphaned composition view
|
|
282
|
+
// desc(s) left over from the previous composition steps.
|
|
283
|
+
for(let i = children.length - 1; i >= 0; i--){
|
|
284
|
+
const c = children[i];
|
|
285
|
+
if (!(c instanceof TextViewDesc) && !(c instanceof CompositionViewDesc)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const dom = c.dom;
|
|
289
|
+
if (view.dom.contains(dom)) continue;
|
|
290
|
+
children.splice(i, 1);
|
|
291
|
+
}
|
|
292
|
+
const contentStart = freezeFrom + 1;
|
|
293
|
+
const { from, to } = view.state.selection;
|
|
294
|
+
const textPos = findTextInFragment(compositionBlock.content, compositionNode.nodeValue ?? "", from - contentStart, to - contentStart);
|
|
295
|
+
if (textPos < 0) return;
|
|
296
|
+
const startPos = contentStart + textPos;
|
|
297
|
+
let topDOM = compositionNode;
|
|
298
|
+
while(topDOM.parentNode && topDOM.parentNode !== parentDesc.contentDOM){
|
|
299
|
+
topDOM = topDOM.parentNode;
|
|
300
|
+
}
|
|
301
|
+
const insertIndex = children.findLastIndex((c)=>c.posBefore <= startPos) + 1;
|
|
302
|
+
children.splice(insertIndex, 0, new CompositionViewDesc(parentDesc, ()=>startPos, topDOM, compositionNode, compositionNode.nodeValue ?? ""));
|
|
303
|
+
}
|