feat(editor): image upload
This commit is contained in:
parent
953361c480
commit
05bf7ccf0b
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user