Skip to content

Commit f91dfff

Browse files
fixup! feat(text-editor): add support for pasting inline images
1 parent 56d3297 commit f91dfff

File tree

2 files changed

+77
-28
lines changed

2 files changed

+77
-28
lines changed

src/components/text-editor/prosemirror-adapter/plugins/image/inserter.ts

+76-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ImageInfo,
88
ImageState,
99
} from '../../../text-editor.types';
10-
import { Node } from 'prosemirror-model';
10+
import { Node, Slice, Fragment } from 'prosemirror-model';
1111
import { imageCache } from './node';
1212

1313
export const pluginKey = new PluginKey('imageInserterPlugin');
@@ -27,8 +27,8 @@ export const createImageInserterPlugin = (
2727
return new Plugin({
2828
key: pluginKey,
2929
props: {
30-
handlePaste: (view, event) => {
31-
return processPasteEvent(view, event);
30+
handlePaste: (view, event, slice) => {
31+
return processPasteEvent(view, event, slice);
3232
},
3333
handleDOMEvents: {
3434
imagePasted: (_, event) => {
@@ -167,15 +167,74 @@ const createFailedThumbnailInserter =
167167
dispatch(tr);
168168
};
169169

170+
/**
171+
* Check if a given ProseMirror node or fragment contains any image nodes.
172+
* @param node - The ProseMirror node or fragment to check.
173+
* @returns A boolean indicating whether the node contains any image nodes.
174+
*/
175+
const isImageNode = (node: Node | Fragment): boolean => {
176+
if (node instanceof Node) {
177+
if (node.type.name === 'image') {
178+
return true;
179+
}
180+
181+
let found = false;
182+
node.content.forEach((child) => {
183+
if (isImageNode(child)) {
184+
found = true;
185+
}
186+
});
187+
188+
return found;
189+
} else if (node instanceof Fragment) {
190+
let found = false;
191+
node.forEach((child) => {
192+
if (isImageNode(child)) {
193+
found = true;
194+
}
195+
});
196+
197+
return found;
198+
}
199+
200+
return false;
201+
};
202+
203+
/**
204+
* Filter out image nodes from a ProseMirror fragment.
205+
* @param fragment - The ProseMirror fragment to filter.
206+
* @returns A new fragment with image nodes removed.
207+
*/
208+
const filterImageNodes = (fragment: Fragment): Fragment => {
209+
const filteredChildren: Node[] = [];
210+
211+
fragment.forEach((child) => {
212+
if (!isImageNode(child)) {
213+
if (child.content.size > 0) {
214+
const filteredContent = filterImageNodes(child.content);
215+
const newNode = child.copy(filteredContent);
216+
filteredChildren.push(newNode);
217+
} else {
218+
filteredChildren.push(child);
219+
}
220+
}
221+
});
222+
223+
return Fragment.fromArray(filteredChildren);
224+
};
225+
170226
/**
171227
* Process a paste event and trigger an imagePasted event if an image file is pasted.
228+
* If an HTML image element is pasted, this image is filtered out from the slice content.
229+
*
172230
* @param view - The ProseMirror editor view.
173231
* @param event - The paste event.
174232
* @returns A boolean; True if an image file was pasted to prevent default paste behavior, otherwise false.
175233
*/
176234
const processPasteEvent = (
177235
view: EditorView,
178236
event: ClipboardEvent,
237+
slice: Slice,
179238
): boolean => {
180239
const clipboardData = event.clipboardData;
181240
if (!clipboardData) {
@@ -202,5 +261,19 @@ const processPasteEvent = (
202261
}
203262
}
204263

264+
const filteredSlice = new Slice(
265+
filterImageNodes(slice.content),
266+
slice.openStart,
267+
slice.openEnd,
268+
);
269+
270+
if (filteredSlice.content.childCount < slice.content.childCount) {
271+
const { state, dispatch } = view;
272+
const tr = state.tr.replaceSelection(filteredSlice);
273+
dispatch(tr);
274+
275+
return true;
276+
}
277+
205278
return files.length > 0;
206279
};

src/components/text-editor/prosemirror-adapter/plugins/image/node.ts

+1-25
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,8 @@ function createImageNodeSpec(language: Languages): NodeSpec {
119119
{
120120
tag: 'img',
121121
getAttrs: (dom: HTMLElement): Attrs => {
122-
const src = dom.getAttribute('src');
123-
if (!isValidSrc(src)) {
124-
return {};
125-
}
126-
127122
return {
128-
src: src,
123+
src: dom.getAttribute('src') || '',
129124
alt: dom.getAttribute('alt') || 'file',
130125
width: dom.style.width || '',
131126
maxWidth: '100%',
@@ -138,25 +133,6 @@ function createImageNodeSpec(language: Languages): NodeSpec {
138133
};
139134
}
140135

141-
function isValidSrc(src: string): boolean {
142-
try {
143-
const parsed = new URL(src, document.baseURI);
144-
145-
if (['http:', 'https:'].includes(parsed.protocol)) {
146-
return true;
147-
}
148-
149-
// Allow png, jpeg, jpg, gif images with base64 encoding
150-
if (parsed.protocol === 'data:') {
151-
return /^data:image\/(png|jpeg|jpg|gif);base64,/.test(src);
152-
}
153-
154-
return false;
155-
} catch {
156-
return false;
157-
}
158-
}
159-
160136
function createStatusSpan(
161137
key: string,
162138
node: Node,

0 commit comments

Comments
 (0)