@handlewithcare/react-prosemirror 2.2.3 → 2.3.0

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
@@ -59,6 +59,7 @@ yarn add @handlewithcare/react-prosemirror prosemirror-view@1.37.1 prosemirror-s
59
59
  - [`useEditorEffect`](#useeditoreffect-1)
60
60
  - [`NodeViewComponentProps`](#nodeviewcomponentprops)
61
61
  - [`useStopEvent`](#usestopevent)
62
+ - [`useIgnoreMutation`](#useignoremutation)
62
63
  - [`useSelectNode`](#useselectnode)
63
64
  - [`useIsNodeSelected`](#useisnodeselected)
64
65
  - [`widget`](#widget)
@@ -685,6 +686,16 @@ This hook can be used within a node view component to register a
685
686
  [stopEvent handler](https://prosemirror.net/docs/ref/#view.NodeView.stopEvent).
686
687
  Events for which this returns true are not handled by the editor.
687
688
 
689
+ ### `useIgnoreMutation`
690
+
691
+ ```tsx
692
+ type useIgnoreMutation = (stopEvent: (view: EditorView, mutation: ViewMutationRecord) => boolean): void
693
+ ```
694
+
695
+ This hook can be used within a node view component to register an
696
+ [ignoreMutation handler](https://prosemirror.net/docs/ref/#view.NodeView.ignoreMutation).
697
+ Mutations for which this returns true are not handled by the editor.
698
+
688
699
  ### `useSelectNode`
689
700
 
690
701
  ```tsx
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "CustomNodeView", {
8
8
  return CustomNodeView;
9
9
  }
10
10
  });
11
+ const _prosemirrorstate = require("prosemirror-state");
11
12
  const _react = /*#__PURE__*/ _interop_require_wildcard(require("react"));
12
13
  const _reactdom = require("react-dom");
13
14
  const _ChildDescriptorsContext = require("../contexts/ChildDescriptorsContext.js");
@@ -64,41 +65,72 @@ const CustomNodeView = /*#__PURE__*/ (0, _react.memo)(function CustomNodeView(pa
64
65
  const nodeDomRef = (0, _react.useRef)(null);
65
66
  const contentDomRef = (0, _react.useRef)(null);
66
67
  const getPosFunc = (0, _react.useRef)(()=>getPos.current()).current;
67
- // this is ill-conceived; should revisit
68
- const initialNode = (0, _react.useRef)(node);
69
- const initialOuterDeco = (0, _react.useRef)(outerDeco);
70
- const initialInnerDeco = (0, _react.useRef)(innerDeco);
68
+ const nodeRef = (0, _react.useRef)(node);
69
+ nodeRef.current = node;
70
+ const outerDecoRef = (0, _react.useRef)(outerDeco);
71
+ outerDecoRef.current = outerDeco;
72
+ const innerDecoRef = (0, _react.useRef)(innerDeco);
73
+ innerDecoRef.current = innerDeco;
71
74
  const customNodeViewRootRef = (0, _react.useRef)(null);
72
75
  const customNodeViewRef = (0, _react.useRef)(null);
73
76
  const shouldRender = (0, _useClientOnly.useClientOnly)();
77
+ // In Strict/Concurrent mode, layout effects can be destroyed/re-run
78
+ // independently of renders. We need to ensure that if the
79
+ // destructor that destroys the node view is called, we then recreate
80
+ // the node view when the layout effect is re-run.
74
81
  (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
75
- if (!customNodeViewRef.current || !customNodeViewRootRef.current || !shouldRender) return;
76
- const { dom } = customNodeViewRef.current;
77
- nodeDomRef.current = customNodeViewRootRef.current;
78
- customNodeViewRootRef.current.appendChild(dom);
82
+ if (!customNodeViewRef.current || !shouldRender) {
83
+ customNodeViewRef.current = customNodeView(nodeRef.current, // customNodeView will only be set if view is set, and we can only reach
84
+ // this line if customNodeView is set
85
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
86
+ view, getPosFunc, outerDecoRef.current, innerDecoRef.current);
87
+ if (customNodeViewRef.current.stopEvent) {
88
+ setStopEvent(customNodeViewRef.current.stopEvent.bind(customNodeViewRef.current));
89
+ }
90
+ if (customNodeViewRef.current.selectNode) {
91
+ setSelectNode(customNodeViewRef.current.selectNode.bind(customNodeViewRef.current), customNodeViewRef.current.deselectNode?.bind(customNodeViewRef.current) ?? (()=>{}));
92
+ }
93
+ if (customNodeViewRef.current.ignoreMutation) {
94
+ setIgnoreMutation(customNodeViewRef.current.ignoreMutation.bind(customNodeViewRef.current));
95
+ }
96
+ if (!customNodeViewRootRef.current) return;
97
+ const { dom } = customNodeViewRef.current;
98
+ nodeDomRef.current = customNodeViewRootRef.current;
99
+ customNodeViewRootRef.current.appendChild(dom);
100
+ // Layout effects can run multiple times — if this effect
101
+ // destroyed and recreated this node view, then we need to
102
+ // resync the selectNode state
103
+ if (view?.state.selection instanceof _prosemirrorstate.NodeSelection && view.state.selection.node === nodeRef.current) {
104
+ customNodeViewRef.current.selectNode?.();
105
+ }
106
+ }
107
+ const nodeView = customNodeViewRef.current;
79
108
  return ()=>{
80
- customNodeViewRef.current?.destroy?.();
109
+ nodeView.destroy?.();
110
+ customNodeViewRef.current = null;
81
111
  };
112
+ // setStopEvent, setSelectNodee, and setIgnoreMutation are all stable
113
+ // functions and don't need to be added to the dependencies. They also
114
+ // can't be, because they come from useNodeViewDescriptor, which
115
+ // _has_ to be called after this hook, so that the effects run
116
+ // in the correct order
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
118
  }, [
83
- customNodeViewRef,
84
- customNodeViewRootRef,
85
- nodeDomRef,
86
- shouldRender
119
+ customNodeView,
120
+ getPosFunc,
121
+ view
87
122
  ]);
88
123
  (0, _useClientLayoutEffect.useClientLayoutEffect)(()=>{
89
- if (!customNodeView || !customNodeViewRef.current || !shouldRender) return;
124
+ if (!customNodeView || !customNodeViewRef.current) return;
90
125
  const { destroy, update } = customNodeViewRef.current;
91
126
  const updated = update?.call(customNodeViewRef.current, node, outerDeco, innerDeco) ?? true;
92
127
  if (updated) return;
93
128
  destroy?.call(customNodeViewRef.current);
94
129
  if (!customNodeViewRootRef.current) return;
95
- initialNode.current = node;
96
- initialOuterDeco.current = outerDeco;
97
- initialInnerDeco.current = innerDeco;
98
- customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach
130
+ customNodeViewRef.current = customNodeView(nodeRef.current, // customNodeView will only be set if view is set, and we can only reach
99
131
  // this line if customNodeView is set
100
132
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101
- view, getPosFunc, initialOuterDeco.current, initialInnerDeco.current);
133
+ view, getPosFunc, outerDecoRef.current, innerDecoRef.current);
102
134
  const { dom } = customNodeViewRef.current;
103
135
  nodeDomRef.current = customNodeViewRootRef.current;
104
136
  customNodeViewRootRef.current.appendChild(dom);
@@ -109,16 +141,9 @@ const CustomNodeView = /*#__PURE__*/ (0, _react.memo)(function CustomNodeView(pa
109
141
  node,
110
142
  outerDeco,
111
143
  getPos,
112
- customNodeViewRef,
113
- customNodeViewRootRef,
114
- initialNode,
115
- initialOuterDeco,
116
- initialInnerDeco,
117
- nodeDomRef,
118
- shouldRender,
119
144
  getPosFunc
120
145
  ]);
121
- const { childDescriptors, nodeViewDescRef } = (0, _useNodeViewDescriptor.useNodeViewDescriptor)(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
146
+ const { childDescriptors, nodeViewDescRef, setStopEvent, setSelectNode, setIgnoreMutation } = (0, _useNodeViewDescriptor.useNodeViewDescriptor)(node, getPosFunc, domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
122
147
  const childContextValue = (0, _react.useMemo)(()=>({
123
148
  parentRef: nodeViewDescRef,
124
149
  siblingsRef: childDescriptors
@@ -127,11 +152,25 @@ const CustomNodeView = /*#__PURE__*/ (0, _react.memo)(function CustomNodeView(pa
127
152
  nodeViewDescRef
128
153
  ]);
129
154
  if (!shouldRender) return null;
155
+ // In order to render the correct element with the correct
156
+ // props below, we have to call the customNodeView in the
157
+ // render function here. We only do this once, and the
158
+ // results are stored in a ref but not actually appended
159
+ // to the DOM until a client effect
130
160
  if (!customNodeViewRef.current) {
131
- customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach
161
+ customNodeViewRef.current = customNodeView(nodeRef.current, // customNodeView will only be set if view is set, and we can only reach
132
162
  // this line if customNodeView is set
133
163
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
134
- view, ()=>getPos.current(), initialOuterDeco.current, initialInnerDeco.current);
164
+ view, ()=>getPos.current(), outerDecoRef.current, innerDecoRef.current);
165
+ if (customNodeViewRef.current.stopEvent) {
166
+ setStopEvent(customNodeViewRef.current.stopEvent.bind(customNodeViewRef.current));
167
+ }
168
+ if (customNodeViewRef.current.selectNode) {
169
+ setSelectNode(customNodeViewRef.current.selectNode.bind(customNodeViewRef.current), customNodeViewRef.current.deselectNode?.bind(customNodeViewRef.current) ?? (()=>{}));
170
+ }
171
+ if (customNodeViewRef.current.ignoreMutation) {
172
+ setIgnoreMutation(customNodeViewRef.current.ignoreMutation.bind(customNodeViewRef.current));
173
+ }
135
174
  }
136
175
  const { contentDOM } = customNodeViewRef.current;
137
176
  contentDomRef.current = contentDOM ?? null;
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "ReactNodeView", {
10
10
  });
11
11
  const _react = /*#__PURE__*/ _interop_require_wildcard(require("react"));
12
12
  const _ChildDescriptorsContext = require("../contexts/ChildDescriptorsContext.js");
13
+ const _IgnoreMutationContext = require("../contexts/IgnoreMutationContext.js");
13
14
  const _NodeViewContext = require("../contexts/NodeViewContext.js");
14
15
  const _SelectNodeContext = require("../contexts/SelectNodeContext.js");
15
16
  const _StopEventContext = require("../contexts/StopEventContext.js");
@@ -69,7 +70,7 @@ const ReactNodeView = /*#__PURE__*/ (0, _react.memo)(function ReactNodeView(para
69
70
  const outputSpec = (0, _react.useMemo)(()=>node.type.spec.toDOM?.(node), [
70
71
  node
71
72
  ]);
72
- const { hasContentDOM, childDescriptors, setStopEvent, setSelectNode, nodeViewDescRef } = (0, _useNodeViewDescriptor.useNodeViewDescriptor)(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
73
+ const { hasContentDOM, childDescriptors, setStopEvent, setSelectNode, setIgnoreMutation, nodeViewDescRef } = (0, _useNodeViewDescriptor.useNodeViewDescriptor)(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
73
74
  const finalProps = {
74
75
  ...props,
75
76
  ...!hasContentDOM && {
@@ -130,7 +131,9 @@ const ReactNodeView = /*#__PURE__*/ (0, _react.memo)(function ReactNodeView(para
130
131
  value: setSelectNode
131
132
  }, /*#__PURE__*/ _react.default.createElement(_StopEventContext.StopEventContext.Provider, {
132
133
  value: setStopEvent
134
+ }, /*#__PURE__*/ _react.default.createElement(_IgnoreMutationContext.IgnoreMutationContext.Provider, {
135
+ value: setIgnoreMutation
133
136
  }, /*#__PURE__*/ _react.default.createElement(_ChildDescriptorsContext.ChildDescriptorsContext.Provider, {
134
137
  value: childContextValue
135
- }, decoratedElement)));
138
+ }, decoratedElement))));
136
139
  });
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "IgnoreMutationContext", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return IgnoreMutationContext;
9
+ }
10
+ });
11
+ const _react = require("react");
12
+ const IgnoreMutationContext = (0, _react.createContext)(null);
@@ -9,10 +9,11 @@ Object.defineProperty(exports, "useClientOnly", {
9
9
  }
10
10
  });
11
11
  const _react = require("react");
12
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
13
+ function unsubscribe() {}
14
+ function subscribe() {
15
+ return unsubscribe;
16
+ }
12
17
  function useClientOnly() {
13
- const [render, setRender] = (0, _react.useState)(false);
14
- (0, _react.useEffect)(()=>{
15
- setRender(true);
16
- }, []);
17
- return render;
18
+ return (0, _react.useSyncExternalStore)(subscribe, ()=>true, ()=>false);
18
19
  }
@@ -229,7 +229,7 @@ function useEditor(mount, options) {
229
229
  cleanup();
230
230
  const docViewDescRef = (0, _react.useRef)(new _viewdesc.NodeViewDesc(undefined, [], ()=>-1, state.doc, [], _prosemirrorview.DecorationSet.empty, tempDom, null, tempDom, ()=>false, ()=>{
231
231
  /* The doc node can't have a node selection*/ }, ()=>{
232
- /* The doc node can't have a node selection*/ }));
232
+ /* The doc node can't have a node selection*/ }, ()=>false));
233
233
  const directEditorProps = {
234
234
  ...options,
235
235
  state,
@@ -23,10 +23,12 @@ function useEditorEventCallback(callback) {
23
23
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
24
24
  args[_key] = arguments[_key];
25
25
  }
26
- if (view) {
27
- return ref.current(view, ...args);
28
- }
29
- return;
26
+ // It's not actually possible for an event handler to run
27
+ // while view is null, since view is only ever set to
28
+ // null in a layout effect that then immediately triggers
29
+ // a re-render which sets view to a new EditorView
30
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
31
+ return ref.current(view, ...args);
30
32
  }, [
31
33
  view
32
34
  ]);
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "useIgnoreMutation", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return useIgnoreMutation;
9
+ }
10
+ });
11
+ const _react = require("react");
12
+ const _IgnoreMutationContext = require("../contexts/IgnoreMutationContext.js");
13
+ const _useEditorEffect = require("./useEditorEffect.js");
14
+ const _useEditorEventCallback = require("./useEditorEventCallback.js");
15
+ function useIgnoreMutation(ignoreMutation) {
16
+ const register = (0, _react.useContext)(_IgnoreMutationContext.IgnoreMutationContext);
17
+ const ignoreMutationMemo = (0, _useEditorEventCallback.useEditorEventCallback)(ignoreMutation);
18
+ (0, _useEditorEffect.useEditorEffect)(()=>{
19
+ register(ignoreMutationMemo);
20
+ }, [
21
+ register,
22
+ ignoreMutationMemo
23
+ ]);
24
+ }
@@ -21,6 +21,10 @@ function useNodeViewDescriptor(node, getPos, domRef, nodeDomRef, innerDecoration
21
21
  const setStopEvent = (0, _react.useCallback)((newStopEvent)=>{
22
22
  stopEvent.current = newStopEvent;
23
23
  }, []);
24
+ const ignoreMutation = (0, _react.useRef)(()=>false);
25
+ const setIgnoreMutation = (0, _react.useCallback)((newIgnoreMutation)=>{
26
+ ignoreMutation.current = newIgnoreMutation;
27
+ }, []);
24
28
  const selectNode = (0, _react.useRef)(()=>{
25
29
  if (!nodeDomRef.current || !node) return;
26
30
  if (nodeDomRef.current.nodeType == 1) nodeDomRef.current.classList.add("ProseMirror-selectednode");
@@ -56,7 +60,7 @@ function useNodeViewDescriptor(node, getPos, domRef, nodeDomRef, innerDecoration
56
60
  if (!node || !nodeDomRef.current) return;
57
61
  const firstChildDesc = childDescriptors.current[0];
58
62
  if (!nodeViewDescRef.current) {
59
- nodeViewDescRef.current = new _viewdesc.NodeViewDesc(parentRef.current, childDescriptors.current, getPos, node, outerDecorations, innerDecorations, domRef?.current ?? nodeDomRef.current, firstChildDesc?.dom.parentElement ?? null, nodeDomRef.current, (event)=>!!stopEvent.current(event), ()=>selectNode.current(), ()=>deselectNode.current());
63
+ nodeViewDescRef.current = new _viewdesc.NodeViewDesc(parentRef.current, childDescriptors.current, getPos, node, outerDecorations, innerDecorations, domRef?.current ?? nodeDomRef.current, firstChildDesc?.dom.parentElement ?? null, nodeDomRef.current, (event)=>!!stopEvent.current(event), ()=>selectNode.current(), ()=>deselectNode.current(), (mutation)=>ignoreMutation.current(mutation));
60
64
  } else {
61
65
  nodeViewDescRef.current.parent = parentRef.current;
62
66
  nodeViewDescRef.current.children = childDescriptors.current;
@@ -111,6 +115,7 @@ function useNodeViewDescriptor(node, getPos, domRef, nodeDomRef, innerDecoration
111
115
  childDescriptors,
112
116
  nodeViewDescRef,
113
117
  setStopEvent,
114
- setSelectNode
118
+ setSelectNode,
119
+ setIgnoreMutation
115
120
  };
116
121
  }
package/dist/cjs/index.js CHANGED
@@ -31,6 +31,9 @@ _export(exports, {
31
31
  useEditorState: function() {
32
32
  return _useEditorState.useEditorState;
33
33
  },
34
+ useIgnoreMutation: function() {
35
+ return _useIgnoreMutation.useIgnoreMutation;
36
+ },
34
37
  useIsNodeSelected: function() {
35
38
  return _useIsNodeSelected.useIsNodeSelected;
36
39
  },
@@ -52,6 +55,7 @@ const _useEditorEventListener = require("./hooks/useEditorEventListener.js");
52
55
  const _useEditorState = require("./hooks/useEditorState.js");
53
56
  const _useStopEvent = require("./hooks/useStopEvent.js");
54
57
  const _useSelectNode = require("./hooks/useSelectNode.js");
58
+ const _useIgnoreMutation = require("./hooks/useIgnoreMutation.js");
55
59
  const _useIsNodeSelected = require("./hooks/useIsNodeSelected.js");
56
60
  const _reactKeys = require("./plugins/reactKeys.js");
57
61
  const _ReactWidgetType = require("./decorations/ReactWidgetType.js");
package/dist/cjs/props.js CHANGED
@@ -23,6 +23,7 @@ _export(exports, {
23
23
  }
24
24
  });
25
25
  const _classnames = /*#__PURE__*/ _interop_require_default(require("classnames"));
26
+ const _csstree = require("css-tree");
26
27
  function _interop_require_default(obj) {
27
28
  return obj && obj.__esModule ? obj : {
28
29
  default: obj
@@ -32,14 +33,16 @@ function kebabCaseToCamelCase(str) {
32
33
  return str.replaceAll(/-[a-z]/g, (g)=>g[1]?.toUpperCase() ?? "");
33
34
  }
34
35
  function cssToStyles(css) {
35
- const stylesheet = new CSSStyleSheet();
36
- stylesheet.insertRule(`* { ${css} }`);
37
- const insertedRule = stylesheet.cssRules[0];
38
- const declaration = insertedRule.style;
36
+ const ast = (0, _csstree.parse)(`* { ${css} }`);
37
+ if (ast.type !== "StyleSheet") return {};
38
+ const rule = ast.children.first;
39
+ if (rule?.type !== "Rule") return {};
40
+ const block = rule.block;
39
41
  const styles = {};
40
- for(let i = 0; i < declaration.length; i++){
41
- const property = declaration.item(i);
42
- const value = declaration.getPropertyValue(property);
42
+ for (const declaration of block.children){
43
+ if (declaration.type !== "Declaration") continue;
44
+ const property = declaration.property;
45
+ const value = declaration.value.type === "Raw" ? declaration.value.value : (0, _csstree.generate)(declaration.value);
43
46
  const camelCasePropertyName = property.startsWith("--") ? property : kebabCaseToCamelCase(property);
44
47
  styles[camelCasePropertyName] = value;
45
48
  }
@@ -551,8 +551,9 @@ let NodeViewDesc = class NodeViewDesc extends ViewDesc {
551
551
  stopEvent;
552
552
  selectNode;
553
553
  deselectNode;
554
- constructor(parent, children, getPos, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, stopEvent, selectNode, deselectNode){
555
- super(parent, children, getPos, dom, contentDOM), this.node = node, this.outerDeco = outerDeco, this.innerDeco = innerDeco, this.nodeDOM = nodeDOM, this.stopEvent = stopEvent, this.selectNode = selectNode, this.deselectNode = deselectNode;
554
+ ignoreMutation;
555
+ constructor(parent, children, getPos, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, stopEvent, selectNode, deselectNode, ignoreMutation){
556
+ super(parent, children, getPos, dom, contentDOM), this.node = node, this.outerDeco = outerDeco, this.innerDeco = innerDeco, this.nodeDOM = nodeDOM, this.stopEvent = stopEvent, this.selectNode = selectNode, this.deselectNode = deselectNode, this.ignoreMutation = ignoreMutation;
556
557
  }
557
558
  updateOuterDeco() {
558
559
  // pass
@@ -612,7 +613,9 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
612
613
  constructor(parent, children, getPos, node, outerDeco, innerDeco, dom, nodeDOM){
613
614
  super(parent, children, getPos, node, outerDeco, innerDeco, dom, null, nodeDOM, ()=>false, ()=>{
614
615
  /* Text nodes can't have node selections */ }, ()=>{
615
- /* Text nodes can't have node selections */ });
616
+ /* Text nodes can't have node selections */ }, (mutation)=>{
617
+ return mutation.type != "characterData" && mutation.type != "selection";
618
+ });
616
619
  }
617
620
  parseRule() {
618
621
  let skip = this.nodeDOM.parentNode;
@@ -639,9 +642,6 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
639
642
  if (dom == this.nodeDOM) return this.posAtStart + Math.min(offset, this.node.text.length);
640
643
  return super.localPosFromDOM(dom, offset, bias);
641
644
  }
642
- ignoreMutation(mutation) {
643
- return mutation.type != "characterData" && mutation.type != "selection";
644
- }
645
645
  markDirty(from, to) {
646
646
  super.markDirty(from, to);
647
647
  if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue.length)) this.dirty = NODE_DIRTY;
@@ -1,3 +1,4 @@
1
+ import { NodeSelection } from "prosemirror-state";
1
2
  import React, { cloneElement, createElement, memo, useContext, useMemo, useRef } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
  import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
@@ -13,41 +14,72 @@ export const CustomNodeView = /*#__PURE__*/ memo(function CustomNodeView(param)
13
14
  const nodeDomRef = useRef(null);
14
15
  const contentDomRef = useRef(null);
15
16
  const getPosFunc = useRef(()=>getPos.current()).current;
16
- // this is ill-conceived; should revisit
17
- const initialNode = useRef(node);
18
- const initialOuterDeco = useRef(outerDeco);
19
- const initialInnerDeco = useRef(innerDeco);
17
+ const nodeRef = useRef(node);
18
+ nodeRef.current = node;
19
+ const outerDecoRef = useRef(outerDeco);
20
+ outerDecoRef.current = outerDeco;
21
+ const innerDecoRef = useRef(innerDeco);
22
+ innerDecoRef.current = innerDeco;
20
23
  const customNodeViewRootRef = useRef(null);
21
24
  const customNodeViewRef = useRef(null);
22
25
  const shouldRender = useClientOnly();
26
+ // In Strict/Concurrent mode, layout effects can be destroyed/re-run
27
+ // independently of renders. We need to ensure that if the
28
+ // destructor that destroys the node view is called, we then recreate
29
+ // the node view when the layout effect is re-run.
23
30
  useClientLayoutEffect(()=>{
24
- if (!customNodeViewRef.current || !customNodeViewRootRef.current || !shouldRender) return;
25
- const { dom } = customNodeViewRef.current;
26
- nodeDomRef.current = customNodeViewRootRef.current;
27
- customNodeViewRootRef.current.appendChild(dom);
31
+ if (!customNodeViewRef.current || !shouldRender) {
32
+ customNodeViewRef.current = customNodeView(nodeRef.current, // customNodeView will only be set if view is set, and we can only reach
33
+ // this line if customNodeView is set
34
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
35
+ view, getPosFunc, outerDecoRef.current, innerDecoRef.current);
36
+ if (customNodeViewRef.current.stopEvent) {
37
+ setStopEvent(customNodeViewRef.current.stopEvent.bind(customNodeViewRef.current));
38
+ }
39
+ if (customNodeViewRef.current.selectNode) {
40
+ setSelectNode(customNodeViewRef.current.selectNode.bind(customNodeViewRef.current), customNodeViewRef.current.deselectNode?.bind(customNodeViewRef.current) ?? (()=>{}));
41
+ }
42
+ if (customNodeViewRef.current.ignoreMutation) {
43
+ setIgnoreMutation(customNodeViewRef.current.ignoreMutation.bind(customNodeViewRef.current));
44
+ }
45
+ if (!customNodeViewRootRef.current) return;
46
+ const { dom } = customNodeViewRef.current;
47
+ nodeDomRef.current = customNodeViewRootRef.current;
48
+ customNodeViewRootRef.current.appendChild(dom);
49
+ // Layout effects can run multiple times — if this effect
50
+ // destroyed and recreated this node view, then we need to
51
+ // resync the selectNode state
52
+ if (view?.state.selection instanceof NodeSelection && view.state.selection.node === nodeRef.current) {
53
+ customNodeViewRef.current.selectNode?.();
54
+ }
55
+ }
56
+ const nodeView = customNodeViewRef.current;
28
57
  return ()=>{
29
- customNodeViewRef.current?.destroy?.();
58
+ nodeView.destroy?.();
59
+ customNodeViewRef.current = null;
30
60
  };
61
+ // setStopEvent, setSelectNodee, and setIgnoreMutation are all stable
62
+ // functions and don't need to be added to the dependencies. They also
63
+ // can't be, because they come from useNodeViewDescriptor, which
64
+ // _has_ to be called after this hook, so that the effects run
65
+ // in the correct order
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
67
  }, [
32
- customNodeViewRef,
33
- customNodeViewRootRef,
34
- nodeDomRef,
35
- shouldRender
68
+ customNodeView,
69
+ getPosFunc,
70
+ view
36
71
  ]);
37
72
  useClientLayoutEffect(()=>{
38
- if (!customNodeView || !customNodeViewRef.current || !shouldRender) return;
73
+ if (!customNodeView || !customNodeViewRef.current) return;
39
74
  const { destroy, update } = customNodeViewRef.current;
40
75
  const updated = update?.call(customNodeViewRef.current, node, outerDeco, innerDeco) ?? true;
41
76
  if (updated) return;
42
77
  destroy?.call(customNodeViewRef.current);
43
78
  if (!customNodeViewRootRef.current) return;
44
- initialNode.current = node;
45
- initialOuterDeco.current = outerDeco;
46
- initialInnerDeco.current = innerDeco;
47
- customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach
79
+ customNodeViewRef.current = customNodeView(nodeRef.current, // customNodeView will only be set if view is set, and we can only reach
48
80
  // this line if customNodeView is set
49
81
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
50
- view, getPosFunc, initialOuterDeco.current, initialInnerDeco.current);
82
+ view, getPosFunc, outerDecoRef.current, innerDecoRef.current);
51
83
  const { dom } = customNodeViewRef.current;
52
84
  nodeDomRef.current = customNodeViewRootRef.current;
53
85
  customNodeViewRootRef.current.appendChild(dom);
@@ -58,16 +90,9 @@ export const CustomNodeView = /*#__PURE__*/ memo(function CustomNodeView(param)
58
90
  node,
59
91
  outerDeco,
60
92
  getPos,
61
- customNodeViewRef,
62
- customNodeViewRootRef,
63
- initialNode,
64
- initialOuterDeco,
65
- initialInnerDeco,
66
- nodeDomRef,
67
- shouldRender,
68
93
  getPosFunc
69
94
  ]);
70
- const { childDescriptors, nodeViewDescRef } = useNodeViewDescriptor(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
95
+ const { childDescriptors, nodeViewDescRef, setStopEvent, setSelectNode, setIgnoreMutation } = useNodeViewDescriptor(node, getPosFunc, domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
71
96
  const childContextValue = useMemo(()=>({
72
97
  parentRef: nodeViewDescRef,
73
98
  siblingsRef: childDescriptors
@@ -76,11 +101,25 @@ export const CustomNodeView = /*#__PURE__*/ memo(function CustomNodeView(param)
76
101
  nodeViewDescRef
77
102
  ]);
78
103
  if (!shouldRender) return null;
104
+ // In order to render the correct element with the correct
105
+ // props below, we have to call the customNodeView in the
106
+ // render function here. We only do this once, and the
107
+ // results are stored in a ref but not actually appended
108
+ // to the DOM until a client effect
79
109
  if (!customNodeViewRef.current) {
80
- customNodeViewRef.current = customNodeView(initialNode.current, // customNodeView will only be set if view is set, and we can only reach
110
+ customNodeViewRef.current = customNodeView(nodeRef.current, // customNodeView will only be set if view is set, and we can only reach
81
111
  // this line if customNodeView is set
82
112
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
83
- view, ()=>getPos.current(), initialOuterDeco.current, initialInnerDeco.current);
113
+ view, ()=>getPos.current(), outerDecoRef.current, innerDecoRef.current);
114
+ if (customNodeViewRef.current.stopEvent) {
115
+ setStopEvent(customNodeViewRef.current.stopEvent.bind(customNodeViewRef.current));
116
+ }
117
+ if (customNodeViewRef.current.selectNode) {
118
+ setSelectNode(customNodeViewRef.current.selectNode.bind(customNodeViewRef.current), customNodeViewRef.current.deselectNode?.bind(customNodeViewRef.current) ?? (()=>{}));
119
+ }
120
+ if (customNodeViewRef.current.ignoreMutation) {
121
+ setIgnoreMutation(customNodeViewRef.current.ignoreMutation.bind(customNodeViewRef.current));
122
+ }
84
123
  }
85
124
  const { contentDOM } = customNodeViewRef.current;
86
125
  contentDomRef.current = contentDOM ?? null;
@@ -1,5 +1,6 @@
1
1
  import React, { cloneElement, memo, useContext, useMemo, useRef } from "react";
2
2
  import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
3
+ import { IgnoreMutationContext } from "../contexts/IgnoreMutationContext.js";
3
4
  import { NodeViewContext } from "../contexts/NodeViewContext.js";
4
5
  import { SelectNodeContext } from "../contexts/SelectNodeContext.js";
5
6
  import { StopEventContext } from "../contexts/StopEventContext.js";
@@ -18,7 +19,7 @@ export const ReactNodeView = /*#__PURE__*/ memo(function ReactNodeView(param) {
18
19
  const outputSpec = useMemo(()=>node.type.spec.toDOM?.(node), [
19
20
  node
20
21
  ]);
21
- const { hasContentDOM, childDescriptors, setStopEvent, setSelectNode, nodeViewDescRef } = useNodeViewDescriptor(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
22
+ const { hasContentDOM, childDescriptors, setStopEvent, setSelectNode, setIgnoreMutation, nodeViewDescRef } = useNodeViewDescriptor(node, ()=>getPos.current(), domRef, nodeDomRef, innerDeco, outerDeco, undefined, contentDomRef);
22
23
  const finalProps = {
23
24
  ...props,
24
25
  ...!hasContentDOM && {
@@ -79,7 +80,9 @@ export const ReactNodeView = /*#__PURE__*/ memo(function ReactNodeView(param) {
79
80
  value: setSelectNode
80
81
  }, /*#__PURE__*/ React.createElement(StopEventContext.Provider, {
81
82
  value: setStopEvent
83
+ }, /*#__PURE__*/ React.createElement(IgnoreMutationContext.Provider, {
84
+ value: setIgnoreMutation
82
85
  }, /*#__PURE__*/ React.createElement(ChildDescriptorsContext.Provider, {
83
86
  value: childContextValue
84
- }, decoratedElement)));
87
+ }, decoratedElement))));
85
88
  });
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const IgnoreMutationContext = createContext(null);
@@ -1,8 +1,9 @@
1
- import { useEffect, useState } from "react";
1
+ import { useSyncExternalStore } from "react";
2
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
3
+ function unsubscribe() {}
4
+ function subscribe() {
5
+ return unsubscribe;
6
+ }
2
7
  export function useClientOnly() {
3
- const [render, setRender] = useState(false);
4
- useEffect(()=>{
5
- setRender(true);
6
- }, []);
7
- return render;
8
+ return useSyncExternalStore(subscribe, ()=>true, ()=>false);
8
9
  }
@@ -220,7 +220,7 @@ let didWarnValueDefaultValue = false;
220
220
  cleanup();
221
221
  const docViewDescRef = useRef(new NodeViewDesc(undefined, [], ()=>-1, state.doc, [], DecorationSet.empty, tempDom, null, tempDom, ()=>false, ()=>{
222
222
  /* The doc node can't have a node selection*/ }, ()=>{
223
- /* The doc node can't have a node selection*/ }));
223
+ /* The doc node can't have a node selection*/ }, ()=>false));
224
224
  const directEditorProps = {
225
225
  ...options,
226
226
  state,
@@ -25,10 +25,12 @@ import { useEditorEffect } from "./useEditorEffect.js";
25
25
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
26
26
  args[_key] = arguments[_key];
27
27
  }
28
- if (view) {
29
- return ref.current(view, ...args);
30
- }
31
- return;
28
+ // It's not actually possible for an event handler to run
29
+ // while view is null, since view is only ever set to
30
+ // null in a layout effect that then immediately triggers
31
+ // a re-render which sets view to a new EditorView
32
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
33
+ return ref.current(view, ...args);
32
34
  }, [
33
35
  view
34
36
  ]);
@@ -0,0 +1,14 @@
1
+ import { useContext } from "react";
2
+ import { IgnoreMutationContext } from "../contexts/IgnoreMutationContext.js";
3
+ import { useEditorEffect } from "./useEditorEffect.js";
4
+ import { useEditorEventCallback } from "./useEditorEventCallback.js";
5
+ export function useIgnoreMutation(ignoreMutation) {
6
+ const register = useContext(IgnoreMutationContext);
7
+ const ignoreMutationMemo = useEditorEventCallback(ignoreMutation);
8
+ useEditorEffect(()=>{
9
+ register(ignoreMutationMemo);
10
+ }, [
11
+ register,
12
+ ignoreMutationMemo
13
+ ]);
14
+ }