@handlewithcare/react-prosemirror 3.1.6 → 3.2.1

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
@@ -85,6 +85,7 @@ import "prosemirror-view/style/prosemirror.css";
85
85
  - [`useEditorEventListener`](#useeditoreventlistener-1)
86
86
  - [`useEditorEffect`](#useeditoreffect-1)
87
87
  - [`NodeViewComponentProps`](#nodeviewcomponentprops)
88
+ - [`useEditorStateSelector`](#useeditorstateselector)
88
89
  - [`useNodePos`](#usenodepos)
89
90
  - [`useStopEvent`](#usestopevent)
90
91
  - [`useIgnoreMutation`](#useignoremutation)
@@ -701,6 +702,17 @@ ref to their top-level DOM element. All node view components that render their
701
702
 
702
703
  [See the above example](#building-node-views-with-react) for more details.
703
704
 
705
+ ### `useEditorStateSelector`
706
+
707
+ ```ts
708
+ type useEditorStateSelector<Result> = (selector: (state: EditorState) => Result): Result
709
+ ```
710
+
711
+ Select a piece of the EditorState, a la Redux’s `useSelector`.
712
+
713
+ This hook will only trigger a re-render of the consuming component if the return
714
+ value of the selector changes.
715
+
704
716
  ### `useNodePos`
705
717
 
706
718
  ```ts
@@ -190,10 +190,10 @@ let ReactEditorView = class ReactEditorView extends _prosemirrorview.EditorView
190
190
  const nextSelection = this.nextProps.state.selection;
191
191
  const currentSelection = this.prevState.selection;
192
192
  const selectionChanged = !nextSelection.eq(currentSelection);
193
- if (selectionChanged && !this.composing) {
193
+ if (selectionChanged) {
194
194
  super.update(this.nextProps);
195
195
  } else {
196
- // If the selection hasn't changed between renders or we're composing, force
196
+ // If the selection hasn't changed between renders, force
197
197
  // prosemirror-view to skip the selectionToDOM call. If a render happens after a DOM
198
198
  // selection change but before the "selectionchange" event fired, calling
199
199
  // selectionToDOM will cause the selection to be reset to its previous position.
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: all[name]
9
+ });
10
+ }
11
+ _export(exports, {
12
+ EditorStateSelectorsProvider: function() {
13
+ return EditorStateSelectorsProvider;
14
+ },
15
+ EditorStateSelectorsRegistrar: function() {
16
+ return EditorStateSelectorsRegistrar;
17
+ }
18
+ });
19
+ const _react = /*#__PURE__*/ _interop_require_wildcard(require("react"));
20
+ const _EditorStateStoreContext = require("../contexts/EditorStateStoreContext.js");
21
+ const _useEditorState = require("../hooks/useEditorState.js");
22
+ function _getRequireWildcardCache(nodeInterop) {
23
+ if (typeof WeakMap !== "function") return null;
24
+ var cacheBabelInterop = new WeakMap();
25
+ var cacheNodeInterop = new WeakMap();
26
+ return (_getRequireWildcardCache = function(nodeInterop) {
27
+ return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
28
+ })(nodeInterop);
29
+ }
30
+ function _interop_require_wildcard(obj, nodeInterop) {
31
+ if (!nodeInterop && obj && obj.__esModule) {
32
+ return obj;
33
+ }
34
+ if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
35
+ return {
36
+ default: obj
37
+ };
38
+ }
39
+ var cache = _getRequireWildcardCache(nodeInterop);
40
+ if (cache && cache.has(obj)) {
41
+ return cache.get(obj);
42
+ }
43
+ var newObj = {
44
+ __proto__: null
45
+ };
46
+ var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
47
+ for(var key in obj){
48
+ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
49
+ var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
50
+ if (desc && (desc.get || desc.set)) {
51
+ Object.defineProperty(newObj, key, desc);
52
+ } else {
53
+ newObj[key] = obj[key];
54
+ }
55
+ }
56
+ }
57
+ newObj.default = obj;
58
+ if (cache) {
59
+ cache.set(obj, newObj);
60
+ }
61
+ return newObj;
62
+ }
63
+ function EditorStateSelectorsRegistrar(param) {
64
+ let { children } = param;
65
+ const store = (0, _react.useMemo)(()=>(0, _EditorStateStoreContext.createEditorStateStore)(), []);
66
+ return /*#__PURE__*/ _react.default.createElement(_EditorStateStoreContext.EditorStateStoreContext.Provider, {
67
+ value: store
68
+ }, children);
69
+ }
70
+ function EditorStateSelectorsProvider(param) {
71
+ let { children } = param;
72
+ const editorState = (0, _useEditorState.useEditorState)();
73
+ const store = (0, _react.useContext)(_EditorStateStoreContext.EditorStateStoreContext);
74
+ // This _must_ be set during render so that child components
75
+ // get the latest values from their selectors during render,
76
+ // if they happen to render before store.notifyListeners()
77
+ // is called
78
+ store.setState(editorState);
79
+ // This means that we get a double-render whenever the selectors update,
80
+ // but everything is consistent during both renders.
81
+ // Only components with new selector values will render on
82
+ // the second render cycle
83
+ (0, _react.useLayoutEffect)(()=>{
84
+ store.notifyListeners();
85
+ }, [
86
+ editorState,
87
+ store
88
+ ]);
89
+ return children;
90
+ }
@@ -10,14 +10,13 @@ Object.defineProperty(exports, "ProseMirror", {
10
10
  });
11
11
  const _react = /*#__PURE__*/ _interop_require_wildcard(require("react"));
12
12
  const _ChildDescriptionsContext = require("../contexts/ChildDescriptionsContext.js");
13
- const _CompositionContext = require("../contexts/CompositionContext.js");
14
13
  const _EditorContext = require("../contexts/EditorContext.js");
15
14
  const _EditorStateContext = require("../contexts/EditorStateContext.js");
16
15
  const _NodeViewContext = require("../contexts/NodeViewContext.js");
17
16
  const _computeDocDeco = require("../decorations/computeDocDeco.js");
18
17
  const _viewDecorations = require("../decorations/viewDecorations.js");
19
18
  const _useEditor = require("../hooks/useEditor.js");
20
- const _reactKeys = require("../plugins/reactKeys.js");
19
+ const _EditorStateSelectorsProvider = require("./EditorStateSelectorsProvider.js");
21
20
  const _LayoutGroup = require("./LayoutGroup.js");
22
21
  const _ProseMirrorDoc = require("./ProseMirrorDoc.js");
23
22
  function _getRequireWildcardCache(nodeInterop) {
@@ -104,26 +103,18 @@ function ProseMirrorInner(param) {
104
103
  decorations,
105
104
  innerDecorations
106
105
  ]);
107
- const freezeFrom = _reactKeys.reactKeysPluginKey.getState(state)?.freezeFrom ?? null;
108
- const compositionContextValue = (0, _react.useMemo)(()=>({
109
- freezeFrom
110
- }), [
111
- freezeFrom
112
- ]);
113
106
  return /*#__PURE__*/ _react.default.createElement(_EditorContext.EditorContext.Provider, {
114
107
  value: editor
115
108
  }, /*#__PURE__*/ _react.default.createElement(_EditorStateContext.EditorStateContext.Provider, {
116
109
  value: state
117
- }, /*#__PURE__*/ _react.default.createElement(_NodeViewContext.NodeViewContext.Provider, {
110
+ }, /*#__PURE__*/ _react.default.createElement(_EditorStateSelectorsProvider.EditorStateSelectorsProvider, null, /*#__PURE__*/ _react.default.createElement(_NodeViewContext.NodeViewContext.Provider, {
118
111
  value: nodeViewContextValue
119
112
  }, /*#__PURE__*/ _react.default.createElement(_ChildDescriptionsContext.ChildDescriptionsContext.Provider, {
120
113
  value: rootChildDescriptionsContextValue
121
- }, /*#__PURE__*/ _react.default.createElement(_CompositionContext.CompositionContext.Provider, {
122
- value: compositionContextValue
123
114
  }, /*#__PURE__*/ _react.default.createElement(_ProseMirrorDoc.DocNodeViewContext.Provider, {
124
115
  value: docNodeViewContextValue
125
116
  }, children))))));
126
117
  }
127
118
  function ProseMirror(props) {
128
- return /*#__PURE__*/ _react.default.createElement(_LayoutGroup.LayoutGroup, null, /*#__PURE__*/ _react.default.createElement(ProseMirrorInner, props));
119
+ return /*#__PURE__*/ _react.default.createElement(_LayoutGroup.LayoutGroup, null, /*#__PURE__*/ _react.default.createElement(_EditorStateSelectorsProvider.EditorStateSelectorsRegistrar, null, /*#__PURE__*/ _react.default.createElement(ProseMirrorInner, props)));
129
120
  }
@@ -20,8 +20,9 @@ _export(exports, {
20
20
  }
21
21
  });
22
22
  const _react = /*#__PURE__*/ _interop_require_wildcard(require("react"));
23
- const _CompositionContext = require("../../contexts/CompositionContext.js");
24
23
  const _NodeViewContext = require("../../contexts/NodeViewContext.js");
24
+ const _useEditorStateSelector = require("../../hooks/useEditorStateSelector.js");
25
+ const _reactKeys = require("../../plugins/reactKeys.js");
25
26
  const _DefaultNodeView = require("./DefaultNodeView.js");
26
27
  const _NodeViewConstructorView = require("./NodeViewConstructorView.js");
27
28
  const _ReactNodeView = require("./ReactNodeView.js");
@@ -69,7 +70,7 @@ function _interop_require_wildcard(obj, nodeInterop) {
69
70
  const NodeView = /*#__PURE__*/ (0, _react.memo)(function NodeView(param) {
70
71
  let { forceRemount, ...props } = param;
71
72
  const renderRef = (0, _react.useRef)(null);
72
- const { freezeFrom } = (0, _react.useContext)(_CompositionContext.CompositionContext);
73
+ const frozen = (0, _useEditorStateSelector.useEditorStateSelector)((state)=>_reactKeys.reactKeysPluginKey.getState(state)?.freezeFrom === props.getPos());
73
74
  const { components, constructors } = (0, _react.useContext)(_NodeViewContext.NodeViewContext);
74
75
  const committedFrozenRef = (0, _react.useRef)(false);
75
76
  const component = components[props.node.type.name] ?? _DefaultNodeView.DefaultNodeView;
@@ -98,11 +99,6 @@ const NodeView = /*#__PURE__*/ (0, _react.memo)(function NodeView(param) {
98
99
  constructor,
99
100
  component
100
101
  ]);
101
- // It's not generally safe to access getPos during render, because the
102
- // component may not re-render when its return value would change. Here it's
103
- // safe because we only use it to _suppress_ commits that would otherwise
104
- // have happened.
105
- const frozen = props.getPos() === freezeFrom;
106
102
  // Protect content while frozen, and also through the single render where we
107
103
  // leave the frozen state: `committedFrozenRef` still reflects the previous
108
104
  // commit, so we keep returning the exact same cached element reference.
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: all[name]
9
+ });
10
+ }
11
+ _export(exports, {
12
+ EditorStateStoreContext: function() {
13
+ return EditorStateStoreContext;
14
+ },
15
+ createEditorStateStore: function() {
16
+ return createEditorStateStore;
17
+ }
18
+ });
19
+ const _react = require("react");
20
+ function createEditorStateStore() {
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ let state = null;
23
+ let pendingNotify = false;
24
+ const listeners = new Set();
25
+ return {
26
+ getState: ()=>state,
27
+ subscribe: (listener)=>{
28
+ listeners.add(listener);
29
+ return ()=>listeners.delete(listener);
30
+ },
31
+ setState: (newState)=>{
32
+ if (state !== newState) {
33
+ state = newState;
34
+ pendingNotify = true;
35
+ }
36
+ },
37
+ notifyListeners: ()=>{
38
+ if (pendingNotify) {
39
+ pendingNotify = false;
40
+ listeners.forEach((l)=>l());
41
+ }
42
+ }
43
+ };
44
+ }
45
+ const EditorStateStoreContext = (0, _react.createContext)(null);
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "useEditorStateSelector", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return useEditorStateSelector;
9
+ }
10
+ });
11
+ const _react = require("react");
12
+ const _EditorStateStoreContext = require("../contexts/EditorStateStoreContext.js");
13
+ function useEditorStateSelector(selector) {
14
+ const store = (0, _react.useContext)(_EditorStateStoreContext.EditorStateStoreContext);
15
+ const selectorRef = (0, _react.useRef)(selector);
16
+ selectorRef.current = selector;
17
+ const getSnapshot = ()=>selectorRef.current(store.getState());
18
+ return (0, _react.useSyncExternalStore)(store.subscribe, getSnapshot, getSnapshot);
19
+ }
package/dist/cjs/index.js CHANGED
@@ -34,6 +34,9 @@ _export(exports, {
34
34
  useEditorState: function() {
35
35
  return _useEditorState.useEditorState;
36
36
  },
37
+ useEditorStateSelector: function() {
38
+ return _useEditorStateSelector.useEditorStateSelector;
39
+ },
37
40
  useIgnoreMutation: function() {
38
41
  return _useIgnoreMutation.useIgnoreMutation;
39
42
  },
@@ -71,6 +74,7 @@ const _useEditorEventCallback = require("./hooks/useEditorEventCallback.js");
71
74
  const _useEditorEventListener = require("./hooks/useEditorEventListener.js");
72
75
  const _useEditorState = require("./hooks/useEditorState.js");
73
76
  const _useNodePos = require("./hooks/useNodePos.js");
77
+ const _useEditorStateSelector = require("./hooks/useEditorStateSelector.js");
74
78
  const _useStopEvent = require("./hooks/useStopEvent.js");
75
79
  const _useSelectNode = require("./hooks/useSelectNode.js");
76
80
  const _useIgnoreMutation = require("./hooks/useIgnoreMutation.js");
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "beforeInputPlugin", {
11
11
  const _prosemirrormodel = require("prosemirror-model");
12
12
  const _prosemirrorstate = require("prosemirror-state");
13
13
  const _ReactEditorView = require("../ReactEditorView.js");
14
+ const _browser = require("../browser.js");
14
15
  const _CursorWrapper = require("../components/CursorWrapper.js");
15
16
  const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
16
17
  const _viewdesc = require("../viewdesc.js");
@@ -93,6 +94,13 @@ function beforeInputPlugin() {
93
94
  view.input.compositionNodes = [];
94
95
  view.input.compositionID++;
95
96
  }
97
+ function teardownCompositionAndUnfreeze(view, event) {
98
+ teardownComposition(view, event.timeStamp);
99
+ view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
100
+ cursorWrapper: null,
101
+ freezeFrom: null
102
+ }));
103
+ }
96
104
  return new _prosemirrorstate.Plugin({
97
105
  view () {
98
106
  return {
@@ -112,11 +120,17 @@ function beforeInputPlugin() {
112
120
  const storedMarks = view.state.selection.empty ? view.state.storedMarks : view.state.storedMarks ?? (view.state.selection instanceof _prosemirrorstate.TextSelection ? view.state.selection.$from.marksAcross(view.state.selection.$to) : null);
113
121
  view.dispatch(view.state.tr.deleteSelection().setStoredMarks(storedMarks));
114
122
  handleGapCursorComposition(view);
115
- if (storedMarks != null) {
123
+ const { selection } = view.state;
124
+ const tr = view.state.tr.delete(selection.from, selection.to);
125
+ const $from = tr.doc.resolve(tr.mapping.map(selection.from));
126
+ const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
127
+ if (storedMarks != null || _browser.browser.safari && isEmptyTextblock) {
116
128
  view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
117
129
  cursorWrapper: (0, _ReactWidgetType.widget)(view.state.selection.from, _CursorWrapper.CursorWrapper, {
118
130
  key: "cursor-wrapper",
119
- marks: storedMarks,
131
+ ...storedMarks !== null && {
132
+ marks: storedMarks
133
+ },
120
134
  side: 0,
121
135
  raw: true
122
136
  })
@@ -167,13 +181,27 @@ function beforeInputPlugin() {
167
181
  compositionend (view, event) {
168
182
  if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
169
183
  if (!view.composing) return false;
170
- teardownComposition(view, event.timeStamp);
171
- view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
172
- cursorWrapper: null,
173
- freezeFrom: null
174
- }));
184
+ teardownCompositionAndUnfreeze(view, event);
175
185
  return true;
176
186
  },
187
+ contextmenu (view, event) {
188
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
189
+ if (!view.composing) return false;
190
+ teardownCompositionAndUnfreeze(view, event);
191
+ return false;
192
+ },
193
+ mousedown (view, event) {
194
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
195
+ if (!view.composing) return false;
196
+ teardownCompositionAndUnfreeze(view, event);
197
+ return false;
198
+ },
199
+ touchstart (view, event) {
200
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
201
+ if (!view.composing) return false;
202
+ teardownCompositionAndUnfreeze(view, event);
203
+ return false;
204
+ },
177
205
  beforeinput (view, event) {
178
206
  if (event.inputType !== "insertFromComposition") {
179
207
  event.preventDefault();
@@ -370,6 +370,7 @@ let ViewDesc = class ViewDesc {
370
370
  // case we just use whatever domFromPos produces as a best effort.
371
371
  setSelection(anchor, head, view) {
372
372
  let force = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false;
373
+ if (view.composing) return;
373
374
  // If the selection falls entirely in a child, give it to that child
374
375
  const from = Math.min(anchor, head), to = Math.max(anchor, head);
375
376
  for(let i = 0, offset = 0; i < this.children.length; i++){
@@ -190,10 +190,10 @@ function changedNodeViews(a, b) {
190
190
  const nextSelection = this.nextProps.state.selection;
191
191
  const currentSelection = this.prevState.selection;
192
192
  const selectionChanged = !nextSelection.eq(currentSelection);
193
- if (selectionChanged && !this.composing) {
193
+ if (selectionChanged) {
194
194
  super.update(this.nextProps);
195
195
  } else {
196
- // If the selection hasn't changed between renders or we're composing, force
196
+ // If the selection hasn't changed between renders, force
197
197
  // prosemirror-view to skip the selectionToDOM call. If a render happens after a DOM
198
198
  // selection change but before the "selectionchange" event fired, calling
199
199
  // selectionToDOM will cause the selection to be reset to its previous position.
@@ -0,0 +1,31 @@
1
+ import React, { useContext, useLayoutEffect, useMemo } from "react";
2
+ import { EditorStateStoreContext, createEditorStateStore } from "../contexts/EditorStateStoreContext.js";
3
+ import { useEditorState } from "../hooks/useEditorState.js";
4
+ export function EditorStateSelectorsRegistrar(param) {
5
+ let { children } = param;
6
+ const store = useMemo(()=>createEditorStateStore(), []);
7
+ return /*#__PURE__*/ React.createElement(EditorStateStoreContext.Provider, {
8
+ value: store
9
+ }, children);
10
+ }
11
+ export function EditorStateSelectorsProvider(param) {
12
+ let { children } = param;
13
+ const editorState = useEditorState();
14
+ const store = useContext(EditorStateStoreContext);
15
+ // This _must_ be set during render so that child components
16
+ // get the latest values from their selectors during render,
17
+ // if they happen to render before store.notifyListeners()
18
+ // is called
19
+ store.setState(editorState);
20
+ // This means that we get a double-render whenever the selectors update,
21
+ // but everything is consistent during both renders.
22
+ // Only components with new selector values will render on
23
+ // the second render cycle
24
+ useLayoutEffect(()=>{
25
+ store.notifyListeners();
26
+ }, [
27
+ editorState,
28
+ store
29
+ ]);
30
+ return children;
31
+ }
@@ -1,13 +1,12 @@
1
1
  import React, { useMemo, useState } from "react";
2
2
  import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
3
- import { CompositionContext } from "../contexts/CompositionContext.js";
4
3
  import { EditorContext } from "../contexts/EditorContext.js";
5
4
  import { EditorStateContext } from "../contexts/EditorStateContext.js";
6
5
  import { NodeViewContext } from "../contexts/NodeViewContext.js";
7
6
  import { computeDocDeco } from "../decorations/computeDocDeco.js";
8
7
  import { viewDecorations } from "../decorations/viewDecorations.js";
9
8
  import { useEditor } from "../hooks/useEditor.js";
10
- import { reactKeysPluginKey } from "../plugins/reactKeys.js";
9
+ import { EditorStateSelectorsProvider, EditorStateSelectorsRegistrar } from "./EditorStateSelectorsProvider.js";
11
10
  import { LayoutGroup } from "./LayoutGroup.js";
12
11
  import { DocNodeViewContext } from "./ProseMirrorDoc.js";
13
12
  function getPos() {
@@ -53,26 +52,18 @@ function ProseMirrorInner(param) {
53
52
  decorations,
54
53
  innerDecorations
55
54
  ]);
56
- const freezeFrom = reactKeysPluginKey.getState(state)?.freezeFrom ?? null;
57
- const compositionContextValue = useMemo(()=>({
58
- freezeFrom
59
- }), [
60
- freezeFrom
61
- ]);
62
55
  return /*#__PURE__*/ React.createElement(EditorContext.Provider, {
63
56
  value: editor
64
57
  }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
65
58
  value: state
66
- }, /*#__PURE__*/ React.createElement(NodeViewContext.Provider, {
59
+ }, /*#__PURE__*/ React.createElement(EditorStateSelectorsProvider, null, /*#__PURE__*/ React.createElement(NodeViewContext.Provider, {
67
60
  value: nodeViewContextValue
68
61
  }, /*#__PURE__*/ React.createElement(ChildDescriptionsContext.Provider, {
69
62
  value: rootChildDescriptionsContextValue
70
- }, /*#__PURE__*/ React.createElement(CompositionContext.Provider, {
71
- value: compositionContextValue
72
63
  }, /*#__PURE__*/ React.createElement(DocNodeViewContext.Provider, {
73
64
  value: docNodeViewContextValue
74
65
  }, children))))));
75
66
  }
76
67
  export function ProseMirror(props) {
77
- return /*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(ProseMirrorInner, props));
68
+ return /*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorStateSelectorsRegistrar, null, /*#__PURE__*/ React.createElement(ProseMirrorInner, props)));
78
69
  }
@@ -1,13 +1,14 @@
1
1
  import React, { createContext, memo, useContext, useLayoutEffect, useMemo, useReducer, useRef } from "react";
2
- import { CompositionContext } from "../../contexts/CompositionContext.js";
3
2
  import { NodeViewContext } from "../../contexts/NodeViewContext.js";
3
+ import { useEditorStateSelector } from "../../hooks/useEditorStateSelector.js";
4
+ import { reactKeysPluginKey } from "../../plugins/reactKeys.js";
4
5
  import { DefaultNodeView } from "./DefaultNodeView.js";
5
6
  import { NodeViewConstructorView } from "./NodeViewConstructorView.js";
6
7
  import { ReactNodeView } from "./ReactNodeView.js";
7
8
  export const NodeView = /*#__PURE__*/ memo(function NodeView(param) {
8
9
  let { forceRemount, ...props } = param;
9
10
  const renderRef = useRef(null);
10
- const { freezeFrom } = useContext(CompositionContext);
11
+ const frozen = useEditorStateSelector((state)=>reactKeysPluginKey.getState(state)?.freezeFrom === props.getPos());
11
12
  const { components, constructors } = useContext(NodeViewContext);
12
13
  const committedFrozenRef = useRef(false);
13
14
  const component = components[props.node.type.name] ?? DefaultNodeView;
@@ -36,11 +37,6 @@ export const NodeView = /*#__PURE__*/ memo(function NodeView(param) {
36
37
  constructor,
37
38
  component
38
39
  ]);
39
- // It's not generally safe to access getPos during render, because the
40
- // component may not re-render when its return value would change. Here it's
41
- // safe because we only use it to _suppress_ commits that would otherwise
42
- // have happened.
43
- const frozen = props.getPos() === freezeFrom;
44
40
  // Protect content while frozen, and also through the single render where we
45
41
  // leave the frozen state: `committedFrozenRef` still reflects the previous
46
42
  // commit, so we keep returning the exact same cached element reference.
@@ -0,0 +1,27 @@
1
+ import { createContext } from "react";
2
+ export function createEditorStateStore() {
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ let state = null;
5
+ let pendingNotify = false;
6
+ const listeners = new Set();
7
+ return {
8
+ getState: ()=>state,
9
+ subscribe: (listener)=>{
10
+ listeners.add(listener);
11
+ return ()=>listeners.delete(listener);
12
+ },
13
+ setState: (newState)=>{
14
+ if (state !== newState) {
15
+ state = newState;
16
+ pendingNotify = true;
17
+ }
18
+ },
19
+ notifyListeners: ()=>{
20
+ if (pendingNotify) {
21
+ pendingNotify = false;
22
+ listeners.forEach((l)=>l());
23
+ }
24
+ }
25
+ };
26
+ }
27
+ export const EditorStateStoreContext = createContext(null);
@@ -0,0 +1,16 @@
1
+ import { useContext, useRef, useSyncExternalStore } from "react";
2
+ import { EditorStateStoreContext } from "../contexts/EditorStateStoreContext.js";
3
+ /**
4
+ * Select a piece of the EditorState, a la Redux’s
5
+ * `useSelector`.
6
+ *
7
+ * This hook will only trigger a re-render of the
8
+ * consuming component if the return value of the selector
9
+ * changes.
10
+ */ export function useEditorStateSelector(selector) {
11
+ const store = useContext(EditorStateStoreContext);
12
+ const selectorRef = useRef(selector);
13
+ selectorRef.current = selector;
14
+ const getSnapshot = ()=>selectorRef.current(store.getState());
15
+ return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
16
+ }
package/dist/esm/index.js CHANGED
@@ -8,6 +8,7 @@ export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js";
8
8
  export { useEditorEventListener } from "./hooks/useEditorEventListener.js";
9
9
  export { useEditorState } from "./hooks/useEditorState.js";
10
10
  export { useNodePos } from "./hooks/useNodePos.js";
11
+ export { useEditorStateSelector } from "./hooks/useEditorStateSelector.js";
11
12
  export { useStopEvent } from "./hooks/useStopEvent.js";
12
13
  export { useSelectNode } from "./hooks/useSelectNode.js";
13
14
  export { useIgnoreMutation } from "./hooks/useIgnoreMutation.js";
@@ -1,6 +1,7 @@
1
1
  import { Fragment, Slice } from "prosemirror-model";
2
2
  import { Plugin, TextSelection } from "prosemirror-state";
3
3
  import { ReactEditorView } from "../ReactEditorView.js";
4
+ import { browser } from "../browser.js";
4
5
  import { CursorWrapper } from "../components/CursorWrapper.js";
5
6
  import { widget } from "../decorations/ReactWidgetType.js";
6
7
  import { CompositionViewDesc, TextViewDesc, findTextInFragment } from "../viewdesc.js";
@@ -83,6 +84,13 @@ export function beforeInputPlugin() {
83
84
  view.input.compositionNodes = [];
84
85
  view.input.compositionID++;
85
86
  }
87
+ function teardownCompositionAndUnfreeze(view, event) {
88
+ teardownComposition(view, event.timeStamp);
89
+ view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
90
+ cursorWrapper: null,
91
+ freezeFrom: null
92
+ }));
93
+ }
86
94
  return new Plugin({
87
95
  view () {
88
96
  return {
@@ -102,11 +110,17 @@ export function beforeInputPlugin() {
102
110
  const storedMarks = view.state.selection.empty ? view.state.storedMarks : view.state.storedMarks ?? (view.state.selection instanceof TextSelection ? view.state.selection.$from.marksAcross(view.state.selection.$to) : null);
103
111
  view.dispatch(view.state.tr.deleteSelection().setStoredMarks(storedMarks));
104
112
  handleGapCursorComposition(view);
105
- if (storedMarks != null) {
113
+ const { selection } = view.state;
114
+ const tr = view.state.tr.delete(selection.from, selection.to);
115
+ const $from = tr.doc.resolve(tr.mapping.map(selection.from));
116
+ const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
117
+ if (storedMarks != null || browser.safari && isEmptyTextblock) {
106
118
  view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
107
119
  cursorWrapper: widget(view.state.selection.from, CursorWrapper, {
108
120
  key: "cursor-wrapper",
109
- marks: storedMarks,
121
+ ...storedMarks !== null && {
122
+ marks: storedMarks
123
+ },
110
124
  side: 0,
111
125
  raw: true
112
126
  })
@@ -157,13 +171,27 @@ export function beforeInputPlugin() {
157
171
  compositionend (view, event) {
158
172
  if (!(view instanceof ReactEditorView)) return false;
159
173
  if (!view.composing) return false;
160
- teardownComposition(view, event.timeStamp);
161
- view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
162
- cursorWrapper: null,
163
- freezeFrom: null
164
- }));
174
+ teardownCompositionAndUnfreeze(view, event);
165
175
  return true;
166
176
  },
177
+ contextmenu (view, event) {
178
+ if (!(view instanceof ReactEditorView)) return false;
179
+ if (!view.composing) return false;
180
+ teardownCompositionAndUnfreeze(view, event);
181
+ return false;
182
+ },
183
+ mousedown (view, event) {
184
+ if (!(view instanceof ReactEditorView)) return false;
185
+ if (!view.composing) return false;
186
+ teardownCompositionAndUnfreeze(view, event);
187
+ return false;
188
+ },
189
+ touchstart (view, event) {
190
+ if (!(view instanceof ReactEditorView)) return false;
191
+ if (!view.composing) return false;
192
+ teardownCompositionAndUnfreeze(view, event);
193
+ return false;
194
+ },
167
195
  beforeinput (view, event) {
168
196
  if (event.inputType !== "insertFromComposition") {
169
197
  event.preventDefault();
@@ -336,6 +336,7 @@ export class ViewDesc {
336
336
  // case we just use whatever domFromPos produces as a best effort.
337
337
  setSelection(anchor, head, view) {
338
338
  let force = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false;
339
+ if (view.composing) return;
339
340
  // If the selection falls entirely in a child, give it to that child
340
341
  const from = Math.min(anchor, head), to = Math.max(anchor, head);
341
342
  for(let i = 0, offset = 0; i < this.children.length; i++){