@handlewithcare/react-prosemirror 3.1.0-tiptap.53 → 3.1.0
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 +3 -0
- package/dist/cjs/ReactEditorView.js +18 -7
- package/dist/cjs/components/ChildNodeViews.js +10 -16
- package/dist/cjs/components/CursorWrapper.js +6 -4
- package/dist/cjs/components/ProseMirror.js +12 -4
- package/dist/cjs/components/TextNodeView.js +7 -216
- package/dist/cjs/components/TrailingHackView.js +0 -70
- package/dist/cjs/components/nodes/NodeView.js +40 -4
- package/dist/cjs/contexts/ChildDescriptionsContext.js +1 -3
- package/dist/cjs/contexts/CompositionContext.js +14 -0
- package/dist/cjs/hooks/useMarkViewDescription.js +2 -63
- package/dist/cjs/hooks/useNodeViewDescription.js +2 -66
- package/dist/cjs/plugins/beforeInputPlugin.js +130 -120
- package/dist/cjs/plugins/reactKeys.js +16 -4
- package/dist/cjs/tiptap/tiptapNodeView.js +10 -9
- package/dist/cjs/viewdesc.js +4 -1
- package/dist/esm/ReactEditorView.js +18 -7
- package/dist/esm/components/ChildNodeViews.js +12 -18
- package/dist/esm/components/CursorWrapper.js +6 -4
- package/dist/esm/components/ProseMirror.js +12 -4
- package/dist/esm/components/TextNodeView.js +5 -165
- package/dist/esm/components/TrailingHackView.js +1 -71
- package/dist/esm/components/nodes/NodeView.js +38 -5
- package/dist/esm/contexts/ChildDescriptionsContext.js +1 -3
- package/dist/esm/contexts/CompositionContext.js +4 -0
- package/dist/esm/hooks/useIsEditorStatic.js +4 -1
- package/dist/esm/hooks/useMarkViewDescription.js +3 -64
- package/dist/esm/hooks/useNodeViewDescription.js +3 -67
- package/dist/esm/plugins/beforeInputPlugin.js +131 -121
- package/dist/esm/plugins/reactKeys.js +16 -4
- package/dist/esm/tiptap/ReactProseMirrorNodeView.js +1 -1
- package/dist/esm/tiptap/TiptapEditorContent.js +8 -1
- package/dist/esm/tiptap/hooks/useIsInReactProseMirror.js +5 -1
- package/dist/esm/tiptap/tiptapNodeView.js +13 -14
- package/dist/esm/viewdesc.js +4 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ReactEditorView.d.ts +8 -4
- package/dist/types/components/ChildNodeViews.d.ts +2 -2
- package/dist/types/components/CursorWrapper.d.ts +1 -1
- package/dist/types/components/TextNodeView.d.ts +6 -18
- package/dist/types/components/TrailingHackView.d.ts +1 -1
- package/dist/types/components/marks/DefaultMarkView.d.ts +1 -1
- package/dist/types/components/marks/MarkView.d.ts +1 -1
- package/dist/types/components/marks/MarkViewConstructorView.d.ts +1 -1
- package/dist/types/components/marks/ReactMarkView.d.ts +1 -1
- package/dist/types/components/nodes/DefaultNodeView.d.ts +1 -1
- package/dist/types/components/nodes/NodeView.d.ts +3 -1
- package/dist/types/components/nodes/NodeViewConstructorView.d.ts +1 -1
- package/dist/types/components/nodes/ReactNodeView.d.ts +1 -1
- package/dist/types/contexts/ChildDescriptionsContext.d.ts +1 -2
- package/dist/types/contexts/CompositionContext.d.ts +4 -0
- package/dist/types/hooks/useEditor.d.ts +2 -2
- package/dist/types/hooks/useIsEditorStatic.d.ts +4 -0
- package/dist/types/hooks/useMarkViewDescription.d.ts +1 -2
- package/dist/types/hooks/useNodeViewDescription.d.ts +1 -2
- package/dist/types/hooks/useReactKeys.d.ts +2 -5
- package/dist/types/plugins/reactKeys.d.ts +5 -5
- package/dist/types/props.d.ts +225 -225
- package/dist/types/tiptap/ReactProseMirrorNodeView.d.ts +1 -1
- package/dist/types/tiptap/TiptapEditorContent.d.ts +10 -1
- package/dist/types/tiptap/hooks/useIsInReactProseMirror.d.ts +5 -0
- package/dist/types/tiptap/tiptapNodeView.d.ts +5 -6
- package/dist/types/viewdesc.d.ts +2 -1
- package/package.json +20 -6
- package/dist/cjs/plugins/componentEventListeners.js +0 -28
- package/dist/cjs/plugins/componentEventListenersPlugin.js +0 -35
- package/dist/cjs/tiptap/utils/ssrJSDOMPatch.js +0 -59
- package/dist/esm/plugins/componentEventListeners.js +0 -18
- package/dist/esm/plugins/componentEventListenersPlugin.js +0 -25
- package/dist/esm/tiptap/utils/ssrJSDOMPatch.js +0 -56
- package/dist/types/plugins/componentEventListeners.d.ts +0 -3
- package/dist/types/plugins/componentEventListenersPlugin.d.ts +0 -4
- package/dist/types/tiptap/utils/ssrJSDOMPatch.d.ts +0 -1
|
@@ -3,7 +3,7 @@ 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,
|
|
6
|
+
import { CompositionViewDesc, TextViewDesc, findTextInFragment } from "../viewdesc.js";
|
|
7
7
|
import { reactKeysPluginKey } from "./reactKeys.js";
|
|
8
8
|
function insertText(view, eventData) {
|
|
9
9
|
let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
|
|
@@ -47,36 +47,66 @@ function handleGapCursorComposition(view) {
|
|
|
47
47
|
tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
|
|
48
48
|
view.dispatch(tr);
|
|
49
49
|
}
|
|
50
|
+
const observeOptions = {
|
|
51
|
+
childList: true,
|
|
52
|
+
characterData: true,
|
|
53
|
+
characterDataOldValue: true,
|
|
54
|
+
attributes: true,
|
|
55
|
+
attributeOldValue: true,
|
|
56
|
+
subtree: true
|
|
57
|
+
};
|
|
50
58
|
export function beforeInputPlugin() {
|
|
51
|
-
let
|
|
59
|
+
let observer = null;
|
|
60
|
+
let preCompositionSnapshot = null;
|
|
61
|
+
function teardownComposition(view, endedAt) {
|
|
62
|
+
view.input.composing = false;
|
|
63
|
+
if (observer) {
|
|
64
|
+
if (view.input.compositionNode && view.dom.contains(view.input.compositionNode)) {
|
|
65
|
+
view.domObserver.queue.push(...observer.takeRecords());
|
|
66
|
+
view.domObserver.flush();
|
|
67
|
+
} else {
|
|
68
|
+
const freezeFrom = reactKeysPluginKey.getState(view.state)?.freezeFrom;
|
|
69
|
+
const frozenNode = freezeFrom == null ? null : view.state.doc.nodeAt(freezeFrom);
|
|
70
|
+
if (freezeFrom != null && frozenNode != null && preCompositionSnapshot) {
|
|
71
|
+
// This is a little hacky — it only works because we always abort
|
|
72
|
+
// compositions if the node after freezeFrom changes, so we can
|
|
73
|
+
// be sure that if a composition was canceled by the user/browser,
|
|
74
|
+
// the content hasn't changed since the composition started
|
|
75
|
+
view.dispatch(view.state.tr.replaceWith(freezeFrom + 1, freezeFrom + 1 + frozenNode.content.size, preCompositionSnapshot));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
observer.disconnect();
|
|
79
|
+
observer = null;
|
|
80
|
+
}
|
|
81
|
+
view.input.compositionEndedAt = endedAt;
|
|
82
|
+
view.input.compositionNode = null;
|
|
83
|
+
view.input.compositionNodes = [];
|
|
84
|
+
view.input.compositionID++;
|
|
85
|
+
}
|
|
52
86
|
return new Plugin({
|
|
87
|
+
view () {
|
|
88
|
+
return {
|
|
89
|
+
update (view) {
|
|
90
|
+
if (!(view instanceof ReactEditorView)) return;
|
|
91
|
+
const frozen = reactKeysPluginKey.getState(view.state)?.freezeFrom != null;
|
|
92
|
+
if (observer && view.composing && !frozen) {
|
|
93
|
+
teardownComposition(view, Date.now());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
},
|
|
53
98
|
props: {
|
|
54
99
|
handleDOMEvents: {
|
|
55
100
|
compositionstart (view) {
|
|
56
101
|
if (!(view instanceof ReactEditorView)) return false;
|
|
57
|
-
view.
|
|
58
|
-
|
|
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;
|
|
63
|
-
compositionMarks = view.state.storedMarks;
|
|
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);
|
|
73
|
-
view.dispatch(tr);
|
|
102
|
+
const storedMarks = view.state.selection.empty ? view.state.storedMarks : view.state.storedMarks ?? (view.state.selection instanceof TextSelection ? view.state.selection.$from.marksAcross(view.state.selection.$to) : null);
|
|
103
|
+
view.dispatch(view.state.tr.deleteSelection().setStoredMarks(storedMarks));
|
|
74
104
|
handleGapCursorComposition(view);
|
|
75
|
-
if (
|
|
105
|
+
if (storedMarks) {
|
|
76
106
|
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
77
|
-
cursorWrapper: widget(state.selection.from, CursorWrapper, {
|
|
107
|
+
cursorWrapper: widget(view.state.selection.from, CursorWrapper, {
|
|
78
108
|
key: "cursor-wrapper",
|
|
79
|
-
marks:
|
|
109
|
+
marks: storedMarks,
|
|
80
110
|
side: 0,
|
|
81
111
|
raw: true
|
|
82
112
|
})
|
|
@@ -87,24 +117,38 @@ export function beforeInputPlugin() {
|
|
|
87
117
|
// node depending on the user's last navigation direction, and the
|
|
88
118
|
// IME composes into whichever one it found.
|
|
89
119
|
} else if (view.state.selection.empty) {
|
|
90
|
-
// @ts-expect-error internal method
|
|
91
120
|
view.domObserver.disconnectSelection();
|
|
92
121
|
try {
|
|
93
122
|
view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
|
|
94
123
|
);
|
|
95
124
|
} finally{
|
|
96
|
-
// @ts-expect-error internal method
|
|
97
125
|
view.domObserver.setCurSelection();
|
|
98
|
-
// @ts-expect-error internal method
|
|
99
126
|
view.domObserver.connectSelection();
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
const freezeFrom = view.state.selection.$from.before();
|
|
130
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
131
|
+
freezeFrom
|
|
132
|
+
}));
|
|
133
|
+
const frozenDom = view.nodeDOM(freezeFrom);
|
|
134
|
+
if (!frozenDom) {
|
|
135
|
+
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
136
|
+
cursorWrapper: null,
|
|
137
|
+
freezeFrom: null
|
|
138
|
+
}));
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
preCompositionSnapshot = view.state.doc.nodeAt(freezeFrom)?.content ?? null;
|
|
107
142
|
view.input.composing = true;
|
|
143
|
+
observer = new MutationObserver((records)=>{
|
|
144
|
+
if (reactKeysPluginKey.getState(view.state)?.freezeFrom == null) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
view.domObserver.queue.push(...records);
|
|
148
|
+
view.domObserver.flush();
|
|
149
|
+
syncCompositionViewDescs(view);
|
|
150
|
+
});
|
|
151
|
+
observer.observe(frozenDom, observeOptions);
|
|
108
152
|
return true;
|
|
109
153
|
},
|
|
110
154
|
compositionupdate () {
|
|
@@ -113,36 +157,11 @@ export function beforeInputPlugin() {
|
|
|
113
157
|
compositionend (view, event) {
|
|
114
158
|
if (!(view instanceof ReactEditorView)) return false;
|
|
115
159
|
if (!view.composing) return false;
|
|
116
|
-
view.
|
|
117
|
-
compositionMarks = null;
|
|
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
|
-
}
|
|
160
|
+
teardownComposition(view, event.timeStamp);
|
|
136
161
|
view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
|
|
137
|
-
cursorWrapper: null
|
|
162
|
+
cursorWrapper: null,
|
|
163
|
+
freezeFrom: null
|
|
138
164
|
}));
|
|
139
|
-
if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
|
|
140
|
-
view.input.compositionNode.remove();
|
|
141
|
-
}
|
|
142
|
-
view.input.compositionEndedAt = event.timeStamp;
|
|
143
|
-
view.input.compositionNode = null;
|
|
144
|
-
view.input.compositionNodes = [];
|
|
145
|
-
view.input.compositionID++;
|
|
146
165
|
return true;
|
|
147
166
|
},
|
|
148
167
|
beforeinput (view, event) {
|
|
@@ -193,63 +212,6 @@ export function beforeInputPlugin() {
|
|
|
193
212
|
insertText(view, event.data);
|
|
194
213
|
break;
|
|
195
214
|
}
|
|
196
|
-
case "insertCompositionText":
|
|
197
|
-
case "deleteCompositionText":
|
|
198
|
-
case "insertFromComposition":
|
|
199
|
-
{
|
|
200
|
-
if (!(view instanceof ReactEditorView)) break;
|
|
201
|
-
const { tr } = view.state;
|
|
202
|
-
// There's always a range on insertCompositionText beforeinput events
|
|
203
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
204
|
-
const range = event.getTargetRanges()[0];
|
|
205
|
-
const start = view.posAtDOM(range.startContainer, range.startOffset);
|
|
206
|
-
const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
|
|
207
|
-
if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (event.data) {
|
|
211
|
-
if (compositionMarks) tr.ensureMarks(compositionMarks);
|
|
212
|
-
tr.insertText(event.data, start, end);
|
|
213
|
-
} else {
|
|
214
|
-
tr.delete(start, end);
|
|
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
|
-
}
|
|
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
|
-
}
|
|
247
|
-
view.dispatch(tr);
|
|
248
|
-
}, {
|
|
249
|
-
once: true
|
|
250
|
-
});
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
215
|
case "deleteWordBackward":
|
|
254
216
|
case "deleteHardLineBackward":
|
|
255
217
|
case "deleteSoftLineBackward":
|
|
@@ -283,11 +245,59 @@ export function beforeInputPlugin() {
|
|
|
283
245
|
}
|
|
284
246
|
});
|
|
285
247
|
}
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
248
|
+
function syncCompositionViewDescs(view) {
|
|
249
|
+
const compositionNode = view.domObserver.lastChangedTextNode;
|
|
250
|
+
if (!compositionNode) return;
|
|
251
|
+
const freezeFrom = reactKeysPluginKey.getState(view.state)?.freezeFrom;
|
|
252
|
+
if (freezeFrom == null) return;
|
|
253
|
+
const compositionBlock = view.state.doc.nodeAt(freezeFrom);
|
|
254
|
+
if (!compositionBlock) return;
|
|
255
|
+
const compositionBlockDesc = view.docView.descAt(freezeFrom);
|
|
256
|
+
if (!compositionBlockDesc) return;
|
|
257
|
+
const desc = view.docView.nearestDesc(compositionNode);
|
|
258
|
+
compositionBlockDesc.node = compositionBlock;
|
|
259
|
+
if (desc instanceof TextViewDesc) {
|
|
260
|
+
if (compositionNode.nodeValue && desc.node.text !== compositionNode.nodeValue) {
|
|
261
|
+
desc.node = view.state.schema.text(compositionNode.nodeValue, desc.node.marks);
|
|
262
|
+
desc.nodeDOM = compositionNode;
|
|
263
|
+
compositionNode.pmViewDesc = desc;
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
291
266
|
}
|
|
292
|
-
|
|
267
|
+
if (desc instanceof CompositionViewDesc) {
|
|
268
|
+
if (compositionNode.nodeValue != null && desc.text !== compositionNode.nodeValue) {
|
|
269
|
+
desc.dom = compositionNode;
|
|
270
|
+
desc.textDOM = compositionNode;
|
|
271
|
+
desc.text = compositionNode.nodeValue;
|
|
272
|
+
compositionNode.pmViewDesc = desc;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const parentDesc = desc?.contentDOM ? desc : compositionBlockDesc;
|
|
277
|
+
const children = parentDesc.children;
|
|
278
|
+
// Drop any text or composition desc in this container whose DOM the
|
|
279
|
+
// IME has detached. This covers two cases: a TextViewDesc the IME subsumed
|
|
280
|
+
// into the composition node, and (on Safari, which replaces the whole text
|
|
281
|
+
// node on each composition update) any orphaned composition view
|
|
282
|
+
// desc(s) left over from the previous composition steps.
|
|
283
|
+
for(let i = children.length - 1; i >= 0; i--){
|
|
284
|
+
const c = children[i];
|
|
285
|
+
if (!(c instanceof TextViewDesc) && !(c instanceof CompositionViewDesc)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const dom = c.dom;
|
|
289
|
+
if (view.dom.contains(dom)) continue;
|
|
290
|
+
children.splice(i, 1);
|
|
291
|
+
}
|
|
292
|
+
const contentStart = freezeFrom + 1;
|
|
293
|
+
const { from, to } = view.state.selection;
|
|
294
|
+
const textPos = findTextInFragment(compositionBlock.content, compositionNode.nodeValue ?? "", from - contentStart, to - contentStart);
|
|
295
|
+
if (textPos < 0) return;
|
|
296
|
+
const startPos = contentStart + textPos;
|
|
297
|
+
let topDOM = compositionNode;
|
|
298
|
+
while(topDOM.parentNode && topDOM.parentNode !== parentDesc.contentDOM){
|
|
299
|
+
topDOM = topDOM.parentNode;
|
|
300
|
+
}
|
|
301
|
+
const insertIndex = children.findLastIndex((c)=>c.posBefore <= startPos) + 1;
|
|
302
|
+
children.splice(insertIndex, 0, new CompositionViewDesc(parentDesc, ()=>startPos, topDOM, compositionNode, compositionNode.nodeValue ?? ""));
|
|
293
303
|
}
|
|
@@ -20,7 +20,8 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
20
20
|
const next = {
|
|
21
21
|
posToKey: new Map(),
|
|
22
22
|
keyToPos: new Map(),
|
|
23
|
-
cursorWrapper: null
|
|
23
|
+
cursorWrapper: null,
|
|
24
|
+
freezeFrom: null
|
|
24
25
|
};
|
|
25
26
|
state.doc.descendants((_, pos)=>{
|
|
26
27
|
const key = createNodeKey();
|
|
@@ -37,19 +38,30 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
|
|
|
37
38
|
* through the transaction to identify its current position,
|
|
38
39
|
* and assign its key to that new position, dropping it if the
|
|
39
40
|
* node was deleted.
|
|
40
|
-
*/ apply (tr, value,
|
|
41
|
+
*/ apply (tr, value, oldState, newState) {
|
|
41
42
|
const meta = tr.getMeta(reactKeysPluginKey);
|
|
42
43
|
const overrides = meta && "overrides" in meta ? meta.overrides : {};
|
|
43
44
|
const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
|
|
45
|
+
const freezeFrom = meta && "freezeFrom" in meta ? meta.freezeFrom : undefined;
|
|
44
46
|
const next = {
|
|
45
47
|
posToKey: new Map(),
|
|
46
48
|
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
|
|
49
|
+
cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? widget(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper,
|
|
50
|
+
freezeFrom: freezeFrom === undefined ? value.freezeFrom !== null ? tr.mapping.map(value.freezeFrom, -1) : null : freezeFrom
|
|
48
51
|
};
|
|
52
|
+
if (value.freezeFrom !== null && next.freezeFrom !== null && tr.getMeta("composition") == null) {
|
|
53
|
+
const oldBlock = oldState.doc.nodeAt(value.freezeFrom);
|
|
54
|
+
const newBlock = newState.doc.nodeAt(next.freezeFrom);
|
|
55
|
+
if (newBlock && !oldBlock?.eq(newBlock)) {
|
|
56
|
+
next.freezeFrom = null;
|
|
57
|
+
next.cursorWrapper = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
49
60
|
if (!tr.docChanged) {
|
|
50
61
|
return {
|
|
51
62
|
...value,
|
|
52
|
-
cursorWrapper: next.cursorWrapper
|
|
63
|
+
cursorWrapper: next.cursorWrapper,
|
|
64
|
+
freezeFrom: next.freezeFrom
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
67
|
const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
|
|
@@ -2,7 +2,7 @@ import { NodeView } from "@tiptap/core";
|
|
|
2
2
|
/**
|
|
3
3
|
* Subclass of Tiptap's NodeView to be used in tiptapNodeView.
|
|
4
4
|
*
|
|
5
|
-
* Allows us to pass in an existing dom and
|
|
5
|
+
* Allows us to pass in an existing dom and contentDOM from React ProseMirror's
|
|
6
6
|
* ViewDesc, so that we can call Tiptap's default stopEvent and ignoreMutation
|
|
7
7
|
* methods
|
|
8
8
|
*/ export class ReactProseMirrorNodeView extends NodeView {
|
|
@@ -49,7 +49,14 @@ function getInstance() {
|
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Renders the actual editable ProseMirror document.
|
|
54
|
+
*
|
|
55
|
+
* This **must** be passed as a child to the `TiptapEditorView`
|
|
56
|
+
* component. It may be wrapped in other components, and other
|
|
57
|
+
* childern may be passed before or after. It must be passed the
|
|
58
|
+
* same `editor` as is passed to the `TiptapEditorView`.
|
|
59
|
+
*/ export function TiptapEditorContent(param) {
|
|
53
60
|
let { editor: editorProp, ...props } = param;
|
|
54
61
|
const editor = editorProp;
|
|
55
62
|
const { onEditorInitialize, onEditorDeinitialize } = useContext(TiptapEditorContext);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useContext } from "react";
|
|
2
2
|
import { EditorContext } from "../../contexts/EditorContext.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if the hook is called in a
|
|
5
|
+
* component that's a descendant of the
|
|
6
|
+
* ProseMirror component
|
|
7
|
+
*/ export function useIsInReactProseMirror() {
|
|
4
8
|
return useContext(EditorContext) !== null;
|
|
5
9
|
}
|
|
@@ -23,11 +23,9 @@ import { useTiptapEditorEventCallback } from "./hooks/useTiptapEditorEventCallba
|
|
|
23
23
|
* codeBlock: nodeView({
|
|
24
24
|
* component: function CodeBlock(nodeViewProps) {
|
|
25
25
|
* return (
|
|
26
|
-
* <
|
|
27
|
-
* <
|
|
28
|
-
*
|
|
29
|
-
* </pre>
|
|
30
|
-
* </AnnotatableNodeViewWrapper>
|
|
26
|
+
* <pre>
|
|
27
|
+
* <NodeViewContent as="code" />
|
|
28
|
+
* </pre>
|
|
31
29
|
* )
|
|
32
30
|
* },
|
|
33
31
|
* extension: CodeBlockExtension,
|
|
@@ -84,15 +82,6 @@ import { useTiptapEditorEventCallback } from "./hooks/useTiptapEditorEventCallba
|
|
|
84
82
|
return result;
|
|
85
83
|
});
|
|
86
84
|
useIgnoreMutation(function(_, mutation) {
|
|
87
|
-
if (ignoreMutation) {
|
|
88
|
-
return ignoreMutation.call({
|
|
89
|
-
name: extension.name,
|
|
90
|
-
editor,
|
|
91
|
-
type: node.type
|
|
92
|
-
}, {
|
|
93
|
-
mutation
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
85
|
if (!editor || !(this.dom instanceof HTMLElement)) return false;
|
|
97
86
|
const nodeView = new ReactProseMirrorNodeView(WrappedComponent, {
|
|
98
87
|
extension,
|
|
@@ -104,6 +93,16 @@ import { useTiptapEditorEventCallback } from "./hooks/useTiptapEditorEventCallba
|
|
|
104
93
|
node,
|
|
105
94
|
view: editor.view
|
|
106
95
|
}, this.dom, this.contentDOM);
|
|
96
|
+
if (ignoreMutation) {
|
|
97
|
+
return ignoreMutation.call({
|
|
98
|
+
name: extension.name,
|
|
99
|
+
editor,
|
|
100
|
+
type: node.type
|
|
101
|
+
}, {
|
|
102
|
+
mutation,
|
|
103
|
+
defaultIgnoreMutation: nodeView.ignoreMutation.bind(nodeView)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
107
106
|
return nodeView.ignoreMutation(mutation) ?? false;
|
|
108
107
|
});
|
|
109
108
|
const { extraClassName, htmlProps } = useMemo(()=>{
|
package/dist/esm/viewdesc.js
CHANGED
|
@@ -382,7 +382,7 @@ export class ViewDesc {
|
|
|
382
382
|
const after = selRange.focusNode.childNodes[selRange.focusOffset];
|
|
383
383
|
if (after && after.contentEditable == "false") force = true;
|
|
384
384
|
}
|
|
385
|
-
if (view.
|
|
385
|
+
if (view.cursorWrapped || !(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
|
|
386
386
|
return;
|
|
387
387
|
}
|
|
388
388
|
// Selection.extend can be used to create an 'inverted' selection
|
|
@@ -691,6 +691,9 @@ export class TextViewDesc extends NodeViewDesc {
|
|
|
691
691
|
isText(text) {
|
|
692
692
|
return this.node.text == text;
|
|
693
693
|
}
|
|
694
|
+
ignoreMutation(mutation) {
|
|
695
|
+
return mutation.type !== "characterData" && mutation.type !== "selection";
|
|
696
|
+
}
|
|
694
697
|
}
|
|
695
698
|
// A dummy desc used to tag trailing BR or IMG nodes created to work
|
|
696
699
|
// around contentEditable terribleness.
|