Skip to content

Commit a5d05d2

Browse files
committed
Patch various rich-text errors
1 parent 1f39350 commit a5d05d2

1 file changed

Lines changed: 47 additions & 3 deletions

File tree

frontend/viewer/src/lib/components/lcm-rich-text-editor/lcm-rich-text-editor.svelte

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" module>
2-
import {type Node} from 'prosemirror-model';
2+
import {Fragment, Slice, type Node} from 'prosemirror-model';
33
import {cn} from '$lib/utils';
44
import {textSchema} from './editor-schema';
55
@@ -93,6 +93,37 @@
9393
editable() {
9494
return !readonly;
9595
},
96+
handlePaste(view, _event, slice) {
97+
if (view.state.doc.content.size === 0) {
98+
// if the field is cleared, the resulting selection breaks paste (on older devices at least)
99+
selectAll(view);
100+
}
101+
102+
// When a user copies a whole field. It includes the trailing <br>. Pasting it again causes errors.
103+
// We still get errores sometimes when multiple lines are pasted, but this seems like a nice improvement.
104+
function withoutBrs(nodes: readonly Node[]): Node[] {
105+
return nodes
106+
.filter(node => node.type.name !== textSchema.nodes.br.name)
107+
.map(node => {
108+
if (node.isText) return node;
109+
return node.copy(Fragment.fromArray(withoutBrs(node.content.content)));
110+
});
111+
}
112+
const cleanFragment = Fragment.fromArray(withoutBrs(slice.content.content));
113+
const cleanSlice = new Slice(cleanFragment, slice.openStart, slice.openEnd);
114+
115+
// Below code is copied from prosemirror's doPaste
116+
// https://github.com/ProseMirror/prosemirror-view/blob/381c163b0abde96cabd609a8c4fc72ed2891b0e1/src/input.ts#L624
117+
function sliceSingleNode(slice: Slice): Node | null {
118+
return slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 ? slice.content.firstChild : null;
119+
}
120+
let singleNode = sliceSingleNode(cleanSlice);
121+
let tr = singleNode
122+
? view.state.tr.replaceSelectionWith(singleNode, false)
123+
: view.state.tr.replaceSelection(slice);
124+
view.dispatch(tr.scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
125+
return true;
126+
},
96127
handleDOMEvents: {
97128
pointerdown() {
98129
pointerDown = true;
@@ -123,8 +154,16 @@
123154
if (usingKeyboard) { // tabbed in
124155
if (IsMobile.value) {
125156
if (prevSelection) {
126-
const prevSelectionForCurrentDoc = Selection.fromJSON(editor.state.doc, prevSelection.toJSON());
127-
setSelection(prevSelectionForCurrentDoc);
157+
// We can land here when the field gets cleared for some reason.
158+
// In that case fromJSON doesn't like the prevSelection (on older devices at least)
159+
if (editor.state.doc.content.size) {
160+
try {
161+
const prevSelectionForCurrentDoc = Selection.fromJSON(editor.state.doc, prevSelection.toJSON());
162+
setSelection(prevSelectionForCurrentDoc);
163+
} catch {
164+
console.warn('Could not restore previous selection', prevSelection.toJSON(), editor.state.doc);
165+
}
166+
}
128167
prevSelection = undefined;
129168
} else {
130169
setSelection(Selection.atEnd(editor.state.doc));
@@ -188,6 +227,11 @@
188227
if (dispatch) dispatch(state.tr.insertText(newLine));
189228
return true;
190229
},
230+
// eslint-disable-next-line @typescript-eslint/naming-convention
231+
'Backspace': (state) => {
232+
// If the field is empty, backspace results in an error (on older devices at least)
233+
return state.doc.content.size === 0;
234+
},
191235
}),
192236
keymap(baseKeymap)
193237
]

0 commit comments

Comments
 (0)