110 lines
2.8 KiB
Vue
110 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
|
|
type ToastUiEditor = {
|
|
destroy: () => void;
|
|
getMarkdown: () => string;
|
|
getHTML: () => string;
|
|
setMarkdown: (markdown: string, cursorToEnd?: boolean) => void;
|
|
setHTML: (html: string, cursorToEnd?: boolean) => void;
|
|
};
|
|
|
|
const props = defineProps<{
|
|
modelValue: string;
|
|
placeholder?: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(event: "update:modelValue", value: string): void;
|
|
}>();
|
|
|
|
const mountEl = ref<HTMLDivElement | null>(null);
|
|
const editor = ref<ToastUiEditor | null>(null);
|
|
const isSyncing = ref(false);
|
|
|
|
function looksLikeHtml(value: string) {
|
|
return /<([a-z][\w-]*)\b[^>]*>/i.test(value);
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!mountEl.value) return;
|
|
const [{ default: Editor }] = await Promise.all([
|
|
import("@toast-ui/editor"),
|
|
import("@toast-ui/editor/dist/toastui-editor.css"),
|
|
]);
|
|
|
|
const initialValue = String(props.modelValue ?? "");
|
|
const instance = new Editor({
|
|
el: mountEl.value,
|
|
initialEditType: "markdown",
|
|
previewStyle: "vertical",
|
|
initialValue: looksLikeHtml(initialValue) ? "" : initialValue,
|
|
placeholder: props.placeholder ?? "Write with Markdown...",
|
|
height: "520px",
|
|
usageStatistics: false,
|
|
events: {
|
|
change: () => {
|
|
if (isSyncing.value) return;
|
|
emit("update:modelValue", instance.getMarkdown());
|
|
},
|
|
},
|
|
});
|
|
|
|
if (looksLikeHtml(initialValue)) {
|
|
isSyncing.value = true;
|
|
instance.setHTML(initialValue, false);
|
|
emit("update:modelValue", instance.getMarkdown());
|
|
isSyncing.value = false;
|
|
}
|
|
|
|
editor.value = instance as unknown as ToastUiEditor;
|
|
});
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(incoming) => {
|
|
const instance = editor.value;
|
|
if (!instance || isSyncing.value) return;
|
|
const next = String(incoming ?? "");
|
|
const currentMarkdown = instance.getMarkdown();
|
|
const currentHtml = instance.getHTML();
|
|
if (next === currentMarkdown || next === currentHtml) return;
|
|
|
|
isSyncing.value = true;
|
|
if (looksLikeHtml(next)) {
|
|
instance.setHTML(next, false);
|
|
emit("update:modelValue", instance.getMarkdown());
|
|
} else {
|
|
instance.setMarkdown(next, false);
|
|
}
|
|
isSyncing.value = false;
|
|
},
|
|
);
|
|
|
|
onBeforeUnmount(() => {
|
|
editor.value?.destroy();
|
|
editor.value = null;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markdown-rich-editor min-h-[420px]">
|
|
<div ref="mountEl" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.markdown-rich-editor :deep(.toastui-editor-defaultUI) {
|
|
border-radius: 0.75rem;
|
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 14%, transparent);
|
|
}
|
|
|
|
.markdown-rich-editor :deep(.toastui-editor-mode-switch) {
|
|
border-top: 1px solid color-mix(in oklab, var(--color-base-content) 12%, transparent);
|
|
}
|
|
|
|
.markdown-rich-editor :deep(.toastui-editor-main-container) {
|
|
min-height: 360px;
|
|
}
|
|
</style>
|