Skip to content

Commit

Permalink
Fixes #4738 - Inline images are not resized during the usage of the e…
Browse files Browse the repository at this point in the history
…ditor
  • Loading branch information
sheremet-va authored and dominikklein committed Aug 4, 2023
1 parent 81448d2 commit e24895a
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 21 deletions.
Expand Up @@ -178,6 +178,7 @@ useHeader({
const loadAvatar = async (input?: HTMLInputElement) => {
const files = input?.files
if (!files) return
const [avatar] = await convertFileList(files)
avatarImage.value = avatar
}
Expand Down
Expand Up @@ -2,7 +2,6 @@

<script setup lang="ts">
import type { FormFieldContext } from '#shared/components/Form/types/field.ts'
import { convertFileList } from '#shared/utils/files.ts'
import type { Editor } from '@tiptap/vue-3'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import { useEventListener } from '@vueuse/core'
Expand All @@ -25,6 +24,9 @@ import type {
import FieldEditorActionBar from './FieldEditorActionBar.vue'
import FieldEditorFooter from './FieldEditorFooter.vue'
import { PLUGIN_NAME as userMentionPluginName } from './suggestions/UserMention.ts'
import { getNodeByName } from '../../utils.ts'
import type { FieldFileContext } from '../FieldFile/types.ts'
import { convertInlineImages } from './utils.ts'
interface Props {
context: FormFieldContext<FieldEditorProps>
Expand Down Expand Up @@ -61,9 +63,58 @@ getCustomExtensions(reactiveContext).forEach((extension) => {
}
})
const hasImageExtension = editorExtensions.some(
(extension) => extension.name === 'image',
)
const showActionBar = ref(false)
const editorValue = ref<string>(VITE_TEST_MODE ? props.context._value : '')
interface LoadImagesOptions {
attachNonInlineFiles: boolean
}
// there is also a gif, but desktop only inlines these two for now
const imagesMimeType = ['image/png', 'image/jpeg']
const loadFiles = (
files: FileList | null | undefined,
editor: Editor | undefined,
options: LoadImagesOptions,
) => {
if (!files) {
return false
}
const inlineImages: File[] = []
const otherFiles: File[] = []
for (const file of files) {
if (imagesMimeType.includes(file.type)) {
inlineImages.push(file)
} else {
otherFiles.push(file)
}
}
if (inlineImages.length && editor) {
convertInlineImages(inlineImages, editor.view.dom).then((urls) => {
if (editor?.isDestroyed) return
editor?.commands.setImages(urls)
})
}
if (options.attachNonInlineFiles && otherFiles.length) {
const attachmentsContext = getNodeByName(
props.context.formId,
'attachments',
)?.context as unknown as FieldFileContext | undefined
attachmentsContext?.uploadFiles(otherFiles)
}
return Boolean(
inlineImages.length || (options.attachNonInlineFiles && otherFiles.length),
)
}
const editor = useEditor({
extensions: editorExtensions,
editorProps: {
Expand All @@ -76,31 +127,30 @@ const editor = useEditor({
},
// add inlined files
handlePaste(view, event) {
if (!editorExtensions.some((n) => n.name === 'image')) {
if (!hasImageExtension) {
return
}
const files = event.clipboardData?.files || null
convertFileList(files).then((urls) => {
editor.value?.commands.setImages(urls)
const loaded = loadFiles(event.clipboardData?.files, editor.value, {
attachNonInlineFiles: false,
})
if (files && files.length) {
if (loaded) {
event.preventDefault()
return true
}
return false
},
handleDrop(view, event) {
if (!editorExtensions.some((n) => n.name === 'image')) {
if (!hasImageExtension) {
return
}
const e = event as unknown as InputEvent
const files = e.dataTransfer?.files || null
convertFileList(files).then((urls) => {
editor.value?.commands.setImages(urls)
const loaded = loadFiles(files, editor.value, {
attachNonInlineFiles: true,
})
if (files && files.length) {
if (loaded) {
event.preventDefault()
return true
}
Expand Down Expand Up @@ -274,6 +324,16 @@ onMounted(() => {
) => void)[]
onLoad.forEach((fn) => fn(editorCustomContext))
onLoad.length = 0
if (VITE_TEST_MODE) {
if (!('editors' in globalThis))
Object.defineProperty(globalThis, 'editors', { value: {} })
Object.defineProperty(
Reflect.get(globalThis, 'editors'),
props.context.node.name,
{ value: editor.value, configurable: true },
)
}
})
</script>

Expand Down
Expand Up @@ -20,7 +20,11 @@ const imageLoaded = ref(false)
const isDraggable = computed(() => props.node.attrs.isDraggable)
const src = computed(() => props.node.attrs.src)
if (needBase64Convert(src.value)) {
loadImageIntoBase64(src.value, props.node.attrs.alt).then((base64) => {
loadImageIntoBase64(
src.value,
props.node.attrs.type,
props.node.attrs.alt,
).then((base64) => {
if (base64) {
props.updateAttributes({ src: base64 })
} else {
Expand Down
Expand Up @@ -33,6 +33,11 @@ export default Image.extend({
return {}
},
},

type: {
default: null,
renderHTML: () => ({}),
},
}
},
addNodeView() {
Expand All @@ -51,6 +56,7 @@ export default Image.extend({
attrs: {
src: image.content,
alt: image.name,
type: image.type,
},
})),
{
Expand Down
@@ -1,7 +1,6 @@
// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/

import { i18n } from '#shared/i18n.ts'
import { convertFileList } from '#shared/utils/files.ts'
import type { ChainedCommands } from '@tiptap/core'
import type { Editor } from '@tiptap/vue-3'
import { computed, onUnmounted } from 'vue'
Expand All @@ -11,6 +10,7 @@ import { PLUGIN_NAME as KnowledgeBaseMentionName } from './suggestions/Knowledge
import { PLUGIN_NAME as TextModuleMentionName } from './suggestions/TextModuleSuggestion.ts'
import { PLUGIN_NAME as UserMentionName } from './suggestions/UserMention.ts'
import type { EditorContentType } from './types.ts'
import { convertInlineImages } from './utils.ts'

export interface EditorButton {
name: string
Expand Down Expand Up @@ -62,8 +62,10 @@ export default function useEditorActions(

onUnmounted(() => {
fileInput?.remove()
fileInput = null
})

// eslint-disable-next-line sonarjs/cognitive-complexity
const getActionsList = (): EditorButton[] => {
return [
{
Expand Down Expand Up @@ -102,12 +104,15 @@ export default function useEditorActions(
command: focused((c) => {
const input = getInputForImage()
input.onchange = async () => {
if (!input.files?.length) return
const files = await convertFileList(input.files)
if (!input.files?.length || !editor.value) return
const files = await convertInlineImages(
input.files,
editor.value.view.dom,
)
c.setImages(files).run()
input.value = ''
}
input.click()
if (!VITE_TEST_MODE) input.click()
}),
},
{
Expand Down
20 changes: 20 additions & 0 deletions app/frontend/shared/components/Form/fields/FieldEditor/utils.ts
@@ -1,5 +1,7 @@
// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/

import { convertFileList } from '#shared/utils/files.ts'

export const populateEditorNewLines = (htmlContent: string): string => {
const body = document.createElement('div')
body.innerHTML = htmlContent
Expand All @@ -17,3 +19,21 @@ export const populateEditorNewLines = (htmlContent: string): string => {
})
return body.innerHTML
}

export const convertInlineImages = (
inlineImages: FileList | File[],
editorElement: HTMLElement,
) => {
return convertFileList(inlineImages, {
compress: true,
onCompress: () => {
const editorWidth = editorElement.clientWidth
const maxWidth = editorWidth > 1000 ? editorWidth : 1000
return {
x: maxWidth,
scale: 2,
type: 'image/jpeg',
}
},
})
}
Expand Up @@ -48,9 +48,7 @@ const canInteract = computed(
const fileInput = ref<HTMLInputElement>()
const onFileChanged = async ($event: Event) => {
const input = $event.target as HTMLInputElement
const { files } = input
const loadFiles = async (files: FileList | File[]) => {
const uploads = await convertFileList(files)
const data = await addFileMutation.send({
Expand All @@ -68,10 +66,22 @@ const onFileChanged = async ($event: Event) => {
}))
uploadFiles.value = [...uploadFiles.value, ...previewableFile]
const input = fileInput.value
if (!input) return
input.value = ''
input.files = null
}
Object.assign(props.context, {
uploadFiles: loadFiles,
})
const onFileChanged = async ($event: Event) => {
const input = $event.target as HTMLInputElement
const { files } = input
if (files) await loadFiles(files)
}
const { waitForConfirmation } = useConfirmation()
const removeFile = async (file: FileUploaded) => {
Expand Down
4 changes: 4 additions & 0 deletions app/frontend/shared/components/Form/fields/FieldFile/types.ts
Expand Up @@ -14,3 +14,7 @@ export type FileUploaded = Pick<StoredFile, 'name' | 'size' | 'type'> & {
content?: string
preview?: string
}

export interface FieldFileContext {
uploadFiles(files: FileList | File[]): Promise<void>
}

0 comments on commit e24895a

Please sign in to comment.