@handlewithcare/react-prosemirror 3.1.0-tiptap.51 → 3.1.0-tiptap.53

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 (58) hide show
  1. package/dist/cjs/ReactEditorView.js +6 -2
  2. package/dist/cjs/components/ChildNodeViews.js +3 -2
  3. package/dist/cjs/components/CursorWrapper.js +3 -4
  4. package/dist/cjs/components/ProseMirror.js +5 -3
  5. package/dist/cjs/components/TextNodeView.js +176 -34
  6. package/dist/cjs/components/TrailingHackView.js +42 -1
  7. package/dist/cjs/components/WidgetView.js +4 -1
  8. package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
  9. package/dist/cjs/decorations/viewDecorations.js +1 -6
  10. package/dist/cjs/hooks/useComponentEventListeners.js +6 -14
  11. package/dist/cjs/hooks/useEditor.js +2 -10
  12. package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
  13. package/dist/cjs/hooks/useNodeViewDescription.js +45 -23
  14. package/dist/cjs/plugins/beforeInputPlugin.js +116 -12
  15. package/dist/cjs/plugins/componentEventListeners.js +2 -9
  16. package/dist/cjs/plugins/reactKeys.js +21 -14
  17. package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +59 -0
  18. package/dist/cjs/viewdesc.js +43 -2
  19. package/dist/esm/ReactEditorView.js +6 -2
  20. package/dist/esm/components/ChildNodeViews.js +4 -3
  21. package/dist/esm/components/CursorWrapper.js +4 -5
  22. package/dist/esm/components/ProseMirror.js +5 -3
  23. package/dist/esm/components/TextNodeView.js +125 -32
  24. package/dist/esm/components/TrailingHackView.js +42 -1
  25. package/dist/esm/components/WidgetView.js +4 -1
  26. package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
  27. package/dist/esm/decorations/viewDecorations.js +1 -6
  28. package/dist/esm/hooks/useComponentEventListeners.js +6 -14
  29. package/dist/esm/hooks/useEditor.js +2 -10
  30. package/dist/esm/hooks/useMarkViewDescription.js +62 -4
  31. package/dist/esm/hooks/useNodeViewDescription.js +46 -24
  32. package/dist/esm/plugins/beforeInputPlugin.js +116 -12
  33. package/dist/esm/plugins/componentEventListeners.js +2 -9
  34. package/dist/esm/plugins/reactKeys.js +21 -14
  35. package/dist/esm/tiptap/hooks/useTiptapEditor.js +7 -1
  36. package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +56 -0
  37. package/dist/esm/viewdesc.js +42 -2
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/dist/types/ReactEditorView.d.ts +3 -2
  40. package/dist/types/components/CursorWrapper.d.ts +2 -4
  41. package/dist/types/components/TextNodeView.d.ts +11 -6
  42. package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
  43. package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
  44. package/dist/types/constants.d.ts +1 -1
  45. package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
  46. package/dist/types/decorations/viewDecorations.d.ts +2 -2
  47. package/dist/types/hooks/useComponentEventListeners.d.ts +1 -1
  48. package/dist/types/hooks/useEditor.d.ts +1 -2
  49. package/dist/types/hooks/useMarkViewDescription.d.ts +2 -1
  50. package/dist/types/hooks/useNodeViewDescription.d.ts +2 -1
  51. package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
  52. package/dist/types/plugins/componentEventListeners.d.ts +2 -3
  53. package/dist/types/plugins/reactKeys.d.ts +9 -8
  54. package/dist/types/props.d.ts +26 -26
  55. package/dist/types/tiptap/hooks/useTiptapEditor.d.ts +7 -0
  56. package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +1 -0
  57. package/dist/types/viewdesc.d.ts +3 -2
  58. package/package.json +2 -1
@@ -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,19 +13,19 @@ 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();
16
+ if (!domSel.isCollapsed) return;
17
17
  const node = innerRef.current;
18
18
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19
19
  domSel.collapse(node.parentNode, domIndex(node) + 1);
20
20
  // @ts-expect-error Internal property - domObserver
21
21
  view.domObserver.connectSelection();
22
- setShouldRender(false);
23
22
  }, []);
24
- return shouldRender ? /*#__PURE__*/ React.createElement("img", {
23
+ return /*#__PURE__*/ React.createElement("img", {
25
24
  ref: innerRef,
26
25
  className: "ProseMirror-separator",
27
26
  // eslint-disable-next-line react/no-unknown-property
28
27
  "mark-placeholder": "true",
29
28
  alt: "",
30
29
  ...props
31
- }) : null;
30
+ });
32
31
  });
@@ -17,12 +17,14 @@ const rootChildDescriptionsContextValue = {
17
17
  },
18
18
  siblingsRef: {
19
19
  current: []
20
- }
20
+ },
21
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
22
+ findCompositionDOM: ()=>{}
21
23
  };
22
24
  function ProseMirrorInner(param) {
23
25
  let { children, nodeViewComponents, markViewComponents, ...props } = param;
24
26
  const [mount, setMount] = useState(null);
25
- const { editor, cursorWrapper, state } = useEditor(mount, props);
27
+ const { editor, state } = useEditor(mount, props);
26
28
  const nodeViewConstructors = editor.view.nodeViews;
27
29
  const nodeViewContextValue = useMemo(()=>{
28
30
  return {
@@ -39,7 +41,7 @@ function ProseMirrorInner(param) {
39
41
  ]);
40
42
  const node = state.doc;
41
43
  const decorations = computeDocDeco(editor.view);
42
- const innerDecorations = viewDecorations(editor.view, cursorWrapper);
44
+ const innerDecorations = viewDecorations(editor.view);
43
45
  const docNodeViewContextValue = useMemo(()=>({
44
46
  setMount,
45
47
  node,
@@ -1,9 +1,9 @@
1
1
  import { TextSelection } from "prosemirror-state";
2
2
  import { DecorationSet } from "prosemirror-view";
3
- import { Component } from "react";
3
+ import React, { Component, createRef, useReducer } from "react";
4
4
  import { ReactEditorView } from "../ReactEditorView.js";
5
5
  import { findDOMNode } from "../findDOMNode.js";
6
- import { CompositionViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
6
+ import { CompositionViewDesc, TextViewDesc, findTextInFragment, sortViewDescs } from "../viewdesc.js";
7
7
  import { wrapInDeco } from "./ChildNodeViews.js";
8
8
  function shallowEqual(objA, objB) {
9
9
  if (objA === objB) {
@@ -28,10 +28,10 @@ function shallowEqual(objA, objB) {
28
28
  return true;
29
29
  }
30
30
  export class TextNodeView extends Component {
31
- viewDescRef = null;
32
- renderRef = null;
33
- wasProtecting = false;
34
- containsCompositionNodeText = true;
31
+ viewDescRef = createMutRef();
32
+ renderRef = createMutRef();
33
+ wasProtecting = createMutRef();
34
+ containsCompositionNodeText = createMutRef();
35
35
  // This is basically NodeViewDesc.localCompositionInfo
36
36
  // from prosemirror-view. It's been slightly adjusted so that
37
37
  // it can be used accurately during render, before we've
@@ -43,17 +43,42 @@ export class TextNodeView extends Component {
43
43
  if (!view.composing) {
44
44
  return false;
45
45
  }
46
- const pos = getPos();
47
- const { from, to } = view.state.selection;
48
- if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + node.nodeSize) {
49
- return false;
46
+ const viewDesc = this.viewDescRef.current;
47
+ // If our DOM text node IS the IME's composition node, protect regardless
48
+ // of where the PM selection currently is. The IME may have replaced a
49
+ // selection that included us — moving the PM selection past us — but our
50
+ // DOM is still part of the in-progress composition. Until another
51
+ // TextNodeView's findCompositionDOM displaces us into a comp desc, only
52
+ // our own protect/no-update is preventing React from rewriting the IME's
53
+ // text. (When we *are* displaced, viewDesc is already a CompositionViewDesc
54
+ // and the existing position-based logic doesn't apply anyway.)
55
+ const ownsCompositionNode = viewDesc instanceof TextViewDesc && viewDesc.nodeDOM === view.input.compositionNode;
56
+ if (!ownsCompositionNode) {
57
+ const pos = getPos();
58
+ const { from, to } = view.state.selection;
59
+ if (!(view.state.selection instanceof TextSelection) || from <= pos || to > pos + node.nodeSize) {
60
+ return false;
61
+ }
50
62
  }
51
- return this.containsCompositionNodeText;
63
+ return !!this.containsCompositionNodeText.current;
52
64
  }
53
65
  handleCompositionEnd = ()=>{
54
- if (!this.wasProtecting) return;
55
- this.forceUpdate();
56
- return;
66
+ if (!this.wasProtecting.current) return;
67
+ const { view } = this.props;
68
+ if (!(view instanceof ReactEditorView)) return;
69
+ // If the IME detached our DOM during composition, React's fiber is now
70
+ // wired to a detached node and will silently send all subsequent updates
71
+ // into the void. Re-attach the orphan (so the upcoming unmount's
72
+ // removeChild has something to remove), then ask our wrapper to mint a
73
+ // new key — that forces React to drop this fiber and mount a fresh one
74
+ // whose stateNode it creates from the current render output.
75
+ const dom = findDOMNode(this);
76
+ if (dom instanceof HTMLElement && !view.dom.contains(dom)) {
77
+ this.reattachAtCorrectPosition(dom);
78
+ this.props.forceRemount();
79
+ } else {
80
+ this.forceUpdate();
81
+ }
57
82
  };
58
83
  create() {
59
84
  const { view, decorations, siblingsRef, parentRef, getPos, node } = this.props;
@@ -81,14 +106,25 @@ export class TextNodeView extends Component {
81
106
  }
82
107
  siblingsRef.current.push(viewDesc);
83
108
  siblingsRef.current.sort(sortViewDescs);
109
+ if (viewDesc instanceof CompositionViewDesc) {
110
+ this.props.findCompositionDOM(viewDesc);
111
+ }
84
112
  return viewDesc;
85
113
  }
86
114
  update() {
87
115
  const { view, node, decorations } = this.props;
88
116
  if (!(view instanceof ReactEditorView)) return false;
89
- const viewDesc = this.viewDescRef;
117
+ const viewDesc = this.viewDescRef.current;
90
118
  if (!viewDesc) return false;
91
- if (this.shouldProtect(this.props) !== viewDesc instanceof CompositionViewDesc) {
119
+ // Don't force destroy/recreate just because we transitioned into protect
120
+ // mode. If our DOM text node is the IME's composition node, we want to
121
+ // keep the TextViewDesc alive so the new composition-text TextNodeView's
122
+ // findCompositionDOM second pass can find us, validate the size mismatch,
123
+ // and displace us into a properly-sized CompositionViewDesc. If we
124
+ // destroyed here, create() would put a wrong-size CompositionViewDesc on
125
+ // T and pre-empt that displacement.
126
+ const ownsCompositionNode = viewDesc instanceof TextViewDesc && viewDesc.nodeDOM === view.input.compositionNode;
127
+ if (!ownsCompositionNode && this.shouldProtect(this.props) !== viewDesc instanceof CompositionViewDesc) {
92
128
  return false;
93
129
  }
94
130
  if (viewDesc instanceof CompositionViewDesc) return false;
@@ -98,7 +134,7 @@ export class TextNodeView extends Component {
98
134
  return viewDesc.matchesNode(node, decorations, DecorationSet.empty) || viewDesc.update(node, decorations, DecorationSet.empty, view);
99
135
  }
100
136
  destroy() {
101
- const viewDesc = this.viewDescRef;
137
+ const viewDesc = this.viewDescRef.current;
102
138
  if (!viewDesc) return;
103
139
  viewDesc.destroy();
104
140
  const siblings = this.props.siblingsRef.current;
@@ -110,43 +146,85 @@ export class TextNodeView extends Component {
110
146
  updateEffect() {
111
147
  if (!this.update()) {
112
148
  this.destroy();
113
- this.viewDescRef = this.create();
149
+ this.viewDescRef.current = this.create();
114
150
  }
115
- const { view, node } = this.props;
151
+ const { view } = this.props;
116
152
  if (!(view instanceof ReactEditorView)) {
117
- this.containsCompositionNodeText = true;
153
+ this.containsCompositionNodeText.current = true;
118
154
  return;
119
155
  }
120
156
  const textNode = view.input.compositionNode;
121
157
  if (!textNode) {
122
- this.containsCompositionNodeText = true;
158
+ this.containsCompositionNodeText.current = true;
123
159
  return;
124
160
  }
125
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
126
- const text = textNode.nodeValue;
127
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
128
- this.containsCompositionNodeText = node.text === text;
161
+ // Resolve the parent textblock containing this text node and ask
162
+ // findTextInFragment whether the IME text node's *current* content can be
163
+ // placed somewhere in the textblock's PM content overlapping the
164
+ // selection. If it can, the composition is still consistent with PM state
165
+ // and we should protect. If it can't (e.g. a remote change overwrote the
166
+ // composing region), PM and the DOM have diverged — abandon protection
167
+ // so the re-render can rewrite the DOM and cancel the composition.
168
+ const $pos = view.state.doc.resolve(this.props.getPos());
169
+ const parent = $pos.parent;
170
+ if (!parent.inlineContent) {
171
+ this.containsCompositionNodeText.current = false;
172
+ return;
173
+ }
174
+ const parentStart = $pos.start();
175
+ const { from, to } = view.state.selection;
176
+ const textPos = findTextInFragment(parent.content, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177
+ textNode.nodeValue, from - parentStart, to - parentStart);
178
+ this.containsCompositionNodeText.current = textPos >= 0;
129
179
  }
130
180
  shouldComponentUpdate(nextProps) {
131
181
  // When leaving the protected state, force a re-render so React's
132
182
  // virtual DOM resyncs with whatever the IME wrote into the real DOM
133
183
  // while we were returning a stale renderRef.
134
- if (this.wasProtecting && !this.shouldProtect(nextProps)) {
184
+ if (this.wasProtecting.current && !this.shouldProtect(nextProps)) {
135
185
  return true;
136
186
  }
137
187
  return !shallowEqual(this.props, nextProps);
138
188
  }
189
+ constructor(props){
190
+ super(props);
191
+ this.viewDescRef.current = null;
192
+ this.renderRef.current = null;
193
+ this.wasProtecting.current = false;
194
+ this.containsCompositionNodeText.current = true;
195
+ }
139
196
  componentDidMount() {
140
- this.viewDescRef = null;
197
+ this.containsCompositionNodeText.current = true;
141
198
  // After a composition, force an update so that we re-check whether we need
142
199
  // to be protecting our rendered content and allow React to re-sync with the
143
200
  // DOM.
144
201
  const { registerEventListener } = this.props;
145
202
  registerEventListener("compositionend", this.handleCompositionEnd);
203
+ this.viewDescRef.current = this.create();
146
204
  this.updateEffect();
147
205
  }
148
206
  componentDidUpdate() {
149
207
  this.updateEffect();
208
+ const { view } = this.props;
209
+ if (!(view instanceof ReactEditorView)) return;
210
+ }
211
+ reattachAtCorrectPosition(dom) {
212
+ const viewDesc = this.viewDescRef.current;
213
+ if (!viewDesc) return;
214
+ let host = viewDesc.parent;
215
+ while(host && !host.contentDOM)host = host.parent;
216
+ if (!host?.contentDOM) return;
217
+ const siblings = viewDesc.parent?.children ?? [];
218
+ const idx = siblings.indexOf(viewDesc);
219
+ let nextDom = null;
220
+ for(let i = idx + 1; i < siblings.length; i++){
221
+ const sib = siblings[i];
222
+ if (sib?.dom && sib.dom.parentNode === host.contentDOM) {
223
+ nextDom = sib.dom;
224
+ break;
225
+ }
226
+ }
227
+ host.contentDOM.insertBefore(dom, nextDom);
150
228
  }
151
229
  componentWillUnmount() {
152
230
  const { unregisterEventListener } = this.props;
@@ -161,11 +239,26 @@ export class TextNodeView extends Component {
161
239
  // we freeze the DOM of this element so that it doesn't
162
240
  // interrupt the composition
163
241
  if (this.shouldProtect(this.props)) {
164
- this.wasProtecting = true;
165
- return this.renderRef;
242
+ this.wasProtecting.current = true;
243
+ return this.renderRef.current;
166
244
  }
167
- this.wasProtecting = false;
168
- this.renderRef = decorations.reduce(wrapInDeco, node.text);
169
- return this.renderRef;
245
+ this.wasProtecting.current = false;
246
+ this.renderRef.current = decorations.reduce(wrapInDeco, node.text);
247
+ return this.renderRef.current;
170
248
  }
171
249
  }
250
+ /**
251
+ * createRef returns a RefObject, even though the docs
252
+ * say that it's acceptible to manage the ref's value
253
+ * yourself.
254
+ */ function createMutRef() {
255
+ return /*#__PURE__*/ createRef();
256
+ }
257
+ export function RemountableTextNodeView(props) {
258
+ const [key, forceRemount] = useReducer((x)=>x + 1, 0);
259
+ return /*#__PURE__*/ React.createElement(TextNodeView, {
260
+ key: key,
261
+ forceRemount: forceRemount,
262
+ ...props
263
+ });
264
+ }
@@ -1,4 +1,5 @@
1
1
  import React, { useContext, useRef, useState } from "react";
2
+ import { ReactEditorView } from "../ReactEditorView.js";
2
3
  import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
3
4
  import { useClientLayoutEffect } from "../hooks/useClientLayoutEffect.js";
4
5
  import { useEditorEffect } from "../hooks/useEditorEffect.js";
@@ -7,9 +8,14 @@ import { TrailingHackViewDesc, sortViewDescs } from "../viewdesc.js";
7
8
  export function TrailingHackView(param) {
8
9
  let { getPos } = param;
9
10
  const [shouldRender, setShouldRender] = useState(true);
11
+ const [shouldReinsert, setShouldReinsert] = useState(false);
10
12
  const { siblingsRef, parentRef } = useContext(ChildDescriptionsContext);
11
13
  const viewDescRef = useRef(null);
12
14
  const ref = useRef(null);
15
+ const preservedRef = useRef(ref.current);
16
+ if (ref.current) {
17
+ preservedRef.current = ref.current;
18
+ }
13
19
  useClientLayoutEffect(()=>{
14
20
  const siblings = siblingsRef.current;
15
21
  return ()=>{
@@ -43,13 +49,47 @@ export function TrailingHackView(param) {
43
49
  const { from } = view.state.selection;
44
50
  if (from === getPos()) {
45
51
  setShouldRender(false);
52
+ setShouldReinsert(true);
46
53
  }
47
54
  });
55
+ // Chrome and Safari will cancel/mangle the composition if the br element isn't
56
+ // still in the DOM after the compositionstart event. We manually add it
57
+ // back to the DOM, without React managing it, so that it can be removed
58
+ // again by the browser when it starts the composition.
59
+ useClientLayoutEffect(()=>{
60
+ if (!shouldReinsert) return;
61
+ const preservedHack = preservedRef.current;
62
+ if (!preservedHack) return;
63
+ if (!viewDescRef.current) return;
64
+ const { parent } = viewDescRef.current;
65
+ if (!parent) return;
66
+ const dom = parent.contentDOM;
67
+ if (!dom) return;
68
+ preservedHack.pmViewDesc = undefined;
69
+ const index = parent.children.indexOf(viewDescRef.current);
70
+ if (index === 0) {
71
+ dom.appendChild(preservedHack);
72
+ } else {
73
+ dom.insertBefore(preservedHack, dom.childNodes.item(index));
74
+ }
75
+ return ()=>{
76
+ try {
77
+ dom.removeChild(preservedHack);
78
+ } catch {
79
+ // It may have already been removed by the browser during
80
+ // the composition, but if we get unmounted before that happens,
81
+ // we need to remove it ourselves
82
+ }
83
+ };
84
+ }, [
85
+ shouldReinsert
86
+ ]);
48
87
  // We need to run the same composition check when we first get mounted,
49
88
  // in case we got mounted in the same render batch as the beginning of
50
89
  // a composition
51
90
  useEditorEffect((view)=>{
52
- if (!view.composing) return;
91
+ if (!(view instanceof ReactEditorView)) return;
92
+ if (!view.compositionStarting) return;
53
93
  const { from } = view.state.selection;
54
94
  if (from === getPos()) {
55
95
  setShouldRender(false);
@@ -59,6 +99,7 @@ export function TrailingHackView(param) {
59
99
  ]);
60
100
  useEditorEventListener("compositionend", ()=>{
61
101
  setShouldRender(true);
102
+ setShouldReinsert(false);
62
103
  });
63
104
  if (!shouldRender) return null;
64
105
  return /*#__PURE__*/ React.createElement("br", {
@@ -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);
@@ -38,6 +39,8 @@ export function WidgetView(param) {
38
39
  ref: domRef,
39
40
  widget: widget,
40
41
  getPos: getPos,
41
- contentEditable: false
42
+ ...!widget.type.spec.raw && {
43
+ contentEditable: false
44
+ }
42
45
  });
43
46
  }
@@ -5,5 +5,7 @@ export const ChildDescriptionsContext = createContext({
5
5
  },
6
6
  siblingsRef: {
7
7
  current: []
8
- }
8
+ },
9
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
10
+ findCompositionDOM: ()=>{}
9
11
  });
@@ -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, cursorWrapper) {
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,16 +25,8 @@ import { unstable_batchedUpdates as batch } from "react-dom";
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(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
- })));
28
+ */ export function useComponentEventListeners(handleDOMEventsProp) {
29
+ const [registry, setRegistry] = useState(new Map());
38
30
  const registerEventListener = useCallback((eventType, handler)=>{
39
31
  const handlers = registry.get(eventType) ?? [];
40
32
  handlers.unshift(handler);
@@ -52,19 +44,19 @@ import { unstable_batchedUpdates as batch } from "react-dom";
52
44
  registry
53
45
  ]);
54
46
  useLayoutEffect(()=>{
55
- if (!existingHandlers) return;
56
- for (const [eventType, handler] of Object.entries(existingHandlers)){
47
+ if (!handleDOMEventsProp) return;
48
+ for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
57
49
  if (!handler) return;
58
50
  registerEventListener(eventType, handler);
59
51
  }
60
52
  return ()=>{
61
- for (const [eventType, handler] of Object.entries(existingHandlers)){
53
+ for (const [eventType, handler] of Object.entries(handleDOMEventsProp)){
62
54
  if (!handler) return;
63
55
  unregisterEventListener(eventType, handler);
64
56
  }
65
57
  };
66
58
  }, [
67
- existingHandlers,
59
+ handleDOMEventsProp,
68
60
  registerEventListener,
69
61
  unregisterEventListener
70
62
  ]);
@@ -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(setCursorWrapper)
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
  }
@@ -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 { ReactMarkViewDesc, sortViewDescs } from "../viewdesc.js";
5
+ import { ReactMarkViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
6
6
  import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
7
7
  import { useEffectEvent } from "./useEffectEvent.js";
8
8
  export function useMarkViewDescription(getDOM, getContentDOM, constructor, props) {
@@ -110,12 +110,70 @@ export function useMarkViewDescription(getDOM, getContentDOM, constructor, props
110
110
  child.parent = viewDesc;
111
111
  }
112
112
  });
113
+ const findCompositionDOM = useCallback((compositionViewDesc)=>{
114
+ const children = childrenRef.current;
115
+ // Because TextNodeViews can't locate the DOM nodes
116
+ // for compositions, we need to override them here
117
+ if (!viewDescRef.current?.contentDOM) return;
118
+ let compositionTopDOM = null;
119
+ for (const childNode of viewDescRef.current.contentDOM.childNodes){
120
+ if (children.every((child)=>child.dom !== childNode)) {
121
+ compositionTopDOM = childNode;
122
+ break;
123
+ }
124
+ }
125
+ if (!compositionTopDOM) {
126
+ // Otherwise the IME extended an existing tracked text node. Take it over.
127
+ const reactView = view;
128
+ const imeTextNode = reactView.input.compositionNode;
129
+ if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
130
+ return;
131
+ }
132
+ const claimedDesc = imeTextNode.pmViewDesc;
133
+ if (!(claimedDesc instanceof TextViewDesc)) return;
134
+ if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
135
+ // Walk up to the direct child of contentDOM that contains the IME text node
136
+ // (could be the text node itself, could be wrapped in a mark span).
137
+ let topDOM = imeTextNode;
138
+ while(topDOM.parentNode !== viewDescRef.current.contentDOM){
139
+ const next = topDOM.parentNode;
140
+ if (!next) return;
141
+ topDOM = next;
142
+ }
143
+ // Detach the displaced TextViewDesc from the sibling list so sibling-size
144
+ // accounting (used by posBeforeChild) doesn't double-count this text node.
145
+ const displacedIdx = children.indexOf(claimedDesc);
146
+ if (displacedIdx >= 0) children.splice(displacedIdx, 1);
147
+ compositionViewDesc.dom = topDOM;
148
+ compositionViewDesc.textDOM = imeTextNode;
149
+ compositionViewDesc.text = imeTextNode.data;
150
+ imeTextNode.pmViewDesc = compositionViewDesc;
151
+ compositionViewDesc._displacedDesc = claimedDesc;
152
+ reactView.input.compositionNodes.push(compositionViewDesc);
153
+ return;
154
+ }
155
+ let textDOM = compositionTopDOM;
156
+ while(textDOM.firstChild){
157
+ textDOM = textDOM.firstChild;
158
+ }
159
+ if (!textDOM || !(textDOM instanceof Text)) {
160
+ console.error(compositionTopDOM, textDOM);
161
+ throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
162
+ }
163
+ compositionViewDesc.dom = compositionTopDOM;
164
+ compositionViewDesc.textDOM = textDOM;
165
+ compositionViewDesc.text = textDOM.data;
166
+ compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
167
+ view.input.compositionNodes.push(compositionViewDesc);
168
+ }, [
169
+ view
170
+ ]);
113
171
  const childContextValue = useMemo(()=>({
114
172
  parentRef: viewDescRef,
115
- siblingsRef: childrenRef
173
+ siblingsRef: childrenRef,
174
+ findCompositionDOM
116
175
  }), [
117
- childrenRef,
118
- viewDescRef
176
+ findCompositionDOM
119
177
  ]);
120
178
  return {
121
179
  childContextValue,