@handlewithcare/react-prosemirror 2.5.3 → 2.5.5

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 CHANGED
@@ -693,15 +693,14 @@ export function SelectionWidget() {
693
693
  ### `NodeViewComponentProps`
694
694
 
695
695
  ```tsx
696
- type NodeViewComponentProps = {
696
+ interface NodeViewComponentProps extends AllHTMLAttributes<HTMLElement> = {
697
697
  nodeProps: {
698
698
  decorations: readonly Decoration[];
699
699
  innerDecorations: DecorationSource;
700
700
  node: Node;
701
- children?: ReactNode | ReactNode[];
702
701
  getPos: () => number;
703
702
  };
704
- } & HTMLAttributes<HTMLElement>;
703
+ };
705
704
  ```
706
705
 
707
706
  The props that will be passed to all node view components. These props map
@@ -32,6 +32,7 @@ function changedNodeViews(a, b) {
32
32
  let ReactEditorView = class ReactEditorView extends _prosemirrorview.EditorView {
33
33
  nextProps;
34
34
  prevState;
35
+ _destroyed;
35
36
  constructor(place, props){
36
37
  // Prevent the base class from destroying the React-managed nodes.
37
38
  // Restore them below after invoking the base class constructor.
@@ -55,6 +56,16 @@ let ReactEditorView = class ReactEditorView extends _prosemirrorview.EditorView
55
56
  this.domObserver.stop();
56
57
  this.domObserver.observer = null;
57
58
  this.domObserver.queue = [];
59
+ const originalOnSelectionChange = this.domObserver.onSelectionChange;
60
+ this.domObserver.onSelectionChange = ()=>{
61
+ // During a composition, we completely pause React-driven
62
+ // selection and DOM updates. Compositions are "fragile";
63
+ // in Safari, even updating the selection to the same
64
+ // position it's already set to will end the current
65
+ // composition.
66
+ if (this.composing) return;
67
+ originalOnSelectionChange();
68
+ };
58
69
  } finally{
59
70
  place.mount.replaceChildren(...reactContent);
60
71
  for (const attr of place.mount.attributes){
@@ -73,10 +84,22 @@ let ReactEditorView = class ReactEditorView extends _prosemirrorview.EditorView
73
84
  this.docView.destroy();
74
85
  // @ts-expect-error this violates the typing but class does it, too.
75
86
  this.docView = null;
87
+ this._destroyed = false;
76
88
  }
77
89
  get props() {
78
90
  return this.nextProps;
79
91
  }
92
+ /**
93
+ * @privateremarks
94
+ *
95
+ * We override this getter because the base implementation
96
+ * relies on checking `docView === null`, but we unconditionally
97
+ * set view.docView in a layout effect in the DocNodeView.
98
+ * This has the effect of "un-destroying" the EditorView,
99
+ * making it impossible to determine whether it's been destroyed.
100
+ */ get isDestroyed() {
101
+ return this._destroyed;
102
+ }
80
103
  setProps(props) {
81
104
  this.update({
82
105
  ...this.props,
@@ -142,6 +165,7 @@ let ReactEditorView = class ReactEditorView extends _prosemirrorview.EditorView
142
165
  super.destroy();
143
166
  } finally{
144
167
  this.dom.replaceChildren(...reactContent);
168
+ this._destroyed = true;
145
169
  }
146
170
  }
147
171
  /**
@@ -16,6 +16,7 @@ const _constants = require("../constants.js");
16
16
  const _beforeInputPlugin = require("../plugins/beforeInputPlugin.js");
17
17
  const _useClientLayoutEffect = require("./useClientLayoutEffect.js");
18
18
  const _useComponentEventListeners = require("./useComponentEventListeners.js");
19
+ const _useEffectEvent = require("./useEffectEvent.js");
19
20
  const _useForceUpdate = require("./useForceUpdate.js");
20
21
  let didWarnValueDefaultValue = false;
21
22
  function useEditor(mount, options) {
@@ -77,30 +78,31 @@ function useEditor(mount, options) {
77
78
  const [view, setView] = (0, _react.useState)(()=>{
78
79
  return new _StaticEditorView.StaticEditorView(directEditorProps);
79
80
  });
81
+ const createEditorView = (0, _useEffectEvent.useEffectEvent)((mount)=>{
82
+ if (mount) {
83
+ const view = new _ReactEditorView.ReactEditorView({
84
+ mount
85
+ }, directEditorProps);
86
+ view.dom.addEventListener("compositionend", forceUpdate);
87
+ return view;
88
+ }
89
+ return new _StaticEditorView.StaticEditorView(directEditorProps);
90
+ });
80
91
  (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
92
+ const view = createEditorView(mount);
93
+ setView(view);
81
94
  return ()=>{
82
95
  view.destroy();
83
96
  };
84
97
  }, [
85
- view
98
+ createEditorView,
99
+ mount
86
100
  ]);
87
- // This rule is concerned about infinite updates due to the
88
- // call to setView. These calls are deliberately conditional,
89
- // so this is not a concern.
90
- // eslint-disable-next-line react-hooks/exhaustive-deps
91
101
  (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
92
- if (mount !== view.dom) {
93
- if (mount) {
94
- const view = new _ReactEditorView.ReactEditorView({
95
- mount
96
- }, directEditorProps);
97
- view.dom.addEventListener("compositionend", forceUpdate);
98
- setView(view);
99
- } else {
100
- const view = new _StaticEditorView.StaticEditorView(directEditorProps);
101
- setView(view);
102
- }
103
- } else if (view instanceof _ReactEditorView.ReactEditorView) {
102
+ // Ensure that the EditorView hasn't been destroyed before
103
+ // running effects. Running effects will reattach selection
104
+ // change listeners if the EditorView has been destroyed.
105
+ if (view instanceof _ReactEditorView.ReactEditorView && !view.isDestroyed) {
104
106
  view.commitPendingEffects();
105
107
  }
106
108
  });
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "useEffectEvent", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return useEffectEvent;
9
+ }
10
+ });
11
+ const _react = require("react");
12
+ function useEffectEvent(fn) {
13
+ const ref = (0, _react.useRef)(fn);
14
+ // Ideally this would be a useInsertionEffect, but
15
+ // that was introduced in React 18 and we still
16
+ // support React 17. useLayoutEffect is safe
17
+ // here as long as the function returned by
18
+ // useEffectEvent isn't called in a layout effect
19
+ // that's defined _before_ the useEffectEvent
20
+ // call, and effect events are never passed
21
+ // to child components.
22
+ (0, _react.useLayoutEffect)(()=>{
23
+ ref.current = fn;
24
+ }, [
25
+ fn
26
+ ]);
27
+ return (0, _react.useCallback)(function() {
28
+ for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
29
+ args[_key] = arguments[_key];
30
+ }
31
+ const f = ref.current;
32
+ return f(...args);
33
+ }, []);
34
+ }
@@ -14,6 +14,7 @@ const _ChildDescriptorsContext = require("../contexts/ChildDescriptorsContext.js
14
14
  const _EditorContext = require("../contexts/EditorContext.js");
15
15
  const _viewdesc = require("../viewdesc.js");
16
16
  const _useClientLayoutEffect = require("./useClientLayoutEffect.js");
17
+ const _useEffectEvent = require("./useEffectEvent.js");
17
18
  function findContentDOM(source, children) {
18
19
  return source?.contentDOM ?? children[0]?.dom?.parentElement ?? null;
19
20
  }
@@ -25,7 +26,7 @@ function useNodeViewDescriptor(ref, constructor, props) {
25
26
  const [contentDOM, setContentDOM] = (0, _react.useState)(null);
26
27
  const viewDescRef = (0, _react.useRef)();
27
28
  const childrenRef = (0, _react.useRef)([]);
28
- const create = (0, _react.useCallback)((props)=>{
29
+ const create = (0, _useEffectEvent.useEffectEvent)(()=>{
29
30
  if (!(view instanceof _ReactEditorView.ReactEditorView)) {
30
31
  return;
31
32
  }
@@ -47,13 +48,8 @@ function useNodeViewDescriptor(ref, constructor, props) {
47
48
  setContentDOM(contentDOM);
48
49
  setNodeDOM(nodeDOM);
49
50
  return viewDesc;
50
- }, [
51
- ref,
52
- parentRef,
53
- constructor,
54
- view
55
- ]);
56
- const update = (0, _react.useCallback)((props)=>{
51
+ });
52
+ const update = (0, _useEffectEvent.useEffectEvent)(()=>{
57
53
  if (!(view instanceof _ReactEditorView.ReactEditorView)) {
58
54
  return false;
59
55
  }
@@ -74,11 +70,8 @@ function useNodeViewDescriptor(ref, constructor, props) {
74
70
  }
75
71
  const { node, decorations, innerDecorations } = props;
76
72
  return viewDesc.matchesNode(node, decorations, innerDecorations) || viewDesc.update(node, decorations, innerDecorations, view);
77
- }, [
78
- ref,
79
- view
80
- ]);
81
- const destroy = (0, _react.useCallback)(()=>{
73
+ });
74
+ const destroy = (0, _useEffectEvent.useEffectEvent)(()=>{
82
75
  const viewDesc = viewDescRef.current;
83
76
  if (!viewDesc) {
84
77
  return;
@@ -92,13 +85,20 @@ function useNodeViewDescriptor(ref, constructor, props) {
92
85
  setDOM(null);
93
86
  setContentDOM(null);
94
87
  setNodeDOM(null);
88
+ });
89
+ (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
90
+ viewDescRef.current = create();
91
+ return ()=>{
92
+ destroy();
93
+ };
95
94
  }, [
96
- siblingsRef
95
+ create,
96
+ destroy
97
97
  ]);
98
98
  (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
99
- if (!update(props)) {
99
+ if (!update()) {
100
100
  destroy();
101
- viewDescRef.current = create(props);
101
+ viewDescRef.current = create();
102
102
  }
103
103
  const viewDesc = viewDescRef.current;
104
104
  if (!viewDesc) {
@@ -137,14 +137,6 @@ function useNodeViewDescriptor(ref, constructor, props) {
137
137
  }
138
138
  }
139
139
  });
140
- (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
141
- return ()=>{
142
- destroy();
143
- viewDescRef.current = undefined;
144
- };
145
- }, [
146
- destroy
147
- ]);
148
140
  const childContextValue = (0, _react.useMemo)(()=>({
149
141
  parentRef: viewDescRef,
150
142
  siblingsRef: childrenRef
@@ -32,6 +32,7 @@ function changedNodeViews(a, b) {
32
32
  */ export class ReactEditorView extends EditorView {
33
33
  nextProps;
34
34
  prevState;
35
+ _destroyed;
35
36
  constructor(place, props){
36
37
  // Prevent the base class from destroying the React-managed nodes.
37
38
  // Restore them below after invoking the base class constructor.
@@ -55,6 +56,16 @@ function changedNodeViews(a, b) {
55
56
  this.domObserver.stop();
56
57
  this.domObserver.observer = null;
57
58
  this.domObserver.queue = [];
59
+ const originalOnSelectionChange = this.domObserver.onSelectionChange;
60
+ this.domObserver.onSelectionChange = ()=>{
61
+ // During a composition, we completely pause React-driven
62
+ // selection and DOM updates. Compositions are "fragile";
63
+ // in Safari, even updating the selection to the same
64
+ // position it's already set to will end the current
65
+ // composition.
66
+ if (this.composing) return;
67
+ originalOnSelectionChange();
68
+ };
58
69
  } finally{
59
70
  place.mount.replaceChildren(...reactContent);
60
71
  for (const attr of place.mount.attributes){
@@ -73,10 +84,22 @@ function changedNodeViews(a, b) {
73
84
  this.docView.destroy();
74
85
  // @ts-expect-error this violates the typing but class does it, too.
75
86
  this.docView = null;
87
+ this._destroyed = false;
76
88
  }
77
89
  get props() {
78
90
  return this.nextProps;
79
91
  }
92
+ /**
93
+ * @privateremarks
94
+ *
95
+ * We override this getter because the base implementation
96
+ * relies on checking `docView === null`, but we unconditionally
97
+ * set view.docView in a layout effect in the DocNodeView.
98
+ * This has the effect of "un-destroying" the EditorView,
99
+ * making it impossible to determine whether it's been destroyed.
100
+ */ get isDestroyed() {
101
+ return this._destroyed;
102
+ }
80
103
  setProps(props) {
81
104
  this.update({
82
105
  ...this.props,
@@ -142,6 +165,7 @@ function changedNodeViews(a, b) {
142
165
  super.destroy();
143
166
  } finally{
144
167
  this.dom.replaceChildren(...reactContent);
168
+ this._destroyed = true;
145
169
  }
146
170
  }
147
171
  /**
@@ -6,6 +6,7 @@ import { EMPTY_STATE } from "../constants.js";
6
6
  import { beforeInputPlugin } from "../plugins/beforeInputPlugin.js";
7
7
  import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
8
8
  import { useComponentEventListeners } from "./useComponentEventListeners.js";
9
+ import { useEffectEvent } from "./useEffectEvent.js";
9
10
  import { useForceUpdate } from "./useForceUpdate.js";
10
11
  let didWarnValueDefaultValue = false;
11
12
  /**
@@ -75,30 +76,31 @@ let didWarnValueDefaultValue = false;
75
76
  const [view, setView] = useState(()=>{
76
77
  return new StaticEditorView(directEditorProps);
77
78
  });
79
+ const createEditorView = useEffectEvent((mount)=>{
80
+ if (mount) {
81
+ const view = new ReactEditorView({
82
+ mount
83
+ }, directEditorProps);
84
+ view.dom.addEventListener("compositionend", forceUpdate);
85
+ return view;
86
+ }
87
+ return new StaticEditorView(directEditorProps);
88
+ });
78
89
  useClientLayoutEffect(()=>{
90
+ const view = createEditorView(mount);
91
+ setView(view);
79
92
  return ()=>{
80
93
  view.destroy();
81
94
  };
82
95
  }, [
83
- view
96
+ createEditorView,
97
+ mount
84
98
  ]);
85
- // This rule is concerned about infinite updates due to the
86
- // call to setView. These calls are deliberately conditional,
87
- // so this is not a concern.
88
- // eslint-disable-next-line react-hooks/exhaustive-deps
89
99
  useClientLayoutEffect(()=>{
90
- if (mount !== view.dom) {
91
- if (mount) {
92
- const view = new ReactEditorView({
93
- mount
94
- }, directEditorProps);
95
- view.dom.addEventListener("compositionend", forceUpdate);
96
- setView(view);
97
- } else {
98
- const view = new StaticEditorView(directEditorProps);
99
- setView(view);
100
- }
101
- } else if (view instanceof ReactEditorView) {
100
+ // Ensure that the EditorView hasn't been destroyed before
101
+ // running effects. Running effects will reattach selection
102
+ // change listeners if the EditorView has been destroyed.
103
+ if (view instanceof ReactEditorView && !view.isDestroyed) {
102
104
  view.commitPendingEffects();
103
105
  }
104
106
  });
@@ -0,0 +1,24 @@
1
+ import { useCallback, useLayoutEffect, useRef } from "react";
2
+ export function useEffectEvent(fn) {
3
+ const ref = useRef(fn);
4
+ // Ideally this would be a useInsertionEffect, but
5
+ // that was introduced in React 18 and we still
6
+ // support React 17. useLayoutEffect is safe
7
+ // here as long as the function returned by
8
+ // useEffectEvent isn't called in a layout effect
9
+ // that's defined _before_ the useEffectEvent
10
+ // call, and effect events are never passed
11
+ // to child components.
12
+ useLayoutEffect(()=>{
13
+ ref.current = fn;
14
+ }, [
15
+ fn
16
+ ]);
17
+ return useCallback(function() {
18
+ for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
19
+ args[_key] = arguments[_key];
20
+ }
21
+ const f = ref.current;
22
+ return f(...args);
23
+ }, []);
24
+ }
@@ -1,9 +1,10 @@
1
- import { useCallback, useContext, useMemo, useRef, useState } from "react";
1
+ import { useContext, useMemo, useRef, useState } from "react";
2
2
  import { ReactEditorView } from "../ReactEditorView.js";
3
3
  import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
4
4
  import { EditorContext } from "../contexts/EditorContext.js";
5
5
  import { CompositionViewDesc, ReactNodeViewDesc, sortViewDescs } from "../viewdesc.js";
6
6
  import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
7
+ import { useEffectEvent } from "./useEffectEvent.js";
7
8
  function findContentDOM(source, children) {
8
9
  return source?.contentDOM ?? children[0]?.dom?.parentElement ?? null;
9
10
  }
@@ -15,7 +16,7 @@ export function useNodeViewDescriptor(ref, constructor, props) {
15
16
  const [contentDOM, setContentDOM] = useState(null);
16
17
  const viewDescRef = useRef();
17
18
  const childrenRef = useRef([]);
18
- const create = useCallback((props)=>{
19
+ const create = useEffectEvent(()=>{
19
20
  if (!(view instanceof ReactEditorView)) {
20
21
  return;
21
22
  }
@@ -37,13 +38,8 @@ export function useNodeViewDescriptor(ref, constructor, props) {
37
38
  setContentDOM(contentDOM);
38
39
  setNodeDOM(nodeDOM);
39
40
  return viewDesc;
40
- }, [
41
- ref,
42
- parentRef,
43
- constructor,
44
- view
45
- ]);
46
- const update = useCallback((props)=>{
41
+ });
42
+ const update = useEffectEvent(()=>{
47
43
  if (!(view instanceof ReactEditorView)) {
48
44
  return false;
49
45
  }
@@ -64,11 +60,8 @@ export function useNodeViewDescriptor(ref, constructor, props) {
64
60
  }
65
61
  const { node, decorations, innerDecorations } = props;
66
62
  return viewDesc.matchesNode(node, decorations, innerDecorations) || viewDesc.update(node, decorations, innerDecorations, view);
67
- }, [
68
- ref,
69
- view
70
- ]);
71
- const destroy = useCallback(()=>{
63
+ });
64
+ const destroy = useEffectEvent(()=>{
72
65
  const viewDesc = viewDescRef.current;
73
66
  if (!viewDesc) {
74
67
  return;
@@ -82,13 +75,20 @@ export function useNodeViewDescriptor(ref, constructor, props) {
82
75
  setDOM(null);
83
76
  setContentDOM(null);
84
77
  setNodeDOM(null);
78
+ });
79
+ useClientLayoutEffect(()=>{
80
+ viewDescRef.current = create();
81
+ return ()=>{
82
+ destroy();
83
+ };
85
84
  }, [
86
- siblingsRef
85
+ create,
86
+ destroy
87
87
  ]);
88
88
  useClientLayoutEffect(()=>{
89
- if (!update(props)) {
89
+ if (!update()) {
90
90
  destroy();
91
- viewDescRef.current = create(props);
91
+ viewDescRef.current = create();
92
92
  }
93
93
  const viewDesc = viewDescRef.current;
94
94
  if (!viewDesc) {
@@ -127,14 +127,6 @@ export function useNodeViewDescriptor(ref, constructor, props) {
127
127
  }
128
128
  }
129
129
  });
130
- useClientLayoutEffect(()=>{
131
- return ()=>{
132
- destroy();
133
- viewDescRef.current = undefined;
134
- };
135
- }, [
136
- destroy
137
- ]);
138
130
  const childContextValue = useMemo(()=>({
139
131
  parentRef: viewDescRef,
140
132
  siblingsRef: childrenRef