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

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 (35) hide show
  1. package/dist/cjs/ReactEditorView.js +2 -0
  2. package/dist/cjs/components/ChildNodeViews.js +14 -9
  3. package/dist/cjs/components/CursorWrapper.js +6 -9
  4. package/dist/cjs/components/TextNodeView.js +109 -38
  5. package/dist/cjs/components/TrailingHackView.js +29 -0
  6. package/dist/cjs/hooks/useComponentEventListeners.js +46 -5
  7. package/dist/cjs/hooks/useEditor.js +3 -6
  8. package/dist/cjs/hooks/useNodeViewDescription.js +37 -16
  9. package/dist/cjs/plugins/beforeInputPlugin.js +41 -43
  10. package/dist/cjs/viewdesc.js +10 -3
  11. package/dist/esm/ReactEditorView.js +2 -0
  12. package/dist/esm/components/ChildNodeViews.js +14 -9
  13. package/dist/esm/components/CursorWrapper.js +7 -10
  14. package/dist/esm/components/TextNodeView.js +109 -38
  15. package/dist/esm/components/TrailingHackView.js +30 -1
  16. package/dist/esm/hooks/useComponentEventListeners.js +53 -12
  17. package/dist/esm/hooks/useEditor.js +3 -6
  18. package/dist/esm/hooks/useNodeViewDescription.js +38 -17
  19. package/dist/esm/plugins/beforeInputPlugin.js +41 -43
  20. package/dist/esm/viewdesc.js +10 -3
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/dist/types/ReactEditorView.d.ts +4 -0
  23. package/dist/types/components/TextNodeView.d.ts +14 -4
  24. package/dist/types/components/TrailingHackView.d.ts +1 -1
  25. package/dist/types/constants.d.ts +1 -1
  26. package/dist/types/contexts/EditorContext.d.ts +1 -1
  27. package/dist/types/hooks/useComponentEventListeners.d.ts +11 -10
  28. package/dist/types/hooks/useEditor.d.ts +2 -2
  29. package/dist/types/hooks/useEditorEventListener.d.ts +1 -1
  30. package/dist/types/props.d.ts +26 -26
  31. package/dist/types/viewdesc.d.ts +2 -2
  32. package/package.json +2 -1
  33. package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +0 -59
  34. package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +0 -56
  35. package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +0 -1
@@ -266,7 +266,9 @@ let ViewDesc = class ViewDesc {
266
266
  prev = i ? this.children[i - 1] : null;
267
267
  if (!prev || prev.dom.parentNode == this.contentDOM) break;
268
268
  }
269
- if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side);
269
+ if (prev && side && enter && !prev.border && !prev.domAtom) {
270
+ return prev.domFromPos(prev.size, side);
271
+ }
270
272
  return {
271
273
  node: this.contentDOM,
272
274
  offset: prev ? (0, _dom.domIndex)(prev.dom) + 1 : 0
@@ -401,7 +403,9 @@ let ViewDesc = class ViewDesc {
401
403
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
402
404
  if (after && after.contentEditable == "false") force = true;
403
405
  }
404
- if (!(force || brKludge && _browser.browser.safari) && (0, _dom.isEquivalentPosition)(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && (0, _dom.isEquivalentPosition)(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
406
+ if (!(force || brKludge && _browser.browser.safari) && (0, _dom.isEquivalentPosition)(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && (0, _dom.isEquivalentPosition)(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
407
+ return;
408
+ }
405
409
  // Selection.extend can be used to create an 'inverted' selection
406
410
  // (one where the focus is before the anchor), but not all
407
411
  // browsers support it yet.
@@ -666,7 +670,10 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
666
670
  skip: skip || true
667
671
  };
668
672
  }
669
- update(_node, _outerDeco, _innerDeco, _view) {
673
+ update(node, outerDeco, _innerDeco, _view) {
674
+ if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
675
+ this.updateOuterDeco(outerDeco);
676
+ this.node = node;
670
677
  this.dirty = NOT_DIRTY;
671
678
  return true;
672
679
  }
@@ -33,6 +33,7 @@ function changedNodeViews(a, b) {
33
33
  nextProps;
34
34
  prevState;
35
35
  _destroyed;
36
+ deferPendingEffects;
36
37
  constructor(place, props){
37
38
  // Prevent the base class from destroying the React-managed nodes.
38
39
  // Restore them below after invoking the base class constructor.
@@ -85,6 +86,7 @@ function changedNodeViews(a, b) {
85
86
  // @ts-expect-error this violates the typing but class does it, too.
86
87
  this.docView = null;
87
88
  this._destroyed = false;
89
+ this.deferPendingEffects = false;
88
90
  }
89
91
  get props() {
90
92
  return this.nextProps;
@@ -51,13 +51,18 @@ const ChildView = /*#__PURE__*/ memo(function ChildView(param) {
51
51
  key: child.key
52
52
  }, (param)=>{
53
53
  let { siblingsRef, parentRef } = param;
54
- return /*#__PURE__*/ React.createElement(TextNodeView, {
55
- view: view,
56
- node: child.node,
57
- getPos: getPos,
58
- siblingsRef: siblingsRef,
59
- parentRef: parentRef,
60
- decorations: child.outerDeco
54
+ return /*#__PURE__*/ React.createElement(EditorContext.Consumer, null, (param)=>{
55
+ let { registerEventListener, unregisterEventListener } = param;
56
+ return /*#__PURE__*/ React.createElement(TextNodeView, {
57
+ view: view,
58
+ node: child.node,
59
+ getPos: getPos,
60
+ siblingsRef: siblingsRef,
61
+ parentRef: parentRef,
62
+ decorations: child.outerDeco,
63
+ registerEventListener: registerEventListener,
64
+ unregisterEventListener: unregisterEventListener
65
+ });
61
66
  });
62
67
  }) : /*#__PURE__*/ React.createElement(NodeView, {
63
68
  key: child.key,
@@ -340,14 +345,14 @@ export const ChildNodeViews = /*#__PURE__*/ memo(function ChildNodeViews(param)
340
345
  component: SeparatorHackView,
341
346
  marks: [],
342
347
  offset: lastChild?.offset ?? 0,
343
- index: (lastChild?.index ?? 0) + 2,
348
+ index: (lastChild?.index ?? 0) + 1,
344
349
  key: "trailing-hack-img"
345
350
  }, {
346
351
  type: "hack",
347
352
  component: TrailingHackView,
348
353
  marks: [],
349
354
  offset: lastChild?.offset ?? 0,
350
- index: (lastChild?.index ?? 0) + 1,
355
+ index: (lastChild?.index ?? 0) + 2,
351
356
  key: "trailing-hack-br"
352
357
  });
353
358
  }
@@ -1,8 +1,9 @@
1
- import React, { forwardRef, useImperativeHandle, useRef } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
2
2
  import { domIndex } from "../dom.js";
3
3
  import { useEditorEffect } from "../hooks/useEditorEffect.js";
4
4
  export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(param, ref) {
5
5
  let { widget, getPos, ...props } = param;
6
+ const [shouldRender, setShouldRender] = useState(true);
6
7
  const innerRef = useRef(null);
7
8
  useImperativeHandle(ref, ()=>{
8
9
  return innerRef.current;
@@ -14,22 +15,18 @@ export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(par
14
15
  // @ts-expect-error Internal property - domSelection
15
16
  const domSel = view.domSelection();
16
17
  const node = innerRef.current;
17
- const img = node.nodeName == "IMG";
18
- if (img) {
19
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20
- domSel.collapse(node.parentNode, domIndex(node) + 1);
21
- } else {
22
- domSel.collapse(node, 0);
23
- }
18
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19
+ domSel.collapse(node.parentNode, domIndex(node) + 1);
24
20
  // @ts-expect-error Internal property - domObserver
25
21
  view.domObserver.connectSelection();
22
+ setShouldRender(false);
26
23
  }, []);
27
- return /*#__PURE__*/ React.createElement("img", {
24
+ return shouldRender ? /*#__PURE__*/ React.createElement("img", {
28
25
  ref: innerRef,
29
26
  className: "ProseMirror-separator",
30
27
  // eslint-disable-next-line react/no-unknown-property
31
28
  "mark-placeholder": "true",
32
29
  alt: "",
33
30
  ...props
34
- });
31
+ }) : null;
35
32
  });
@@ -1,5 +1,7 @@
1
+ import { TextSelection } from "prosemirror-state";
1
2
  import { DecorationSet } from "prosemirror-view";
2
3
  import { Component } from "react";
4
+ import { ReactEditorView } from "../ReactEditorView.js";
3
5
  import { findDOMNode } from "../findDOMNode.js";
4
6
  import { CompositionViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
5
7
  import { wrapInDeco } from "./ChildNodeViews.js";
@@ -28,72 +30,141 @@ function shallowEqual(objA, objB) {
28
30
  export class TextNodeView extends Component {
29
31
  viewDescRef = null;
30
32
  renderRef = null;
31
- updateEffect() {
33
+ wasProtecting = false;
34
+ containsCompositionNodeText = true;
35
+ // This is basically NodeViewDesc.localCompositionInfo
36
+ // from prosemirror-view. It's been slightly adjusted so that
37
+ // it can be used accurately during render, before we've
38
+ // necessarily found (or even let the browser create)
39
+ // view.input.compositionNode
40
+ shouldProtect(props) {
41
+ const { view, getPos, node } = props;
42
+ if (!(view instanceof ReactEditorView)) return false;
43
+ if (!view.composing) {
44
+ return false;
45
+ }
46
+ const pos = getPos();
47
+ const { from, to } = view.state.selection;
48
+ if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + node.nodeSize) {
49
+ return false;
50
+ }
51
+ return this.containsCompositionNodeText;
52
+ }
53
+ handleCompositionEnd = ()=>{
54
+ if (!this.wasProtecting) return;
55
+ this.forceUpdate();
56
+ return;
57
+ };
58
+ create() {
32
59
  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
60
  const dom = findDOMNode(this);
36
- // We only need to explicitly create a CompositionViewDesc
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
- }
61
+ if (!dom && !view.composing) return null;
49
62
  let textNode = dom;
50
- while(textNode.firstChild){
63
+ while(textNode?.firstChild){
51
64
  textNode = textNode.firstChild;
52
65
  }
53
- if (!this.viewDescRef || this.viewDescRef instanceof CompositionViewDesc) {
54
- this.viewDescRef = new TextViewDesc(undefined, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
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;
66
+ if (!(textNode instanceof Text)) {
67
+ textNode = null;
64
68
  }
65
- if (!siblingsRef.current.includes(this.viewDescRef)) {
66
- siblingsRef.current.push(this.viewDescRef);
69
+ let viewDesc;
70
+ if (this.shouldProtect(this.props)) {
71
+ viewDesc = new CompositionViewDesc(parentRef.current, getPos, // If we can't
72
+ // actually find the correct DOM nodes from here (
73
+ // which is the case in a composition in a newly
74
+ // created text node), we let our parent do it.
75
+ // Passing a valid element here just so that the
76
+ // ViewDesc constructor doesn't blow up.
77
+ dom ?? document.createElement("div"), textNode ?? document.createTextNode(node.text ?? ""), node.text ?? "");
78
+ } else {
79
+ if (!dom || !textNode) return null;
80
+ viewDesc = new TextViewDesc(parentRef.current, [], getPos, node, decorations, DecorationSet.empty, dom, textNode);
67
81
  }
82
+ siblingsRef.current.push(viewDesc);
68
83
  siblingsRef.current.sort(sortViewDescs);
84
+ return viewDesc;
85
+ }
86
+ update() {
87
+ const { view, node, decorations } = this.props;
88
+ if (!(view instanceof ReactEditorView)) return false;
89
+ const viewDesc = this.viewDescRef;
90
+ if (!viewDesc) return false;
91
+ if (this.shouldProtect(this.props) !== viewDesc instanceof CompositionViewDesc) {
92
+ return false;
93
+ }
94
+ if (viewDesc instanceof CompositionViewDesc) return false;
95
+ const dom = findDOMNode(this);
96
+ if (!dom || dom !== viewDesc.dom) return false;
97
+ if (!dom.contains(viewDesc.nodeDOM)) return false;
98
+ return viewDesc.matchesNode(node, decorations, DecorationSet.empty) || viewDesc.update(node, decorations, DecorationSet.empty, view);
99
+ }
100
+ destroy() {
101
+ const viewDesc = this.viewDescRef;
102
+ if (!viewDesc) return;
103
+ viewDesc.destroy();
104
+ const siblings = this.props.siblingsRef.current;
105
+ if (siblings.includes(viewDesc)) {
106
+ const index = siblings.indexOf(viewDesc);
107
+ siblings.splice(index, 1);
108
+ }
109
+ }
110
+ updateEffect() {
111
+ if (!this.update()) {
112
+ this.destroy();
113
+ this.viewDescRef = this.create();
114
+ }
115
+ const { view, node } = this.props;
116
+ if (!(view instanceof ReactEditorView)) {
117
+ this.containsCompositionNodeText = true;
118
+ return;
119
+ }
120
+ const textNode = view.input.compositionNode;
121
+ if (!textNode) {
122
+ this.containsCompositionNodeText = true;
123
+ return;
124
+ }
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;
69
129
  }
70
130
  shouldComponentUpdate(nextProps) {
131
+ // When leaving the protected state, force a re-render so React's
132
+ // virtual DOM resyncs with whatever the IME wrote into the real DOM
133
+ // while we were returning a stale renderRef.
134
+ if (this.wasProtecting && !this.shouldProtect(nextProps)) {
135
+ return true;
136
+ }
71
137
  return !shallowEqual(this.props, nextProps);
72
138
  }
73
139
  componentDidMount() {
140
+ this.viewDescRef = null;
141
+ // After a composition, force an update so that we re-check whether we need
142
+ // to be protecting our rendered content and allow React to re-sync with the
143
+ // DOM.
144
+ const { registerEventListener } = this.props;
145
+ registerEventListener("compositionend", this.handleCompositionEnd);
74
146
  this.updateEffect();
75
147
  }
76
148
  componentDidUpdate() {
77
149
  this.updateEffect();
78
150
  }
79
151
  componentWillUnmount() {
80
- const { siblingsRef } = this.props;
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
- }
152
+ const { unregisterEventListener } = this.props;
153
+ unregisterEventListener("compositionend", this.handleCompositionEnd);
154
+ this.destroy();
86
155
  }
87
156
  render() {
88
- const { view, getPos, node, decorations } = this.props;
157
+ const { node, decorations } = this.props;
89
158
  // During a composition, it's crucial that we don't try to
90
159
  // update the DOM that the user is working in. If there's
91
160
  // an active composition and the selection is in this node,
92
161
  // we freeze the DOM of this element so that it doesn't
93
162
  // interrupt the composition
94
- if (view.composing && view.state.selection.from >= getPos() && view.state.selection.from <= getPos() + node.nodeSize) {
163
+ if (this.shouldProtect(this.props)) {
164
+ this.wasProtecting = true;
95
165
  return this.renderRef;
96
166
  }
167
+ this.wasProtecting = false;
97
168
  this.renderRef = decorations.reduce(wrapInDeco, node.text);
98
169
  return this.renderRef;
99
170
  }
@@ -1,9 +1,12 @@
1
- import React, { useContext, useRef } from "react";
1
+ import React, { useContext, useRef, useState } from "react";
2
2
  import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
3
3
  import { useClientLayoutEffect } from "../hooks/useClientLayoutEffect.js";
4
+ import { useEditorEffect } from "../hooks/useEditorEffect.js";
5
+ import { useEditorEventListener } from "../hooks/useEditorEventListener.js";
4
6
  import { TrailingHackViewDesc, sortViewDescs } from "../viewdesc.js";
5
7
  export function TrailingHackView(param) {
6
8
  let { getPos } = param;
9
+ const [shouldRender, setShouldRender] = useState(true);
7
10
  const { siblingsRef, parentRef } = useContext(ChildDescriptionsContext);
8
11
  const viewDescRef = useRef(null);
9
12
  const ref = useRef(null);
@@ -32,6 +35,32 @@ export function TrailingHackView(param) {
32
35
  }
33
36
  siblingsRef.current.sort(sortViewDescs);
34
37
  });
38
+ // At the start of a composition, the browser will automatically delete
39
+ // the trailing hack br element. We need to unmount ourselves _before_
40
+ // that happens, so that React doesn't try to remove the already-removed
41
+ // br node when this component gets unmounted
42
+ useEditorEventListener("compositionstart", (view)=>{
43
+ const { from } = view.state.selection;
44
+ if (from === getPos()) {
45
+ setShouldRender(false);
46
+ }
47
+ });
48
+ // We need to run the same composition check when we first get mounted,
49
+ // in case we got mounted in the same render batch as the beginning of
50
+ // a composition
51
+ useEditorEffect((view)=>{
52
+ if (!view.composing) return;
53
+ const { from } = view.state.selection;
54
+ if (from === getPos()) {
55
+ setShouldRender(false);
56
+ }
57
+ }, [
58
+ getPos
59
+ ]);
60
+ useEditorEventListener("compositionend", ()=>{
61
+ setShouldRender(true);
62
+ });
63
+ if (!shouldRender) return null;
35
64
  return /*#__PURE__*/ React.createElement("br", {
36
65
  ref: ref,
37
66
  className: "ProseMirror-trailingBreak"
@@ -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,19 +14,27 @@ import { componentEventListeners } from "../plugins/componentEventListeners.js";
14
14
  * @privateRemarks
15
15
  *
16
16
  * This hook uses a combination of mutable and immutable updates to give
17
- * us precise control over when we re-create the 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() {
29
- const [registry, setRegistry] = useState(new Map());
28
+ */ export function useComponentEventListeners(existingHandlers) {
29
+ const [registry, setRegistry] = useState(new Map(Object.entries(existingHandlers ?? {}).map((param)=>{
30
+ let [eventName, handler] = param;
31
+ return [
32
+ eventName,
33
+ handler ? [
34
+ handler
35
+ ] : []
36
+ ];
37
+ })));
30
38
  const registerEventListener = useCallback((eventType, handler)=>{
31
39
  const handlers = registry.get(eventType) ?? [];
32
40
  handlers.unshift(handler);
@@ -43,12 +51,45 @@ import { componentEventListeners } from "../plugins/componentEventListeners.js";
43
51
  }, [
44
52
  registry
45
53
  ]);
46
- const componentEventListenersPlugin = useMemo(()=>componentEventListeners(registry), [
54
+ useLayoutEffect(()=>{
55
+ if (!existingHandlers) return;
56
+ for (const [eventType, handler] of Object.entries(existingHandlers)){
57
+ if (!handler) return;
58
+ registerEventListener(eventType, handler);
59
+ }
60
+ return ()=>{
61
+ for (const [eventType, handler] of Object.entries(existingHandlers)){
62
+ if (!handler) return;
63
+ unregisterEventListener(eventType, handler);
64
+ }
65
+ };
66
+ }, [
67
+ existingHandlers,
68
+ registerEventListener,
69
+ unregisterEventListener
70
+ ]);
71
+ const handleDOMEvents = useMemo(()=>{
72
+ const domEventHandlers = {};
73
+ for (const [eventType, handlers] of registry.entries()){
74
+ function handleEvent(view, event) {
75
+ for (const handler of handlers){
76
+ let handled = false;
77
+ batch(()=>{
78
+ handled = !!handler(view, event);
79
+ });
80
+ if (handled || event.defaultPrevented) return true;
81
+ }
82
+ return false;
83
+ }
84
+ domEventHandlers[eventType] = handleEvent;
85
+ }
86
+ return domEventHandlers;
87
+ }, [
47
88
  registry
48
89
  ]);
49
90
  return {
50
91
  registerEventListener,
51
92
  unregisterEventListener,
52
- componentEventListenersPlugin
93
+ handleDOMEvents
53
94
  };
54
95
  }
@@ -30,7 +30,7 @@ let didWarnValueDefaultValue = false;
30
30
  const defaultState = options.defaultState ?? EMPTY_STATE;
31
31
  const [_state, setState] = useState(defaultState);
32
32
  const state = options.state ?? _state;
33
- const { 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);
@@ -101,8 +100,6 @@ let didWarnValueDefaultValue = false;
101
100
  // running effects. Running effects will reattach selection
102
101
  // change listeners if the EditorView has been destroyed.
103
102
  if (view instanceof ReactEditorView && !view.isDestroyed) {
104
- // Plugins might dispatch transactions from their
105
- // view update lifecycle hooks
106
103
  flushSyncRef.current = false;
107
104
  view.commitPendingEffects();
108
105
  flushSyncRef.current = true;
@@ -1,8 +1,10 @@
1
1
  import { useCallback, useContext, useMemo, useRef } from "react";
2
2
  import { ReactEditorView } from "../ReactEditorView.js";
3
+ import { CursorWrapper } from "../components/CursorWrapper.js";
3
4
  import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
4
5
  import { EditorContext } from "../contexts/EditorContext.js";
5
- import { CompositionViewDesc, ReactNodeViewDesc, sortViewDescs } from "../viewdesc.js";
6
+ import { ReactWidgetType } from "../decorations/ReactWidgetType.js";
7
+ import { CompositionViewDesc, MarkViewDesc, ReactNodeViewDesc, WidgetViewDesc, sortViewDescs } from "../viewdesc.js";
6
8
  import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
7
9
  import { useEffectEvent } from "./useEffectEvent.js";
8
10
  export function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
@@ -131,25 +133,44 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
131
133
  children.sort(sortViewDescs);
132
134
  for (const child of children){
133
135
  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;
136
+ }
137
+ if (!props.node.isTextblock) return;
138
+ // Because TextNodeViews can't locate the DOM nodes
139
+ // for compositions, we need to override them here
140
+ if (!viewDescRef.current?.contentDOM) return;
141
+ const compositionChildIndex = children.findIndex((child)=>child instanceof CompositionViewDesc);
142
+ if (compositionChildIndex === -1) return;
143
+ const compositionViewDesc = children[compositionChildIndex];
144
+ if (!(compositionViewDesc instanceof CompositionViewDesc)) return;
145
+ let compositionTopDOM = null;
146
+ let search = children[compositionChildIndex - 1];
147
+ while(search instanceof MarkViewDesc){
148
+ search = search.children[0];
149
+ }
150
+ if (search instanceof WidgetViewDesc && search.widget.type instanceof ReactWidgetType && search.widget.type.Component === CursorWrapper) {
151
+ compositionTopDOM = search.dom.nextSibling;
152
+ } else {
153
+ for (const childNode of viewDescRef.current.contentDOM.childNodes){
154
+ if (children.every((child)=>child.dom !== childNode)) {
155
+ compositionTopDOM = childNode;
156
+ break;
142
157
  }
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
158
  }
152
159
  }
160
+ if (!compositionTopDOM) return;
161
+ let textDOM = compositionTopDOM;
162
+ while(textDOM.firstChild){
163
+ textDOM = textDOM.firstChild;
164
+ }
165
+ if (!textDOM || !(textDOM instanceof Text)) {
166
+ console.error(compositionTopDOM, textDOM);
167
+ throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
168
+ }
169
+ compositionViewDesc.dom = compositionTopDOM;
170
+ compositionViewDesc.textDOM = textDOM;
171
+ compositionViewDesc.text = textDOM.data;
172
+ compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
173
+ view.input.compositionNodes.push(compositionViewDesc);
153
174
  });
154
175
  const childContextValue = useMemo(()=>({
155
176
  parentRef: viewDescRef,