You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
3.9 KiB
160 lines
3.9 KiB
<script setup lang="ts">
|
|
import 'vditor/dist/index.css'
|
|
import { createPostBodyMarkdownEditorBridge, type CreateVditorLikeOptions } from './post-body-markdown-editor-vditor-bridge'
|
|
import { initializePostBodyMarkdownEditorVditor } from './post-body-markdown-editor-vditor-init'
|
|
import { buildQuickNoteEditorVditorOptions } from './quick-note-editor-vditor-config'
|
|
|
|
const props = defineProps<{
|
|
modelValue: string
|
|
resizeSignal?: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [string]
|
|
'content-change': [string]
|
|
}>()
|
|
|
|
const toast = useToast()
|
|
const mountEl = ref<HTMLElement | null>(null)
|
|
const initErrorMessage = ref('')
|
|
|
|
const vditorCtor = shallowRef<null | (new (element: HTMLElement, options: Record<string, unknown>) => {
|
|
getValue: () => string
|
|
setValue: (value: string, render?: boolean) => void
|
|
destroy: () => void
|
|
})>(null)
|
|
let unmounted = false
|
|
|
|
const bridge = createPostBodyMarkdownEditorBridge({
|
|
getModelValue: () => props.modelValue,
|
|
emitUpdate: (value) => emit('update:modelValue', value),
|
|
createEditor: ({ element, value, onInput }: CreateVditorLikeOptions) => {
|
|
const Vditor = vditorCtor.value
|
|
if (!Vditor) {
|
|
throw new Error('Vditor constructor is not ready')
|
|
}
|
|
return new Vditor(element, buildQuickNoteEditorVditorOptions({
|
|
value,
|
|
onInput: (nextValue) => {
|
|
emit('content-change', nextValue)
|
|
onInput(nextValue)
|
|
},
|
|
onUploadError: () => {
|
|
toast.add({ title: '图片上传失败', color: 'warning' })
|
|
},
|
|
}))
|
|
},
|
|
})
|
|
|
|
function flushValue() {
|
|
const current = bridge.getEditor()?.getValue()
|
|
if (typeof current !== 'string') {
|
|
return
|
|
}
|
|
emit('content-change', current)
|
|
if (current === props.modelValue) {
|
|
return
|
|
}
|
|
emit('update:modelValue', current)
|
|
}
|
|
|
|
function triggerImmediateSync() {
|
|
if (!import.meta.client) {
|
|
return
|
|
}
|
|
requestAnimationFrame(() => {
|
|
flushValue()
|
|
})
|
|
}
|
|
|
|
defineExpose<{
|
|
flushValue: () => void
|
|
}>({
|
|
flushValue,
|
|
})
|
|
|
|
onMounted(async () => {
|
|
if (!import.meta.client) {
|
|
return
|
|
}
|
|
await initializePostBodyMarkdownEditorVditor({
|
|
importVditor: () => import('vditor'),
|
|
isUnmounted: () => unmounted,
|
|
onReady: (ctor) => {
|
|
vditorCtor.value = ctor
|
|
if (mountEl.value) {
|
|
bridge.mount(mountEl.value)
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
initErrorMessage.value = '编辑器加载失败,请刷新重试'
|
|
toast.add({
|
|
title: initErrorMessage.value,
|
|
color: 'error',
|
|
})
|
|
console.error('Failed to initialize quick note editor', error)
|
|
},
|
|
})
|
|
})
|
|
|
|
watch(() => props.modelValue, () => {
|
|
bridge.syncFromProps()
|
|
})
|
|
|
|
watch(() => props.resizeSignal, async () => {
|
|
if (!import.meta.client) {
|
|
return
|
|
}
|
|
await nextTick()
|
|
const editor = bridge.getEditor() as { resize?: () => void } | null
|
|
editor?.resize?.()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
unmounted = true
|
|
bridge.unmount()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<ClientOnly>
|
|
<div v-if="initErrorMessage" class="h-full w-full min-w-0 rounded-lg border border-error/50 bg-error/10 px-4 py-8 text-center">
|
|
<p class="text-sm text-error">
|
|
{{ initErrorMessage }}
|
|
</p>
|
|
</div>
|
|
<div
|
|
v-else
|
|
@keyup.capture="triggerImmediateSync"
|
|
@compositionend.capture="triggerImmediateSync"
|
|
@paste.capture="triggerImmediateSync"
|
|
@cut.capture="triggerImmediateSync"
|
|
ref="mountEl"
|
|
class="h-full w-full min-w-0 rounded-lg overflow-hidden ring ring-default"
|
|
/>
|
|
<template #fallback>
|
|
<div class="text-muted text-sm py-10 text-center border border-default rounded-lg">
|
|
编辑器加载中…
|
|
</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.vditor) {
|
|
height: 100% !important;
|
|
display: flex;
|
|
min-height: 0;
|
|
flex-direction: column;
|
|
}
|
|
|
|
:deep(.vditor-content) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
:deep(.vditor-ir) {
|
|
height: 100% !important;
|
|
overflow: auto;
|
|
}
|
|
</style>
|
|
|