debian-mirror-gitlab/app/assets/javascripts/content_editor/extensions/image.js
2021-09-30 23:02:18 +05:30

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,
};
};