@handlewithcare/react-prosemirror 3.1.0-tiptap.53 → 3.1.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.
Files changed (73) hide show
  1. package/README.md +3 -0
  2. package/dist/cjs/ReactEditorView.js +18 -7
  3. package/dist/cjs/components/ChildNodeViews.js +10 -16
  4. package/dist/cjs/components/CursorWrapper.js +6 -4
  5. package/dist/cjs/components/ProseMirror.js +12 -4
  6. package/dist/cjs/components/TextNodeView.js +7 -216
  7. package/dist/cjs/components/TrailingHackView.js +0 -70
  8. package/dist/cjs/components/nodes/NodeView.js +40 -4
  9. package/dist/cjs/contexts/ChildDescriptionsContext.js +1 -3
  10. package/dist/cjs/contexts/CompositionContext.js +14 -0
  11. package/dist/cjs/hooks/useMarkViewDescription.js +2 -63
  12. package/dist/cjs/hooks/useNodeViewDescription.js +2 -66
  13. package/dist/cjs/plugins/beforeInputPlugin.js +130 -120
  14. package/dist/cjs/plugins/reactKeys.js +16 -4
  15. package/dist/cjs/tiptap/tiptapNodeView.js +10 -9
  16. package/dist/cjs/viewdesc.js +4 -1
  17. package/dist/esm/ReactEditorView.js +18 -7
  18. package/dist/esm/components/ChildNodeViews.js +12 -18
  19. package/dist/esm/components/CursorWrapper.js +6 -4
  20. package/dist/esm/components/ProseMirror.js +12 -4
  21. package/dist/esm/components/TextNodeView.js +5 -165
  22. package/dist/esm/components/TrailingHackView.js +1 -71
  23. package/dist/esm/components/nodes/NodeView.js +38 -5
  24. package/dist/esm/contexts/ChildDescriptionsContext.js +1 -3
  25. package/dist/esm/contexts/CompositionContext.js +4 -0
  26. package/dist/esm/hooks/useIsEditorStatic.js +4 -1
  27. package/dist/esm/hooks/useMarkViewDescription.js +3 -64
  28. package/dist/esm/hooks/useNodeViewDescription.js +3 -67
  29. package/dist/esm/plugins/beforeInputPlugin.js +131 -121
  30. package/dist/esm/plugins/reactKeys.js +16 -4
  31. package/dist/esm/tiptap/ReactProseMirrorNodeView.js +1 -1
  32. package/dist/esm/tiptap/TiptapEditorContent.js +8 -1
  33. package/dist/esm/tiptap/hooks/useIsInReactProseMirror.js +5 -1
  34. package/dist/esm/tiptap/tiptapNodeView.js +13 -14
  35. package/dist/esm/viewdesc.js +4 -1
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/dist/types/ReactEditorView.d.ts +8 -4
  38. package/dist/types/components/ChildNodeViews.d.ts +2 -2
  39. package/dist/types/components/CursorWrapper.d.ts +1 -1
  40. package/dist/types/components/TextNodeView.d.ts +6 -18
  41. package/dist/types/components/TrailingHackView.d.ts +1 -1
  42. package/dist/types/components/marks/DefaultMarkView.d.ts +1 -1
  43. package/dist/types/components/marks/MarkView.d.ts +1 -1
  44. package/dist/types/components/marks/MarkViewConstructorView.d.ts +1 -1
  45. package/dist/types/components/marks/ReactMarkView.d.ts +1 -1
  46. package/dist/types/components/nodes/DefaultNodeView.d.ts +1 -1
  47. package/dist/types/components/nodes/NodeView.d.ts +3 -1
  48. package/dist/types/components/nodes/NodeViewConstructorView.d.ts +1 -1
  49. package/dist/types/components/nodes/ReactNodeView.d.ts +1 -1
  50. package/dist/types/contexts/ChildDescriptionsContext.d.ts +1 -2
  51. package/dist/types/contexts/CompositionContext.d.ts +4 -0
  52. package/dist/types/hooks/useEditor.d.ts +2 -2
  53. package/dist/types/hooks/useIsEditorStatic.d.ts +4 -0
  54. package/dist/types/hooks/useMarkViewDescription.d.ts +1 -2
  55. package/dist/types/hooks/useNodeViewDescription.d.ts +1 -2
  56. package/dist/types/hooks/useReactKeys.d.ts +2 -5
  57. package/dist/types/plugins/reactKeys.d.ts +5 -5
  58. package/dist/types/props.d.ts +225 -225
  59. package/dist/types/tiptap/ReactProseMirrorNodeView.d.ts +1 -1
  60. package/dist/types/tiptap/TiptapEditorContent.d.ts +10 -1
  61. package/dist/types/tiptap/hooks/useIsInReactProseMirror.d.ts +5 -0
  62. package/dist/types/tiptap/tiptapNodeView.d.ts +5 -6
  63. package/dist/types/viewdesc.d.ts +2 -1
  64. package/package.json +20 -6
  65. package/dist/cjs/plugins/componentEventListeners.js +0 -28
  66. package/dist/cjs/plugins/componentEventListenersPlugin.js +0 -35
  67. package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +0 -59
  68. package/dist/esm/plugins/componentEventListeners.js +0 -18
  69. package/dist/esm/plugins/componentEventListenersPlugin.js +0 -25
  70. package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +0 -56
  71. package/dist/types/plugins/componentEventListeners.d.ts +0 -3
  72. package/dist/types/plugins/componentEventListenersPlugin.d.ts +0 -4
  73. package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +0 -1
@@ -120,71 +120,10 @@ function useMarkViewDescription(getDOM, getContentDOM, constructor, props) {
120
120
  child.parent = viewDesc;
121
121
  }
122
122
  });
123
- const findCompositionDOM = (0, _react.useCallback)((compositionViewDesc)=>{
124
- const children = childrenRef.current;
125
- // Because TextNodeViews can't locate the DOM nodes
126
- // for compositions, we need to override them here
127
- if (!viewDescRef.current?.contentDOM) return;
128
- let compositionTopDOM = null;
129
- for (const childNode of viewDescRef.current.contentDOM.childNodes){
130
- if (children.every((child)=>child.dom !== childNode)) {
131
- compositionTopDOM = childNode;
132
- break;
133
- }
134
- }
135
- if (!compositionTopDOM) {
136
- // Otherwise the IME extended an existing tracked text node. Take it over.
137
- const reactView = view;
138
- const imeTextNode = reactView.input.compositionNode;
139
- if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
140
- return;
141
- }
142
- const claimedDesc = imeTextNode.pmViewDesc;
143
- if (!(claimedDesc instanceof _viewdesc.TextViewDesc)) return;
144
- if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
145
- // Walk up to the direct child of contentDOM that contains the IME text node
146
- // (could be the text node itself, could be wrapped in a mark span).
147
- let topDOM = imeTextNode;
148
- while(topDOM.parentNode !== viewDescRef.current.contentDOM){
149
- const next = topDOM.parentNode;
150
- if (!next) return;
151
- topDOM = next;
152
- }
153
- // Detach the displaced TextViewDesc from the sibling list so sibling-size
154
- // accounting (used by posBeforeChild) doesn't double-count this text node.
155
- const displacedIdx = children.indexOf(claimedDesc);
156
- if (displacedIdx >= 0) children.splice(displacedIdx, 1);
157
- compositionViewDesc.dom = topDOM;
158
- compositionViewDesc.textDOM = imeTextNode;
159
- compositionViewDesc.text = imeTextNode.data;
160
- imeTextNode.pmViewDesc = compositionViewDesc;
161
- compositionViewDesc._displacedDesc = claimedDesc;
162
- reactView.input.compositionNodes.push(compositionViewDesc);
163
- return;
164
- }
165
- let textDOM = compositionTopDOM;
166
- while(textDOM.firstChild){
167
- textDOM = textDOM.firstChild;
168
- }
169
- if (!textDOM || !(textDOM instanceof Text)) {
170
- console.error(compositionTopDOM, textDOM);
171
- throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
172
- }
173
- compositionViewDesc.dom = compositionTopDOM;
174
- compositionViewDesc.textDOM = textDOM;
175
- compositionViewDesc.text = textDOM.data;
176
- compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
177
- view.input.compositionNodes.push(compositionViewDesc);
178
- }, [
179
- view
180
- ]);
181
123
  const childContextValue = (0, _react.useMemo)(()=>({
182
124
  parentRef: viewDescRef,
183
- siblingsRef: childrenRef,
184
- findCompositionDOM
185
- }), [
186
- findCompositionDOM
187
- ]);
125
+ siblingsRef: childrenRef
126
+ }), []);
188
127
  return {
189
128
  childContextValue,
190
129
  contentDOM: contentDOMRef.current ?? viewDescRef.current?.dom,
@@ -143,74 +143,10 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
143
143
  child.parent = viewDesc;
144
144
  }
145
145
  });
146
- const findCompositionDOM = (0, _react.useCallback)((compositionViewDesc)=>{
147
- if (!props.node.isTextblock) return;
148
- const children = childrenRef.current;
149
- // Because TextNodeViews can't locate the DOM nodes
150
- // for compositions, we need to override them here
151
- if (!viewDescRef.current?.contentDOM) return;
152
- let compositionTopDOM = null;
153
- for (const childNode of viewDescRef.current.contentDOM.childNodes){
154
- if (children.every((child)=>child.dom !== childNode)) {
155
- compositionTopDOM = childNode;
156
- break;
157
- }
158
- }
159
- if (!compositionTopDOM) {
160
- // Otherwise the IME extended an existing tracked text node. Take it over.
161
- const reactView = view;
162
- const imeTextNode = reactView.input.compositionNode;
163
- if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
164
- return;
165
- }
166
- const claimedDesc = imeTextNode.pmViewDesc;
167
- if (!(claimedDesc instanceof _viewdesc.TextViewDesc)) return;
168
- if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
169
- // Walk up to the direct child of contentDOM that contains the IME text node
170
- // (could be the text node itself, could be wrapped in a mark span).
171
- let topDOM = imeTextNode;
172
- while(topDOM.parentNode !== viewDescRef.current.contentDOM){
173
- const next = topDOM.parentNode;
174
- if (!next) return;
175
- topDOM = next;
176
- }
177
- // Detach the displaced TextViewDesc from the sibling list so sibling-size
178
- // accounting (used by posBeforeChild) doesn't double-count this text node.
179
- const displacedIdx = children.indexOf(claimedDesc);
180
- if (displacedIdx >= 0) children.splice(displacedIdx, 1);
181
- reactView.displacedNodes.push(claimedDesc);
182
- compositionViewDesc.dom = topDOM;
183
- compositionViewDesc.textDOM = imeTextNode;
184
- compositionViewDesc.text = imeTextNode.data;
185
- imeTextNode.pmViewDesc = compositionViewDesc;
186
- compositionViewDesc._displacedDesc = claimedDesc;
187
- reactView.input.compositionNodes.push(compositionViewDesc);
188
- return;
189
- }
190
- let textDOM = compositionTopDOM;
191
- while(textDOM.firstChild){
192
- textDOM = textDOM.firstChild;
193
- }
194
- if (!textDOM || !(textDOM instanceof Text)) {
195
- console.error(compositionTopDOM, textDOM);
196
- throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
197
- }
198
- compositionViewDesc.dom = compositionTopDOM;
199
- compositionViewDesc.textDOM = textDOM;
200
- compositionViewDesc.text = textDOM.data;
201
- compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
202
- view.input.compositionNodes.push(compositionViewDesc);
203
- }, [
204
- props.node.isTextblock,
205
- view
206
- ]);
207
146
  const childContextValue = (0, _react.useMemo)(()=>({
208
147
  parentRef: viewDescRef,
209
- siblingsRef: childrenRef,
210
- findCompositionDOM
211
- }), [
212
- findCompositionDOM
213
- ]);
148
+ siblingsRef: childrenRef
149
+ }), []);
214
150
  return {
215
151
  childContextValue,
216
152
  contentDOM: contentDOMRef.current,
@@ -57,36 +57,66 @@ function handleGapCursorComposition(view) {
57
57
  tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
58
58
  view.dispatch(tr);
59
59
  }
60
+ const observeOptions = {
61
+ childList: true,
62
+ characterData: true,
63
+ characterDataOldValue: true,
64
+ attributes: true,
65
+ attributeOldValue: true,
66
+ subtree: true
67
+ };
60
68
  function beforeInputPlugin() {
61
- let compositionMarks = null;
69
+ let observer = null;
70
+ let preCompositionSnapshot = null;
71
+ function teardownComposition(view, endedAt) {
72
+ view.input.composing = false;
73
+ if (observer) {
74
+ if (view.input.compositionNode && view.dom.contains(view.input.compositionNode)) {
75
+ view.domObserver.queue.push(...observer.takeRecords());
76
+ view.domObserver.flush();
77
+ } else {
78
+ const freezeFrom = _reactKeys.reactKeysPluginKey.getState(view.state)?.freezeFrom;
79
+ const frozenNode = freezeFrom == null ? null : view.state.doc.nodeAt(freezeFrom);
80
+ if (freezeFrom != null && frozenNode != null && preCompositionSnapshot) {
81
+ // This is a little hacky — it only works because we always abort
82
+ // compositions if the node after freezeFrom changes, so we can
83
+ // be sure that if a composition was canceled by the user/browser,
84
+ // the content hasn't changed since the composition started
85
+ view.dispatch(view.state.tr.replaceWith(freezeFrom + 1, freezeFrom + 1 + frozenNode.content.size, preCompositionSnapshot));
86
+ }
87
+ }
88
+ observer.disconnect();
89
+ observer = null;
90
+ }
91
+ view.input.compositionEndedAt = endedAt;
92
+ view.input.compositionNode = null;
93
+ view.input.compositionNodes = [];
94
+ view.input.compositionID++;
95
+ }
62
96
  return new _prosemirrorstate.Plugin({
97
+ view () {
98
+ return {
99
+ update (view) {
100
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) return;
101
+ const frozen = _reactKeys.reactKeysPluginKey.getState(view.state)?.freezeFrom != null;
102
+ if (observer && view.composing && !frozen) {
103
+ teardownComposition(view, Date.now());
104
+ }
105
+ }
106
+ };
107
+ },
63
108
  props: {
64
109
  handleDOMEvents: {
65
110
  compositionstart (view) {
66
111
  if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
67
- view.compositionStarting = true;
68
- const { state } = view;
69
- const { selection } = state;
70
- const isEmptyTr = state.tr.delete(selection.from, selection.to);
71
- const $from = isEmptyTr.doc.resolve(isEmptyTr.mapping.map(selection.from));
72
- const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
73
- compositionMarks = view.state.storedMarks;
74
- // Render a CursorWrapper with empty marks if starting a composition in an
75
- // empty textblock with no marks. This prevents the browser from adding a
76
- // <br> to the text block when it becomes empty (either via canceling the
77
- // composition with the escape key or deleting all composition text when
78
- // the composition node is the only text node in the text block)
79
- if (compositionMarks === null && isEmptyTextblock) {
80
- compositionMarks = [];
81
- }
82
- const tr = view.state.tr.setStoredMarks(null);
83
- view.dispatch(tr);
112
+ 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
+ view.dispatch(view.state.tr.deleteSelection().setStoredMarks(storedMarks));
84
114
  handleGapCursorComposition(view);
85
- if (compositionMarks) {
115
+ if (storedMarks) {
86
116
  view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
87
- cursorWrapper: (0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
117
+ cursorWrapper: (0, _ReactWidgetType.widget)(view.state.selection.from, _CursorWrapper.CursorWrapper, {
88
118
  key: "cursor-wrapper",
89
- marks: compositionMarks,
119
+ marks: storedMarks,
90
120
  side: 0,
91
121
  raw: true
92
122
  })
@@ -97,24 +127,38 @@ function beforeInputPlugin() {
97
127
  // node depending on the user's last navigation direction, and the
98
128
  // IME composes into whichever one it found.
99
129
  } else if (view.state.selection.empty) {
100
- // @ts-expect-error internal method
101
130
  view.domObserver.disconnectSelection();
102
131
  try {
103
132
  view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
104
133
  );
105
134
  } finally{
106
- // @ts-expect-error internal method
107
135
  view.domObserver.setCurSelection();
108
- // @ts-expect-error internal method
109
136
  view.domObserver.connectSelection();
110
137
  }
111
138
  }
112
- view.compositionStarting = false;
113
- // We set composing to true after creating the cursor wrapper
114
- // so that no existing text nodes try to protect themselves
115
- // while we're creating the cursor wrapper, which may need
116
- // to split a text node.
139
+ const freezeFrom = view.state.selection.$from.before();
140
+ view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
141
+ freezeFrom
142
+ }));
143
+ const frozenDom = view.nodeDOM(freezeFrom);
144
+ if (!frozenDom) {
145
+ view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
146
+ cursorWrapper: null,
147
+ freezeFrom: null
148
+ }));
149
+ return false;
150
+ }
151
+ preCompositionSnapshot = view.state.doc.nodeAt(freezeFrom)?.content ?? null;
117
152
  view.input.composing = true;
153
+ observer = new MutationObserver((records)=>{
154
+ if (_reactKeys.reactKeysPluginKey.getState(view.state)?.freezeFrom == null) {
155
+ return;
156
+ }
157
+ view.domObserver.queue.push(...records);
158
+ view.domObserver.flush();
159
+ syncCompositionViewDescs(view);
160
+ });
161
+ observer.observe(frozenDom, observeOptions);
118
162
  return true;
119
163
  },
120
164
  compositionupdate () {
@@ -123,36 +167,11 @@ function beforeInputPlugin() {
123
167
  compositionend (view, event) {
124
168
  if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
125
169
  if (!view.composing) return false;
126
- view.input.composing = false;
127
- compositionMarks = null;
128
- for (const displaced of view.displacedNodes){
129
- // Put the displaced TextViewDesc back into its parent's child list.
130
- const parent = displaced.parent;
131
- if (parent && !parent.children.includes(displaced)) {
132
- parent.children.push(displaced);
133
- parent.children.sort(_viewdesc.sortViewDescs);
134
- }
135
- // Restore pmViewDesc claim on the text node.
136
- displaced.dom.pmViewDesc = displaced;
137
- // Truncate the IME text node back to what the displaced PM node says it
138
- // is. The composed content lives in PM state; the next React render will
139
- // mount a sibling TextNodeView that inserts its own DOM (e.g.
140
- // `<span class="word">k</span>`) right after this node.
141
- const claimedText = displaced.node.text ?? "";
142
- if (displaced.nodeDOM.nodeValue !== claimedText) {
143
- displaced.nodeDOM.nodeValue = claimedText;
144
- }
145
- }
170
+ teardownComposition(view, event.timeStamp);
146
171
  view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
147
- cursorWrapper: null
172
+ cursorWrapper: null,
173
+ freezeFrom: null
148
174
  }));
149
- if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
150
- view.input.compositionNode.remove();
151
- }
152
- view.input.compositionEndedAt = event.timeStamp;
153
- view.input.compositionNode = null;
154
- view.input.compositionNodes = [];
155
- view.input.compositionID++;
156
175
  return true;
157
176
  },
158
177
  beforeinput (view, event) {
@@ -203,63 +222,6 @@ function beforeInputPlugin() {
203
222
  insertText(view, event.data);
204
223
  break;
205
224
  }
206
- case "insertCompositionText":
207
- case "deleteCompositionText":
208
- case "insertFromComposition":
209
- {
210
- if (!(view instanceof _ReactEditorView.ReactEditorView)) break;
211
- const { tr } = view.state;
212
- // There's always a range on insertCompositionText beforeinput events
213
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
214
- const range = event.getTargetRanges()[0];
215
- const start = view.posAtDOM(range.startContainer, range.startOffset);
216
- const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
217
- if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
218
- return;
219
- }
220
- if (event.data) {
221
- if (compositionMarks) tr.ensureMarks(compositionMarks);
222
- tr.insertText(event.data, start, end);
223
- } else {
224
- tr.delete(start, end);
225
- }
226
- // When updating a composition within an existing text node,
227
- // we need to avoid remounting it. If the composition is at
228
- // the very beginning of the text node, the start position of
229
- // that node will either be mapped forward (if inserting new
230
- // content) or deleted (if replacing existing content).
231
- //
232
- // This will cause the reactKeys plugin to mint a new key for
233
- // that node, which triggers a remount. So we check to see whether
234
- // we're working on a composition at the very beginning of a text
235
- // node, and if so, tell the react keys plugin not to change the
236
- // key for that node.
237
- //
238
- // We need to check that the marks are the same — if they're not,
239
- // then we're inserting text _before_ this text node, not at the
240
- // start of it, so we actually _do_ want to map the exsting node
241
- // forward.
242
- const $start = view.state.doc.resolve(start);
243
- const $end = view.state.doc.resolve(end);
244
- const marks = compositionMarks ?? $start.marksAcross($end) ?? [];
245
- if ($start.textOffset === 0 && $end.nodeAfter?.marks.every((m)=>m.isInSet(marks))) {
246
- tr.setMeta(_reactKeys.reactKeysPluginKey, {
247
- overrides: {
248
- [start]: start
249
- }
250
- });
251
- }
252
- view.dom.addEventListener("input", ()=>{
253
- const sel = view.domSelectionRange();
254
- if (sel.focusNode && sel.focusNode.nodeType === 3) {
255
- view.input.compositionNode = sel.focusNode;
256
- }
257
- view.dispatch(tr);
258
- }, {
259
- once: true
260
- });
261
- break;
262
- }
263
225
  case "deleteWordBackward":
264
226
  case "deleteHardLineBackward":
265
227
  case "deleteSoftLineBackward":
@@ -293,11 +255,59 @@ function beforeInputPlugin() {
293
255
  }
294
256
  });
295
257
  }
296
- function isCompositionNodeOrphaned(tn) {
297
- if (tn.pmViewDesc) return false;
298
- for(let parent = tn.parentNode; parent; parent = parent.parentNode){
299
- const desc = parent.pmViewDesc;
300
- if (desc instanceof _viewdesc.TextViewDesc && desc.nodeDOM === tn) return false;
258
+ function syncCompositionViewDescs(view) {
259
+ const compositionNode = view.domObserver.lastChangedTextNode;
260
+ if (!compositionNode) return;
261
+ const freezeFrom = _reactKeys.reactKeysPluginKey.getState(view.state)?.freezeFrom;
262
+ if (freezeFrom == null) return;
263
+ const compositionBlock = view.state.doc.nodeAt(freezeFrom);
264
+ if (!compositionBlock) return;
265
+ const compositionBlockDesc = view.docView.descAt(freezeFrom);
266
+ if (!compositionBlockDesc) return;
267
+ const desc = view.docView.nearestDesc(compositionNode);
268
+ compositionBlockDesc.node = compositionBlock;
269
+ if (desc instanceof _viewdesc.TextViewDesc) {
270
+ if (compositionNode.nodeValue && desc.node.text !== compositionNode.nodeValue) {
271
+ desc.node = view.state.schema.text(compositionNode.nodeValue, desc.node.marks);
272
+ desc.nodeDOM = compositionNode;
273
+ compositionNode.pmViewDesc = desc;
274
+ }
275
+ return;
301
276
  }
302
- return true;
277
+ if (desc instanceof _viewdesc.CompositionViewDesc) {
278
+ if (compositionNode.nodeValue != null && desc.text !== compositionNode.nodeValue) {
279
+ desc.dom = compositionNode;
280
+ desc.textDOM = compositionNode;
281
+ desc.text = compositionNode.nodeValue;
282
+ compositionNode.pmViewDesc = desc;
283
+ }
284
+ return;
285
+ }
286
+ const parentDesc = desc?.contentDOM ? desc : compositionBlockDesc;
287
+ const children = parentDesc.children;
288
+ // Drop any text or composition desc in this container whose DOM the
289
+ // IME has detached. This covers two cases: a TextViewDesc the IME subsumed
290
+ // into the composition node, and (on Safari, which replaces the whole text
291
+ // node on each composition update) any orphaned composition view
292
+ // desc(s) left over from the previous composition steps.
293
+ for(let i = children.length - 1; i >= 0; i--){
294
+ const c = children[i];
295
+ if (!(c instanceof _viewdesc.TextViewDesc) && !(c instanceof _viewdesc.CompositionViewDesc)) {
296
+ continue;
297
+ }
298
+ const dom = c.dom;
299
+ if (view.dom.contains(dom)) continue;
300
+ children.splice(i, 1);
301
+ }
302
+ const contentStart = freezeFrom + 1;
303
+ const { from, to } = view.state.selection;
304
+ const textPos = (0, _viewdesc.findTextInFragment)(compositionBlock.content, compositionNode.nodeValue ?? "", from - contentStart, to - contentStart);
305
+ if (textPos < 0) return;
306
+ const startPos = contentStart + textPos;
307
+ let topDOM = compositionNode;
308
+ while(topDOM.parentNode && topDOM.parentNode !== parentDesc.contentDOM){
309
+ topDOM = topDOM.parentNode;
310
+ }
311
+ const insertIndex = children.findLastIndex((c)=>c.posBefore <= startPos) + 1;
312
+ children.splice(insertIndex, 0, new _viewdesc.CompositionViewDesc(parentDesc, ()=>startPos, topDOM, compositionNode, compositionNode.nodeValue ?? ""));
303
313
  }
@@ -35,7 +35,8 @@ function reactKeys() {
35
35
  const next = {
36
36
  posToKey: new Map(),
37
37
  keyToPos: new Map(),
38
- cursorWrapper: null
38
+ cursorWrapper: null,
39
+ freezeFrom: null
39
40
  };
40
41
  state.doc.descendants((_, pos)=>{
41
42
  const key = createNodeKey();
@@ -52,19 +53,30 @@ function reactKeys() {
52
53
  * through the transaction to identify its current position,
53
54
  * and assign its key to that new position, dropping it if the
54
55
  * node was deleted.
55
- */ apply (tr, value, _, newState) {
56
+ */ apply (tr, value, oldState, newState) {
56
57
  const meta = tr.getMeta(reactKeysPluginKey);
57
58
  const overrides = meta && "overrides" in meta ? meta.overrides : {};
58
59
  const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
60
+ const freezeFrom = meta && "freezeFrom" in meta ? meta.freezeFrom : undefined;
59
61
  const next = {
60
62
  posToKey: new Map(),
61
63
  keyToPos: new Map(),
62
- cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? (0, _ReactWidgetType.widget)(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper
64
+ cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? (0, _ReactWidgetType.widget)(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper,
65
+ freezeFrom: freezeFrom === undefined ? value.freezeFrom !== null ? tr.mapping.map(value.freezeFrom, -1) : null : freezeFrom
63
66
  };
67
+ if (value.freezeFrom !== null && next.freezeFrom !== null && tr.getMeta("composition") == null) {
68
+ const oldBlock = oldState.doc.nodeAt(value.freezeFrom);
69
+ const newBlock = newState.doc.nodeAt(next.freezeFrom);
70
+ if (newBlock && !oldBlock?.eq(newBlock)) {
71
+ next.freezeFrom = null;
72
+ next.cursorWrapper = null;
73
+ }
74
+ }
64
75
  if (!tr.docChanged) {
65
76
  return {
66
77
  ...value,
67
- cursorWrapper: next.cursorWrapper
78
+ cursorWrapper: next.cursorWrapper,
79
+ freezeFrom: next.freezeFrom
68
80
  };
69
81
  }
70
82
  const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
@@ -116,15 +116,6 @@ function tiptapNodeView(param) {
116
116
  return result;
117
117
  });
118
118
  (0, _useIgnoreMutation.useIgnoreMutation)(function(_, mutation) {
119
- if (ignoreMutation) {
120
- return ignoreMutation.call({
121
- name: extension.name,
122
- editor,
123
- type: node.type
124
- }, {
125
- mutation
126
- });
127
- }
128
119
  if (!editor || !(this.dom instanceof HTMLElement)) return false;
129
120
  const nodeView = new _ReactProseMirrorNodeView.ReactProseMirrorNodeView(WrappedComponent, {
130
121
  extension,
@@ -136,6 +127,16 @@ function tiptapNodeView(param) {
136
127
  node,
137
128
  view: editor.view
138
129
  }, this.dom, this.contentDOM);
130
+ if (ignoreMutation) {
131
+ return ignoreMutation.call({
132
+ name: extension.name,
133
+ editor,
134
+ type: node.type
135
+ }, {
136
+ mutation,
137
+ defaultIgnoreMutation: nodeView.ignoreMutation.bind(nodeView)
138
+ });
139
+ }
139
140
  return nodeView.ignoreMutation(mutation) ?? false;
140
141
  });
141
142
  const { extraClassName, htmlProps } = (0, _react1.useMemo)(()=>{
@@ -416,7 +416,7 @@ let ViewDesc = class ViewDesc {
416
416
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
417
417
  if (after && after.contentEditable == "false") force = true;
418
418
  }
419
- if (view.composing || !(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)) {
419
+ if (view.cursorWrapped || !(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)) {
420
420
  return;
421
421
  }
422
422
  // Selection.extend can be used to create an 'inverted' selection
@@ -715,6 +715,9 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
715
715
  isText(text) {
716
716
  return this.node.text == text;
717
717
  }
718
+ ignoreMutation(mutation) {
719
+ return mutation.type !== "characterData" && mutation.type !== "selection";
720
+ }
718
721
  };
719
722
  let TrailingHackViewDesc = class TrailingHackViewDesc extends ViewDesc {
720
723
  parseRule() {
@@ -33,10 +33,7 @@ function changedNodeViews(a, b) {
33
33
  nextProps;
34
34
  prevState;
35
35
  _destroyed;
36
- // TODO: Probably refactor? It's used in TrailingHackView to detect
37
- // whether it was mounted during a compositionstart event handler
38
- compositionStarting;
39
- displacedNodes;
36
+ cursorWrapped;
40
37
  constructor(place, props){
41
38
  // Prevent the base class from destroying the React-managed nodes.
42
39
  // Restore them below after invoking the base class constructor.
@@ -89,8 +86,7 @@ function changedNodeViews(a, b) {
89
86
  // @ts-expect-error this violates the typing but class does it, too.
90
87
  this.docView = null;
91
88
  this._destroyed = false;
92
- this.compositionStarting = false;
93
- this.displacedNodes = [];
89
+ this.cursorWrapped = false;
94
90
  }
95
91
  get props() {
96
92
  return this.nextProps;
@@ -191,7 +187,22 @@ function changedNodeViews(a, b) {
191
187
  // this ensures that the base class validates the DOM selection and invokes
192
188
  // node view selection callbacks.
193
189
  this.docView.markDirty(-1, -1);
194
- super.update(this.nextProps);
190
+ const selectionChanged = !this.state.selection.eq(this.prevState.selection);
191
+ if (selectionChanged) {
192
+ super.update(this.nextProps);
193
+ } else {
194
+ // If the selection hasn't changed between renders, force prosemirror-view to
195
+ // skip the selectionToDOM call. If a render happens after a DOM selection change
196
+ // but before the "selectionchange" event fired, calling selectionToDOM will cause
197
+ // the selection to be reset its the previous position.
198
+ this.domObserver.setCurSelection();
199
+ this.input.mouseDown = {
200
+ allowDefault: false,
201
+ delayedSelectionSync: false
202
+ };
203
+ super.update(this.nextProps);
204
+ this.input.mouseDown = null;
205
+ }
195
206
  // Store the new previous state.
196
207
  this.prevState = this.state;
197
208
  }
@@ -7,11 +7,11 @@ import { htmlAttrsToReactProps, mergeReactProps } from "../props.js";
7
7
  import { sameOuterDeco } from "../viewdesc.js";
8
8
  import { NativeWidgetView } from "./NativeWidgetView.js";
9
9
  import { SeparatorHackView } from "./SeparatorHackView.js";
10
- import { RemountableTextNodeView } from "./TextNodeView.js";
10
+ import { TextNodeView } from "./TextNodeView.js";
11
11
  import { TrailingHackView } from "./TrailingHackView.js";
12
12
  import { WidgetView } from "./WidgetView.js";
13
13
  import { MarkView } from "./marks/MarkView.js";
14
- import { NodeView } from "./nodes/NodeView.js";
14
+ import { RemountableNodeView } from "./nodes/NodeView.js";
15
15
  export function wrapInDeco(reactNode, deco) {
16
16
  const { nodeName, ...attrs } = deco.type.attrs;
17
17
  const props = htmlAttrsToReactProps(attrs);
@@ -50,22 +50,16 @@ const ChildView = /*#__PURE__*/ memo(function ChildView(param) {
50
50
  }) : child.node.isText ? /*#__PURE__*/ React.createElement(ChildDescriptionsContext.Consumer, {
51
51
  key: child.key
52
52
  }, (param)=>{
53
- let { siblingsRef, parentRef, findCompositionDOM } = param;
54
- return /*#__PURE__*/ React.createElement(EditorContext.Consumer, null, (param)=>{
55
- let { registerEventListener, unregisterEventListener } = param;
56
- return /*#__PURE__*/ React.createElement(RemountableTextNodeView, {
57
- view: view,
58
- node: child.node,
59
- getPos: getPos,
60
- siblingsRef: siblingsRef,
61
- parentRef: parentRef,
62
- findCompositionDOM: findCompositionDOM,
63
- decorations: child.outerDeco,
64
- registerEventListener: registerEventListener,
65
- unregisterEventListener: unregisterEventListener
66
- });
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
67
61
  });
68
- }) : /*#__PURE__*/ React.createElement(NodeView, {
62
+ }) : /*#__PURE__*/ React.createElement(RemountableNodeView, {
69
63
  key: child.key,
70
64
  node: child.node,
71
65
  getPos: getPos,
@@ -213,7 +207,7 @@ const ChildElement = /*#__PURE__*/ memo(function ChildElement(param) {
213
207
  mark: mark,
214
208
  getPos: getPos,
215
209
  inline: false
216
- }, element), /*#__PURE__*/ React.createElement(NodeView, {
210
+ }, element), /*#__PURE__*/ React.createElement(RemountableNodeView, {
217
211
  key: child.key,
218
212
  outerDeco: child.outerDeco,
219
213
  node: child.node,