@handlewithcare/react-prosemirror 3.0.4 → 3.0.6
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 +36 -6
- package/dist/cjs/components/ChildNodeViews.js +3 -1
- package/dist/cjs/components/CursorWrapper.js +6 -11
- package/dist/cjs/components/ProseMirror.js +2 -2
- package/dist/cjs/components/WidgetView.js +1 -0
- package/dist/cjs/hooks/useComponentEventListeners.js +37 -4
- package/dist/cjs/hooks/useEditor.js +4 -6
- package/dist/cjs/plugins/beforeInputPlugin.js +32 -2
- package/dist/cjs/testing/editorViewTestHelpers.js +1 -1
- package/dist/esm/components/ChildNodeViews.js +3 -1
- package/dist/esm/components/CursorWrapper.js +7 -12
- package/dist/esm/components/ProseMirror.js +2 -2
- package/dist/esm/components/WidgetView.js +1 -0
- package/dist/esm/hooks/useComponentEventListeners.js +44 -11
- package/dist/esm/hooks/useEditor.js +4 -6
- package/dist/esm/plugins/beforeInputPlugin.js +33 -3
- package/dist/esm/testing/editorViewTestHelpers.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/components/CursorWrapper.d.ts +2 -4
- package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
- package/dist/types/contexts/EditorContext.d.ts +2 -3
- package/dist/types/hooks/useComponentEventListeners.d.ts +11 -10
- package/dist/types/hooks/useEditor.d.ts +3 -3
- package/dist/types/hooks/useEditorEventListener.d.ts +1 -1
- package/dist/types/props.d.ts +1 -1
- package/dist/types/testing/editorViewTestHelpers.d.ts +1 -0
- package/package.json +1 -1
- package/dist/cjs/plugins/componentEventListeners.js +0 -35
- package/dist/cjs/plugins/componentEventListenersPlugin.js +0 -35
- package/dist/esm/plugins/componentEventListeners.js +0 -25
- package/dist/esm/plugins/componentEventListenersPlugin.js +0 -25
- package/dist/types/plugins/componentEventListeners.d.ts +0 -4
- package/dist/types/plugins/componentEventListenersPlugin.d.ts +0 -4
package/README.md
CHANGED
|
@@ -837,10 +837,40 @@ significant contributors help shape our roadmap priorities.
|
|
|
837
837
|
[Become a Sponsor](https://handlewithcare.dev/pitter-patter/#become-a-sponsor)
|
|
838
838
|
|
|
839
839
|
<h3>Sponsors</h3>
|
|
840
|
-
|
|
841
|
-
<
|
|
842
|
-
<
|
|
843
|
-
<
|
|
844
|
-
<
|
|
845
|
-
|
|
840
|
+
|
|
841
|
+
<table>
|
|
842
|
+
<tbody>
|
|
843
|
+
<tr>
|
|
844
|
+
<td align="center" valign="top" >
|
|
845
|
+
<a href="https://moment.dev/">
|
|
846
|
+
<img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Moment.png" alt="Moment" height="128">
|
|
847
|
+
<br>
|
|
848
|
+
Moment
|
|
849
|
+
</a>
|
|
850
|
+
</td>
|
|
851
|
+
<td align="center" valign="top" >
|
|
852
|
+
<a href="https://www.lingco.io/">
|
|
853
|
+
<img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Lingco.png" alt="Lingco" height="128">
|
|
854
|
+
<br>
|
|
855
|
+
Lingco
|
|
856
|
+
</a>
|
|
857
|
+
</td>
|
|
858
|
+
<td align="center" valign="top" >
|
|
859
|
+
<a href="https://dskrpt.de/">
|
|
860
|
+
<img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Dskrpt.png" alt="dskrpt" height="128">
|
|
861
|
+
<br>
|
|
862
|
+
dskrpt
|
|
863
|
+
</a>
|
|
864
|
+
</td>
|
|
865
|
+
<td align="center" valign="top" >
|
|
866
|
+
<a href="https://char.com/">
|
|
867
|
+
<img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Fastrepl.png" alt="Fastrepl" height="128">
|
|
868
|
+
<br>
|
|
869
|
+
Fastrepl
|
|
870
|
+
</a>
|
|
871
|
+
</td>
|
|
872
|
+
</tr>
|
|
873
|
+
</tbody>
|
|
874
|
+
</table>
|
|
875
|
+
|
|
846
876
|
<!--sponsorsend-->
|
|
@@ -244,7 +244,9 @@ function adjustWidgetMarksBack(widgetChildren, nodeChild) {
|
|
|
244
244
|
const child = widgetChildren[i];
|
|
245
245
|
if (// Using internal Decoration property, "type"
|
|
246
246
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
-
child.widget.type.side < 0
|
|
247
|
+
child.widget.type.side < 0 || // Using internal Decoration property, "type"
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
249
|
+
child.widget.type.spec.marks) {
|
|
248
250
|
continue;
|
|
249
251
|
}
|
|
250
252
|
child.marks = child.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread);
|
|
@@ -54,7 +54,6 @@ function _interop_require_wildcard(obj, nodeInterop) {
|
|
|
54
54
|
}
|
|
55
55
|
const CursorWrapper = /*#__PURE__*/ (0, _react.forwardRef)(function CursorWrapper(param, ref) {
|
|
56
56
|
let { widget, getPos, ...props } = param;
|
|
57
|
-
const [shouldRender, setShouldRender] = (0, _react.useState)(true);
|
|
58
57
|
const innerRef = (0, _react.useRef)(null);
|
|
59
58
|
(0, _react.useImperativeHandle)(ref, ()=>{
|
|
60
59
|
return innerRef.current;
|
|
@@ -65,27 +64,23 @@ const CursorWrapper = /*#__PURE__*/ (0, _react.forwardRef)(function CursorWrappe
|
|
|
65
64
|
view.domObserver.disconnectSelection();
|
|
66
65
|
// @ts-expect-error Internal property - domSelection
|
|
67
66
|
const domSel = view.domSelection();
|
|
68
|
-
const range = document.createRange();
|
|
69
67
|
const node = innerRef.current;
|
|
70
68
|
const img = node.nodeName == "IMG";
|
|
71
|
-
if (img
|
|
72
|
-
|
|
69
|
+
if (img) {
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
71
|
+
domSel.collapse(node.parentNode, (0, _dom.domIndex)(node) + 1);
|
|
73
72
|
} else {
|
|
74
|
-
|
|
73
|
+
domSel.collapse(node, 0);
|
|
75
74
|
}
|
|
76
|
-
range.collapse(false);
|
|
77
|
-
domSel.removeAllRanges();
|
|
78
|
-
domSel.addRange(range);
|
|
79
|
-
setShouldRender(false);
|
|
80
75
|
// @ts-expect-error Internal property - domObserver
|
|
81
76
|
view.domObserver.connectSelection();
|
|
82
77
|
}, []);
|
|
83
|
-
return
|
|
78
|
+
return /*#__PURE__*/ _react.default.createElement("img", {
|
|
84
79
|
ref: innerRef,
|
|
85
80
|
className: "ProseMirror-separator",
|
|
86
81
|
// eslint-disable-next-line react/no-unknown-property
|
|
87
82
|
"mark-placeholder": "true",
|
|
88
83
|
alt: "",
|
|
89
84
|
...props
|
|
90
|
-
})
|
|
85
|
+
});
|
|
91
86
|
});
|
|
@@ -73,7 +73,7 @@ const rootChildDescriptionsContextValue = {
|
|
|
73
73
|
function ProseMirrorInner(param) {
|
|
74
74
|
let { children, nodeViewComponents, markViewComponents, ...props } = param;
|
|
75
75
|
const [mount, setMount] = (0, _react.useState)(null);
|
|
76
|
-
const { editor, state } = (0, _useEditor.useEditor)(mount, props);
|
|
76
|
+
const { editor, cursorWrapper, state } = (0, _useEditor.useEditor)(mount, props);
|
|
77
77
|
const nodeViewConstructors = editor.view.nodeViews;
|
|
78
78
|
const nodeViewContextValue = (0, _react.useMemo)(()=>{
|
|
79
79
|
return {
|
|
@@ -90,7 +90,7 @@ function ProseMirrorInner(param) {
|
|
|
90
90
|
]);
|
|
91
91
|
const node = state.doc;
|
|
92
92
|
const decorations = (0, _computeDocDeco.computeDocDeco)(editor.view);
|
|
93
|
-
const innerDecorations = (0, _viewDecorations.viewDecorations)(editor.view,
|
|
93
|
+
const innerDecorations = (0, _viewDecorations.viewDecorations)(editor.view, cursorWrapper);
|
|
94
94
|
const docNodeViewContextValue = (0, _react.useMemo)(()=>({
|
|
95
95
|
setMount,
|
|
96
96
|
node,
|
|
@@ -78,6 +78,7 @@ function WidgetView(param) {
|
|
|
78
78
|
viewDescRef.current.parent = parentRef.current;
|
|
79
79
|
viewDescRef.current.widget = widget;
|
|
80
80
|
viewDescRef.current.dom = domRef.current;
|
|
81
|
+
viewDescRef.current.dom.pmViewDesc = viewDescRef.current;
|
|
81
82
|
}
|
|
82
83
|
if (!siblingsRef.current.includes(viewDescRef.current)) {
|
|
83
84
|
siblingsRef.current.push(viewDescRef.current);
|
|
@@ -9,8 +9,8 @@ Object.defineProperty(exports, "useComponentEventListeners", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _react = require("react");
|
|
12
|
-
const
|
|
13
|
-
function useComponentEventListeners() {
|
|
12
|
+
const _reactdom = require("react-dom");
|
|
13
|
+
function useComponentEventListeners(handleDOMEventsProp) {
|
|
14
14
|
const [registry, setRegistry] = (0, _react.useState)(new Map());
|
|
15
15
|
const registerEventListener = (0, _react.useCallback)((eventType, handler)=>{
|
|
16
16
|
const handlers = registry.get(eventType) ?? [];
|
|
@@ -28,12 +28,45 @@ function useComponentEventListeners() {
|
|
|
28
28
|
}, [
|
|
29
29
|
registry
|
|
30
30
|
]);
|
|
31
|
-
|
|
31
|
+
(0, _react.useLayoutEffect)(()=>{
|
|
32
|
+
if (!handleDOMEventsProp) return;
|
|
33
|
+
for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
|
|
34
|
+
if (!handler) return;
|
|
35
|
+
registerEventListener(eventType, handler);
|
|
36
|
+
}
|
|
37
|
+
return ()=>{
|
|
38
|
+
for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
|
|
39
|
+
if (!handler) return;
|
|
40
|
+
unregisterEventListener(eventType, handler);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}, [
|
|
44
|
+
handleDOMEventsProp,
|
|
45
|
+
registerEventListener,
|
|
46
|
+
unregisterEventListener
|
|
47
|
+
]);
|
|
48
|
+
const handleDOMEvents = (0, _react.useMemo)(()=>{
|
|
49
|
+
const domEventHandlers = {};
|
|
50
|
+
for (const [eventType, handlers] of registry.entries()){
|
|
51
|
+
function handleEvent(view, event) {
|
|
52
|
+
for (const handler of handlers){
|
|
53
|
+
let handled = false;
|
|
54
|
+
(0, _reactdom.unstable_batchedUpdates)(()=>{
|
|
55
|
+
handled = !!handler(view, event);
|
|
56
|
+
});
|
|
57
|
+
if (handled || event.defaultPrevented) return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
domEventHandlers[eventType] = handleEvent;
|
|
62
|
+
}
|
|
63
|
+
return domEventHandlers;
|
|
64
|
+
}, [
|
|
32
65
|
registry
|
|
33
66
|
]);
|
|
34
67
|
return {
|
|
35
68
|
registerEventListener,
|
|
36
69
|
unregisterEventListener,
|
|
37
|
-
|
|
70
|
+
handleDOMEvents
|
|
38
71
|
};
|
|
39
72
|
}
|
|
@@ -32,7 +32,7 @@ function useEditor(mount, options) {
|
|
|
32
32
|
const defaultState = options.defaultState ?? _constants.EMPTY_STATE;
|
|
33
33
|
const [_state, setState] = (0, _react.useState)(defaultState);
|
|
34
34
|
const state = options.state ?? _state;
|
|
35
|
-
const {
|
|
35
|
+
const { handleDOMEvents, registerEventListener, unregisterEventListener } = (0, _useComponentEventListeners.useComponentEventListeners)(options.handleDOMEvents);
|
|
36
36
|
const setCursorWrapper = (0, _react.useCallback)((deco)=>{
|
|
37
37
|
(0, _reactdom.flushSync)(()=>{
|
|
38
38
|
_setCursorWrapper(deco);
|
|
@@ -40,11 +40,9 @@ function useEditor(mount, options) {
|
|
|
40
40
|
}, []);
|
|
41
41
|
const plugins = (0, _react.useMemo)(()=>[
|
|
42
42
|
...options.plugins ?? [],
|
|
43
|
-
componentEventListenersPlugin,
|
|
44
43
|
(0, _beforeInputPlugin.beforeInputPlugin)(setCursorWrapper)
|
|
45
44
|
], [
|
|
46
45
|
options.plugins,
|
|
47
|
-
componentEventListenersPlugin,
|
|
48
46
|
setCursorWrapper
|
|
49
47
|
]);
|
|
50
48
|
const dispatchTransaction = (0, _react.useCallback)(function dispatchTransaction(tr) {
|
|
@@ -73,7 +71,8 @@ function useEditor(mount, options) {
|
|
|
73
71
|
...options,
|
|
74
72
|
state,
|
|
75
73
|
plugins,
|
|
76
|
-
dispatchTransaction
|
|
74
|
+
dispatchTransaction,
|
|
75
|
+
handleDOMEvents
|
|
77
76
|
};
|
|
78
77
|
const [view, setView] = (0, _react.useState)(()=>{
|
|
79
78
|
return new _StaticEditorView.StaticEditorView(directEditorProps);
|
|
@@ -109,13 +108,11 @@ function useEditor(mount, options) {
|
|
|
109
108
|
view.update(directEditorProps);
|
|
110
109
|
const editor = (0, _react.useMemo)(()=>({
|
|
111
110
|
view,
|
|
112
|
-
cursorWrapper,
|
|
113
111
|
flushSyncRef,
|
|
114
112
|
registerEventListener,
|
|
115
113
|
unregisterEventListener,
|
|
116
114
|
isStatic: options.static ?? false
|
|
117
115
|
}), [
|
|
118
|
-
cursorWrapper,
|
|
119
116
|
options.static,
|
|
120
117
|
registerEventListener,
|
|
121
118
|
unregisterEventListener,
|
|
@@ -123,6 +120,7 @@ function useEditor(mount, options) {
|
|
|
123
120
|
]);
|
|
124
121
|
return {
|
|
125
122
|
editor,
|
|
123
|
+
cursorWrapper,
|
|
126
124
|
state
|
|
127
125
|
};
|
|
128
126
|
}
|
|
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "beforeInputPlugin", {
|
|
|
8
8
|
return beforeInputPlugin;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
const _prosemirrormodel = require("prosemirror-model");
|
|
11
12
|
const _prosemirrorstate = require("prosemirror-state");
|
|
12
13
|
const _CursorWrapper = require("../components/CursorWrapper.js");
|
|
13
14
|
const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
|
|
@@ -25,6 +26,34 @@ function insertText(view, eventData) {
|
|
|
25
26
|
view.dispatch(tr);
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
29
|
+
// Taken from https://github.com/ProseMirror/prosemirror-gapcursor/blob/master/src/index.ts#L67-L84
|
|
30
|
+
// This is a hack that, when a composition starts while a gap cursor
|
|
31
|
+
// is active, quickly creates an inline context for the composition to
|
|
32
|
+
// happen in, to avoid it being aborted by the DOM selection being
|
|
33
|
+
// moved into a valid position.
|
|
34
|
+
//
|
|
35
|
+
// We can't rely on the actual hack from prosemirror-gapcursor, because
|
|
36
|
+
// it happens too late. We snapshot the DOM during compositionstart, but
|
|
37
|
+
// the gapcursor hack runs in beforeinput (after compositionstart).
|
|
38
|
+
function handleGapCursorComposition(view) {
|
|
39
|
+
// @ts-expect-error Internal property - jsonID
|
|
40
|
+
if (!(view.state.selection.jsonID === "gapcursor")) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const { $from } = view.state.selection;
|
|
44
|
+
const insert = $from.parent.contentMatchAt($from.index())// All schemas _must_ have a text node type
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
46
|
+
.findWrapping(view.state.schema.nodes.text);
|
|
47
|
+
if (!insert) return;
|
|
48
|
+
let fragment = _prosemirrormodel.Fragment.empty;
|
|
49
|
+
for(let i = insert.length - 1; i >= 0; i--){
|
|
50
|
+
fragment = _prosemirrormodel.Fragment.from(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
51
|
+
insert[i].createAndFill(null, fragment));
|
|
52
|
+
}
|
|
53
|
+
const tr = view.state.tr.replace($from.pos, $from.pos, new _prosemirrormodel.Slice(fragment, 0, 0));
|
|
54
|
+
tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
55
|
+
view.dispatch(tr);
|
|
56
|
+
}
|
|
28
57
|
function beforeInputPlugin(setCursorWrapper) {
|
|
29
58
|
let compositionMarks = null;
|
|
30
59
|
let precompositionSnapshot = null;
|
|
@@ -32,10 +61,11 @@ function beforeInputPlugin(setCursorWrapper) {
|
|
|
32
61
|
props: {
|
|
33
62
|
handleDOMEvents: {
|
|
34
63
|
compositionstart (view) {
|
|
64
|
+
compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
|
|
65
|
+
view.dispatch(view.state.tr.deleteSelection());
|
|
66
|
+
handleGapCursorComposition(view);
|
|
35
67
|
const { state } = view;
|
|
36
|
-
view.dispatch(state.tr.deleteSelection());
|
|
37
68
|
const $pos = state.selection.$from;
|
|
38
|
-
compositionMarks = state.storedMarks ?? $pos.marks();
|
|
39
69
|
if (compositionMarks) {
|
|
40
70
|
setCursorWrapper((0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
|
|
41
71
|
key: "cursor-wrapper",
|
|
@@ -51,7 +51,7 @@ function tempEditor(param) {
|
|
|
51
51
|
const state = _prosemirrorstate.EditorState.create({
|
|
52
52
|
doc: startDoc,
|
|
53
53
|
schema: _prosemirrortestbuilder.schema,
|
|
54
|
-
selection: selection ?? startDoc.tag?.a ? _prosemirrorstate.TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined,
|
|
54
|
+
selection: selection ?? (startDoc.tag?.a ? _prosemirrorstate.TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined),
|
|
55
55
|
plugins: [
|
|
56
56
|
...plugins ?? [],
|
|
57
57
|
(0, _reactKeys.reactKeys)()
|
|
@@ -185,7 +185,9 @@ function adjustWidgetMarksBack(widgetChildren, nodeChild) {
|
|
|
185
185
|
const child = widgetChildren[i];
|
|
186
186
|
if (// Using internal Decoration property, "type"
|
|
187
187
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
-
child.widget.type.side < 0
|
|
188
|
+
child.widget.type.side < 0 || // Using internal Decoration property, "type"
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
|
+
child.widget.type.spec.marks) {
|
|
189
191
|
continue;
|
|
190
192
|
}
|
|
191
193
|
child.marks = child.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React, { forwardRef, useImperativeHandle, useRef
|
|
1
|
+
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
|
2
2
|
import { domIndex } from "../dom.js";
|
|
3
3
|
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
4
4
|
export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(param, ref) {
|
|
5
5
|
let { widget, getPos, ...props } = param;
|
|
6
|
-
const [shouldRender, setShouldRender] = useState(true);
|
|
7
6
|
const innerRef = useRef(null);
|
|
8
7
|
useImperativeHandle(ref, ()=>{
|
|
9
8
|
return innerRef.current;
|
|
@@ -14,27 +13,23 @@ export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(par
|
|
|
14
13
|
view.domObserver.disconnectSelection();
|
|
15
14
|
// @ts-expect-error Internal property - domSelection
|
|
16
15
|
const domSel = view.domSelection();
|
|
17
|
-
const range = document.createRange();
|
|
18
16
|
const node = innerRef.current;
|
|
19
17
|
const img = node.nodeName == "IMG";
|
|
20
|
-
if (img
|
|
21
|
-
|
|
18
|
+
if (img) {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
20
|
+
domSel.collapse(node.parentNode, domIndex(node) + 1);
|
|
22
21
|
} else {
|
|
23
|
-
|
|
22
|
+
domSel.collapse(node, 0);
|
|
24
23
|
}
|
|
25
|
-
range.collapse(false);
|
|
26
|
-
domSel.removeAllRanges();
|
|
27
|
-
domSel.addRange(range);
|
|
28
|
-
setShouldRender(false);
|
|
29
24
|
// @ts-expect-error Internal property - domObserver
|
|
30
25
|
view.domObserver.connectSelection();
|
|
31
26
|
}, []);
|
|
32
|
-
return
|
|
27
|
+
return /*#__PURE__*/ React.createElement("img", {
|
|
33
28
|
ref: innerRef,
|
|
34
29
|
className: "ProseMirror-separator",
|
|
35
30
|
// eslint-disable-next-line react/no-unknown-property
|
|
36
31
|
"mark-placeholder": "true",
|
|
37
32
|
alt: "",
|
|
38
33
|
...props
|
|
39
|
-
})
|
|
34
|
+
});
|
|
40
35
|
});
|
|
@@ -22,7 +22,7 @@ const rootChildDescriptionsContextValue = {
|
|
|
22
22
|
function ProseMirrorInner(param) {
|
|
23
23
|
let { children, nodeViewComponents, markViewComponents, ...props } = param;
|
|
24
24
|
const [mount, setMount] = useState(null);
|
|
25
|
-
const { editor, state } = useEditor(mount, props);
|
|
25
|
+
const { editor, cursorWrapper, state } = useEditor(mount, props);
|
|
26
26
|
const nodeViewConstructors = editor.view.nodeViews;
|
|
27
27
|
const nodeViewContextValue = useMemo(()=>{
|
|
28
28
|
return {
|
|
@@ -39,7 +39,7 @@ function ProseMirrorInner(param) {
|
|
|
39
39
|
]);
|
|
40
40
|
const node = state.doc;
|
|
41
41
|
const decorations = computeDocDeco(editor.view);
|
|
42
|
-
const innerDecorations = viewDecorations(editor.view,
|
|
42
|
+
const innerDecorations = viewDecorations(editor.view, cursorWrapper);
|
|
43
43
|
const docNodeViewContextValue = useMemo(()=>({
|
|
44
44
|
setMount,
|
|
45
45
|
node,
|
|
@@ -27,6 +27,7 @@ export function WidgetView(param) {
|
|
|
27
27
|
viewDescRef.current.parent = parentRef.current;
|
|
28
28
|
viewDescRef.current.widget = widget;
|
|
29
29
|
viewDescRef.current.dom = domRef.current;
|
|
30
|
+
viewDescRef.current.dom.pmViewDesc = viewDescRef.current;
|
|
30
31
|
}
|
|
31
32
|
if (!siblingsRef.current.includes(viewDescRef.current)) {
|
|
32
33
|
siblingsRef.current.push(viewDescRef.current);
|
|
@@ -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,18 +14,18 @@ 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() {
|
|
28
|
+
*/ export function useComponentEventListeners(handleDOMEventsProp) {
|
|
29
29
|
const [registry, setRegistry] = useState(new Map());
|
|
30
30
|
const registerEventListener = useCallback((eventType, handler)=>{
|
|
31
31
|
const handlers = registry.get(eventType) ?? [];
|
|
@@ -43,12 +43,45 @@ import { componentEventListeners } from "../plugins/componentEventListeners.js";
|
|
|
43
43
|
}, [
|
|
44
44
|
registry
|
|
45
45
|
]);
|
|
46
|
-
|
|
46
|
+
useLayoutEffect(()=>{
|
|
47
|
+
if (!handleDOMEventsProp) return;
|
|
48
|
+
for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
|
|
49
|
+
if (!handler) return;
|
|
50
|
+
registerEventListener(eventType, handler);
|
|
51
|
+
}
|
|
52
|
+
return ()=>{
|
|
53
|
+
for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
|
|
54
|
+
if (!handler) return;
|
|
55
|
+
unregisterEventListener(eventType, handler);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}, [
|
|
59
|
+
handleDOMEventsProp,
|
|
60
|
+
registerEventListener,
|
|
61
|
+
unregisterEventListener
|
|
62
|
+
]);
|
|
63
|
+
const handleDOMEvents = useMemo(()=>{
|
|
64
|
+
const domEventHandlers = {};
|
|
65
|
+
for (const [eventType, handlers] of registry.entries()){
|
|
66
|
+
function handleEvent(view, event) {
|
|
67
|
+
for (const handler of handlers){
|
|
68
|
+
let handled = false;
|
|
69
|
+
batch(()=>{
|
|
70
|
+
handled = !!handler(view, event);
|
|
71
|
+
});
|
|
72
|
+
if (handled || event.defaultPrevented) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
domEventHandlers[eventType] = handleEvent;
|
|
77
|
+
}
|
|
78
|
+
return domEventHandlers;
|
|
79
|
+
}, [
|
|
47
80
|
registry
|
|
48
81
|
]);
|
|
49
82
|
return {
|
|
50
83
|
registerEventListener,
|
|
51
84
|
unregisterEventListener,
|
|
52
|
-
|
|
85
|
+
handleDOMEvents
|
|
53
86
|
};
|
|
54
87
|
}
|
|
@@ -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);
|
|
@@ -107,13 +106,11 @@ let didWarnValueDefaultValue = false;
|
|
|
107
106
|
view.update(directEditorProps);
|
|
108
107
|
const editor = useMemo(()=>({
|
|
109
108
|
view,
|
|
110
|
-
cursorWrapper,
|
|
111
109
|
flushSyncRef,
|
|
112
110
|
registerEventListener,
|
|
113
111
|
unregisterEventListener,
|
|
114
112
|
isStatic: options.static ?? false
|
|
115
113
|
}), [
|
|
116
|
-
cursorWrapper,
|
|
117
114
|
options.static,
|
|
118
115
|
registerEventListener,
|
|
119
116
|
unregisterEventListener,
|
|
@@ -121,6 +118,7 @@ let didWarnValueDefaultValue = false;
|
|
|
121
118
|
]);
|
|
122
119
|
return {
|
|
123
120
|
editor,
|
|
121
|
+
cursorWrapper,
|
|
124
122
|
state
|
|
125
123
|
};
|
|
126
124
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment, Slice } from "prosemirror-model";
|
|
2
|
+
import { Plugin, TextSelection } from "prosemirror-state";
|
|
2
3
|
import { CursorWrapper } from "../components/CursorWrapper.js";
|
|
3
4
|
import { widget } from "../decorations/ReactWidgetType.js";
|
|
4
5
|
function insertText(view, eventData) {
|
|
@@ -15,6 +16,34 @@ function insertText(view, eventData) {
|
|
|
15
16
|
view.dispatch(tr);
|
|
16
17
|
return true;
|
|
17
18
|
}
|
|
19
|
+
// Taken from https://github.com/ProseMirror/prosemirror-gapcursor/blob/master/src/index.ts#L67-L84
|
|
20
|
+
// This is a hack that, when a composition starts while a gap cursor
|
|
21
|
+
// is active, quickly creates an inline context for the composition to
|
|
22
|
+
// happen in, to avoid it being aborted by the DOM selection being
|
|
23
|
+
// moved into a valid position.
|
|
24
|
+
//
|
|
25
|
+
// We can't rely on the actual hack from prosemirror-gapcursor, because
|
|
26
|
+
// it happens too late. We snapshot the DOM during compositionstart, but
|
|
27
|
+
// the gapcursor hack runs in beforeinput (after compositionstart).
|
|
28
|
+
function handleGapCursorComposition(view) {
|
|
29
|
+
// @ts-expect-error Internal property - jsonID
|
|
30
|
+
if (!(view.state.selection.jsonID === "gapcursor")) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const { $from } = view.state.selection;
|
|
34
|
+
const insert = $from.parent.contentMatchAt($from.index())// All schemas _must_ have a text node type
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
36
|
+
.findWrapping(view.state.schema.nodes.text);
|
|
37
|
+
if (!insert) return;
|
|
38
|
+
let fragment = Fragment.empty;
|
|
39
|
+
for(let i = insert.length - 1; i >= 0; i--){
|
|
40
|
+
fragment = Fragment.from(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41
|
+
insert[i].createAndFill(null, fragment));
|
|
42
|
+
}
|
|
43
|
+
const tr = view.state.tr.replace($from.pos, $from.pos, new Slice(fragment, 0, 0));
|
|
44
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
45
|
+
view.dispatch(tr);
|
|
46
|
+
}
|
|
18
47
|
export function beforeInputPlugin(setCursorWrapper) {
|
|
19
48
|
let compositionMarks = null;
|
|
20
49
|
let precompositionSnapshot = null;
|
|
@@ -22,10 +51,11 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
22
51
|
props: {
|
|
23
52
|
handleDOMEvents: {
|
|
24
53
|
compositionstart (view) {
|
|
54
|
+
compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
|
|
55
|
+
view.dispatch(view.state.tr.deleteSelection());
|
|
56
|
+
handleGapCursorComposition(view);
|
|
25
57
|
const { state } = view;
|
|
26
|
-
view.dispatch(state.tr.deleteSelection());
|
|
27
58
|
const $pos = state.selection.$from;
|
|
28
|
-
compositionMarks = state.storedMarks ?? $pos.marks();
|
|
29
59
|
if (compositionMarks) {
|
|
30
60
|
setCursorWrapper(widget(state.selection.from, CursorWrapper, {
|
|
31
61
|
key: "cursor-wrapper",
|
|
@@ -28,7 +28,7 @@ export function tempEditor(param) {
|
|
|
28
28
|
const state = EditorState.create({
|
|
29
29
|
doc: startDoc,
|
|
30
30
|
schema,
|
|
31
|
-
selection: selection ?? startDoc.tag?.a ? TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined,
|
|
31
|
+
selection: selection ?? (startDoc.tag?.a ? TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined),
|
|
32
32
|
plugins: [
|
|
33
33
|
...plugins ?? [],
|
|
34
34
|
reactKeys()
|