@handlewithcare/react-prosemirror 3.1.0-tiptap.52 → 3.1.0-tiptap.53

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 (44) hide show
  1. package/dist/cjs/ReactEditorView.js +6 -0
  2. package/dist/cjs/components/ChildNodeViews.js +16 -10
  3. package/dist/cjs/components/CursorWrapper.js +3 -7
  4. package/dist/cjs/components/ProseMirror.js +5 -3
  5. package/dist/cjs/components/TextNodeView.js +260 -47
  6. package/dist/cjs/components/TrailingHackView.js +70 -0
  7. package/dist/cjs/components/WidgetView.js +3 -1
  8. package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
  9. package/dist/cjs/decorations/viewDecorations.js +1 -6
  10. package/dist/cjs/hooks/useEditor.js +2 -10
  11. package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
  12. package/dist/cjs/hooks/useNodeViewDescription.js +64 -21
  13. package/dist/cjs/plugins/beforeInputPlugin.js +147 -45
  14. package/dist/cjs/plugins/reactKeys.js +21 -14
  15. package/dist/cjs/viewdesc.js +52 -4
  16. package/dist/esm/ReactEditorView.js +6 -0
  17. package/dist/esm/components/ChildNodeViews.js +17 -11
  18. package/dist/esm/components/CursorWrapper.js +3 -7
  19. package/dist/esm/components/ProseMirror.js +5 -3
  20. package/dist/esm/components/TextNodeView.js +209 -45
  21. package/dist/esm/components/TrailingHackView.js +71 -1
  22. package/dist/esm/components/WidgetView.js +3 -1
  23. package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
  24. package/dist/esm/decorations/viewDecorations.js +1 -6
  25. package/dist/esm/hooks/useEditor.js +2 -10
  26. package/dist/esm/hooks/useMarkViewDescription.js +62 -4
  27. package/dist/esm/hooks/useNodeViewDescription.js +65 -22
  28. package/dist/esm/plugins/beforeInputPlugin.js +147 -45
  29. package/dist/esm/plugins/reactKeys.js +21 -14
  30. package/dist/esm/viewdesc.js +51 -4
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/types/ReactEditorView.d.ts +6 -1
  33. package/dist/types/components/TextNodeView.d.ts +20 -5
  34. package/dist/types/components/TrailingHackView.d.ts +1 -1
  35. package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
  36. package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
  37. package/dist/types/decorations/viewDecorations.d.ts +2 -2
  38. package/dist/types/hooks/useEditor.d.ts +1 -2
  39. package/dist/types/hooks/useMarkViewDescription.d.ts +2 -1
  40. package/dist/types/hooks/useNodeViewDescription.d.ts +2 -1
  41. package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
  42. package/dist/types/plugins/reactKeys.d.ts +9 -8
  43. package/dist/types/viewdesc.d.ts +3 -2
  44. package/package.json +3 -1
@@ -120,12 +120,70 @@ 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
+ ]);
123
181
  const childContextValue = (0, _react.useMemo)(()=>({
124
182
  parentRef: viewDescRef,
125
- siblingsRef: childrenRef
183
+ siblingsRef: childrenRef,
184
+ findCompositionDOM
126
185
  }), [
127
- childrenRef,
128
- viewDescRef
186
+ findCompositionDOM
129
187
  ]);
130
188
  return {
131
189
  childContextValue,
@@ -141,32 +141,75 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
141
141
  children.sort(_viewdesc.sortViewDescs);
142
142
  for (const child of children){
143
143
  child.parent = viewDesc;
144
- // Because TextNodeViews can't locate the DOM nodes
145
- // for compositions, we need to override them here
146
- if (child instanceof _viewdesc.CompositionViewDesc) {
147
- const compositionTopDOM = viewDesc?.contentDOM?.firstChild;
148
- if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
149
- let textDOM = compositionTopDOM;
150
- while(textDOM.firstChild){
151
- textDOM = textDOM.firstChild;
152
- }
153
- if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
154
- child.dom = compositionTopDOM;
155
- child.textDOM = textDOM;
156
- child.text = textDOM.data;
157
- child.textDOM.pmViewDesc = child;
158
- // It should not be possible to be in a composition because one could
159
- // not start between the renders that switch the view type.
160
- view.input.compositionNodes.push(child);
161
- }
162
144
  }
163
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
+ ]);
164
207
  const childContextValue = (0, _react.useMemo)(()=>({
165
208
  parentRef: viewDescRef,
166
- siblingsRef: childrenRef
209
+ siblingsRef: childrenRef,
210
+ findCompositionDOM
167
211
  }), [
168
- childrenRef,
169
- viewDescRef
212
+ findCompositionDOM
170
213
  ]);
171
214
  return {
172
215
  childContextValue,
@@ -10,8 +10,11 @@ Object.defineProperty(exports, "beforeInputPlugin", {
10
10
  });
11
11
  const _prosemirrormodel = require("prosemirror-model");
12
12
  const _prosemirrorstate = require("prosemirror-state");
13
+ const _ReactEditorView = require("../ReactEditorView.js");
13
14
  const _CursorWrapper = require("../components/CursorWrapper.js");
14
15
  const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
16
+ const _viewdesc = require("../viewdesc.js");
17
+ const _reactKeys = require("./reactKeys.js");
15
18
  function insertText(view, eventData) {
16
19
  let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
17
20
  if (eventData === null) return false;
@@ -54,33 +57,63 @@ function handleGapCursorComposition(view) {
54
57
  tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
55
58
  view.dispatch(tr);
56
59
  }
57
- function beforeInputPlugin(setCursorWrapper) {
60
+ function beforeInputPlugin() {
58
61
  let compositionMarks = null;
59
- let precompositionSnapshot = null;
60
62
  return new _prosemirrorstate.Plugin({
61
63
  props: {
62
64
  handleDOMEvents: {
63
65
  compositionstart (view) {
64
- compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
65
- view.dispatch(view.state.tr.deleteSelection());
66
- handleGapCursorComposition(view);
66
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
67
+ view.compositionStarting = true;
67
68
  const { state } = view;
68
- const $pos = state.selection.$from;
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);
84
+ handleGapCursorComposition(view);
69
85
  if (compositionMarks) {
70
- setCursorWrapper((0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
71
- key: "cursor-wrapper",
72
- marks: compositionMarks
86
+ view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
87
+ cursorWrapper: (0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
88
+ key: "cursor-wrapper",
89
+ marks: compositionMarks,
90
+ side: 0,
91
+ raw: true
92
+ })
73
93
  }));
94
+ // Pin the DOM cursor to PM's canonical position before the IME
95
+ // captures wherever the browser happened to leave it. Without this,
96
+ // a cursor at a mark boundary lands in either the left or right text
97
+ // node depending on the user's last navigation direction, and the
98
+ // IME composes into whichever one it found.
99
+ } else if (view.state.selection.empty) {
100
+ // @ts-expect-error internal method
101
+ view.domObserver.disconnectSelection();
102
+ try {
103
+ view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
104
+ );
105
+ } finally{
106
+ // @ts-expect-error internal method
107
+ view.domObserver.setCurSelection();
108
+ // @ts-expect-error internal method
109
+ view.domObserver.connectSelection();
110
+ }
74
111
  }
75
- // Snapshot the siblings of the node that contains the
76
- // current cursor. We'll restore this later, so that React
77
- // doesn't panic about unknown DOM nodes.
78
- const { node: parent } = view.domAtPos($pos.pos);
79
- precompositionSnapshot = [];
80
- for (const node of parent.childNodes){
81
- precompositionSnapshot.push(node);
82
- }
83
- // @ts-expect-error Internal property - input
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.
84
117
  view.input.composing = true;
85
118
  return true;
86
119
  },
@@ -88,40 +121,44 @@ function beforeInputPlugin(setCursorWrapper) {
88
121
  return true;
89
122
  },
90
123
  compositionend (view, event) {
91
- // @ts-expect-error Internal property - input
124
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
125
+ if (!view.composing) return false;
92
126
  view.input.composing = false;
93
- const { state } = view;
94
- const { node: parent } = view.domAtPos(state.selection.from);
95
- if (precompositionSnapshot) {
96
- // Restore the snapshot of the parent node's children
97
- // from before the composition started. This gives us a
98
- // clean slate from which to dispatch our transaction
99
- // and trigger a React update.
100
- precompositionSnapshot.forEach((prevNode, i)=>{
101
- if (parent.childNodes.length <= i) {
102
- parent.appendChild(prevNode);
103
- return;
104
- }
105
- parent.replaceChild(prevNode, parent.childNodes.item(i));
106
- });
107
- if (parent.childNodes.length > precompositionSnapshot.length) {
108
- for(let i = precompositionSnapshot.length; i < parent.childNodes.length; i++){
109
- parent.removeChild(parent.childNodes.item(i));
110
- }
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;
111
144
  }
112
145
  }
113
- if (event.data) {
114
- insertText(view, event.data, {
115
- marks: compositionMarks
116
- });
146
+ view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
147
+ cursorWrapper: null
148
+ }));
149
+ if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
150
+ view.input.compositionNode.remove();
117
151
  }
118
- compositionMarks = null;
119
- precompositionSnapshot = null;
120
- setCursorWrapper(null);
152
+ view.input.compositionEndedAt = event.timeStamp;
153
+ view.input.compositionNode = null;
154
+ view.input.compositionNodes = [];
155
+ view.input.compositionID++;
121
156
  return true;
122
157
  },
123
158
  beforeinput (view, event) {
124
- event.preventDefault();
159
+ if (event.inputType !== "insertFromComposition") {
160
+ event.preventDefault();
161
+ }
125
162
  switch(event.inputType){
126
163
  case "insertParagraph":
127
164
  case "insertLineBreak":
@@ -166,6 +203,63 @@ function beforeInputPlugin(setCursorWrapper) {
166
203
  insertText(view, event.data);
167
204
  break;
168
205
  }
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
+ }
169
263
  case "deleteWordBackward":
170
264
  case "deleteHardLineBackward":
171
265
  case "deleteSoftLineBackward":
@@ -199,3 +293,11 @@ function beforeInputPlugin(setCursorWrapper) {
199
293
  }
200
294
  });
201
295
  }
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;
301
+ }
302
+ return true;
303
+ }
@@ -20,20 +20,22 @@ _export(exports, {
20
20
  }
21
21
  });
22
22
  const _prosemirrorstate = require("prosemirror-state");
23
+ const _prosemirrorview = require("prosemirror-view");
24
+ const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
23
25
  function createNodeKey() {
24
26
  const key = Math.floor(Math.random() * 0xffffffffffff).toString(16);
25
27
  return key;
26
28
  }
27
29
  const reactKeysPluginKey = new _prosemirrorstate.PluginKey("@handlewithcare/react-prosemirror/reactKeys");
28
30
  function reactKeys() {
29
- let composing = false;
30
31
  return new _prosemirrorstate.Plugin({
31
32
  key: reactKeysPluginKey,
32
33
  state: {
33
34
  init (_, state) {
34
35
  const next = {
35
36
  posToKey: new Map(),
36
- keyToPos: new Map()
37
+ keyToPos: new Map(),
38
+ cursorWrapper: null
37
39
  };
38
40
  state.doc.descendants((_, pos)=>{
39
41
  const key = createNodeKey();
@@ -51,14 +53,20 @@ function reactKeys() {
51
53
  * and assign its key to that new position, dropping it if the
52
54
  * node was deleted.
53
55
  */ apply (tr, value, _, newState) {
54
- if (!tr.docChanged || composing) {
55
- return value;
56
- }
57
- const overrides = tr.getMeta(reactKeysPluginKey)?.overrides;
56
+ const meta = tr.getMeta(reactKeysPluginKey);
57
+ const overrides = meta && "overrides" in meta ? meta.overrides : {};
58
+ const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
58
59
  const next = {
59
60
  posToKey: new Map(),
60
- keyToPos: new Map()
61
+ 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
61
63
  };
64
+ if (!tr.docChanged) {
65
+ return {
66
+ ...value,
67
+ cursorWrapper: next.cursorWrapper
68
+ };
69
+ }
62
70
  const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
63
71
  let [a] = param, [b] = param1;
64
72
  return a - b;
@@ -84,13 +92,12 @@ function reactKeys() {
84
92
  }
85
93
  },
86
94
  props: {
87
- handleDOMEvents: {
88
- compositionstart: ()=>{
89
- composing = true;
90
- },
91
- compositionend: ()=>{
92
- composing = false;
93
- }
95
+ decorations (state) {
96
+ const deco = reactKeysPluginKey.getState(state)?.cursorWrapper;
97
+ if (!deco) return _prosemirrorview.DecorationSet.empty;
98
+ return _prosemirrorview.DecorationSet.create(state.doc, [
99
+ deco
100
+ ]);
94
101
  }
95
102
  }
96
103
  });
@@ -36,6 +36,9 @@ _export(exports, {
36
36
  WidgetViewDesc: function() {
37
37
  return WidgetViewDesc;
38
38
  },
39
+ findTextInFragment: function() {
40
+ return findTextInFragment;
41
+ },
39
42
  sameOuterDeco: function() {
40
43
  return sameOuterDeco;
41
44
  },
@@ -49,7 +52,17 @@ const _dom = require("./dom.js");
49
52
  function sortViewDescs(a, b) {
50
53
  if (a instanceof TrailingHackViewDesc) return 1;
51
54
  if (b instanceof TrailingHackViewDesc) return -1;
52
- return a.getPos() - b.getPos();
55
+ const posDiff = a.getPos() - b.getPos();
56
+ if (posDiff !== 0) return posDiff;
57
+ // When two descs share the same PM position (e.g. a zero-width widget
58
+ // and a text node that starts at the same position), fall back to DOM
59
+ // order so that the viewdesc children match the actual DOM layout.
60
+ // Without this, position computations like `posBeforeChild` can return
61
+ // the wrong PM position for the widget's container.
62
+ const cmp = a.dom.compareDocumentPosition(b.dom);
63
+ if (cmp & 4 /* DOCUMENT_POSITION_FOLLOWING */ ) return -1;
64
+ if (cmp & 2 /* DOCUMENT_POSITION_PRECEDING */ ) return 1;
65
+ return 0;
53
66
  }
54
67
  const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
55
68
  let ViewDesc = class ViewDesc {
@@ -266,7 +279,9 @@ let ViewDesc = class ViewDesc {
266
279
  prev = i ? this.children[i - 1] : null;
267
280
  if (!prev || prev.dom.parentNode == this.contentDOM) break;
268
281
  }
269
- if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side);
282
+ if (prev && side && enter && !prev.border && !prev.domAtom) {
283
+ return prev.domFromPos(prev.size, side);
284
+ }
270
285
  return {
271
286
  node: this.contentDOM,
272
287
  offset: prev ? (0, _dom.domIndex)(prev.dom) + 1 : 0
@@ -401,7 +416,9 @@ let ViewDesc = class ViewDesc {
401
416
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
402
417
  if (after && after.contentEditable == "false") force = true;
403
418
  }
404
- if (!(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)) return;
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)) {
420
+ return;
421
+ }
405
422
  // Selection.extend can be used to create an 'inverted' selection
406
423
  // (one where the focus is before the anchor), but not all
407
424
  // browsers support it yet.
@@ -666,7 +683,10 @@ let TextViewDesc = class TextViewDesc extends NodeViewDesc {
666
683
  skip: skip || true
667
684
  };
668
685
  }
669
- update(_node, _outerDeco, _innerDeco, _view) {
686
+ update(node, outerDeco, _innerDeco, _view) {
687
+ if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
688
+ this.updateOuterDeco(outerDeco);
689
+ this.node = node;
670
690
  this.dirty = NOT_DIRTY;
671
691
  return true;
672
692
  }
@@ -782,3 +802,31 @@ function sameOuterDeco(a, b) {
782
802
  for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
783
803
  return true;
784
804
  }
805
+ function findTextInFragment(frag, text, from, to) {
806
+ for(let i = 0, pos = 0; i < frag.childCount && pos <= to;){
807
+ const child = frag.child(i++);
808
+ const childStart = pos;
809
+ pos += child.nodeSize;
810
+ if (!child.isText) continue;
811
+ let str = child.text;
812
+ while(i < frag.childCount){
813
+ const next = frag.child(i++);
814
+ pos += next.nodeSize;
815
+ if (!next.isText) break;
816
+ str += next.text;
817
+ }
818
+ if (pos >= from) {
819
+ if (pos >= to && str.slice(to - text.length - childStart, to - childStart) === text) {
820
+ return to - text.length;
821
+ }
822
+ const found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
823
+ if (found >= 0 && found + text.length + childStart >= from) {
824
+ return childStart + found;
825
+ }
826
+ if (from === to && str.length >= to + text.length - childStart && str.slice(to - childStart, to - childStart + text.length) === text) {
827
+ return to;
828
+ }
829
+ }
830
+ }
831
+ return -1;
832
+ }
@@ -33,6 +33,10 @@ 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
40
  constructor(place, props){
37
41
  // Prevent the base class from destroying the React-managed nodes.
38
42
  // Restore them below after invoking the base class constructor.
@@ -85,6 +89,8 @@ function changedNodeViews(a, b) {
85
89
  // @ts-expect-error this violates the typing but class does it, too.
86
90
  this.docView = null;
87
91
  this._destroyed = false;
92
+ this.compositionStarting = false;
93
+ this.displacedNodes = [];
88
94
  }
89
95
  get props() {
90
96
  return this.nextProps;
@@ -7,7 +7,7 @@ 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 { TextNodeView } from "./TextNodeView.js";
10
+ import { RemountableTextNodeView } 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";
@@ -50,14 +50,20 @@ 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 } = 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
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
+ });
61
67
  });
62
68
  }) : /*#__PURE__*/ React.createElement(NodeView, {
63
69
  key: child.key,
@@ -340,14 +346,14 @@ export const ChildNodeViews = /*#__PURE__*/ memo(function ChildNodeViews(param)
340
346
  component: SeparatorHackView,
341
347
  marks: [],
342
348
  offset: lastChild?.offset ?? 0,
343
- index: (lastChild?.index ?? 0) + 2,
349
+ index: (lastChild?.index ?? 0) + 1,
344
350
  key: "trailing-hack-img"
345
351
  }, {
346
352
  type: "hack",
347
353
  component: TrailingHackView,
348
354
  marks: [],
349
355
  offset: lastChild?.offset ?? 0,
350
- index: (lastChild?.index ?? 0) + 1,
356
+ index: (lastChild?.index ?? 0) + 2,
351
357
  key: "trailing-hack-br"
352
358
  });
353
359
  }