feat(editor): image upload
This commit is contained in:
parent
953361c480
commit
05bf7ccf0b
@ -151,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="editor-toolbar__segment">
|
<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">
|
<span class="icon">
|
||||||
<icon icon="fa-image" />
|
<icon icon="fa-image" />
|
||||||
</span>
|
</span>
|
||||||
@ -369,38 +369,64 @@
|
|||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<input type="file" ref="uploadInputRef" class="is-hidden" @change="addImage"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, type PropType} from 'vue'
|
import {ref} from 'vue'
|
||||||
import {Editor} from '@tiptap/vue-3'
|
import {Editor} from '@tiptap/vue-3'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
export type UploadCallback = (files: File[] | FileList) => Promise<string[]>
|
||||||
editor: {
|
|
||||||
default: null,
|
const {
|
||||||
type: Editor as PropType<Editor>,
|
editor = null,
|
||||||
},
|
uploadCallback,
|
||||||
})
|
} = defineProps<{
|
||||||
|
editor: Editor,
|
||||||
|
uploadCallback?: UploadCallback,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['imageAdded'])
|
||||||
|
|
||||||
const tableMode = ref(false)
|
const tableMode = ref(false)
|
||||||
|
const uploadInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
function toggleTableMode() {
|
function toggleTableMode() {
|
||||||
tableMode.value = !tableMode.value
|
tableMode.value = !tableMode.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function addImage() {
|
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')
|
const url = window.prompt('URL')
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
props.editor?.chain().focus().setImage({ src: url }).run()
|
editor?.chain().focus().setImage({ src: url }).run()
|
||||||
|
emit('imageAdded')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLink() {
|
function setLink() {
|
||||||
const previousUrl = props.editor.getAttributes('link').href
|
const previousUrl = editor.getAttributes('link').href
|
||||||
const url = window.prompt('URL', previousUrl)
|
const url = window.prompt('URL', previousUrl)
|
||||||
|
|
||||||
// cancelled
|
// cancelled
|
||||||
@ -410,13 +436,13 @@ function setLink() {
|
|||||||
|
|
||||||
// empty
|
// empty
|
||||||
if (url === '') {
|
if (url === '') {
|
||||||
props.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// update link
|
// update link
|
||||||
props.editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.extendMarkRange('link')
|
.extendMarkRange('link')
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tiptap">
|
<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" />
|
<editor-content class="tiptap__editor" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -42,6 +47,7 @@ import {EditorContent, useEditor, VueNodeViewRenderer} from '@tiptap/vue-3'
|
|||||||
import {lowlight} from 'lowlight'
|
import {lowlight} from 'lowlight'
|
||||||
|
|
||||||
import CodeBlock from './CodeBlock.vue'
|
import CodeBlock from './CodeBlock.vue'
|
||||||
|
import type {UploadCallback} from '@/components/base/EditorToolbar.vue'
|
||||||
|
|
||||||
// const CustomDocument = Document.extend({
|
// const CustomDocument = Document.extend({
|
||||||
// content: 'taskList',
|
// content: 'taskList',
|
||||||
@ -72,34 +78,38 @@ const CustomTableCell = TableCell.extend({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const {
|
||||||
|
modelValue = '',
|
||||||
|
uploadCallback,
|
||||||
|
} = defineProps<{
|
||||||
modelValue?: string,
|
modelValue?: string,
|
||||||
}>(), {
|
uploadCallback?: UploadCallback,
|
||||||
modelValue: '',
|
}>()
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
const inputHTML = ref('')
|
const inputHTML = ref('')
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => modelValue,
|
||||||
() => {
|
() => {
|
||||||
if (!props.modelValue.startsWith(TIPTAP_TEXT_VALUE_PREFIX)) {
|
if (!modelValue.startsWith(TIPTAP_TEXT_VALUE_PREFIX)) {
|
||||||
// convert Markdown to HTML
|
// 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 },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const debouncedInputHTML = refDebounced(inputHTML, 1000)
|
const debouncedInputHTML = refDebounced(inputHTML, 1000)
|
||||||
|
|
||||||
watch(debouncedInputHTML, (value) => {
|
watch(debouncedInputHTML, () => bubbleChanges())
|
||||||
emit('update:modelValue', TIPTAP_TEXT_VALUE_PREFIX + value)
|
|
||||||
emit('change', TIPTAP_TEXT_VALUE_PREFIX + value) // FIXME: remove this
|
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({
|
const editor = useEditor({
|
||||||
content: inputHTML.value,
|
content: inputHTML.value,
|
||||||
@ -122,7 +132,7 @@ const editor = useEditor({
|
|||||||
// // start
|
// // start
|
||||||
// Document,
|
// Document,
|
||||||
// // Text,
|
// // Text,
|
||||||
// Image,
|
Image,
|
||||||
|
|
||||||
// // Tasks
|
// // Tasks
|
||||||
// CustomDocument,
|
// CustomDocument,
|
||||||
|
@ -18,8 +18,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<editor
|
<editor
|
||||||
:is-edit-enabled="canWrite"
|
:is-edit-enabled="canWrite"
|
||||||
:upload-callback="attachmentUpload"
|
:upload-callback="uploadCallback"
|
||||||
:upload-enabled="true"
|
|
||||||
:placeholder="$t('task.description.placeholder')"
|
:placeholder="$t('task.description.placeholder')"
|
||||||
:empty-text="$t('task.description.empty')"
|
:empty-text="$t('task.description.empty')"
|
||||||
:show-save="true"
|
:show-save="true"
|
||||||
@ -41,19 +40,17 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||||||
import {useTaskStore} from '@/stores/tasks'
|
import {useTaskStore} from '@/stores/tasks'
|
||||||
import TaskModel from '@/models/task'
|
import TaskModel from '@/models/task'
|
||||||
|
|
||||||
const props = defineProps({
|
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
|
||||||
modelValue: {
|
|
||||||
type: Object as PropType<ITask>,
|
const {
|
||||||
required: true,
|
modelValue,
|
||||||
},
|
attachmentUpload,
|
||||||
attachmentUpload: {
|
canWrite,
|
||||||
required: true,
|
} = defineProps<{
|
||||||
},
|
modelValue: ITask,
|
||||||
canWrite: {
|
attachmentUpload: AttachmentUploadFunction,
|
||||||
type: Boolean,
|
canWrite: boolean,
|
||||||
required: true,
|
}>()
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
@ -67,7 +64,7 @@ const taskStore = useTaskStore()
|
|||||||
const loading = computed(() => taskStore.isLoading)
|
const loading = computed(() => taskStore.isLoading)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
props.modelValue,
|
() => modelValue,
|
||||||
(value) => {
|
(value) => {
|
||||||
task.value = value
|
task.value = value
|
||||||
},
|
},
|
||||||
@ -106,5 +103,20 @@ async function save() {
|
|||||||
saving.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user