@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
@@ -3,7 +3,7 @@ import { Plugin, TextSelection } from "prosemirror-state";
3
3
  import { ReactEditorView } from "../ReactEditorView.js";
4
4
  import { CursorWrapper } from "../components/CursorWrapper.js";
5
5
  import { widget } from "../decorations/ReactWidgetType.js";
6
- import { TextViewDesc, sortViewDescs } from "../viewdesc.js";
6
+ import { CompositionViewDesc, TextViewDesc, findTextInFragment } from "../viewdesc.js";
7
7
  import { reactKeysPluginKey } from "./reactKeys.js";
8
8
  function insertText(view, eventData) {
9
9
  let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
@@ -47,36 +47,66 @@ function handleGapCursorComposition(view) {
47
47
  tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
48
48
  view.dispatch(tr);
49
49
  }
50
+ const observeOptions = {
51
+ childList: true,
52
+ characterData: true,
53
+ characterDataOldValue: true,
54
+ attributes: true,
55
+ attributeOldValue: true,
56
+ subtree: true
57
+ };
50
58
  export function beforeInputPlugin() {
51
- let compositionMarks = null;
59
+ let observer = null;
60
+ let preCompositionSnapshot = null;
61
+ function teardownComposition(view, endedAt) {
62
+ view.input.composing = false;
63
+ if (observer) {
64
+ if (view.input.compositionNode && view.dom.contains(view.input.compositionNode)) {
65
+ view.domObserver.queue.push(...observer.takeRecords());
66
+ view.domObserver.flush();
67
+ } else {
68
+ const freezeFrom = reactKeysPluginKey.getState(view.state)?.freezeFrom;
69
+ const frozenNode = freezeFrom == null ? null : view.state.doc.nodeAt(freezeFrom);
70
+ if (freezeFrom != null && frozenNode != null && preCompositionSnapshot) {
71
+ // This is a little hacky — it only works because we always abort
72
+ // compositions if the node after freezeFrom changes, so we can
73
+ // be sure that if a composition was canceled by the user/browser,
74
+ // the content hasn't changed since the composition started
75
+ view.dispatch(view.state.tr.replaceWith(freezeFrom + 1, freezeFrom + 1 + frozenNode.content.size, preCompositionSnapshot));
76
+ }
77
+ }
78
+ observer.disconnect();
79
+ observer = null;
80
+ }
81
+ view.input.compositionEndedAt = endedAt;
82
+ view.input.compositionNode = null;
83
+ view.input.compositionNodes = [];
84
+ view.input.compositionID++;
85
+ }
52
86
  return new Plugin({
87
+ view () {
88
+ return {
89
+ update (view) {
90
+ if (!(view instanceof ReactEditorView)) return;
91
+ const frozen = reactKeysPluginKey.getState(view.state)?.freezeFrom != null;
92
+ if (observer && view.composing && !frozen) {
93
+ teardownComposition(view, Date.now());
94
+ }
95
+ }
96
+ };
97
+ },
53
98
  props: {
54
99
  handleDOMEvents: {
55
100
  compositionstart (view) {
56
101
  if (!(view instanceof ReactEditorView)) return false;
57
- view.compositionStarting = true;
58
- const { state } = view;
59
- const { selection } = state;
60
- const isEmptyTr = state.tr.delete(selection.from, selection.to);
61
- const $from = isEmptyTr.doc.resolve(isEmptyTr.mapping.map(selection.from));
62
- const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
63
- compositionMarks = view.state.storedMarks;
64
- // Render a CursorWrapper with empty marks if starting a composition in an
65
- // empty textblock with no marks. This prevents the browser from adding a
66
- // <br> to the text block when it becomes empty (either via canceling the
67
- // composition with the escape key or deleting all composition text when
68
- // the composition node is the only text node in the text block)
69
- if (compositionMarks === null && isEmptyTextblock) {
70
- compositionMarks = [];
71
- }
72
- const tr = view.state.tr.setStoredMarks(null);
73
- view.dispatch(tr);
102
+ 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
+ view.dispatch(view.state.tr.deleteSelection().setStoredMarks(storedMarks));
74
104
  handleGapCursorComposition(view);
75
- if (compositionMarks) {
105
+ if (storedMarks) {
76
106
  view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
77
- cursorWrapper: widget(state.selection.from, CursorWrapper, {
107
+ cursorWrapper: widget(view.state.selection.from, CursorWrapper, {
78
108
  key: "cursor-wrapper",
79
- marks: compositionMarks,
109
+ marks: storedMarks,
80
110
  side: 0,
81
111
  raw: true
82
112
  })
@@ -87,24 +117,38 @@ export function beforeInputPlugin() {
87
117
  // node depending on the user's last navigation direction, and the
88
118
  // IME composes into whichever one it found.
89
119
  } else if (view.state.selection.empty) {
90
- // @ts-expect-error internal method
91
120
  view.domObserver.disconnectSelection();
92
121
  try {
93
122
  view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
94
123
  );
95
124
  } finally{
96
- // @ts-expect-error internal method
97
125
  view.domObserver.setCurSelection();
98
- // @ts-expect-error internal method
99
126
  view.domObserver.connectSelection();
100
127
  }
101
128
  }
102
- view.compositionStarting = false;
103
- // We set composing to true after creating the cursor wrapper
104
- // so that no existing text nodes try to protect themselves
105
- // while we're creating the cursor wrapper, which may need
106
- // to split a text node.
129
+ const freezeFrom = view.state.selection.$from.before();
130
+ view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
131
+ freezeFrom
132
+ }));
133
+ const frozenDom = view.nodeDOM(freezeFrom);
134
+ if (!frozenDom) {
135
+ view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
136
+ cursorWrapper: null,
137
+ freezeFrom: null
138
+ }));
139
+ return false;
140
+ }
141
+ preCompositionSnapshot = view.state.doc.nodeAt(freezeFrom)?.content ?? null;
107
142
  view.input.composing = true;
143
+ observer = new MutationObserver((records)=>{
144
+ if (reactKeysPluginKey.getState(view.state)?.freezeFrom == null) {
145
+ return;
146
+ }
147
+ view.domObserver.queue.push(...records);
148
+ view.domObserver.flush();
149
+ syncCompositionViewDescs(view);
150
+ });
151
+ observer.observe(frozenDom, observeOptions);
108
152
  return true;
109
153
  },
110
154
  compositionupdate () {
@@ -113,36 +157,11 @@ export function beforeInputPlugin() {
113
157
  compositionend (view, event) {
114
158
  if (!(view instanceof ReactEditorView)) return false;
115
159
  if (!view.composing) return false;
116
- view.input.composing = false;
117
- compositionMarks = null;
118
- for (const displaced of view.displacedNodes){
119
- // Put the displaced TextViewDesc back into its parent's child list.
120
- const parent = displaced.parent;
121
- if (parent && !parent.children.includes(displaced)) {
122
- parent.children.push(displaced);
123
- parent.children.sort(sortViewDescs);
124
- }
125
- // Restore pmViewDesc claim on the text node.
126
- displaced.dom.pmViewDesc = displaced;
127
- // Truncate the IME text node back to what the displaced PM node says it
128
- // is. The composed content lives in PM state; the next React render will
129
- // mount a sibling TextNodeView that inserts its own DOM (e.g.
130
- // `<span class="word">k</span>`) right after this node.
131
- const claimedText = displaced.node.text ?? "";
132
- if (displaced.nodeDOM.nodeValue !== claimedText) {
133
- displaced.nodeDOM.nodeValue = claimedText;
134
- }
135
- }
160
+ teardownComposition(view, event.timeStamp);
136
161
  view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
137
- cursorWrapper: null
162
+ cursorWrapper: null,
163
+ freezeFrom: null
138
164
  }));
139
- if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
140
- view.input.compositionNode.remove();
141
- }
142
- view.input.compositionEndedAt = event.timeStamp;
143
- view.input.compositionNode = null;
144
- view.input.compositionNodes = [];
145
- view.input.compositionID++;
146
165
  return true;
147
166
  },
148
167
  beforeinput (view, event) {
@@ -193,63 +212,6 @@ export function beforeInputPlugin() {
193
212
  insertText(view, event.data);
194
213
  break;
195
214
  }
196
- case "insertCompositionText":
197
- case "deleteCompositionText":
198
- case "insertFromComposition":
199
- {
200
- if (!(view instanceof ReactEditorView)) break;
201
- const { tr } = view.state;
202
- // There's always a range on insertCompositionText beforeinput events
203
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204
- const range = event.getTargetRanges()[0];
205
- const start = view.posAtDOM(range.startContainer, range.startOffset);
206
- const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
207
- if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
208
- return;
209
- }
210
- if (event.data) {
211
- if (compositionMarks) tr.ensureMarks(compositionMarks);
212
- tr.insertText(event.data, start, end);
213
- } else {
214
- tr.delete(start, end);
215
- }
216
- // When updating a composition within an existing text node,
217
- // we need to avoid remounting it. If the composition is at
218
- // the very beginning of the text node, the start position of
219
- // that node will either be mapped forward (if inserting new
220
- // content) or deleted (if replacing existing content).
221
- //
222
- // This will cause the reactKeys plugin to mint a new key for
223
- // that node, which triggers a remount. So we check to see whether
224
- // we're working on a composition at the very beginning of a text
225
- // node, and if so, tell the react keys plugin not to change the
226
- // key for that node.
227
- //
228
- // We need to check that the marks are the same — if they're not,
229
- // then we're inserting text _before_ this text node, not at the
230
- // start of it, so we actually _do_ want to map the exsting node
231
- // forward.
232
- const $start = view.state.doc.resolve(start);
233
- const $end = view.state.doc.resolve(end);
234
- const marks = compositionMarks ?? $start.marksAcross($end) ?? [];
235
- if ($start.textOffset === 0 && $end.nodeAfter?.marks.every((m)=>m.isInSet(marks))) {
236
- tr.setMeta(reactKeysPluginKey, {
237
- overrides: {
238
- [start]: start
239
- }
240
- });
241
- }
242
- view.dom.addEventListener("input", ()=>{
243
- const sel = view.domSelectionRange();
244
- if (sel.focusNode && sel.focusNode.nodeType === 3) {
245
- view.input.compositionNode = sel.focusNode;
246
- }
247
- view.dispatch(tr);
248
- }, {
249
- once: true
250
- });
251
- break;
252
- }
253
215
  case "deleteWordBackward":
254
216
  case "deleteHardLineBackward":
255
217
  case "deleteSoftLineBackward":
@@ -283,11 +245,59 @@ export function beforeInputPlugin() {
283
245
  }
284
246
  });
285
247
  }
286
- function isCompositionNodeOrphaned(tn) {
287
- if (tn.pmViewDesc) return false;
288
- for(let parent = tn.parentNode; parent; parent = parent.parentNode){
289
- const desc = parent.pmViewDesc;
290
- if (desc instanceof TextViewDesc && desc.nodeDOM === tn) return false;
248
+ function syncCompositionViewDescs(view) {
249
+ const compositionNode = view.domObserver.lastChangedTextNode;
250
+ if (!compositionNode) return;
251
+ const freezeFrom = reactKeysPluginKey.getState(view.state)?.freezeFrom;
252
+ if (freezeFrom == null) return;
253
+ const compositionBlock = view.state.doc.nodeAt(freezeFrom);
254
+ if (!compositionBlock) return;
255
+ const compositionBlockDesc = view.docView.descAt(freezeFrom);
256
+ if (!compositionBlockDesc) return;
257
+ const desc = view.docView.nearestDesc(compositionNode);
258
+ compositionBlockDesc.node = compositionBlock;
259
+ if (desc instanceof TextViewDesc) {
260
+ if (compositionNode.nodeValue && desc.node.text !== compositionNode.nodeValue) {
261
+ desc.node = view.state.schema.text(compositionNode.nodeValue, desc.node.marks);
262
+ desc.nodeDOM = compositionNode;
263
+ compositionNode.pmViewDesc = desc;
264
+ }
265
+ return;
291
266
  }
292
- return true;
267
+ if (desc instanceof CompositionViewDesc) {
268
+ if (compositionNode.nodeValue != null && desc.text !== compositionNode.nodeValue) {
269
+ desc.dom = compositionNode;
270
+ desc.textDOM = compositionNode;
271
+ desc.text = compositionNode.nodeValue;
272
+ compositionNode.pmViewDesc = desc;
273
+ }
274
+ return;
275
+ }
276
+ const parentDesc = desc?.contentDOM ? desc : compositionBlockDesc;
277
+ const children = parentDesc.children;
278
+ // Drop any text or composition desc in this container whose DOM the
279
+ // IME has detached. This covers two cases: a TextViewDesc the IME subsumed
280
+ // into the composition node, and (on Safari, which replaces the whole text
281
+ // node on each composition update) any orphaned composition view
282
+ // desc(s) left over from the previous composition steps.
283
+ for(let i = children.length - 1; i >= 0; i--){
284
+ const c = children[i];
285
+ if (!(c instanceof TextViewDesc) && !(c instanceof CompositionViewDesc)) {
286
+ continue;
287
+ }
288
+ const dom = c.dom;
289
+ if (view.dom.contains(dom)) continue;
290
+ children.splice(i, 1);
291
+ }
292
+ const contentStart = freezeFrom + 1;
293
+ const { from, to } = view.state.selection;
294
+ const textPos = findTextInFragment(compositionBlock.content, compositionNode.nodeValue ?? "", from - contentStart, to - contentStart);
295
+ if (textPos < 0) return;
296
+ const startPos = contentStart + textPos;
297
+ let topDOM = compositionNode;
298
+ while(topDOM.parentNode && topDOM.parentNode !== parentDesc.contentDOM){
299
+ topDOM = topDOM.parentNode;
300
+ }
301
+ const insertIndex = children.findLastIndex((c)=>c.posBefore <= startPos) + 1;
302
+ children.splice(insertIndex, 0, new CompositionViewDesc(parentDesc, ()=>startPos, topDOM, compositionNode, compositionNode.nodeValue ?? ""));
293
303
  }
@@ -20,7 +20,8 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
20
20
  const next = {
21
21
  posToKey: new Map(),
22
22
  keyToPos: new Map(),
23
- cursorWrapper: null
23
+ cursorWrapper: null,
24
+ freezeFrom: null
24
25
  };
25
26
  state.doc.descendants((_, pos)=>{
26
27
  const key = createNodeKey();
@@ -37,19 +38,30 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
37
38
  * through the transaction to identify its current position,
38
39
  * and assign its key to that new position, dropping it if the
39
40
  * node was deleted.
40
- */ apply (tr, value, _, newState) {
41
+ */ apply (tr, value, oldState, newState) {
41
42
  const meta = tr.getMeta(reactKeysPluginKey);
42
43
  const overrides = meta && "overrides" in meta ? meta.overrides : {};
43
44
  const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
45
+ const freezeFrom = meta && "freezeFrom" in meta ? meta.freezeFrom : undefined;
44
46
  const next = {
45
47
  posToKey: new Map(),
46
48
  keyToPos: new Map(),
47
- cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? widget(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper
49
+ cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? widget(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper,
50
+ freezeFrom: freezeFrom === undefined ? value.freezeFrom !== null ? tr.mapping.map(value.freezeFrom, -1) : null : freezeFrom
48
51
  };
52
+ if (value.freezeFrom !== null && next.freezeFrom !== null && tr.getMeta("composition") == null) {
53
+ const oldBlock = oldState.doc.nodeAt(value.freezeFrom);
54
+ const newBlock = newState.doc.nodeAt(next.freezeFrom);
55
+ if (newBlock && !oldBlock?.eq(newBlock)) {
56
+ next.freezeFrom = null;
57
+ next.cursorWrapper = null;
58
+ }
59
+ }
49
60
  if (!tr.docChanged) {
50
61
  return {
51
62
  ...value,
52
- cursorWrapper: next.cursorWrapper
63
+ cursorWrapper: next.cursorWrapper,
64
+ freezeFrom: next.freezeFrom
53
65
  };
54
66
  }
55
67
  const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
@@ -2,7 +2,7 @@ import { NodeView } from "@tiptap/core";
2
2
  /**
3
3
  * Subclass of Tiptap's NodeView to be used in tiptapNodeView.
4
4
  *
5
- * Allows us to pass in an existing dom and contentODM from React ProseMirror's
5
+ * Allows us to pass in an existing dom and contentDOM from React ProseMirror's
6
6
  * ViewDesc, so that we can call Tiptap's default stopEvent and ignoreMutation
7
7
  * methods
8
8
  */ export class ReactProseMirrorNodeView extends NodeView {
@@ -49,7 +49,14 @@ function getInstance() {
49
49
  }
50
50
  };
51
51
  }
52
- export function TiptapEditorContent(param) {
52
+ /**
53
+ * Renders the actual editable ProseMirror document.
54
+ *
55
+ * This **must** be passed as a child to the `TiptapEditorView`
56
+ * component. It may be wrapped in other components, and other
57
+ * childern may be passed before or after. It must be passed the
58
+ * same `editor` as is passed to the `TiptapEditorView`.
59
+ */ export function TiptapEditorContent(param) {
53
60
  let { editor: editorProp, ...props } = param;
54
61
  const editor = editorProp;
55
62
  const { onEditorInitialize, onEditorDeinitialize } = useContext(TiptapEditorContext);
@@ -1,5 +1,9 @@
1
1
  import { useContext } from "react";
2
2
  import { EditorContext } from "../../contexts/EditorContext.js";
3
- export function useIsInReactProseMirror() {
3
+ /**
4
+ * Returns true if the hook is called in a
5
+ * component that's a descendant of the
6
+ * ProseMirror component
7
+ */ export function useIsInReactProseMirror() {
4
8
  return useContext(EditorContext) !== null;
5
9
  }
@@ -23,11 +23,9 @@ import { useTiptapEditorEventCallback } from "./hooks/useTiptapEditorEventCallba
23
23
  * codeBlock: nodeView({
24
24
  * component: function CodeBlock(nodeViewProps) {
25
25
  * return (
26
- * <AnnotatableNodeViewWrapper {...nodeViewProps}>
27
- * <pre>
28
- * <NodeViewContent as="code" />
29
- * </pre>
30
- * </AnnotatableNodeViewWrapper>
26
+ * <pre>
27
+ * <NodeViewContent as="code" />
28
+ * </pre>
31
29
  * )
32
30
  * },
33
31
  * extension: CodeBlockExtension,
@@ -84,15 +82,6 @@ import { useTiptapEditorEventCallback } from "./hooks/useTiptapEditorEventCallba
84
82
  return result;
85
83
  });
86
84
  useIgnoreMutation(function(_, mutation) {
87
- if (ignoreMutation) {
88
- return ignoreMutation.call({
89
- name: extension.name,
90
- editor,
91
- type: node.type
92
- }, {
93
- mutation
94
- });
95
- }
96
85
  if (!editor || !(this.dom instanceof HTMLElement)) return false;
97
86
  const nodeView = new ReactProseMirrorNodeView(WrappedComponent, {
98
87
  extension,
@@ -104,6 +93,16 @@ import { useTiptapEditorEventCallback } from "./hooks/useTiptapEditorEventCallba
104
93
  node,
105
94
  view: editor.view
106
95
  }, this.dom, this.contentDOM);
96
+ if (ignoreMutation) {
97
+ return ignoreMutation.call({
98
+ name: extension.name,
99
+ editor,
100
+ type: node.type
101
+ }, {
102
+ mutation,
103
+ defaultIgnoreMutation: nodeView.ignoreMutation.bind(nodeView)
104
+ });
105
+ }
107
106
  return nodeView.ignoreMutation(mutation) ?? false;
108
107
  });
109
108
  const { extraClassName, htmlProps } = useMemo(()=>{
@@ -382,7 +382,7 @@ export class ViewDesc {
382
382
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
383
383
  if (after && after.contentEditable == "false") force = true;
384
384
  }
385
- if (view.composing || !(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
385
+ if (view.cursorWrapped || !(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
386
386
  return;
387
387
  }
388
388
  // Selection.extend can be used to create an 'inverted' selection
@@ -691,6 +691,9 @@ export class TextViewDesc extends NodeViewDesc {
691
691
  isText(text) {
692
692
  return this.node.text == text;
693
693
  }
694
+ ignoreMutation(mutation) {
695
+ return mutation.type !== "characterData" && mutation.type !== "selection";
696
+ }
694
697
  }
695
698
  // A dummy desc used to tag trailing BR or IMG nodes created to work
696
699
  // around contentEditable terribleness.