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

<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>