@handlewithcare/react-prosemirror 3.1.0-tiptap.51 → 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 (58) hide show
  1. package/dist/cjs/ReactEditorView.js +6 -2
  2. package/dist/cjs/components/ChildNodeViews.js +3 -2
  3. package/dist/cjs/components/CursorWrapper.js +3 -4
  4. package/dist/cjs/components/ProseMirror.js +5 -3
  5. package/dist/cjs/components/TextNodeView.js +176 -34
  6. package/dist/cjs/components/TrailingHackView.js +42 -1
  7. package/dist/cjs/components/WidgetView.js +4 -1
  8. package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
  9. package/dist/cjs/decorations/viewDecorations.js +1 -6
  10. package/dist/cjs/hooks/useComponentEventListeners.js +6 -14
  11. package/dist/cjs/hooks/useEditor.js +2 -10
  12. package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
  13. package/dist/cjs/hooks/useNodeViewDescription.js +45 -23
  14. package/dist/cjs/plugins/beforeInputPlugin.js +116 -12
  15. package/dist/cjs/plugins/componentEventListeners.js +2 -9
  16. package/dist/cjs/plugins/reactKeys.js +21 -14
  17. package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +59 -0
  18. package/dist/cjs/viewdesc.js +43 -2
  19. package/dist/esm/ReactEditorView.js +6 -2
  20. package/dist/esm/components/ChildNodeViews.js +4 -3
  21. package/dist/esm/components/CursorWrapper.js +4 -5
  22. package/dist/esm/components/ProseMirror.js +5 -3
  23. package/dist/esm/components/TextNodeView.js +125 -32
  24. package/dist/esm/components/TrailingHackView.js +42 -1
  25. package/dist/esm/components/WidgetView.js +4 -1
  26. package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
  27. package/dist/esm/decorations/viewDecorations.js +1 -6
  28. package/dist/esm/hooks/useComponentEventListeners.js +6 -14
  29. package/dist/esm/hooks/useEditor.js +2 -10
  30. package/dist/esm/hooks/useMarkViewDescription.js +62 -4
  31. package/dist/esm/hooks/useNodeViewDescription.js +46 -24
  32. package/dist/esm/plugins/beforeInputPlugin.js +116 -12
  33. package/dist/esm/plugins/componentEventListeners.js +2 -9
  34. package/dist/esm/plugins/reactKeys.js +21 -14
  35. package/dist/esm/tiptap/hooks/useTiptapEditor.js +7 -1
  36. package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +56 -0
  37. package/dist/esm/viewdesc.js +42 -2
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/dist/types/ReactEditorView.d.ts +3 -2
  40. package/dist/types/components/CursorWrapper.d.ts +2 -4
  41. package/dist/types/components/TextNodeView.d.ts +11 -6
  42. package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
  43. package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
  44. package/dist/types/constants.d.ts +1 -1
  45. package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
  46. package/dist/types/decorations/viewDecorations.d.ts +2 -2
  47. package/dist/types/hooks/useComponentEventListeners.d.ts +1 -1
  48. package/dist/types/hooks/useEditor.d.ts +1 -2
  49. package/dist/types/hooks/useMarkViewDescription.d.ts +2 -1
  50. package/dist/types/hooks/useNodeViewDescription.d.ts +2 -1
  51. package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
  52. package/dist/types/plugins/componentEventListeners.d.ts +2 -3
  53. package/dist/types/plugins/reactKeys.d.ts +9 -8
  54. package/dist/types/props.d.ts +26 -26
  55. package/dist/types/tiptap/hooks/useTiptapEditor.d.ts +7 -0
  56. package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +1 -0
  57. package/dist/types/viewdesc.d.ts +3 -2
  58. package/package.json +2 -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,
@@ -10,10 +10,8 @@ Object.defineProperty(exports, "useNodeViewDescription", {
10
10
  });
11
11
  const _react = require("react");
12
12
  const _ReactEditorView = require("../ReactEditorView.js");
13
- const _CursorWrapper = require("../components/CursorWrapper.js");
14
13
  const _ChildDescriptionsContext = require("../contexts/ChildDescriptionsContext.js");
15
14
  const _EditorContext = require("../contexts/EditorContext.js");
16
- const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
17
15
  const _viewdesc = require("../viewdesc.js");
18
16
  const _useClientLayoutEffect = require("./useClientLayoutEffect.js");
19
17
  const _useEffectEvent = require("./useEffectEvent.js");
@@ -144,30 +142,51 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
144
142
  for (const child of children){
145
143
  child.parent = viewDesc;
146
144
  }
145
+ });
146
+ const findCompositionDOM = (0, _react.useCallback)((compositionViewDesc)=>{
147
147
  if (!props.node.isTextblock) return;
148
+ const children = childrenRef.current;
148
149
  // Because TextNodeViews can't locate the DOM nodes
149
150
  // for compositions, we need to override them here
150
151
  if (!viewDescRef.current?.contentDOM) return;
151
- const compositionChildIndex = children.findIndex((child)=>child instanceof _viewdesc.CompositionViewDesc);
152
- if (compositionChildIndex === -1) return;
153
- const compositionViewDesc = children[compositionChildIndex];
154
- if (!(compositionViewDesc instanceof _viewdesc.CompositionViewDesc)) return;
155
152
  let compositionTopDOM = null;
156
- let search = children[compositionChildIndex - 1];
157
- while(search instanceof _viewdesc.MarkViewDesc){
158
- search = search.children[0];
159
- }
160
- if (search instanceof _viewdesc.WidgetViewDesc && search.widget.type instanceof _ReactWidgetType.ReactWidgetType && search.widget.type.Component === _CursorWrapper.CursorWrapper) {
161
- compositionTopDOM = search.dom.nextSibling;
162
- } else {
163
- for (const childNode of viewDescRef.current.contentDOM.childNodes){
164
- if (children.every((child)=>child.dom !== childNode)) {
165
- compositionTopDOM = childNode;
166
- break;
167
- }
153
+ for (const childNode of viewDescRef.current.contentDOM.childNodes){
154
+ if (children.every((child)=>child.dom !== childNode)) {
155
+ compositionTopDOM = childNode;
156
+ break;
168
157
  }
169
158
  }
170
- if (!compositionTopDOM) return;
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
+ }
171
190
  let textDOM = compositionTopDOM;
172
191
  while(textDOM.firstChild){
173
192
  textDOM = textDOM.firstChild;
@@ -181,13 +200,16 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
181
200
  compositionViewDesc.text = textDOM.data;
182
201
  compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
183
202
  view.input.compositionNodes.push(compositionViewDesc);
184
- });
203
+ }, [
204
+ props.node.isTextblock,
205
+ view
206
+ ]);
185
207
  const childContextValue = (0, _react.useMemo)(()=>({
186
208
  parentRef: viewDescRef,
187
- siblingsRef: childrenRef
209
+ siblingsRef: childrenRef,
210
+ findCompositionDOM
188
211
  }), [
189
- childrenRef,
190
- viewDescRef
212
+ findCompositionDOM
191
213
  ]);
192
214
  return {
193
215
  childContextValue,
@@ -13,6 +13,8 @@ const _prosemirrorstate = require("prosemirror-state");
13
13
  const _ReactEditorView = require("../ReactEditorView.js");
14
14
  const _CursorWrapper = require("../components/CursorWrapper.js");
15
15
  const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
16
+ const _viewdesc = require("../viewdesc.js");
17
+ const _reactKeys = require("./reactKeys.js");
16
18
  function insertText(view, eventData) {
17
19
  let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
18
20
  if (eventData === null) return false;
@@ -55,26 +57,64 @@ function handleGapCursorComposition(view) {
55
57
  tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
56
58
  view.dispatch(tr);
57
59
  }
58
- function beforeInputPlugin(setCursorWrapper) {
60
+ function beforeInputPlugin() {
59
61
  let compositionMarks = null;
60
62
  return new _prosemirrorstate.Plugin({
61
63
  props: {
62
64
  handleDOMEvents: {
63
65
  compositionstart (view) {
64
66
  if (!(view instanceof _ReactEditorView.ReactEditorView)) return false;
65
- view.input.composing = true;
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;
66
73
  compositionMarks = view.state.storedMarks;
67
- const tr = view.state.tr.deleteSelection().setStoredMarks(null);
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);
68
83
  view.dispatch(tr);
69
84
  handleGapCursorComposition(view);
70
- const { state } = view;
71
- if (compositionMarks?.length) {
72
- setCursorWrapper((0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
73
- key: "cursor-wrapper",
74
- marks: compositionMarks,
75
- side: 1
85
+ if (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
+ })
76
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
+ }
77
111
  }
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.
117
+ view.input.composing = true;
78
118
  return true;
79
119
  },
80
120
  compositionupdate () {
@@ -85,17 +125,40 @@ function beforeInputPlugin(setCursorWrapper) {
85
125
  if (!view.composing) return false;
86
126
  view.input.composing = false;
87
127
  compositionMarks = null;
88
- setCursorWrapper(null);
89
- if (view.input.compositionNode && !view.input.compositionNode.pmViewDesc) {
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
+ }
146
+ view.dispatch(view.state.tr.setMeta(_reactKeys.reactKeysPluginKey, {
147
+ cursorWrapper: null
148
+ }));
149
+ if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
90
150
  view.input.compositionNode.remove();
91
151
  }
92
152
  view.input.compositionEndedAt = event.timeStamp;
93
153
  view.input.compositionNode = null;
154
+ view.input.compositionNodes = [];
94
155
  view.input.compositionID++;
95
156
  return true;
96
157
  },
97
158
  beforeinput (view, event) {
98
- event.preventDefault();
159
+ if (event.inputType !== "insertFromComposition") {
160
+ event.preventDefault();
161
+ }
99
162
  switch(event.inputType){
100
163
  case "insertParagraph":
101
164
  case "insertLineBreak":
@@ -141,7 +204,10 @@ function beforeInputPlugin(setCursorWrapper) {
141
204
  break;
142
205
  }
143
206
  case "insertCompositionText":
207
+ case "deleteCompositionText":
208
+ case "insertFromComposition":
144
209
  {
210
+ if (!(view instanceof _ReactEditorView.ReactEditorView)) break;
145
211
  const { tr } = view.state;
146
212
  // There's always a range on insertCompositionText beforeinput events
147
213
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -157,7 +223,37 @@ function beforeInputPlugin(setCursorWrapper) {
157
223
  } else {
158
224
  tr.delete(start, end);
159
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
+ }
160
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
+ }
161
257
  view.dispatch(tr);
162
258
  }, {
163
259
  once: true
@@ -197,3 +293,11 @@ function beforeInputPlugin(setCursorWrapper) {
197
293
  }
198
294
  });
199
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
+ }
@@ -8,7 +8,6 @@ Object.defineProperty(exports, "componentEventListeners", {
8
8
  return componentEventListeners;
9
9
  }
10
10
  });
11
- const _prosemirrorstate = require("prosemirror-state");
12
11
  const _reactdom = require("react-dom");
13
12
  function componentEventListeners(eventHandlerRegistry) {
14
13
  const domEventHandlers = {};
@@ -17,7 +16,7 @@ function componentEventListeners(eventHandlerRegistry) {
17
16
  for (const handler of handlers){
18
17
  let handled = false;
19
18
  (0, _reactdom.unstable_batchedUpdates)(()=>{
20
- handled = !!handler.call(this, view, event);
19
+ handled = !!handler(view, event);
21
20
  });
22
21
  if (handled || event.defaultPrevented) return true;
23
22
  }
@@ -25,11 +24,5 @@ function componentEventListeners(eventHandlerRegistry) {
25
24
  }
26
25
  domEventHandlers[eventType] = handleEvent;
27
26
  }
28
- const plugin = new _prosemirrorstate.Plugin({
29
- key: new _prosemirrorstate.PluginKey("@handlewithcare/react-prosemirror/componentEventListeners"),
30
- props: {
31
- handleDOMEvents: domEventHandlers
32
- }
33
- });
34
- return plugin;
27
+ return domEventHandlers;
35
28
  }
@@ -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
  });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * This file is used to patch global DOM variables in a NodeJS environment.
3
+ * This is needed for ProseMirror to work in a NodeJS environment.
4
+ */ "use strict";
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ if (typeof window === "undefined") {
9
+ // Make sure to import JSDOM only in a NodeJS environment.
10
+ // The magic comments prevent bundlers from statically analyzing this require:
11
+ // - webpackIgnore: true → webpack / Next.js
12
+ // - @vite-ignore → Vite
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
14
+ const jsdom = require(/* webpackIgnore: true */ /* @vite-ignore */ "jsdom");
15
+ const html = `
16
+ <!DOCTYPE html>
17
+ <html>
18
+ <head>
19
+ <title>Testing</title>
20
+ </head>
21
+ <body></body>
22
+ </html>
23
+ `;
24
+ const { window: window1 } = new jsdom.JSDOM(html);
25
+ global.window = window1;
26
+ global.document = window1.document;
27
+ // Use Object.defineProperty for navigator since it's read-only in Node.js 22+
28
+ Object.defineProperty(global, "navigator", {
29
+ value: window1.navigator,
30
+ writable: true,
31
+ configurable: true
32
+ });
33
+ global.innerHeight = 0;
34
+ global.SVGElement = window1.SVGElement;
35
+ // @ts-expect-error stub getSelection for SSR
36
+ document.getSelection = ()=>({});
37
+ document.createRange = ()=>({
38
+ setStart () {},
39
+ setEnd () {},
40
+ // @ts-expect-error stub getBoundingClientRect for SSR
41
+ getClientRects () {
42
+ return {
43
+ left: 0,
44
+ top: 0,
45
+ right: 0,
46
+ bottom: 0
47
+ };
48
+ },
49
+ // @ts-expect-error stub getBoundingClientRect for SSR
50
+ getBoundingClientRect () {
51
+ return {
52
+ left: 0,
53
+ top: 0,
54
+ right: 0,
55
+ bottom: 0
56
+ };
57
+ }
58
+ });
59
+ }
@@ -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 {
@@ -403,7 +416,7 @@ let ViewDesc = class ViewDesc {
403
416
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
404
417
  if (after && after.contentEditable == "false") force = true;
405
418
  }
406
- 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)) {
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)) {
407
420
  return;
408
421
  }
409
422
  // Selection.extend can be used to create an 'inverted' selection
@@ -789,3 +802,31 @@ function sameOuterDeco(a, b) {
789
802
  for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
790
803
  return true;
791
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,7 +33,10 @@ function changedNodeViews(a, b) {
33
33
  nextProps;
34
34
  prevState;
35
35
  _destroyed;
36
- deferPendingEffects;
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;
37
40
  constructor(place, props){
38
41
  // Prevent the base class from destroying the React-managed nodes.
39
42
  // Restore them below after invoking the base class constructor.
@@ -86,7 +89,8 @@ function changedNodeViews(a, b) {
86
89
  // @ts-expect-error this violates the typing but class does it, too.
87
90
  this.docView = null;
88
91
  this._destroyed = false;
89
- this.deferPendingEffects = false;
92
+ this.compositionStarting = false;
93
+ this.displacedNodes = [];
90
94
  }
91
95
  get props() {
92
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,15 +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 } = param;
53
+ let { siblingsRef, parentRef, findCompositionDOM } = param;
54
54
  return /*#__PURE__*/ React.createElement(EditorContext.Consumer, null, (param)=>{
55
55
  let { registerEventListener, unregisterEventListener } = param;
56
- return /*#__PURE__*/ React.createElement(TextNodeView, {
56
+ return /*#__PURE__*/ React.createElement(RemountableTextNodeView, {
57
57
  view: view,
58
58
  node: child.node,
59
59
  getPos: getPos,
60
60
  siblingsRef: siblingsRef,
61
61
  parentRef: parentRef,
62
+ findCompositionDOM: findCompositionDOM,
62
63
  decorations: child.outerDeco,
63
64
  registerEventListener: registerEventListener,
64
65
  unregisterEventListener: unregisterEventListener