feat(editor): image upload
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
kolaente 2023-10-20 22:43:10 +02:00
parent 953361c480
commit 05bf7ccf0b
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
3 changed files with 90 additions and 42 deletions

View File

@ -151,7 +151,7 @@
</div>
<div class="editor-toolbar__segment">
<BaseButton class="editor-toolbar__button" @click="addImage" title="Add image from URL">
<BaseButton class="editor-toolbar__button" @click="uploadInputRef?.click()" title="Add image">
<span class="icon">
<icon icon="fa-image" />
</span>
@ -369,38 +369,64 @@
</BaseButton>
</div>
</div>
<input type="file" ref="uploadInputRef" class="is-hidden" @change="addImage"/>
</div>
</template>
<script setup lang="ts">
import {ref, type PropType} from 'vue'
import {ref} from 'vue'
import {Editor} from '@tiptap/vue-3'
import BaseButton from '@/components/base/BaseButton.vue'
const props = defineProps({
editor: {
default: null,
type: Editor as PropType<Editor>,
},
})
export type UploadCallback = (files: File[] | FileList) => Promise<string[]>
const {
editor = null,
uploadCallback,
} = defineProps<{
editor: Editor,
uploadCallback?: UploadCallback,
}>()
const emit = defineEmits(['imageAdded'])
const tableMode = ref(false)
const uploadInputRef = ref<HTMLInputElement | null>(null)
function toggleTableMode() {
tableMode.value = !tableMode.value
}
function addImage() {
if (typeof uploadCallback !== 'undefined') {
const files = uploadInputRef.value?.files
if (!files || files.length === 0) {
return
}
uploadCallback(files).then(urls => {
urls.forEach(url => {
editor?.chain().focus().setImage({ src: url }).run()
})
emit('imageAdded')
})
return
}
const url = window.prompt('URL')
if (url) {
props.editor?.chain().focus().setImage({ src: url }).run()
editor?.chain().focus().setImage({ src: url }).run()
emit('imageAdded')
}
}
function setLink() {
const previousUrl = props.editor.getAttributes('link').href
const previousUrl = editor.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
@ -410,13 +436,13 @@ function setLink() {
// empty
if (url === '') {
props.editor.chain().focus().extendMarkRange('link').unsetLink().run()
editor.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
props.editor
editor
.chain()
.focus()
.extendMarkRange('link')

View File

@ -1,6 +1,11 @@
<template>
<div class="tiptap">
<EditorToolbar v-if="editor" :editor="editor" />
<EditorToolbar
v-if="editor"
:editor="editor"
:upload-callback="uploadCallback"
@image-added="bubbleChanges"
/>
<editor-content class="tiptap__editor" :editor="editor" />
</div>
</template>
@ -42,6 +47,7 @@ import {EditorContent, useEditor, VueNodeViewRenderer} from '@tiptap/vue-3'
import {lowlight} from 'lowlight'
import CodeBlock from './CodeBlock.vue'
import type {UploadCallback} from '@/components/base/EditorToolbar.vue'
// const CustomDocument = Document.extend({
// content: 'taskList',
@ -72,34 +78,38 @@ const CustomTableCell = TableCell.extend({
},
})
const props = withDefaults(defineProps<{
const {
modelValue = '',
uploadCallback,
} = defineProps<{
modelValue?: string,
}>(), {
modelValue: '',
})
uploadCallback?: UploadCallback,
}>()
const emit = defineEmits(['update:modelValue', 'change'])
const inputHTML = ref('')
watch(
() => props.modelValue,
() => modelValue,
() => {
if (!props.modelValue.startsWith(TIPTAP_TEXT_VALUE_PREFIX)) {
if (!modelValue.startsWith(TIPTAP_TEXT_VALUE_PREFIX)) {
// convert Markdown to HTML
return TIPTAP_TEXT_VALUE_PREFIX + marked.parse(props.modelValue)
return TIPTAP_TEXT_VALUE_PREFIX + marked.parse(modelValue)
}
return props.modelValue.replace(tiptapRegex, '')
return modelValue.replace(tiptapRegex, '')
},
{ immediate: true },
)
const debouncedInputHTML = refDebounced(inputHTML, 1000)
watch(debouncedInputHTML, (value) => {
emit('update:modelValue', TIPTAP_TEXT_VALUE_PREFIX + value)
emit('change', TIPTAP_TEXT_VALUE_PREFIX + value) // FIXME: remove this
})
watch(debouncedInputHTML, () => bubbleChanges())
function bubbleChanges() {
emit('update:modelValue', TIPTAP_TEXT_VALUE_PREFIX + inputHTML.value)
emit('change', TIPTAP_TEXT_VALUE_PREFIX + inputHTML.value) // FIXME: remove this
}
const editor = useEditor({
content: inputHTML.value,
@ -122,7 +132,7 @@ const editor = useEditor({
// // start
// Document,
// // Text,
// Image,
Image,
// // Tasks
// CustomDocument,

View File

@ -18,8 +18,7 @@
</h3>
<editor
:is-edit-enabled="canWrite"
:upload-callback="attachmentUpload"
:upload-enabled="true"
:upload-callback="uploadCallback"
:placeholder="$t('task.description.placeholder')"
:empty-text="$t('task.description.empty')"
:show-save="true"
@ -41,19 +40,17 @@ import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import TaskModel from '@/models/task'
const props = defineProps({
modelValue: {
type: Object as PropType<ITask>,
required: true,
},
attachmentUpload: {
required: true,
},
canWrite: {
type: Boolean,
required: true,
},
})
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
const {
modelValue,
attachmentUpload,
canWrite,
} = defineProps<{
modelValue: ITask,
attachmentUpload: AttachmentUploadFunction,
canWrite: boolean,
}>()
const emit = defineEmits(['update:modelValue'])
@ -67,7 +64,7 @@ const taskStore = useTaskStore()
const loading = computed(() => taskStore.isLoading)
watch(
props.modelValue,
() => modelValue,
(value) => {
task.value = value
},
@ -106,5 +103,20 @@ async function save() {
saving.value = false
}
}
async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
const uploadPromises: Promise<string>[] = []
files.forEach((file: File) => {
const promise = new Promise<string>((resolve) => {
attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
})
uploadPromises.push(promise)
})
return await Promise.all(uploadPromises)
}
</script>