@handlewithcare/react-prosemirror 2.4.12 → 2.5.1

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 (149) hide show
  1. package/dist/cjs/AbstractEditorView.js +4 -0
  2. package/dist/cjs/ReactEditorView.js +156 -0
  3. package/dist/cjs/StaticEditorView.js +86 -0
  4. package/dist/cjs/components/ChildNodeViews.js +58 -29
  5. package/dist/cjs/components/CustomNodeView.js +77 -137
  6. package/dist/cjs/{hooks/useNodePos.js → components/DefaultNodeView.js} +24 -26
  7. package/dist/cjs/components/DocNodeView.js +33 -41
  8. package/dist/cjs/components/MarkView.js +1 -2
  9. package/dist/cjs/components/NativeWidgetView.js +2 -3
  10. package/dist/cjs/components/NodeView.js +31 -21
  11. package/dist/cjs/components/ProseMirror.js +25 -17
  12. package/dist/cjs/components/ProseMirrorDoc.js +7 -27
  13. package/dist/cjs/components/ReactNodeView.js +98 -61
  14. package/dist/cjs/components/SeparatorHackView.js +1 -2
  15. package/dist/cjs/components/TextNodeView.js +4 -5
  16. package/dist/cjs/components/TrailingHackView.js +1 -2
  17. package/dist/cjs/components/WidgetView.js +2 -4
  18. package/dist/cjs/constants.js +33 -0
  19. package/dist/cjs/hooks/useEditor.js +33 -229
  20. package/dist/cjs/hooks/useEditorEffect.js +2 -2
  21. package/dist/cjs/hooks/useEditorEventCallback.js +8 -5
  22. package/dist/cjs/hooks/useIgnoreMutation.js +1 -1
  23. package/dist/cjs/hooks/useNodeViewDescriptor.js +123 -80
  24. package/dist/cjs/hooks/useReactKeys.js +1 -1
  25. package/dist/cjs/hooks/useSelectNode.js +9 -7
  26. package/dist/cjs/hooks/useStopEvent.js +1 -1
  27. package/dist/cjs/plugins/beforeInputPlugin.js +12 -0
  28. package/dist/cjs/testing/editorViewTestHelpers.js +0 -2
  29. package/dist/cjs/viewdesc.js +104 -25
  30. package/dist/esm/AbstractEditorView.js +1 -0
  31. package/dist/esm/ReactEditorView.js +156 -0
  32. package/dist/esm/StaticEditorView.js +76 -0
  33. package/dist/esm/components/ChildNodeViews.js +59 -31
  34. package/dist/esm/components/CustomNodeView.js +78 -138
  35. package/dist/esm/components/DefaultNodeView.js +16 -0
  36. package/dist/esm/components/DocNodeView.js +33 -41
  37. package/dist/esm/components/MarkView.js +1 -2
  38. package/dist/esm/components/NativeWidgetView.js +2 -3
  39. package/dist/esm/components/NodeView.js +32 -22
  40. package/dist/esm/components/ProseMirror.js +25 -17
  41. package/dist/esm/components/ProseMirrorDoc.js +7 -28
  42. package/dist/esm/components/ReactNodeView.js +99 -62
  43. package/dist/esm/components/SeparatorHackView.js +1 -2
  44. package/dist/esm/components/TextNodeView.js +4 -5
  45. package/dist/esm/components/TrailingHackView.js +1 -2
  46. package/dist/esm/components/WidgetView.js +2 -4
  47. package/dist/esm/constants.js +15 -0
  48. package/dist/esm/hooks/useEditor.js +29 -218
  49. package/dist/esm/hooks/useEditorEffect.js +2 -2
  50. package/dist/esm/hooks/useEditorEventCallback.js +8 -5
  51. package/dist/esm/hooks/useIgnoreMutation.js +1 -1
  52. package/dist/esm/hooks/useNodeViewDescriptor.js +125 -82
  53. package/dist/esm/hooks/useReactKeys.js +1 -1
  54. package/dist/esm/hooks/useSelectNode.js +9 -7
  55. package/dist/esm/hooks/useStopEvent.js +1 -1
  56. package/dist/esm/plugins/beforeInputPlugin.js +12 -0
  57. package/dist/esm/testing/editorViewTestHelpers.js +0 -2
  58. package/dist/esm/viewdesc.js +94 -18
  59. package/dist/tsconfig.tsbuildinfo +1 -1
  60. package/dist/types/AbstractEditorView.d.ts +27 -0
  61. package/dist/types/ReactEditorView.d.ts +80 -0
  62. package/dist/types/StaticEditorView.d.ts +24 -0
  63. package/dist/types/components/ChildNodeViews.d.ts +2 -2
  64. package/dist/types/components/CustomNodeView.d.ts +3 -3
  65. package/dist/types/components/DefaultNodeView.d.ts +3 -0
  66. package/dist/types/components/DocNodeView.d.ts +9 -17
  67. package/dist/types/components/MarkView.d.ts +2 -2
  68. package/dist/types/components/NativeWidgetView.d.ts +2 -2
  69. package/dist/types/components/NodeView.d.ts +5 -5
  70. package/dist/types/components/NodeViewComponentProps.d.ts +3 -4
  71. package/dist/types/components/ProseMirrorDoc.d.ts +14 -8
  72. package/dist/types/components/ReactNodeView.d.ts +4 -2
  73. package/dist/types/components/SeparatorHackView.d.ts +2 -2
  74. package/dist/types/components/TextNodeView.d.ts +4 -3
  75. package/dist/types/components/TrailingHackView.d.ts +2 -2
  76. package/dist/types/components/WidgetView.d.ts +2 -2
  77. package/dist/types/constants.d.ts +4 -0
  78. package/dist/types/contexts/EditorContext.d.ts +6 -4
  79. package/dist/types/contexts/IgnoreMutationContext.d.ts +2 -1
  80. package/dist/types/contexts/NodeViewContext.d.ts +3 -1
  81. package/dist/types/contexts/SelectNodeContext.d.ts +3 -1
  82. package/dist/types/contexts/StopEventContext.d.ts +2 -1
  83. package/dist/types/decorations/computeDocDeco.d.ts +3 -2
  84. package/dist/types/decorations/viewDecorations.d.ts +3 -2
  85. package/dist/types/hooks/useEditor.d.ts +5 -46
  86. package/dist/types/hooks/useNodeViewDescriptor.d.ts +18 -10
  87. package/dist/types/hooks/useReactKeys.d.ts +1 -1
  88. package/dist/types/hooks/useSelectNode.d.ts +2 -1
  89. package/dist/types/props.d.ts +3 -3
  90. package/dist/types/viewdesc.d.ts +29 -11
  91. package/package.json +7 -3
  92. package/dist/cjs/components/Editor.js +0 -28
  93. package/dist/cjs/components/NodeViews.js +0 -73
  94. package/dist/cjs/components/__tests__/LayoutGroup.test.js +0 -141
  95. package/dist/cjs/components/__tests__/ProseMirror.test.js +0 -255
  96. package/dist/cjs/contexts/NodeViewsContext.js +0 -10
  97. package/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js +0 -107
  98. package/dist/cjs/hooks/__tests__/useNodeViews.test.js +0 -159
  99. package/dist/cjs/hooks/useClientOnly.js +0 -19
  100. package/dist/cjs/hooks/useEditorView.js +0 -100
  101. package/dist/cjs/hooks/useNodeViews.js +0 -100
  102. package/dist/cjs/nodeViews/createReactNodeViewConstructor.js +0 -244
  103. package/dist/cjs/nodeViews/phrasingContentTags.js +0 -57
  104. package/dist/cjs/plugins/__tests__/react.test.js +0 -139
  105. package/dist/cjs/plugins/react.js +0 -71
  106. package/dist/cjs/selection/SelectionDOMObserver.js +0 -171
  107. package/dist/cjs/selection/hasFocusAndSelection.js +0 -35
  108. package/dist/cjs/selection/selectionFromDOM.js +0 -77
  109. package/dist/cjs/selection/selectionToDOM.js +0 -226
  110. package/dist/cjs/ssr.js +0 -85
  111. package/dist/esm/components/Editor.js +0 -15
  112. package/dist/esm/components/NodeViews.js +0 -26
  113. package/dist/esm/components/__tests__/LayoutGroup.test.js +0 -98
  114. package/dist/esm/components/__tests__/ProseMirror.test.js +0 -207
  115. package/dist/esm/contexts/NodeViewsContext.js +0 -9
  116. package/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js +0 -98
  117. package/dist/esm/hooks/__tests__/useNodeViews.test.js +0 -116
  118. package/dist/esm/hooks/useClientOnly.js +0 -9
  119. package/dist/esm/hooks/useEditorView.js +0 -99
  120. package/dist/esm/hooks/useNodePos.js +0 -16
  121. package/dist/esm/hooks/useNodeViews.js +0 -53
  122. package/dist/esm/nodeViews/createReactNodeViewConstructor.js +0 -214
  123. package/dist/esm/nodeViews/phrasingContentTags.js +0 -49
  124. package/dist/esm/plugins/__tests__/react.test.js +0 -135
  125. package/dist/esm/plugins/react.js +0 -64
  126. package/dist/esm/selection/SelectionDOMObserver.js +0 -161
  127. package/dist/esm/selection/hasFocusAndSelection.js +0 -17
  128. package/dist/esm/selection/selectionFromDOM.js +0 -59
  129. package/dist/esm/selection/selectionToDOM.js +0 -196
  130. package/dist/esm/ssr.js +0 -82
  131. package/dist/types/components/Editor.d.ts +0 -7
  132. package/dist/types/components/NodeViews.d.ts +0 -6
  133. package/dist/types/components/__tests__/LayoutGroup.test.d.ts +0 -1
  134. package/dist/types/contexts/NodeViewsContext.d.ts +0 -19
  135. package/dist/types/hooks/__tests__/useEditorViewLayoutEffect.test.d.ts +0 -1
  136. package/dist/types/hooks/__tests__/useNodeViews.test.d.ts +0 -1
  137. package/dist/types/hooks/useClientOnly.d.ts +0 -1
  138. package/dist/types/hooks/useEditorView.d.ts +0 -23
  139. package/dist/types/hooks/useNodePos.d.ts +0 -9
  140. package/dist/types/hooks/useNodeViews.d.ts +0 -5
  141. package/dist/types/nodeViews/createReactNodeViewConstructor.d.ts +0 -48
  142. package/dist/types/nodeViews/phrasingContentTags.d.ts +0 -1
  143. package/dist/types/plugins/__tests__/react.test.d.ts +0 -1
  144. package/dist/types/plugins/react.d.ts +0 -21
  145. package/dist/types/selection/SelectionDOMObserver.d.ts +0 -33
  146. package/dist/types/selection/hasFocusAndSelection.d.ts +0 -3
  147. package/dist/types/selection/selectionFromDOM.d.ts +0 -4
  148. package/dist/types/selection/selectionToDOM.d.ts +0 -9
  149. package/dist/types/ssr.d.ts +0 -19
@@ -1,214 +0,0 @@
1
- import React, { forwardRef, useContext, useImperativeHandle, useState } from "react";
2
- import { createPortal } from "react-dom";
3
- import { NodeViewsContext } from "../contexts/NodeViewsContext.js";
4
- import { useEditorEffect } from "../hooks/useEditorEffect.js";
5
- import { NodePosProvider } from "../hooks/useNodePos.js";
6
- import { ROOT_NODE_KEY, createNodeKey, reactPluginKey } from "../plugins/react.js";
7
- import { phrasingContentTags } from "./phrasingContentTags.js";
8
- /**
9
- * Identifies a node view constructor as having been created
10
- * by @nytimes/react-prosemirror
11
- */ export const REACT_NODE_VIEW = Symbol("react node view");
12
- let didWarnReactPlugin = false;
13
- /**
14
- * Searches upward for the nearest node with a node key,
15
- * returning the first node key it finds associated with
16
- * a React node view.
17
- *
18
- * Returns the root key if no ancestor nodes have node keys.
19
- */ export function findNodeKeyUp(editorView, pos) {
20
- const pluginState = reactPluginKey.getState(editorView.state);
21
- if (!pluginState) return ROOT_NODE_KEY;
22
- const $pos = editorView.state.doc.resolve(pos);
23
- for(let d = $pos.depth; d > 0; d--){
24
- const ancestorNodeTypeName = $pos.node(d).type.name;
25
- const ancestorNodeView = editorView.props.nodeViews?.[ancestorNodeTypeName];
26
- if (!ancestorNodeView?.[REACT_NODE_VIEW]) continue;
27
- const ancestorPos = $pos.before(d);
28
- const ancestorKey = pluginState.posToKey.get(ancestorPos);
29
- if (ancestorKey) return ancestorKey;
30
- }
31
- return ROOT_NODE_KEY;
32
- }
33
- /**
34
- * Factory function for creating nodeViewConstructors that
35
- * render as React components.
36
- *
37
- * `NodeView` can be any React component that takes
38
- * `NodeViewComponentProps`. It will be passed all of the
39
- * arguments to the `nodeViewConstructor` except for
40
- * `editorView`. NodeView components that need access
41
- * directly to the EditorView should use the
42
- * `useEditorViewEvent` and `useEditorViewLayoutEffect`
43
- * hooks to ensure safe access.
44
- *
45
- * For contentful Nodes, the NodeView component will also
46
- * be passed a `children` prop containing an empty element.
47
- * ProseMirror will render content nodes into this element.
48
- */ export function createReactNodeViewConstructor(nodeViewConstructor, registerPortal) {
49
- function nodeViewConstructorWrapper(node, editorView, getPos, decorations, innerDecorations) {
50
- const nodeView = nodeViewConstructor(node, editorView, getPos, decorations, innerDecorations);
51
- const { component: NodeView } = nodeView;
52
- if (!NodeView) {
53
- return nodeView;
54
- }
55
- const reactPluginState = reactPluginKey.getState(editorView.state);
56
- if (!reactPluginState) {
57
- if (!didWarnReactPlugin) {
58
- console.error("The React ProseMirror plugin is required to use React node views. " + "Make sure to add it to the ProseMirror editor state.");
59
- didWarnReactPlugin = true;
60
- }
61
- return nodeView;
62
- }
63
- const { dom , contentDOM } = nodeView;
64
- // Use a span if the provided contentDOM is in the "phrasing" content
65
- // category. Otherwise use a div. This is our best attempt at not
66
- // breaking the intended content model, for now.
67
- //
68
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content
69
- const ContentDOMWrapper = contentDOM && (phrasingContentTags.includes(contentDOM.tagName.toLocaleLowerCase()) ? "span" : "div");
70
- const nodeKey = reactPluginState.posToKey.get(getPos()) ?? createNodeKey();
71
- /**
72
- * Wrapper component to provide some imperative handles for updating
73
- * and re-rendering its child. Takes and renders an arbitrary ElementType
74
- * that expects NodeViewComponentProps as props.
75
- */ const NodeViewWrapper = /*#__PURE__*/ forwardRef(function NodeViewWrapper(param, ref) {
76
- let { initialState } = param;
77
- const [node, setNode] = useState(initialState.node);
78
- const [decorations, setDecorations] = useState(initialState.decorations);
79
- const [isSelected, setIsSelected] = useState(initialState.isSelected);
80
- const nodeViews = useContext(NodeViewsContext);
81
- const childNodeViews = nodeViews[nodeKey];
82
- const [childNodeViewPortals, setChildNodeViewPortals] = useState(childNodeViews?.map((param)=>{
83
- let { portal } = param;
84
- return portal;
85
- }));
86
- // `getPos` is technically derived from the EditorView
87
- // state, so it's not safe to call until after the EditorView
88
- // has been updated
89
- useEditorEffect(()=>{
90
- setChildNodeViewPortals(childNodeViews?.sort((a, b)=>a.getPos() - b.getPos()).map((param)=>{
91
- let { portal } = param;
92
- return portal;
93
- }));
94
- }, [
95
- childNodeViews
96
- ]);
97
- const [contentDOMWrapper, setContentDOMWrapper] = useState(null);
98
- const [contentDOMParent, setContentDOMParent] = useState(null);
99
- useImperativeHandle(ref, ()=>({
100
- node,
101
- contentDOMWrapper: contentDOMWrapper,
102
- contentDOMParent: contentDOMParent,
103
- setNode,
104
- setDecorations,
105
- setIsSelected
106
- }), [
107
- node,
108
- contentDOMWrapper,
109
- contentDOMParent
110
- ]);
111
- return /*#__PURE__*/ React.createElement(NodePosProvider, {
112
- nodeKey: nodeKey
113
- }, /*#__PURE__*/ React.createElement(NodeView, {
114
- node: node,
115
- decorations: decorations,
116
- isSelected: isSelected
117
- }, childNodeViewPortals, ContentDOMWrapper && /*#__PURE__*/ React.createElement(ContentDOMWrapper, {
118
- style: {
119
- display: "contents"
120
- },
121
- ref: (nextContentDOMWrapper)=>{
122
- setContentDOMWrapper(nextContentDOMWrapper);
123
- // we preserve a reference to the contentDOMWrapper'
124
- // parent so that later we can reassemble the DOM hierarchy
125
- // React expects when cleaning up the ContentDOMWrapper element
126
- if (nextContentDOMWrapper?.parentNode) {
127
- setContentDOMParent(nextContentDOMWrapper.parentNode);
128
- }
129
- }
130
- })));
131
- });
132
- NodeViewWrapper.displayName = `NodeView(${NodeView.displayName ?? NodeView.name})`;
133
- let componentRef = null;
134
- const element = /*#__PURE__*/ React.createElement(NodeViewWrapper, {
135
- initialState: {
136
- node,
137
- decorations,
138
- isSelected: false
139
- },
140
- ref: (c)=>{
141
- componentRef = c;
142
- if (!componentRef || componentRef.node.isLeaf) return;
143
- const contentDOMWrapper = componentRef.contentDOMWrapper;
144
- if (!contentDOMWrapper || !(contentDOMWrapper instanceof HTMLElement)) {
145
- return;
146
- }
147
- // We always set contentDOM when !node.isLeaf, which is checked above
148
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
149
- contentDOMWrapper.appendChild(contentDOM);
150
- // Synchronize the ProseMirror selection to the DOM, because mounting the
151
- // component changes the DOM outside of a ProseMirror update.
152
- const { node } = componentRef;
153
- const pos = getPos();
154
- const end = pos + node.nodeSize;
155
- const { from , to } = editorView.state.selection;
156
- if (editorView.hasFocus() && pos < from && to < end) {
157
- // This call seems like it should be a no-op, given the editor already has
158
- // focus, but it causes ProseMirror to synchronize the DOM selection with
159
- // its state again, placing the DOM selection in a reasonable place within
160
- // the node.
161
- editorView.focus();
162
- }
163
- }
164
- });
165
- const portal = /*#__PURE__*/ createPortal(element, dom, nodeKey);
166
- const unregisterPortal = registerPortal(editorView, getPos, portal);
167
- return {
168
- ignoreMutation (record) {
169
- return !contentDOM?.contains(record.target);
170
- },
171
- ...nodeView,
172
- selectNode () {
173
- componentRef?.setIsSelected(true);
174
- nodeView.selectNode?.();
175
- },
176
- deselectNode () {
177
- componentRef?.setIsSelected(false);
178
- nodeView.deselectNode?.();
179
- },
180
- update (node, decorations, innerDecorations) {
181
- // If this node view's parent has been removed from the registry, we
182
- // need to rebuild it and its children with new registry keys
183
- const positionRegistry = reactPluginKey.getState(editorView.state);
184
- if (positionRegistry && nodeKey !== positionRegistry.posToKey.get(getPos())) {
185
- return false;
186
- }
187
- if (nodeView.update?.(node, decorations, innerDecorations) === false) {
188
- return false;
189
- }
190
- if (node.type === componentRef?.node.type) {
191
- componentRef?.setNode(node);
192
- componentRef?.setDecorations(decorations);
193
- return true;
194
- }
195
- return false;
196
- },
197
- destroy () {
198
- // React expects the contentDOMParent to be a child of the
199
- // DOM element where the portal was mounted, but in some situations
200
- // contenteditable may have already detached the contentDOMParent
201
- // from the DOM. Here we attempt to reassemble the DOM that React
202
- // expects when cleaning up the portal.
203
- if (componentRef?.contentDOMParent) {
204
- this.dom.appendChild(componentRef.contentDOMParent);
205
- }
206
- unregisterPortal();
207
- nodeView.destroy?.();
208
- }
209
- };
210
- }
211
- return Object.assign(nodeViewConstructorWrapper, {
212
- [REACT_NODE_VIEW]: true
213
- });
214
- }
@@ -1,49 +0,0 @@
1
- export const phrasingContentTags = [
2
- "abbr",
3
- "audio",
4
- "b",
5
- "bdo",
6
- "br",
7
- "button",
8
- "canvas",
9
- "cite",
10
- "code",
11
- "data",
12
- "datalist",
13
- "dfn",
14
- "em",
15
- "embed",
16
- "i",
17
- "iframe",
18
- "img",
19
- "input",
20
- "kbd",
21
- "keygen",
22
- "label",
23
- "mark",
24
- "math",
25
- "meter",
26
- "noscript",
27
- "object",
28
- "output",
29
- "picture",
30
- "progress",
31
- "q",
32
- "ruby",
33
- "s",
34
- "samp",
35
- "script",
36
- "select",
37
- "small",
38
- "span",
39
- "strong",
40
- "sub",
41
- "sup",
42
- "svg",
43
- "textarea",
44
- "time",
45
- "u",
46
- "var",
47
- "video",
48
- "wbr"
49
- ];
@@ -1,135 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Schema } from "prosemirror-model";
2
- import { EditorState } from "prosemirror-state";
3
- import { findWrapping } from "prosemirror-transform";
4
- import { react, reactPluginKey } from "../react.js";
5
- const schema = new Schema({
6
- nodes: {
7
- doc: {
8
- content: "block+"
9
- },
10
- paragraph: {
11
- group: "block",
12
- content: "inline*"
13
- },
14
- list: {
15
- group: "block",
16
- content: "list_item+"
17
- },
18
- list_item: {
19
- content: "paragraph+"
20
- },
21
- text: {
22
- group: "inline"
23
- }
24
- }
25
- });
26
- describe("reactNodeViewPlugin", ()=>{
27
- it("should create a unique key for each node", ()=>{
28
- const editorState = EditorState.create({
29
- doc: schema.topNodeType.create(null, [
30
- schema.nodes.paragraph.create(),
31
- schema.nodes.paragraph.create(),
32
- schema.nodes.paragraph.create()
33
- ]),
34
- plugins: [
35
- react()
36
- ]
37
- });
38
- const pluginState = reactPluginKey.getState(editorState);
39
- expect(pluginState.posToKey.size).toBe(3);
40
- });
41
- it("should maintain key stability when possible", ()=>{
42
- const initialEditorState = EditorState.create({
43
- doc: schema.topNodeType.create(null, [
44
- schema.nodes.paragraph.create(),
45
- schema.nodes.paragraph.create(),
46
- schema.nodes.paragraph.create()
47
- ]),
48
- plugins: [
49
- react()
50
- ]
51
- });
52
- const initialPluginState = reactPluginKey.getState(initialEditorState);
53
- const nextEditorState = initialEditorState.apply(initialEditorState.tr.insertText("Hello, world!", 1));
54
- const nextPluginState = reactPluginKey.getState(nextEditorState);
55
- expect(Array.from(nextPluginState.keyToPos.keys())).toEqual(Array.from(initialPluginState.keyToPos.keys()));
56
- });
57
- it("should create unique keys for new nodes", ()=>{
58
- const initialEditorState = EditorState.create({
59
- doc: schema.topNodeType.create(null, [
60
- schema.nodes.paragraph.create(),
61
- schema.nodes.paragraph.create(),
62
- schema.nodes.paragraph.create()
63
- ]),
64
- plugins: [
65
- react()
66
- ]
67
- });
68
- const initialPluginState = reactPluginKey.getState(initialEditorState);
69
- const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(0, schema.nodes.list.createAndFill()));
70
- const nextPluginState = reactPluginKey.getState(nextEditorState);
71
- // Adds new keys for new nodes
72
- expect(nextPluginState.keyToPos.size).toBe(6);
73
- // Maintains keys for previous nodes that are still there
74
- Array.from(initialPluginState.keyToPos.keys()).forEach((key)=>{
75
- expect(Array.from(nextPluginState.keyToPos.keys())).toContain(key);
76
- });
77
- });
78
- it("should maintain key stability when splitting a node", ()=>{
79
- const initialEditorState = EditorState.create({
80
- doc: schema.topNodeType.create(null, [
81
- schema.nodes.list.create(null, [
82
- schema.nodes.list_item.create(null, [
83
- schema.nodes.paragraph.create(null, [
84
- schema.text("first")
85
- ])
86
- ])
87
- ])
88
- ]),
89
- plugins: [
90
- react()
91
- ]
92
- });
93
- const initialPluginState = reactPluginKey.getState(initialEditorState);
94
- const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(1, schema.nodes.list_item.create(null, [
95
- schema.nodes.paragraph.create(null, [
96
- schema.text("second")
97
- ])
98
- ])));
99
- const nextPluginState = reactPluginKey.getState(nextEditorState);
100
- // The new list item was inserted before the original one,
101
- // pushing it further into the document. The original list
102
- // item should keep its original key, and the new list item
103
- // should be assigned a new one
104
- expect(nextPluginState.posToKey.get(11)).toBe(initialPluginState.posToKey.get(1));
105
- expect(nextPluginState.posToKey.get(1)).not.toBe(initialPluginState.posToKey.get(1));
106
- });
107
- it("should maintain key stability when wrapping a node", ()=>{
108
- const initialEditorState = EditorState.create({
109
- doc: schema.topNodeType.create(null, [
110
- schema.nodes.paragraph.create(null, [
111
- schema.text("content")
112
- ])
113
- ]),
114
- plugins: [
115
- react()
116
- ]
117
- });
118
- const initialPluginState = reactPluginKey.getState(initialEditorState);
119
- const start = 1;
120
- const end = 9;
121
- const tr = initialEditorState.tr.delete(start, end);
122
- const $start = tr.doc.resolve(start);
123
- const range = $start.blockRange();
124
- const wrapping = range && findWrapping(range, schema.nodes.list, null);
125
- tr.wrap(range, wrapping);
126
- const nextEditorState = initialEditorState.apply(tr);
127
- const nextPluginState = reactPluginKey.getState(nextEditorState);
128
- // The new list and list item nodes were wrapped around the
129
- // paragraph, pushing it further into the document. The paragraph
130
- // should keep its original key, and the new nodes
131
- // should be assigned a new one
132
- expect(nextPluginState.posToKey.get(2)).toBe(initialPluginState.posToKey.get(0));
133
- expect(nextPluginState.posToKey.get(0)).not.toBe(initialPluginState.posToKey.get(0));
134
- });
135
- });
@@ -1,64 +0,0 @@
1
- import { Plugin, PluginKey } from "prosemirror-state";
2
- /**
3
- * This is a stand-in for the doc node itself, which doesn't have a
4
- * unique position to map to.
5
- */ export const ROOT_NODE_KEY = Symbol("@nytimes/react-prosemirror/root-node-key");
6
- export function createNodeKey() {
7
- return Math.floor(Math.random() * 0xffffff).toString(16);
8
- }
9
- export const reactPluginKey = new PluginKey("@nytimes/react-prosemirror/react");
10
- /**
11
- * Tracks a unique key for each (non-text) node in the
12
- * document, identified by its current position. Keys are
13
- * (mostly) stable across transaction applications. The
14
- * key for a given node can be accessed by that node's
15
- * current position in the document, and vice versa.
16
- */ export function react() {
17
- return new Plugin({
18
- key: reactPluginKey,
19
- state: {
20
- init (_, state) {
21
- const next = {
22
- posToKey: new Map(),
23
- keyToPos: new Map()
24
- };
25
- state.doc.descendants((node, pos)=>{
26
- if (node.isText) return false;
27
- const key = createNodeKey();
28
- next.posToKey.set(pos, key);
29
- next.keyToPos.set(key, pos);
30
- return true;
31
- });
32
- return next;
33
- },
34
- /**
35
- * Keeps node keys (mostly) stable across transactions.
36
- *
37
- * To accomplish this, we map each node position backwards
38
- * through the transaction to identify its previous position,
39
- * and thereby retrieve its previous key.
40
- */ apply (tr, value, _, newState) {
41
- if (!tr.docChanged) return value;
42
- const next = {
43
- posToKey: new Map(),
44
- keyToPos: new Map()
45
- };
46
- for (const [pos, key] of value.posToKey.entries()){
47
- const { pos: newPos , deleted } = tr.mapping.mapResult(pos);
48
- if (deleted) continue;
49
- next.posToKey.set(newPos, key);
50
- next.keyToPos.set(key, newPos);
51
- }
52
- newState.doc.descendants((node, pos)=>{
53
- if (node.isText) return false;
54
- if (next.posToKey.has(pos)) return true;
55
- const key = createNodeKey();
56
- next.posToKey.set(pos, key);
57
- next.keyToPos.set(key, pos);
58
- return true;
59
- });
60
- return next;
61
- }
62
- }
63
- });
64
- }
@@ -1,161 +0,0 @@
1
- import { Selection } from "prosemirror-state";
2
- import { browser } from "../browser.js";
3
- import { parentNode, selectionCollapsed } from "../dom.js";
4
- import { hasFocusAndSelection } from "./hasFocusAndSelection.js";
5
- import { selectionFromDOM } from "./selectionFromDOM.js";
6
- import { isEquivalentPosition, selectionToDOM } from "./selectionToDOM.js";
7
- let SelectionState = class SelectionState {
8
- anchorNode = null;
9
- anchorOffset = 0;
10
- focusNode = null;
11
- focusOffset = 0;
12
- set(sel) {
13
- this.anchorNode = sel.anchorNode;
14
- this.anchorOffset = sel.anchorOffset;
15
- this.focusNode = sel.focusNode;
16
- this.focusOffset = sel.focusOffset;
17
- }
18
- clear() {
19
- this.anchorNode = this.focusNode = null;
20
- }
21
- eq(sel) {
22
- return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset && sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset;
23
- }
24
- };
25
- export class SelectionDOMObserver {
26
- view;
27
- flushingSoon;
28
- currentSelection;
29
- suppressingSelectionUpdates;
30
- constructor(view){
31
- this.view = view;
32
- this.flushingSoon = -1;
33
- this.currentSelection = new SelectionState();
34
- this.suppressingSelectionUpdates = false;
35
- this.view = view;
36
- this.onSelectionChange = this.onSelectionChange.bind(this);
37
- }
38
- connectSelection() {
39
- this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
40
- }
41
- disconnectSelection() {
42
- this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
43
- }
44
- stop() {
45
- this.disconnectSelection();
46
- }
47
- start() {
48
- this.connectSelection();
49
- }
50
- suppressSelectionUpdates() {
51
- this.suppressingSelectionUpdates = true;
52
- setTimeout(()=>this.suppressingSelectionUpdates = false, 50);
53
- }
54
- setCurSelection() {
55
- // @ts-expect-error Internal method
56
- this.currentSelection.set(this.view.domSelectionRange());
57
- }
58
- ignoreSelectionChange(sel) {
59
- if (!sel.focusNode) return true;
60
- const ancestors = new Set();
61
- let container;
62
- for(let scan = sel.focusNode; scan; scan = parentNode(scan))ancestors.add(scan);
63
- for(let scan = sel.anchorNode; scan; scan = parentNode(scan))if (ancestors.has(scan)) {
64
- container = scan;
65
- break;
66
- }
67
- // @ts-expect-error Internal property (docView)
68
- const desc = container && this.view.docView.nearestDesc(container);
69
- if (desc && desc.ignoreMutation({
70
- type: "selection",
71
- target: container?.nodeType == 3 ? container?.parentNode : container
72
- })) {
73
- this.setCurSelection();
74
- return true;
75
- }
76
- return;
77
- }
78
- registerMutation() {
79
- // pass
80
- }
81
- flushSoon() {
82
- if (this.flushingSoon < 0) this.flushingSoon = window.setTimeout(()=>{
83
- this.flushingSoon = -1;
84
- this.flush();
85
- }, 20);
86
- }
87
- updateSelection() {
88
- const { view } = this;
89
- const compositionID = // @ts-expect-error Internal property (input)
90
- view.input.compositionPendingChanges || // @ts-expect-error Internal property (input)
91
- (view.composing ? view.input.compositionID : 0);
92
- // @ts-expect-error Internal property (input)
93
- view.input.compositionPendingChanges = 0;
94
- const origin = // @ts-expect-error Internal property (input)
95
- view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null;
96
- const newSel = selectionFromDOM(view, origin);
97
- if (newSel && !view.state.selection.eq(newSel)) {
98
- const tr = view.state.tr.setSelection(newSel);
99
- if (origin == "pointer") tr.setMeta("pointer", true);
100
- else if (origin == "key") tr.scrollIntoView();
101
- if (compositionID) tr.setMeta("composition", compositionID);
102
- view.dispatch(tr);
103
- }
104
- }
105
- selectionToDOM() {
106
- const { view } = this;
107
- selectionToDOM(view);
108
- // @ts-expect-error Internal property (domSelectionRange)
109
- const sel = view.domSelectionRange();
110
- this.currentSelection.set(sel);
111
- }
112
- flush() {
113
- const { view } = this;
114
- // @ts-expect-error Internal property (docView)
115
- if (!view.docView || this.flushingSoon > -1) return;
116
- // @ts-expect-error Internal property (domSelectionRange)
117
- const sel = view.domSelectionRange();
118
- const newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
119
- let readSel = null;
120
- // If it looks like the browser has reset the selection to the
121
- // start of the document after focus, restore the selection from
122
- // the state
123
- if (newSel && // @ts-expect-error Internal property (input)
124
- view.input.lastFocus > Date.now() - 200 && // @ts-expect-error Internal property (input)
125
- Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 && selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) && readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
126
- // @ts-expect-error Internal property (input)
127
- view.input.lastFocus = 0;
128
- selectionToDOM(view);
129
- this.currentSelection.set(sel);
130
- // @ts-expect-error Internal property (scrollToSelection)
131
- view.scrollToSelection();
132
- } else if (newSel) {
133
- this.updateSelection();
134
- if (!this.currentSelection.eq(sel)) selectionToDOM(view);
135
- this.currentSelection.set(sel);
136
- }
137
- }
138
- forceFlush() {
139
- if (this.flushingSoon > -1) {
140
- window.clearTimeout(this.flushingSoon);
141
- this.flushingSoon = -1;
142
- this.flush();
143
- }
144
- }
145
- onSelectionChange() {
146
- if (!hasFocusAndSelection(this.view)) return;
147
- if (this.view.composing) return;
148
- if (this.suppressingSelectionUpdates) return selectionToDOM(this.view);
149
- // Deletions on IE11 fire their events in the wrong order, giving
150
- // us a selection change event before the DOM changes are
151
- // reported.
152
- if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.empty) {
153
- // @ts-expect-error Internal method
154
- const sel = this.view.domSelectionRange();
155
- // Selection.isCollapsed isn't reliable on IE
156
- if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
157
- sel.anchorNode, sel.anchorOffset)) return this.flushSoon();
158
- }
159
- this.flush();
160
- }
161
- }
@@ -1,17 +0,0 @@
1
- export function hasFocusAndSelection(view) {
2
- if (view.editable && !view.hasFocus()) return false;
3
- return hasSelection(view);
4
- }
5
- export function hasSelection(view) {
6
- // @ts-expect-error Internal method
7
- const sel = view.domSelectionRange();
8
- if (!sel.anchorNode) return false;
9
- try {
10
- // Firefox will raise 'permission denied' errors when accessing
11
- // properties of `sel.anchorNode` when it's in a generated CSS
12
- // element.
13
- return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) && (view.editable || view.dom.contains(sel.focusNode?.nodeType == 3 ? sel.focusNode?.parentNode : sel.focusNode));
14
- } catch (_) {
15
- return false;
16
- }
17
- }