Skip to content
Snippets Groups Projects

Enhance image extension to support image uploads

Merged Enrique Alcántara requested to merge 328641-insert-images/enhanced-image-extension into master
All threads resolved!
Files
6
import { Image } from '@tiptap/extension-image';
import { Image } from '@tiptap/extension-image';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
 
import { Plugin, PluginKey } from 'prosemirror-state';
 
import { __ } from '~/locale';
 
import ImageWrapper from '../components/wrappers/image.vue';
 
import { uploadFile } from '../services/upload_file';
 
import { getImageAlt, readFileAsDataURL } from '../services/utils';
 
 
export const acceptedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
 
 
const resolveImageEl = (element) =>
 
element.nodeName === 'IMG' ? element : element.querySelector('img');
 
 
const startFileUpload = async ({ editor, file, uploadsPath, renderMarkdown }) => {
 
const encodedSrc = await readFileAsDataURL(file);
 
const { view } = editor;
 
 
editor.commands.setImage({ uploading: true, src: encodedSrc });
 
 
const { state } = view;
 
const position = state.selection.from - 1;
 
const { tr } = state;
 
 
try {
 
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
 
 
view.dispatch(
 
tr.setNodeMarkup(position, undefined, {
 
uploading: false,
 
src: encodedSrc,
 
alt: getImageAlt(src),
 
canonicalSrc,
 
}),
 
);
 
} catch (e) {
 
editor.commands.deleteRange({ from: position, to: position + 1 });
 
editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
 
}
 
};
 
 
const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
 
if (acceptedMimes.includes(file?.type)) {
 
startFileUpload({ editor, file, uploadsPath, renderMarkdown });
 
 
return true;
 
}
 
 
return false;
 
};
const ExtendedImage = Image.extend({
const ExtendedImage = Image.extend({
 
defaultOptions: {
 
...Image.options,
 
uploadsPath: null,
 
renderMarkdown: null,
 
},
addAttributes() {
addAttributes() {
return {
return {
...this.parent?.(),
...this.parent?.(),
 
uploading: {
 
default: false,
 
},
src: {
src: {
default: null,
default: null,
/*
/*
@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
* attribute.
* attribute.
*/
*/
parseHTML: (element) => {
parseHTML: (element) => {
const img = element.querySelector('img');
const img = resolveImageEl(element);
return {
return {
src: img.dataset.src || img.getAttribute('src'),
src: img.dataset.src || img.getAttribute('src'),
};
};
},
},
},
},
 
canonicalSrc: {
 
default: null,
 
parseHTML: (element) => {
 
return {
 
canonicalSrc: element.dataset.canonicalSrc,
 
};
 
},
 
},
alt: {
alt: {
default: null,
default: null,
parseHTML: (element) => {
parseHTML: (element) => {
const img = element.querySelector('img');
const img = resolveImageEl(element);
return {
return {
alt: img.getAttribute('alt'),
alt: img.getAttribute('alt'),
@@ -44,9 +107,58 @@ const ExtendedImage = Image.extend({
@@ -44,9 +107,58 @@ const ExtendedImage = Image.extend({
},
},
];
];
},
},
 
addCommands() {
 
return {
 
...this.parent(),
 
uploadImage: ({ file }) => () => {
 
const { uploadsPath, renderMarkdown } = this.options;
 
 
handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
 
},
 
};
 
},
 
addProseMirrorPlugins() {
 
const { editor } = this;
 
 
return [
 
new Plugin({
 
key: new PluginKey('handleDropAndPasteImages'),
 
props: {
 
handlePaste: (_, event) => {
 
const { uploadsPath, renderMarkdown } = this.options;
 
 
return handleFileEvent({
 
editor,
 
file: event.clipboardData.files[0],
 
uploadsPath,
 
renderMarkdown,
 
});
 
},
 
handleDrop: (_, event) => {
 
const { uploadsPath, renderMarkdown } = this.options;
 
 
return handleFileEvent({
 
editor,
 
file: event.dataTransfer.files[0],
 
uploadsPath,
 
renderMarkdown,
 
});
 
},
 
},
 
}),
 
];
 
},
 
addNodeView() {
 
return VueNodeViewRenderer(ImageWrapper);
 
},
});
});
const serializer = defaultMarkdownSerializer.nodes.image;
const serializer = (state, node) => {
 
const { alt, canonicalSrc, src, title } = node.attrs;
 
const quotedTitle = title ? ` ${state.quote(title)}` : '';
 
 
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
 
};
export const configure = ({ renderMarkdown, uploadsPath }) => {
export const configure = ({ renderMarkdown, uploadsPath }) => {
return {
return {
Loading