Files
clientsflow/frontend/app/components/workspace/documents/MarkdownRichEditor.client.vue

107 lines
2.7 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: "wysiwyg",
previewStyle: "tab",
initialValue: looksLikeHtml(initialValue) ? "" : initialValue,
placeholder: props.placeholder ?? "Write with Markdown...",
height: "520px",
hideModeSwitch: true,
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-main-container) {
min-height: 360px;
}
</style>