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

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 (39) hide show
  1. package/dist/cjs/ReactEditorView.js +0 -2
  2. package/dist/cjs/components/ChildNodeViews.js +9 -14
  3. package/dist/cjs/components/CursorWrapper.js +9 -6
  4. package/dist/cjs/components/TextNodeView.js +38 -109
  5. package/dist/cjs/components/TrailingHackView.js +0 -29
  6. package/dist/cjs/components/WidgetView.js +1 -0
  7. package/dist/cjs/hooks/useComponentEventListeners.js +6 -14
  8. package/dist/cjs/hooks/useNodeViewDescription.js +16 -37
  9. package/dist/cjs/plugins/beforeInputPlugin.js +43 -41
  10. package/dist/cjs/plugins/componentEventListeners.js +2 -9
  11. package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +59 -0
  12. package/dist/cjs/viewdesc.js +3 -10
  13. package/dist/esm/ReactEditorView.js +0 -2
  14. package/dist/esm/components/ChildNodeViews.js +9 -14
  15. package/dist/esm/components/CursorWrapper.js +10 -7
  16. package/dist/esm/components/TextNodeView.js +38 -109
  17. package/dist/esm/components/TrailingHackView.js +1 -30
  18. package/dist/esm/components/WidgetView.js +1 -0
  19. package/dist/esm/hooks/useComponentEventListeners.js +6 -14
  20. package/dist/esm/hooks/useNodeViewDescription.js +17 -38
  21. package/dist/esm/plugins/beforeInputPlugin.js +43 -41
  22. package/dist/esm/plugins/componentEventListeners.js +2 -9
  23. package/dist/esm/tiptap/hooks/useTiptapEditor.js +7 -1
  24. package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +56 -0
  25. package/dist/esm/viewdesc.js +3 -10
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/types/ReactEditorView.d.ts +0 -4
  28. package/dist/types/components/CursorWrapper.d.ts +2 -4
  29. package/dist/types/components/TextNodeView.d.ts +4 -14
  30. package/dist/types/components/TrailingHackView.d.ts +1 -1
  31. package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
  32. package/dist/types/constants.d.ts +1 -1
  33. package/dist/types/hooks/useComponentEventListeners.d.ts +1 -1
  34. package/dist/types/plugins/componentEventListeners.d.ts +2 -3
  35. package/dist/types/props.d.ts +26 -26
  36. package/dist/types/tiptap/hooks/useTiptapEditor.d.ts +7 -0
  37. package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +1 -0
  38. package/dist/types/viewdesc.d.ts +2 -2
  39. package/package.json +1 -2
@@ -1,6 +1,5 @@
1
1
  import { Fragment, Slice } from "prosemirror-model";
2
2
  import { Plugin, TextSelection } from "prosemirror-state";
3
- import { ReactEditorView } from "../ReactEditorView.js";
4
3
  import { CursorWrapper } from "../components/CursorWrapper.js";
5
4
  import { widget } from "../decorations/ReactWidgetType.js";
6
5
  function insertText(view, eventData) {
@@ -47,41 +46,68 @@ function handleGapCursorComposition(view) {
47
46
  }
48
47
  export function beforeInputPlugin(setCursorWrapper) {
49
48
  let compositionMarks = null;
49
+ let precompositionSnapshot = null;
50
50
  return new Plugin({
51
51
  props: {
52
52
  handleDOMEvents: {
53
53
  compositionstart (view) {
54
- if (!(view instanceof ReactEditorView)) return false;
55
- view.input.composing = true;
56
- compositionMarks = view.state.storedMarks;
57
- const tr = view.state.tr.deleteSelection().setStoredMarks(null);
58
- view.dispatch(tr);
54
+ compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
55
+ view.dispatch(view.state.tr.deleteSelection());
59
56
  handleGapCursorComposition(view);
60
57
  const { state } = view;
61
- if (compositionMarks?.length) {
58
+ const $pos = state.selection.$from;
59
+ if (compositionMarks) {
62
60
  setCursorWrapper(widget(state.selection.from, CursorWrapper, {
63
61
  key: "cursor-wrapper",
64
- marks: compositionMarks,
65
- side: 1
62
+ marks: compositionMarks
66
63
  }));
67
64
  }
65
+ // Snapshot the siblings of the node that contains the
66
+ // current cursor. We'll restore this later, so that React
67
+ // doesn't panic about unknown DOM nodes.
68
+ const { node: parent } = view.domAtPos($pos.pos);
69
+ precompositionSnapshot = [];
70
+ for (const node of parent.childNodes){
71
+ precompositionSnapshot.push(node);
72
+ }
73
+ // @ts-expect-error Internal property - input
74
+ view.input.composing = true;
68
75
  return true;
69
76
  },
70
77
  compositionupdate () {
71
78
  return true;
72
79
  },
73
80
  compositionend (view, event) {
74
- if (!(view instanceof ReactEditorView)) return false;
75
- if (!view.composing) return false;
81
+ // @ts-expect-error Internal property - input
76
82
  view.input.composing = false;
83
+ const { state } = view;
84
+ const { node: parent } = view.domAtPos(state.selection.from);
85
+ if (precompositionSnapshot) {
86
+ // Restore the snapshot of the parent node's children
87
+ // from before the composition started. This gives us a
88
+ // clean slate from which to dispatch our transaction
89
+ // and trigger a React update.
90
+ precompositionSnapshot.forEach((prevNode, i)=>{
91
+ if (parent.childNodes.length <= i) {
92
+ parent.appendChild(prevNode);
93
+ return;
94
+ }
95
+ parent.replaceChild(prevNode, parent.childNodes.item(i));
96
+ });
97
+ if (parent.childNodes.length > precompositionSnapshot.length) {
98
+ for(let i = precompositionSnapshot.length; i < parent.childNodes.length; i++){
99
+ parent.removeChild(parent.childNodes.item(i));
100
+ }
101
+ }
102
+ }
103
+ if (event.data) {
104
+ insertText(view, event.data, {
105
+ marks: compositionMarks
106
+ });
107
+ }
77
108
  compositionMarks = null;
109
+ precompositionSnapshot = null;
78
110
  setCursorWrapper(null);
79
- if (view.input.compositionNode && !view.input.compositionNode.pmViewDesc) {
80
- view.input.compositionNode.remove();
81
- }
82
- view.input.compositionEndedAt = event.timeStamp;
83
- view.input.compositionNode = null;
84
- view.input.compositionID++;
85
111
  return true;
86
112
  },
87
113
  beforeinput (view, event) {
@@ -130,30 +156,6 @@ export function beforeInputPlugin(setCursorWrapper) {
130
156
  insertText(view, event.data);
131
157
  break;
132
158
  }
133
- case "insertCompositionText":
134
- {
135
- const { tr } = view.state;
136
- // There's always a range on insertCompositionText beforeinput events
137
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
138
- const range = event.getTargetRanges()[0];
139
- const start = view.posAtDOM(range.startContainer, range.startOffset);
140
- const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
141
- if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
142
- return;
143
- }
144
- if (event.data) {
145
- if (compositionMarks) tr.ensureMarks(compositionMarks);
146
- tr.insertText(event.data, start, end);
147
- } else {
148
- tr.delete(start, end);
149
- }
150
- view.dom.addEventListener("input", ()=>{
151
- view.dispatch(tr);
152
- }, {
153
- once: true
154
- });
155
- break;
156
- }
157
159
  case "deleteWordBackward":
158
160
  case "deleteHardLineBackward":
159
161
  case "deleteSoftLineBackward":
@@ -1,4 +1,3 @@
1
- import { Plugin, PluginKey } from "prosemirror-state";
2
1
  import { unstable_batchedUpdates as batch } from "react-dom";
3
2
  export function componentEventListeners(eventHandlerRegistry) {
4
3
  const domEventHandlers = {};
@@ -7,7 +6,7 @@ export function componentEventListeners(eventHandlerRegistry) {
7
6
  for (const handler of handlers){
8
7
  let handled = false;
9
8
  batch(()=>{
10
- handled = !!handler.call(this, view, event);
9
+ handled = !!handler(view, event);
11
10
  });
12
11
  if (handled || event.defaultPrevented) return true;
13
12
  }
@@ -15,11 +14,5 @@ export function componentEventListeners(eventHandlerRegistry) {
15
14
  }
16
15
  domEventHandlers[eventType] = handleEvent;
17
16
  }
18
- const plugin = new Plugin({
19
- key: new PluginKey("@handlewithcare/react-prosemirror/componentEventListeners"),
20
- props: {
21
- handleDOMEvents: domEventHandlers
22
- }
23
- });
24
- return plugin;
17
+ return domEventHandlers;
25
18
  }
@@ -2,7 +2,13 @@ import { StaticEditorView } from "../../StaticEditorView.js";
2
2
  import { ReactProseMirror } from "../extensions/ReactProseMirror.js";
3
3
  import { ReactProseMirrorCommands } from "../extensions/ReactProseMirrorCommands.js";
4
4
  import { useEditor } from "./useEditor.js";
5
- export function useTiptapEditor(options, deps) {
5
+ /**
6
+ * Create a React ProseMirror integrated Tiptap Editor instance.
7
+ * @param options The editor options
8
+ * @param deps The dependencies to watch for changes
9
+ * @returns The editor instance
10
+ * @example const editor = useEditor({ extensions: [...] })
11
+ */ export function useTiptapEditor(options, deps) {
6
12
  const extensions = [
7
13
  ReactProseMirror,
8
14
  ...options.extensions ?? []
@@ -0,0 +1,56 @@
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
+ */ if (typeof window === "undefined") {
5
+ // Make sure to import JSDOM only in a NodeJS environment.
6
+ // The magic comments prevent bundlers from statically analyzing this require:
7
+ // - webpackIgnore: true → webpack / Next.js
8
+ // - @vite-ignore → Vite
9
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
10
+ const jsdom = require(/* webpackIgnore: true */ /* @vite-ignore */ "jsdom");
11
+ const html = `
12
+ <!DOCTYPE html>
13
+ <html>
14
+ <head>
15
+ <title>Testing</title>
16
+ </head>
17
+ <body></body>
18
+ </html>
19
+ `;
20
+ const { window: window1 } = new jsdom.JSDOM(html);
21
+ global.window = window1;
22
+ global.document = window1.document;
23
+ // Use Object.defineProperty for navigator since it's read-only in Node.js 22+
24
+ Object.defineProperty(global, "navigator", {
25
+ value: window1.navigator,
26
+ writable: true,
27
+ configurable: true
28
+ });
29
+ global.innerHeight = 0;
30
+ global.SVGElement = window1.SVGElement;
31
+ // @ts-expect-error stub getSelection for SSR
32
+ document.getSelection = ()=>({});
33
+ document.createRange = ()=>({
34
+ setStart () {},
35
+ setEnd () {},
36
+ // @ts-expect-error stub getBoundingClientRect for SSR
37
+ getClientRects () {
38
+ return {
39
+ left: 0,
40
+ top: 0,
41
+ right: 0,
42
+ bottom: 0
43
+ };
44
+ },
45
+ // @ts-expect-error stub getBoundingClientRect for SSR
46
+ getBoundingClientRect () {
47
+ return {
48
+ left: 0,
49
+ top: 0,
50
+ right: 0,
51
+ bottom: 0
52
+ };
53
+ }
54
+ });
55
+ }
56
+ export { };
@@ -235,9 +235,7 @@ export class ViewDesc {
235
235
  prev = i ? this.children[i - 1] : null;
236
236
  if (!prev || prev.dom.parentNode == this.contentDOM) break;
237
237
  }
238
- if (prev && side && enter && !prev.border && !prev.domAtom) {
239
- return prev.domFromPos(prev.size, side);
240
- }
238
+ if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side);
241
239
  return {
242
240
  node: this.contentDOM,
243
241
  offset: prev ? domIndex(prev.dom) + 1 : 0
@@ -372,9 +370,7 @@ export class ViewDesc {
372
370
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
373
371
  if (after && after.contentEditable == "false") force = true;
374
372
  }
375
- if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
376
- return;
377
- }
373
+ if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
378
374
  // Selection.extend can be used to create an 'inverted' selection
379
375
  // (one where the focus is before the anchor), but not all
380
376
  // browsers support it yet.
@@ -649,10 +645,7 @@ export class TextViewDesc extends NodeViewDesc {
649
645
  skip: skip || true
650
646
  };
651
647
  }
652
- update(node, outerDeco, _innerDeco, _view) {
653
- if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
654
- this.updateOuterDeco(outerDeco);
655
- this.node = node;
648
+ update(_node, _outerDeco, _innerDeco, _view) {
656
649
  this.dirty = NOT_DIRTY;
657
650
  return true;
658
651
  }