@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.
- package/dist/cjs/ReactEditorView.js +0 -2
- package/dist/cjs/components/ChildNodeViews.js +9 -14
- package/dist/cjs/components/CursorWrapper.js +9 -6
- package/dist/cjs/components/TextNodeView.js +38 -109
- package/dist/cjs/components/TrailingHackView.js +0 -29
- package/dist/cjs/components/WidgetView.js +1 -0
- package/dist/cjs/hooks/useComponentEventListeners.js +6 -14
- package/dist/cjs/hooks/useNodeViewDescription.js +16 -37
- package/dist/cjs/plugins/beforeInputPlugin.js +43 -41
- package/dist/cjs/plugins/componentEventListeners.js +2 -9
- package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +59 -0
- package/dist/cjs/viewdesc.js +3 -10
- package/dist/esm/ReactEditorView.js +0 -2
- package/dist/esm/components/ChildNodeViews.js +9 -14
- package/dist/esm/components/CursorWrapper.js +10 -7
- package/dist/esm/components/TextNodeView.js +38 -109
- package/dist/esm/components/TrailingHackView.js +1 -30
- package/dist/esm/components/WidgetView.js +1 -0
- package/dist/esm/hooks/useComponentEventListeners.js +6 -14
- package/dist/esm/hooks/useNodeViewDescription.js +17 -38
- package/dist/esm/plugins/beforeInputPlugin.js +43 -41
- package/dist/esm/plugins/componentEventListeners.js +2 -9
- package/dist/esm/tiptap/hooks/useTiptapEditor.js +7 -1
- package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +56 -0
- package/dist/esm/viewdesc.js +3 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +0 -4
- package/dist/types/components/CursorWrapper.d.ts +2 -4
- package/dist/types/components/TextNodeView.d.ts +4 -14
- package/dist/types/components/TrailingHackView.d.ts +1 -1
- package/dist/types/components/WidgetViewComponentProps.d.ts +4 -3
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/hooks/useComponentEventListeners.d.ts +1 -1
- package/dist/types/plugins/componentEventListeners.d.ts +2 -3
- 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 +2 -2
- 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
|
-
|
|
55
|
-
view.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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
|
@@ -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(
|
|
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
|
}
|