Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/content_editor/extensions/image.js')
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js130
1 files changed, 124 insertions, 6 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 287216e68d5..4dd8a1376ad 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,10 +1,65 @@
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({
+ defaultOptions: {
+ ...Image.options,
+ uploadsPath: null,
+ renderMarkdown: null,
+ },
addAttributes() {
return {
...this.parent?.(),
+ uploading: {
+ default: false,
+ },
src: {
default: null,
/*
@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
* attribute.
*/
parseHTML: (element) => {
- const img = element.querySelector('img');
+ 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 = element.querySelector('img');
+ const img = resolveImageEl(element);
return {
alt: img.getAttribute('alt'),
@@ -44,7 +107,62 @@ const ExtendedImage = Image.extend({
},
];
},
-}).configure({ inline: true });
+ 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 tiptapExtension = ExtendedImage;
-export const serializer = defaultMarkdownSerializer.nodes.image;
+export const configure = ({ renderMarkdown, uploadsPath }) => {
+ return {
+ tiptapExtension: ExtendedImage.configure({ inline: true, renderMarkdown, uploadsPath }),
+ serializer,
+ };
+};