diff options
Diffstat (limited to 'app/assets/javascripts/content_editor/extensions/image.js')
-rw-r--r-- | app/assets/javascripts/content_editor/extensions/image.js | 130 |
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, + }; +}; |