@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.
- package/dist/cjs/ReactEditorView.js +6 -2
- package/dist/cjs/components/ChildNodeViews.js +3 -2
- package/dist/cjs/components/CursorWrapper.js +3 -4
- package/dist/cjs/components/ProseMirror.js +5 -3
- package/dist/cjs/components/TextNodeView.js +176 -34
- package/dist/cjs/components/TrailingHackView.js +42 -1
- package/dist/cjs/components/WidgetView.js +4 -1
- package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
- package/dist/cjs/decorations/viewDecorations.js +1 -6
- package/dist/cjs/hooks/useComponentEventListeners.js +6 -14
- package/dist/cjs/hooks/useEditor.js +2 -10
- package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
- package/dist/cjs/hooks/useNodeViewDescription.js +45 -23
- package/dist/cjs/plugins/beforeInputPlugin.js +116 -12
- package/dist/cjs/plugins/componentEventListeners.js +2 -9
- package/dist/cjs/plugins/reactKeys.js +21 -14
- package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +59 -0
- package/dist/cjs/viewdesc.js +43 -2
- package/dist/esm/ReactEditorView.js +6 -2
- package/dist/esm/components/ChildNodeViews.js +4 -3
- package/dist/esm/components/CursorWrapper.js +4 -5
- package/dist/esm/components/ProseMirror.js +5 -3
- package/dist/esm/components/TextNodeView.js +125 -32
- package/dist/esm/components/TrailingHackView.js +42 -1
- package/dist/esm/components/WidgetView.js +4 -1
- package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
- package/dist/esm/decorations/viewDecorations.js +1 -6
- package/dist/esm/hooks/useComponentEventListeners.js +6 -14
- package/dist/esm/hooks/useEditor.js +2 -10
- package/dist/esm/hooks/useMarkViewDescription.js +62 -4
- package/dist/esm/hooks/useNodeViewDescription.js +46 -24
- package/dist/esm/plugins/beforeInputPlugin.js +116 -12
- package/dist/esm/plugins/componentEventListeners.js +2 -9
- package/dist/esm/plugins/reactKeys.js +21 -14
- package/dist/esm/tiptap/hooks/useTiptapEditor.js +7 -1
- package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +56 -0
- package/dist/esm/viewdesc.js +42 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +3 -2
- package/dist/types/components/CursorWrapper.d.ts +2 -4
- package/dist/types/components/TextNodeView.d.ts +11 -6
- package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
- package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
- package/dist/types/decorations/viewDecorations.d.ts +2 -2
- package/dist/types/hooks/useComponentEventListeners.d.ts +1 -1
- package/dist/types/hooks/useEditor.d.ts +1 -2
- package/dist/types/hooks/useMarkViewDescription.d.ts +2 -1
- package/dist/types/hooks/useNodeViewDescription.d.ts +2 -1
- package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
- package/dist/types/plugins/componentEventListeners.d.ts +2 -3
- package/dist/types/plugins/reactKeys.d.ts +9 -8
- package/dist/types/props.d.ts +26 -26
- package/dist/types/tiptap/hooks/useTiptapEditor.d.ts +7 -0
- package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +1 -0
- package/dist/types/viewdesc.d.ts +3 -2
- package/package.json +2 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useCallback, useContext, useMemo, useRef } from "react";
|
|
2
2
|
import { ReactEditorView } from "../ReactEditorView.js";
|
|
3
|
-
import { CursorWrapper } from "../components/CursorWrapper.js";
|
|
4
3
|
import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
|
|
5
4
|
import { EditorContext } from "../contexts/EditorContext.js";
|
|
6
|
-
import {
|
|
7
|
-
import { CompositionViewDesc, MarkViewDesc, ReactNodeViewDesc, WidgetViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
5
|
+
import { ReactNodeViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
8
6
|
import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
|
|
9
7
|
import { useEffectEvent } from "./useEffectEvent.js";
|
|
10
8
|
export function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
|
|
@@ -134,30 +132,51 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
134
132
|
for (const child of children){
|
|
135
133
|
child.parent = viewDesc;
|
|
136
134
|
}
|
|
135
|
+
});
|
|
136
|
+
const findCompositionDOM = useCallback((compositionViewDesc)=>{
|
|
137
137
|
if (!props.node.isTextblock) return;
|
|
138
|
+
const children = childrenRef.current;
|
|
138
139
|
// Because TextNodeViews can't locate the DOM nodes
|
|
139
140
|
// for compositions, we need to override them here
|
|
140
141
|
if (!viewDescRef.current?.contentDOM) return;
|
|
141
|
-
const compositionChildIndex = children.findIndex((child)=>child instanceof CompositionViewDesc);
|
|
142
|
-
if (compositionChildIndex === -1) return;
|
|
143
|
-
const compositionViewDesc = children[compositionChildIndex];
|
|
144
|
-
if (!(compositionViewDesc instanceof CompositionViewDesc)) return;
|
|
145
142
|
let compositionTopDOM = null;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (search instanceof WidgetViewDesc && search.widget.type instanceof ReactWidgetType && search.widget.type.Component === CursorWrapper) {
|
|
151
|
-
compositionTopDOM = search.dom.nextSibling;
|
|
152
|
-
} else {
|
|
153
|
-
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
154
|
-
if (children.every((child)=>child.dom !== childNode)) {
|
|
155
|
-
compositionTopDOM = childNode;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
143
|
+
for (const childNode of viewDescRef.current.contentDOM.childNodes){
|
|
144
|
+
if (children.every((child)=>child.dom !== childNode)) {
|
|
145
|
+
compositionTopDOM = childNode;
|
|
146
|
+
break;
|
|
158
147
|
}
|
|
159
148
|
}
|
|
160
|
-
if (!compositionTopDOM)
|
|
149
|
+
if (!compositionTopDOM) {
|
|
150
|
+
// Otherwise the IME extended an existing tracked text node. Take it over.
|
|
151
|
+
const reactView = view;
|
|
152
|
+
const imeTextNode = reactView.input.compositionNode;
|
|
153
|
+
if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const claimedDesc = imeTextNode.pmViewDesc;
|
|
157
|
+
if (!(claimedDesc instanceof TextViewDesc)) return;
|
|
158
|
+
if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
|
|
159
|
+
// Walk up to the direct child of contentDOM that contains the IME text node
|
|
160
|
+
// (could be the text node itself, could be wrapped in a mark span).
|
|
161
|
+
let topDOM = imeTextNode;
|
|
162
|
+
while(topDOM.parentNode !== viewDescRef.current.contentDOM){
|
|
163
|
+
const next = topDOM.parentNode;
|
|
164
|
+
if (!next) return;
|
|
165
|
+
topDOM = next;
|
|
166
|
+
}
|
|
167
|
+
// Detach the displaced TextViewDesc from the sibling list so sibling-size
|
|
168
|
+
// accounting (used by posBeforeChild) doesn't double-count this text node.
|
|
169
|
+
const displacedIdx = children.indexOf(claimedDesc);
|
|
170
|
+
if (displacedIdx >= 0) children.splice(displacedIdx, 1);
|
|
171
|
+
reactView.displacedNodes.push(claimedDesc);
|
|
172
|
+
compositionViewDesc.dom = topDOM;
|
|
173
|
+
compositionViewDesc.textDOM = imeTextNode;
|
|
174
|
+
compositionViewDesc.text = imeTextNode.data;
|
|
175
|
+
imeTextNode.pmViewDesc = compositionViewDesc;
|
|
176
|
+
compositionViewDesc._displacedDesc = claimedDesc;
|
|
177
|
+
reactView.input.compositionNodes.push(compositionViewDesc);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
161
180
|
let textDOM = compositionTopDOM;
|
|
162
181
|
while(textDOM.firstChild){
|
|
163
182
|
textDOM = textDOM.firstChild;
|
|
@@ -171,13 +190,16 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
|
|
|
171
190
|
compositionViewDesc.text = textDOM.data;
|
|
172
191
|
compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
|
|
173
192
|
view.input.compositionNodes.push(compositionViewDesc);
|
|
174
|
-
}
|
|
193
|
+
}, [
|
|
194
|
+
props.node.isTextblock,
|
|
195
|
+
view
|
|
196
|
+
]);
|
|
175
197
|
const childContextValue = useMemo(()=>({
|
|
176
198
|
parentRef: viewDescRef,
|
|
177
|
-
siblingsRef: childrenRef
|
|
199
|
+
siblingsRef: childrenRef,
|
|
200
|
+
findCompositionDOM
|
|
178
201
|
}), [
|
|
179
|
-
|
|
180
|
-
viewDescRef
|
|
202
|
+
findCompositionDOM
|
|
181
203
|
]);
|
|
182
204
|
return {
|
|
183
205
|
childContextValue,
|
|
@@ -3,6 +3,8 @@ import { Plugin, TextSelection } from "prosemirror-state";
|
|
|
3
3
|
import { ReactEditorView } from "../ReactEditorView.js";
|
|
4
4
|
import { CursorWrapper } from "../components/CursorWrapper.js";
|
|
5
5
|
import { widget } from "../decorations/ReactWidgetType.js";
|
|
6
|
+
import { TextViewDesc, sortViewDescs } from "../viewdesc.js";
|
|
7
|
+
import { reactKeysPluginKey } from "./reactKeys.js";
|
|
6
8
|
function insertText(view, eventData) {
|
|
7
9
|
let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
8
10
|
if (eventData === null) return false;
|
|
@@ -45,26 +47,64 @@ function handleGapCursorComposition(view) {
|
|
|
45
47
|
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
46
48
|
view.dispatch(tr);
|
|
47
49
|
}
|
|
48
|
-
export function beforeInputPlugin(
|
|
50
|
+
export function beforeInputPlugin() {
|
|
49
51
|
let compositionMarks = null;
|
|
50
52
|
return new Plugin({
|
|
51
53
|
props: {
|
|
52
54
|
handleDOMEvents: {
|
|
53
55
|
compositionstart (view) {
|
|
54
56
|
if (!(view instanceof ReactEditorView)) return false;
|
|
55
|
-
view.
|
|
57
|
+
view.compositionStarting = true;
|
|
58
|
+
const { state } = view;
|
|
59
|
+
const { selection } = state;
|
|
60
|
+
const isEmptyTr = state.tr.delete(selection.from, selection.to);
|
|
61
|
+
const $from = isEmptyTr.doc.resolve(isEmptyTr.mapping.map(selection.from));
|
|
62
|
+
const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
|
|
56
63
|
compositionMarks = view.state.storedMarks;
|
|
57
|
-
|
|
64
|
+
// Render a CursorWrapper with empty marks if starting a composition in an
|
|
65
|
+
// empty textblock with no marks. This prevents the browser from adding a
|
|
66
|
+
// <br> to the text block when it becomes empty (either via canceling the
|
|
67
|
+
// composition with the escape key or deleting all composition text when
|
|
68
|
+
// the composition node is the only text node in the text block)
|
|
69
|
+
if (compositionMarks === null && isEmptyTextblock) {
|
|
70
|
+
compositionMarks = [];
|
|
71
|
+
}
|
|
72
|
+
const tr = view.state.tr.setStoredMarks(null);
|
|
58
73
|
view.dispatch(tr);
|
|
59
74
|
handleGapCursorComposition(view);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
if (compositionMarks) {
|
|
76
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
77
|
+
cursorWrapper: widget(state.selection.from, CursorWrapper, {
|
|
78
|
+
key: "cursor-wrapper",
|
|
79
|
+
marks: compositionMarks,
|
|
80
|
+
side: 0,
|
|
81
|
+
raw: true
|
|
82
|
+
})
|
|
66
83
|
}));
|
|
84
|
+
// Pin the DOM cursor to PM's canonical position before the IME
|
|
85
|
+
// captures wherever the browser happened to leave it. Without this,
|
|
86
|
+
// a cursor at a mark boundary lands in either the left or right text
|
|
87
|
+
// node depending on the user's last navigation direction, and the
|
|
88
|
+
// IME composes into whichever one it found.
|
|
89
|
+
} else if (view.state.selection.empty) {
|
|
90
|
+
// @ts-expect-error internal method
|
|
91
|
+
view.domObserver.disconnectSelection();
|
|
92
|
+
try {
|
|
93
|
+
view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
|
|
94
|
+
);
|
|
95
|
+
} finally{
|
|
96
|
+
// @ts-expect-error internal method
|
|
97
|
+
view.domObserver.setCurSelection();
|
|
98
|
+
// @ts-expect-error internal method
|
|
99
|
+
view.domObserver.connectSelection();
|
|
100
|
+
}
|
|
67
101
|
}
|
|
102
|
+
view.compositionStarting = false;
|
|
103
|
+
// We set composing to true after creating the cursor wrapper
|
|
104
|
+
// so that no existing text nodes try to protect themselves
|
|
105
|
+
// while we're creating the cursor wrapper, which may need
|
|
106
|
+
// to split a text node.
|
|
107
|
+
view.input.composing = true;
|
|
68
108
|
return true;
|
|
69
109
|
},
|
|
70
110
|
compositionupdate () {
|
|
@@ -75,17 +115,40 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
75
115
|
if (!view.composing) return false;
|
|
76
116
|
view.input.composing = false;
|
|
77
117
|
compositionMarks = null;
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
for (const displaced of view.displacedNodes){
|
|
119
|
+
// Put the displaced TextViewDesc back into its parent's child list.
|
|
120
|
+
const parent = displaced.parent;
|
|
121
|
+
if (parent && !parent.children.includes(displaced)) {
|
|
122
|
+
parent.children.push(displaced);
|
|
123
|
+
parent.children.sort(sortViewDescs);
|
|
124
|
+
}
|
|
125
|
+
// Restore pmViewDesc claim on the text node.
|
|
126
|
+
displaced.dom.pmViewDesc = displaced;
|
|
127
|
+
// Truncate the IME text node back to what the displaced PM node says it
|
|
128
|
+
// is. The composed content lives in PM state; the next React render will
|
|
129
|
+
// mount a sibling TextNodeView that inserts its own DOM (e.g.
|
|
130
|
+
// `<span class="word">k</span>`) right after this node.
|
|
131
|
+
const claimedText = displaced.node.text ?? "";
|
|
132
|
+
if (displaced.nodeDOM.nodeValue !== claimedText) {
|
|
133
|
+
displaced.nodeDOM.nodeValue = claimedText;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
137
|
+
cursorWrapper: null
|
|
138
|
+
}));
|
|
139
|
+
if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
|
|
80
140
|
view.input.compositionNode.remove();
|
|
81
141
|
}
|
|
82
142
|
view.input.compositionEndedAt = event.timeStamp;
|
|
83
143
|
view.input.compositionNode = null;
|
|
144
|
+
view.input.compositionNodes = [];
|
|
84
145
|
view.input.compositionID++;
|
|
85
146
|
return true;
|
|
86
147
|
},
|
|
87
148
|
beforeinput (view, event) {
|
|
88
|
-
event.
|
|
149
|
+
if (event.inputType !== "insertFromComposition") {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
}
|
|
89
152
|
switch(event.inputType){
|
|
90
153
|
case "insertParagraph":
|
|
91
154
|
case "insertLineBreak":
|
|
@@ -131,7 +194,10 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
131
194
|
break;
|
|
132
195
|
}
|
|
133
196
|
case "insertCompositionText":
|
|
197
|
+
case "deleteCompositionText":
|
|
198
|
+
case "insertFromComposition":
|
|
134
199
|
{
|
|
200
|
+
if (!(view instanceof ReactEditorView)) break;
|
|
135
201
|
const { tr } = view.state;
|
|
136
202
|
// There's always a range on insertCompositionText beforeinput events
|
|
137
203
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -147,7 +213,37 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
147
213
|
} else {
|
|
148
214
|
tr.delete(start, end);
|
|
149
215
|
}
|
|
216
|
+
// When updating a composition within an existing text node,
|
|
217
|
+
// we need to avoid remounting it. If the composition is at
|
|
218
|
+
// the very beginning of the text node, the start position of
|
|
219
|
+
// that node will either be mapped forward (if inserting new
|
|
220
|
+
// content) or deleted (if replacing existing content).
|
|
221
|
+
//
|
|
222
|
+
// This will cause the reactKeys plugin to mint a new key for
|
|
223
|
+
// that node, which triggers a remount. So we check to see whether
|
|
224
|
+
// we're working on a composition at the very beginning of a text
|
|
225
|
+
// node, and if so, tell the react keys plugin not to change the
|
|
226
|
+
// key for that node.
|
|
227
|
+
//
|
|
228
|
+
// We need to check that the marks are the same — if they're not,
|
|
229
|
+
// then we're inserting text _before_ this text node, not at the
|
|
230
|
+
// start of it, so we actually _do_ want to map the exsting node
|
|
231
|
+
// forward.
|
|
232
|
+
const $start = view.state.doc.resolve(start);
|
|
233
|
+
const $end = view.state.doc.resolve(end);
|
|
234
|
+
const marks = compositionMarks ?? $start.marksAcross($end) ?? [];
|
|
235
|
+
if ($start.textOffset === 0 && $end.nodeAfter?.marks.every((m)=>m.isInSet(marks))) {
|
|
236
|
+
tr.setMeta(reactKeysPluginKey, {
|
|
237
|
+
overrides: {
|
|
238
|
+
[start]: start
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
150
242
|
view.dom.addEventListener("input", ()=>{
|
|
243
|
+
const sel = view.domSelectionRange();
|
|
244
|
+
if (sel.focusNode && sel.focusNode.nodeType === 3) {
|
|
245
|
+
view.input.compositionNode = sel.focusNode;
|
|
246
|
+
}
|
|
151
247
|
view.dispatch(tr);
|
|
152
248
|
}, {
|
|
153
249
|
once: true
|
|
@@ -187,3 +283,11 @@ export function beforeInputPlugin(setCursorWrapper) {
|
|
|
187
283
|
}
|
|
188
284
|
});
|
|
189
285
|
}
|
|
286
|
+
function isCompositionNodeOrphaned(tn) {
|
|
287
|
+
if (tn.pmViewDesc) return false;
|
|
288
|
+
for(let parent = tn.parentNode; parent; parent = parent.parentNode){
|
|
289
|
+
const desc = parent.pmViewDesc;
|
|
290
|
+
if (desc instanceof TextViewDesc && desc.nodeDOM === tn) return false;
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
19
|
-
key: new PluginKey("@handlewithcare/react-prosemirror/componentEventListeners"),
|
|
20
|
-
props: {
|
|
21
|
-
handleDOMEvents: domEventHandlers
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
return plugin;
|
|
17
|
+
return domEventHandlers;
|
|
25
18
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
|
+
import { DecorationSet } from "prosemirror-view";
|
|
3
|
+
import { widget } from "../decorations/ReactWidgetType.js";
|
|
2
4
|
export function createNodeKey() {
|
|
3
5
|
const key = Math.floor(Math.random() * 0xffffffffffff).toString(16);
|
|
4
6
|
return key;
|
|
@@ -11,14 +13,14 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
11
13
|
* key for a given node can be accessed by that node's
|
|
12
14
|
* current position in the document, and vice versa.
|
|
13
15
|
*/ export function reactKeys() {
|
|
14
|
-
let composing = false;
|
|
15
16
|
return new Plugin({
|
|
16
17
|
key: reactKeysPluginKey,
|
|
17
18
|
state: {
|
|
18
19
|
init (_, state) {
|
|
19
20
|
const next = {
|
|
20
21
|
posToKey: new Map(),
|
|
21
|
-
keyToPos: new Map()
|
|
22
|
+
keyToPos: new Map(),
|
|
23
|
+
cursorWrapper: null
|
|
22
24
|
};
|
|
23
25
|
state.doc.descendants((_, pos)=>{
|
|
24
26
|
const key = createNodeKey();
|
|
@@ -36,14 +38,20 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
36
38
|
* and assign its key to that new position, dropping it if the
|
|
37
39
|
* node was deleted.
|
|
38
40
|
*/ apply (tr, value, _, newState) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const overrides = tr.getMeta(reactKeysPluginKey)?.overrides;
|
|
41
|
+
const meta = tr.getMeta(reactKeysPluginKey);
|
|
42
|
+
const overrides = meta && "overrides" in meta ? meta.overrides : {};
|
|
43
|
+
const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
|
|
43
44
|
const next = {
|
|
44
45
|
posToKey: new Map(),
|
|
45
|
-
keyToPos: new Map()
|
|
46
|
+
keyToPos: new Map(),
|
|
47
|
+
cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? widget(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper
|
|
46
48
|
};
|
|
49
|
+
if (!tr.docChanged) {
|
|
50
|
+
return {
|
|
51
|
+
...value,
|
|
52
|
+
cursorWrapper: next.cursorWrapper
|
|
53
|
+
};
|
|
54
|
+
}
|
|
47
55
|
const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
|
|
48
56
|
let [a] = param, [b] = param1;
|
|
49
57
|
return a - b;
|
|
@@ -69,13 +77,12 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
69
77
|
}
|
|
70
78
|
},
|
|
71
79
|
props: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
80
|
+
decorations (state) {
|
|
81
|
+
const deco = reactKeysPluginKey.getState(state)?.cursorWrapper;
|
|
82
|
+
if (!deco) return DecorationSet.empty;
|
|
83
|
+
return DecorationSet.create(state.doc, [
|
|
84
|
+
deco
|
|
85
|
+
]);
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
});
|
|
@@ -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
|
-
|
|
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 { };
|
package/dist/esm/viewdesc.js
CHANGED
|
@@ -16,7 +16,17 @@ import { domIndex, isEquivalentPosition } from "./dom.js";
|
|
|
16
16
|
export function sortViewDescs(a, b) {
|
|
17
17
|
if (a instanceof TrailingHackViewDesc) return 1;
|
|
18
18
|
if (b instanceof TrailingHackViewDesc) return -1;
|
|
19
|
-
|
|
19
|
+
const posDiff = a.getPos() - b.getPos();
|
|
20
|
+
if (posDiff !== 0) return posDiff;
|
|
21
|
+
// When two descs share the same PM position (e.g. a zero-width widget
|
|
22
|
+
// and a text node that starts at the same position), fall back to DOM
|
|
23
|
+
// order so that the viewdesc children match the actual DOM layout.
|
|
24
|
+
// Without this, position computations like `posBeforeChild` can return
|
|
25
|
+
// the wrong PM position for the widget's container.
|
|
26
|
+
const cmp = a.dom.compareDocumentPosition(b.dom);
|
|
27
|
+
if (cmp & 4 /* DOCUMENT_POSITION_FOLLOWING */ ) return -1;
|
|
28
|
+
if (cmp & 2 /* DOCUMENT_POSITION_PRECEDING */ ) return 1;
|
|
29
|
+
return 0;
|
|
20
30
|
}
|
|
21
31
|
const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
|
|
22
32
|
// Superclass for the various kinds of descriptions. Defines their
|
|
@@ -372,7 +382,7 @@ export class ViewDesc {
|
|
|
372
382
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
373
383
|
if (after && after.contentEditable == "false") force = true;
|
|
374
384
|
}
|
|
375
|
-
if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
|
|
385
|
+
if (view.composing || !(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
|
|
376
386
|
return;
|
|
377
387
|
}
|
|
378
388
|
// Selection.extend can be used to create an 'inverted' selection
|
|
@@ -770,3 +780,33 @@ export function sameOuterDeco(a, b) {
|
|
|
770
780
|
for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
|
|
771
781
|
return true;
|
|
772
782
|
}
|
|
783
|
+
// Find a piece of text in an inline fragment, overlapping from-to.
|
|
784
|
+
// Ported from prosemirror-view's findTextInFragment.
|
|
785
|
+
export function findTextInFragment(frag, text, from, to) {
|
|
786
|
+
for(let i = 0, pos = 0; i < frag.childCount && pos <= to;){
|
|
787
|
+
const child = frag.child(i++);
|
|
788
|
+
const childStart = pos;
|
|
789
|
+
pos += child.nodeSize;
|
|
790
|
+
if (!child.isText) continue;
|
|
791
|
+
let str = child.text;
|
|
792
|
+
while(i < frag.childCount){
|
|
793
|
+
const next = frag.child(i++);
|
|
794
|
+
pos += next.nodeSize;
|
|
795
|
+
if (!next.isText) break;
|
|
796
|
+
str += next.text;
|
|
797
|
+
}
|
|
798
|
+
if (pos >= from) {
|
|
799
|
+
if (pos >= to && str.slice(to - text.length - childStart, to - childStart) === text) {
|
|
800
|
+
return to - text.length;
|
|
801
|
+
}
|
|
802
|
+
const found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
|
|
803
|
+
if (found >= 0 && found + text.length + childStart >= from) {
|
|
804
|
+
return childStart + found;
|
|
805
|
+
}
|
|
806
|
+
if (from === to && str.length >= to + text.length - childStart && str.slice(to - childStart, to - childStart + text.length) === text) {
|
|
807
|
+
return to;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return -1;
|
|
812
|
+
}
|