@handlewithcare/react-prosemirror 3.0.3 → 3.0.5

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.
package/README.md CHANGED
@@ -95,6 +95,7 @@ import "prosemirror-view/style/prosemirror.css";
95
95
  - [When should I use this?](#when-should-i-use-this)
96
96
  - [Migrations](#migrations)
97
97
  - [Looking for someone to collaborate with?](#looking-for-someone-to-collaborate-with)
98
+ - [Sponsors](#sponsors)
98
99
 
99
100
  <!-- tocstop -->
100
101
 
@@ -818,3 +819,28 @@ Reach out to [Handle with Care](https://handlewithcare.dev/#get-in-touch)! We're
818
819
  a product development collective with years of experience bringing excellent
819
820
  ideas to life. We love React and ProseMirror, and we're always looking for new
820
821
  folks to work with!
822
+
823
+ <!-- NOTE: This section is autogenerated. Do not manually edit.-->
824
+ <!--sponsorsstart-->
825
+
826
+ ## Sponsors
827
+
828
+ The following companies, organizations, and individuals support Pitter Patter's
829
+ ongoing development.
830
+
831
+ Sponsors receive regular updates about development progress, including previews
832
+ of upcoming features, priority getting issues addressed as we release features,
833
+ and a direct line of communication to us for support. Funders above a certain
834
+ threshold join our steering committee — a lightweight governance model where
835
+ significant contributors help shape our roadmap priorities.
836
+
837
+ [Become a Sponsor](https://handlewithcare.dev/pitter-patter/#become-a-sponsor)
838
+
839
+ <h3>Sponsors</h3>
840
+ <p>
841
+ <a href="https://www.moment.dev/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Moment.png" alt="Moment" height="128"></a>
842
+ <a href="https://www.lingco.io/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Lingco.png" alt="Lingco" height="128"></a>
843
+ <a href="https://dskrpt.de/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Dskrpt.png" alt="Dskrpt" height="128"></a>
844
+ <a href="https://github.com/fastrepl/"><img src="https://media.githubusercontent.com/media/handlewithcarecollective/pitter-patter-sponsors/main/logos/Fastrepl.png" alt="Fastrepl" height="128"></a>
845
+ </p>
846
+ <!--sponsorsend-->
@@ -244,7 +244,9 @@ function adjustWidgetMarksBack(widgetChildren, nodeChild) {
244
244
  const child = widgetChildren[i];
245
245
  if (// Using internal Decoration property, "type"
246
246
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
- child.widget.type.side < 0) {
247
+ child.widget.type.side < 0 || // Using internal Decoration property, "type"
248
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
249
+ child.widget.type.spec.marks) {
248
250
  continue;
249
251
  }
250
252
  child.marks = child.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread);
@@ -54,7 +54,6 @@ function _interop_require_wildcard(obj, nodeInterop) {
54
54
  }
55
55
  const CursorWrapper = /*#__PURE__*/ (0, _react.forwardRef)(function CursorWrapper(param, ref) {
56
56
  let { widget, getPos, ...props } = param;
57
- const [shouldRender, setShouldRender] = (0, _react.useState)(true);
58
57
  const innerRef = (0, _react.useRef)(null);
59
58
  (0, _react.useImperativeHandle)(ref, ()=>{
60
59
  return innerRef.current;
@@ -65,27 +64,23 @@ const CursorWrapper = /*#__PURE__*/ (0, _react.forwardRef)(function CursorWrappe
65
64
  view.domObserver.disconnectSelection();
66
65
  // @ts-expect-error Internal property - domSelection
67
66
  const domSel = view.domSelection();
68
- const range = document.createRange();
69
67
  const node = innerRef.current;
70
68
  const img = node.nodeName == "IMG";
71
- if (img && node.parentNode) {
72
- range.setEnd(node.parentNode, (0, _dom.domIndex)(node) + 1);
69
+ if (img) {
70
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
71
+ domSel.collapse(node.parentNode, (0, _dom.domIndex)(node) + 1);
73
72
  } else {
74
- range.setEnd(node, 0);
73
+ domSel.collapse(node, 0);
75
74
  }
76
- range.collapse(false);
77
- domSel.removeAllRanges();
78
- domSel.addRange(range);
79
- setShouldRender(false);
80
75
  // @ts-expect-error Internal property - domObserver
81
76
  view.domObserver.connectSelection();
82
77
  }, []);
83
- return shouldRender ? /*#__PURE__*/ _react.default.createElement("img", {
78
+ return /*#__PURE__*/ _react.default.createElement("img", {
84
79
  ref: innerRef,
85
80
  className: "ProseMirror-separator",
86
81
  // eslint-disable-next-line react/no-unknown-property
87
82
  "mark-placeholder": "true",
88
83
  alt: "",
89
84
  ...props
90
- }) : null;
85
+ });
91
86
  });
@@ -73,7 +73,7 @@ const rootChildDescriptionsContextValue = {
73
73
  function ProseMirrorInner(param) {
74
74
  let { children, nodeViewComponents, markViewComponents, ...props } = param;
75
75
  const [mount, setMount] = (0, _react.useState)(null);
76
- const { editor, state } = (0, _useEditor.useEditor)(mount, props);
76
+ const { editor, cursorWrapper, state } = (0, _useEditor.useEditor)(mount, props);
77
77
  const nodeViewConstructors = editor.view.nodeViews;
78
78
  const nodeViewContextValue = (0, _react.useMemo)(()=>{
79
79
  return {
@@ -90,7 +90,7 @@ function ProseMirrorInner(param) {
90
90
  ]);
91
91
  const node = state.doc;
92
92
  const decorations = (0, _computeDocDeco.computeDocDeco)(editor.view);
93
- const innerDecorations = (0, _viewDecorations.viewDecorations)(editor.view, editor.cursorWrapper);
93
+ const innerDecorations = (0, _viewDecorations.viewDecorations)(editor.view, cursorWrapper);
94
94
  const docNodeViewContextValue = (0, _react.useMemo)(()=>({
95
95
  setMount,
96
96
  node,
@@ -109,13 +109,11 @@ function useEditor(mount, options) {
109
109
  view.update(directEditorProps);
110
110
  const editor = (0, _react.useMemo)(()=>({
111
111
  view,
112
- cursorWrapper,
113
112
  flushSyncRef,
114
113
  registerEventListener,
115
114
  unregisterEventListener,
116
115
  isStatic: options.static ?? false
117
116
  }), [
118
- cursorWrapper,
119
117
  options.static,
120
118
  registerEventListener,
121
119
  unregisterEventListener,
@@ -123,6 +121,7 @@ function useEditor(mount, options) {
123
121
  ]);
124
122
  return {
125
123
  editor,
124
+ cursorWrapper,
126
125
  state
127
126
  };
128
127
  }
@@ -38,6 +38,13 @@ function useMarkViewDescription(getDOM, getContentDOM, constructor, props) {
38
38
  const children = childrenRef.current;
39
39
  const contentDOM = getContentDOM(markView);
40
40
  const viewDesc = new _viewdesc.ReactMarkViewDesc(parent, children, getPos, mark, dom, contentDOM ?? markView.dom, markView);
41
+ // When create() runs after a destroy() (either here in a layout
42
+ // effect or in refUpdated), the inherited children still reference
43
+ // the just-destroyed desc. Re-parent them onto this fresh desc
44
+ // before any other code can observe the stale pointer.
45
+ for (const child of children){
46
+ child.parent = viewDesc;
47
+ }
41
48
  contentDOMRef.current = contentDOM;
42
49
  return viewDesc;
43
50
  });
@@ -39,6 +39,9 @@ function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
39
39
  const contentDOM = getContentDOM(nodeView);
40
40
  const nodeDOM = nodeView.dom;
41
41
  const viewDesc = new _viewdesc.ReactNodeViewDesc(parent, children, getPos, node, decorations, innerDecorations, dom, contentDOM, nodeDOM, nodeView);
42
+ for (const child of children){
43
+ child.parent = viewDesc;
44
+ }
42
45
  const siblings = siblingsRef.current;
43
46
  if (!siblings.includes(viewDesc)) {
44
47
  siblings.push(viewDesc);
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "beforeInputPlugin", {
8
8
  return beforeInputPlugin;
9
9
  }
10
10
  });
11
+ const _prosemirrormodel = require("prosemirror-model");
11
12
  const _prosemirrorstate = require("prosemirror-state");
12
13
  const _CursorWrapper = require("../components/CursorWrapper.js");
13
14
  const _ReactWidgetType = require("../decorations/ReactWidgetType.js");
@@ -25,6 +26,34 @@ function insertText(view, eventData) {
25
26
  view.dispatch(tr);
26
27
  return true;
27
28
  }
29
+ // Taken from https://github.com/ProseMirror/prosemirror-gapcursor/blob/master/src/index.ts#L67-L84
30
+ // This is a hack that, when a composition starts while a gap cursor
31
+ // is active, quickly creates an inline context for the composition to
32
+ // happen in, to avoid it being aborted by the DOM selection being
33
+ // moved into a valid position.
34
+ //
35
+ // We can't rely on the actual hack from prosemirror-gapcursor, because
36
+ // it happens too late. We snapshot the DOM during compositionstart, but
37
+ // the gapcursor hack runs in beforeinput (after compositionstart).
38
+ function handleGapCursorComposition(view) {
39
+ // @ts-expect-error Internal property - jsonID
40
+ if (!(view.state.selection.jsonID === "gapcursor")) {
41
+ return;
42
+ }
43
+ const { $from } = view.state.selection;
44
+ const insert = $from.parent.contentMatchAt($from.index())// All schemas _must_ have a text node type
45
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
46
+ .findWrapping(view.state.schema.nodes.text);
47
+ if (!insert) return;
48
+ let fragment = _prosemirrormodel.Fragment.empty;
49
+ for(let i = insert.length - 1; i >= 0; i--){
50
+ fragment = _prosemirrormodel.Fragment.from(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
51
+ insert[i].createAndFill(null, fragment));
52
+ }
53
+ const tr = view.state.tr.replace($from.pos, $from.pos, new _prosemirrormodel.Slice(fragment, 0, 0));
54
+ tr.setSelection(_prosemirrorstate.TextSelection.near(tr.doc.resolve($from.pos + 1)));
55
+ view.dispatch(tr);
56
+ }
28
57
  function beforeInputPlugin(setCursorWrapper) {
29
58
  let compositionMarks = null;
30
59
  let precompositionSnapshot = null;
@@ -32,10 +61,11 @@ function beforeInputPlugin(setCursorWrapper) {
32
61
  props: {
33
62
  handleDOMEvents: {
34
63
  compositionstart (view) {
64
+ compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
65
+ view.dispatch(view.state.tr.deleteSelection());
66
+ handleGapCursorComposition(view);
35
67
  const { state } = view;
36
- view.dispatch(state.tr.deleteSelection());
37
68
  const $pos = state.selection.$from;
38
- compositionMarks = state.storedMarks ?? $pos.marks();
39
69
  if (compositionMarks) {
40
70
  setCursorWrapper((0, _ReactWidgetType.widget)(state.selection.from, _CursorWrapper.CursorWrapper, {
41
71
  key: "cursor-wrapper",
@@ -51,7 +51,7 @@ function tempEditor(param) {
51
51
  const state = _prosemirrorstate.EditorState.create({
52
52
  doc: startDoc,
53
53
  schema: _prosemirrortestbuilder.schema,
54
- selection: selection ?? startDoc.tag?.a ? _prosemirrorstate.TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined,
54
+ selection: selection ?? (startDoc.tag?.a ? _prosemirrorstate.TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined),
55
55
  plugins: [
56
56
  ...plugins ?? [],
57
57
  (0, _reactKeys.reactKeys)()
@@ -185,7 +185,9 @@ function adjustWidgetMarksBack(widgetChildren, nodeChild) {
185
185
  const child = widgetChildren[i];
186
186
  if (// Using internal Decoration property, "type"
187
187
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
- child.widget.type.side < 0) {
188
+ child.widget.type.side < 0 || // Using internal Decoration property, "type"
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
190
+ child.widget.type.spec.marks) {
189
191
  continue;
190
192
  }
191
193
  child.marks = child.marks.reduce((acc, mark)=>mark.addToSet(acc), marksToSpread);
@@ -1,9 +1,8 @@
1
- import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useRef } from "react";
2
2
  import { domIndex } from "../dom.js";
3
3
  import { useEditorEffect } from "../hooks/useEditorEffect.js";
4
4
  export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(param, ref) {
5
5
  let { widget, getPos, ...props } = param;
6
- const [shouldRender, setShouldRender] = useState(true);
7
6
  const innerRef = useRef(null);
8
7
  useImperativeHandle(ref, ()=>{
9
8
  return innerRef.current;
@@ -14,27 +13,23 @@ export const CursorWrapper = /*#__PURE__*/ forwardRef(function CursorWrapper(par
14
13
  view.domObserver.disconnectSelection();
15
14
  // @ts-expect-error Internal property - domSelection
16
15
  const domSel = view.domSelection();
17
- const range = document.createRange();
18
16
  const node = innerRef.current;
19
17
  const img = node.nodeName == "IMG";
20
- if (img && node.parentNode) {
21
- range.setEnd(node.parentNode, domIndex(node) + 1);
18
+ if (img) {
19
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20
+ domSel.collapse(node.parentNode, domIndex(node) + 1);
22
21
  } else {
23
- range.setEnd(node, 0);
22
+ domSel.collapse(node, 0);
24
23
  }
25
- range.collapse(false);
26
- domSel.removeAllRanges();
27
- domSel.addRange(range);
28
- setShouldRender(false);
29
24
  // @ts-expect-error Internal property - domObserver
30
25
  view.domObserver.connectSelection();
31
26
  }, []);
32
- return shouldRender ? /*#__PURE__*/ React.createElement("img", {
27
+ return /*#__PURE__*/ React.createElement("img", {
33
28
  ref: innerRef,
34
29
  className: "ProseMirror-separator",
35
30
  // eslint-disable-next-line react/no-unknown-property
36
31
  "mark-placeholder": "true",
37
32
  alt: "",
38
33
  ...props
39
- }) : null;
34
+ });
40
35
  });
@@ -22,7 +22,7 @@ const rootChildDescriptionsContextValue = {
22
22
  function ProseMirrorInner(param) {
23
23
  let { children, nodeViewComponents, markViewComponents, ...props } = param;
24
24
  const [mount, setMount] = useState(null);
25
- const { editor, state } = useEditor(mount, props);
25
+ const { editor, cursorWrapper, state } = useEditor(mount, props);
26
26
  const nodeViewConstructors = editor.view.nodeViews;
27
27
  const nodeViewContextValue = useMemo(()=>{
28
28
  return {
@@ -39,7 +39,7 @@ function ProseMirrorInner(param) {
39
39
  ]);
40
40
  const node = state.doc;
41
41
  const decorations = computeDocDeco(editor.view);
42
- const innerDecorations = viewDecorations(editor.view, editor.cursorWrapper);
42
+ const innerDecorations = viewDecorations(editor.view, cursorWrapper);
43
43
  const docNodeViewContextValue = useMemo(()=>({
44
44
  setMount,
45
45
  node,
@@ -107,13 +107,11 @@ let didWarnValueDefaultValue = false;
107
107
  view.update(directEditorProps);
108
108
  const editor = useMemo(()=>({
109
109
  view,
110
- cursorWrapper,
111
110
  flushSyncRef,
112
111
  registerEventListener,
113
112
  unregisterEventListener,
114
113
  isStatic: options.static ?? false
115
114
  }), [
116
- cursorWrapper,
117
115
  options.static,
118
116
  registerEventListener,
119
117
  unregisterEventListener,
@@ -121,6 +119,7 @@ let didWarnValueDefaultValue = false;
121
119
  ]);
122
120
  return {
123
121
  editor,
122
+ cursorWrapper,
124
123
  state
125
124
  };
126
125
  }
@@ -28,6 +28,13 @@ export function useMarkViewDescription(getDOM, getContentDOM, constructor, props
28
28
  const children = childrenRef.current;
29
29
  const contentDOM = getContentDOM(markView);
30
30
  const viewDesc = new ReactMarkViewDesc(parent, children, getPos, mark, dom, contentDOM ?? markView.dom, markView);
31
+ // When create() runs after a destroy() (either here in a layout
32
+ // effect or in refUpdated), the inherited children still reference
33
+ // the just-destroyed desc. Re-parent them onto this fresh desc
34
+ // before any other code can observe the stale pointer.
35
+ for (const child of children){
36
+ child.parent = viewDesc;
37
+ }
31
38
  contentDOMRef.current = contentDOM;
32
39
  return viewDesc;
33
40
  });
@@ -29,6 +29,9 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
29
29
  const contentDOM = getContentDOM(nodeView);
30
30
  const nodeDOM = nodeView.dom;
31
31
  const viewDesc = new ReactNodeViewDesc(parent, children, getPos, node, decorations, innerDecorations, dom, contentDOM, nodeDOM, nodeView);
32
+ for (const child of children){
33
+ child.parent = viewDesc;
34
+ }
32
35
  const siblings = siblingsRef.current;
33
36
  if (!siblings.includes(viewDesc)) {
34
37
  siblings.push(viewDesc);
@@ -1,4 +1,5 @@
1
- import { Plugin } from "prosemirror-state";
1
+ import { Fragment, Slice } from "prosemirror-model";
2
+ import { Plugin, TextSelection } from "prosemirror-state";
2
3
  import { CursorWrapper } from "../components/CursorWrapper.js";
3
4
  import { widget } from "../decorations/ReactWidgetType.js";
4
5
  function insertText(view, eventData) {
@@ -15,6 +16,34 @@ function insertText(view, eventData) {
15
16
  view.dispatch(tr);
16
17
  return true;
17
18
  }
19
+ // Taken from https://github.com/ProseMirror/prosemirror-gapcursor/blob/master/src/index.ts#L67-L84
20
+ // This is a hack that, when a composition starts while a gap cursor
21
+ // is active, quickly creates an inline context for the composition to
22
+ // happen in, to avoid it being aborted by the DOM selection being
23
+ // moved into a valid position.
24
+ //
25
+ // We can't rely on the actual hack from prosemirror-gapcursor, because
26
+ // it happens too late. We snapshot the DOM during compositionstart, but
27
+ // the gapcursor hack runs in beforeinput (after compositionstart).
28
+ function handleGapCursorComposition(view) {
29
+ // @ts-expect-error Internal property - jsonID
30
+ if (!(view.state.selection.jsonID === "gapcursor")) {
31
+ return;
32
+ }
33
+ const { $from } = view.state.selection;
34
+ const insert = $from.parent.contentMatchAt($from.index())// All schemas _must_ have a text node type
35
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
36
+ .findWrapping(view.state.schema.nodes.text);
37
+ if (!insert) return;
38
+ let fragment = Fragment.empty;
39
+ for(let i = insert.length - 1; i >= 0; i--){
40
+ fragment = Fragment.from(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41
+ insert[i].createAndFill(null, fragment));
42
+ }
43
+ const tr = view.state.tr.replace($from.pos, $from.pos, new Slice(fragment, 0, 0));
44
+ tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
45
+ view.dispatch(tr);
46
+ }
18
47
  export function beforeInputPlugin(setCursorWrapper) {
19
48
  let compositionMarks = null;
20
49
  let precompositionSnapshot = null;
@@ -22,10 +51,11 @@ export function beforeInputPlugin(setCursorWrapper) {
22
51
  props: {
23
52
  handleDOMEvents: {
24
53
  compositionstart (view) {
54
+ compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
55
+ view.dispatch(view.state.tr.deleteSelection());
56
+ handleGapCursorComposition(view);
25
57
  const { state } = view;
26
- view.dispatch(state.tr.deleteSelection());
27
58
  const $pos = state.selection.$from;
28
- compositionMarks = state.storedMarks ?? $pos.marks();
29
59
  if (compositionMarks) {
30
60
  setCursorWrapper(widget(state.selection.from, CursorWrapper, {
31
61
  key: "cursor-wrapper",
@@ -28,7 +28,7 @@ export function tempEditor(param) {
28
28
  const state = EditorState.create({
29
29
  doc: startDoc,
30
30
  schema,
31
- selection: selection ?? startDoc.tag?.a ? TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined,
31
+ selection: selection ?? (startDoc.tag?.a ? TextSelection.create(startDoc, startDoc.tag.a, startDoc.tag?.b) : undefined),
32
32
  plugins: [
33
33
  ...plugins ?? [],
34
34
  reactKeys()