|
1 | 1 | <script lang="ts" module> |
2 | | - import {type Node} from 'prosemirror-model'; |
| 2 | + import {Fragment, Slice, type Node} from 'prosemirror-model'; |
3 | 3 | import {cn} from '$lib/utils'; |
4 | 4 | import {textSchema} from './editor-schema'; |
5 | 5 |
|
|
93 | 93 | editable() { |
94 | 94 | return !readonly; |
95 | 95 | }, |
| 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 | + }, |
96 | 127 | handleDOMEvents: { |
97 | 128 | pointerdown() { |
98 | 129 | pointerDown = true; |
|
123 | 154 | if (usingKeyboard) { // tabbed in |
124 | 155 | if (IsMobile.value) { |
125 | 156 | 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 | + } |
128 | 167 | prevSelection = undefined; |
129 | 168 | } else { |
130 | 169 | setSelection(Selection.atEnd(editor.state.doc)); |
|
188 | 227 | if (dispatch) dispatch(state.tr.insertText(newLine)); |
189 | 228 | return true; |
190 | 229 | }, |
| 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 | + }, |
191 | 235 | }), |
192 | 236 | keymap(baseKeymap) |
193 | 237 | ] |
|
0 commit comments