168 lines
4.4 KiB
JavaScript
168 lines
4.4 KiB
JavaScript
import { Image } from '@tiptap/extension-image';
|
|
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({
|
|
defaultOptions: {
|
|
...Image.options,
|
|
uploadsPath: null,
|
|
renderMarkdown: null,
|
|
},
|
|
addAttributes() {
|
|
return {
|
|
...this.parent?.(),
|
|
uploading: {
|
|
default: false,
|
|
},
|
|
src: {
|
|
default: null,
|
|
/*
|
|
* GitLab Flavored Markdown provides lazy loading for rendering images. As
|
|
* as result, the src attribute of the image may contain an embedded resource
|
|
* instead of the actual image URL. The image URL is moved to the data-src
|
|
* attribute.
|
|
*/
|
|
parseHTML: (element) => {
|
|
const img = resolveImageEl(element);
|
|
|
|
return {
|
|
src: img.dataset.src || img.getAttribute('src'),
|
|
};
|
|
},
|
|
},
|
|
canonicalSrc: {
|
|
default: null,
|
|
parseHTML: (element) => {
|
|
return {
|
|
canonicalSrc: element.dataset.canonicalSrc,
|
|
};
|
|
},
|
|
},
|
|
alt: {
|
|
default: null,
|
|
parseHTML: (element) => {
|
|
const img = resolveImageEl(element);
|
|
|
|
return {
|
|
alt: img.getAttribute('alt'),
|
|
};
|
|
},
|
|
},
|
|
};
|
|
},
|
|
parseHTML() {
|
|
return [
|
|
{
|
|
priority: 100,
|
|
tag: 'a.no-attachment-icon',
|
|
},
|
|
{
|
|
tag: 'img[src]',
|
|
},
|
|
];
|
|
},
|
|
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 = (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 }) => {
|
|
return {
|
|
tiptapExtension: ExtendedImage.configure({ inline: true, renderMarkdown, uploadsPath }),
|
|
serializer,
|
|
};
|
|
};
|