@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.
Files changed (33) hide show
  1. package/README.md +36 -6
  2. package/dist/cjs/components/ChildNodeViews.js +3 -1
  3. package/dist/cjs/components/CursorWrapper.js +6 -11
  4. package/dist/cjs/components/ProseMirror.js +2 -2
  5. package/dist/cjs/components/WidgetView.js +1 -0
  6. package/dist/cjs/hooks/useComponentEventListeners.js +37 -4
  7. package/dist/cjs/hooks/useEditor.js +4 -6
  8. package/dist/cjs/plugins/beforeInputPlugin.js +32 -2
  9. package/dist/cjs/testing/editorViewTestHelpers.js +1 -1
  10. package/dist/esm/components/ChildNodeViews.js +3 -1
  11. package/dist/esm/components/CursorWrapper.js +7 -12
  12. package/dist/esm/components/ProseMirror.js +2 -2
  13. package/dist/esm/components/WidgetView.js +1 -0
  14. package/dist/esm/hooks/useComponentEventListeners.js +44 -11
  15. package/dist/esm/hooks/useEditor.js +4 -6
  16. package/dist/esm/plugins/beforeInputPlugin.js +33 -3
  17. package/dist/esm/testing/editorViewTestHelpers.js +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/dist/types/components/CursorWrapper.d.ts +2 -4
  20. package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
  21. package/dist/types/contexts/EditorContext.d.ts +2 -3
  22. package/dist/types/hooks/useComponentEventListeners.d.ts +11 -10
  23. package/dist/types/hooks/useEditor.d.ts +3 -3
  24. package/dist/types/hooks/useEditorEventListener.d.ts +1 -1
  25. package/dist/types/props.d.ts +1 -1
  26. package/dist/types/testing/editorViewTestHelpers.d.ts +1 -0
  27. package/package.json +1 -1
  28. package/dist/cjs/plugins/componentEventListeners.js +0 -35
  29. package/dist/cjs/plugins/componentEventListenersPlugin.js +0 -35
  30. package/dist/esm/plugins/componentEventListeners.js +0 -25
  31. package/dist/esm/plugins/componentEventListenersPlugin.js +0 -25
  32. package/dist/types/plugins/componentEventListeners.d.ts +0 -4
  33. 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
- <p>
841
- <a href="https://www.moment.dev/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Moment.png" alt="Moment" height="128"></a>
842
- <a href="https://www.lingco.io/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Lingco.png" alt="Lingco" height="128"></a>
843
- <a href="https://dskrpt.de/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Dskrpt.png" alt="Dskrpt" height="128"></a>
844
- <a href="https://github.com/fastrepl/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Fastrepl.png" alt="Fastrepl" height="128"></a>
845
- </p>
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 && node.parentNode) {
72
- range.setEnd(node.parentNode, (0, _dom.domIndex)(node) + 1);
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
- range.setEnd(node, 0);
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 shouldRender ? /*#__PURE__*/ _react.default.createElement("img", {
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
- }) : null;
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, editor.cursorWrapper);
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 _componentEventListeners = require("../plugins/componentEventListeners.js");
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
- const componentEventListenersPlugin = (0, _react.useMemo)(()=>(0, _componentEventListeners.componentEventListeners)(registry), [
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
- componentEventListenersPlugin
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 { componentEventListenersPlugin, registerEventListener, unregisterEventListener } = (0, _useComponentEventListeners.useComponentEventListeners)();
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, useState } from "react";
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 && node.parentNode) {
21
- range.setEnd(node.parentNode, domIndex(node) + 1);
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
- range.setEnd(node, 0);
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 shouldRender ? /*#__PURE__*/ React.createElement("img", {
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
- }) : null;
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, editor.cursorWrapper);
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 { componentEventListeners } from "../plugins/componentEventListeners.js";
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 ProseMirror plugin.
17
+ * us precise control over when we re-create the event listeners.
18
18
  *
19
- * The plugin has a mutable reference to the set of handlers for each
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 ProseMirror plugin whenever a new event type is
22
- * registered. We avoid producing a new ProseMirrer plugin in any other
23
- * scenario to avoid the performance overhead of reconfiguring the plugins
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
- const componentEventListenersPlugin = useMemo(()=>componentEventListeners(registry), [
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
- componentEventListenersPlugin
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 { componentEventListenersPlugin, registerEventListener, unregisterEventListener } = useComponentEventListeners();
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 { Plugin } from "prosemirror-state";
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()