123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- <script lang="ts">
- const editorEvents = ['load', 'change', 'caretChange', 'focus', 'blur', 'keydown', 'keyup', 'beforePreviewRender', 'beforeConvertWysiwygToMarkdown'];
- export default { name: 'TuiEditor' };
- </script>
- <script setup lang="ts">
- import { onMounted, ref, toRefs, watch, PropType, onUnmounted, nextTick } from 'vue';
- import { useFormItem } from 'element-plus';
- import { vOnClickOutside } from '@vueuse/components';
- import { decodeHTML } from 'entities';
- import Editor, { EditorType, PreviewStyle, EditorOptions } from '@toast-ui/editor';
- import chart from '@toast-ui/editor-plugin-chart';
- import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';
- import tableMergedCell from '@toast-ui/editor-plugin-table-merged-cell';
- import uml from '@toast-ui/editor-plugin-uml';
- import Prism from 'prismjs';
- import { addImageBlobHook, toggleFullScreen, clickOutside } from './utils';
- import '@toast-ui/editor/dist/i18n/zh-cn';
- import '@toast-ui/editor/dist/i18n/zh-tw';
- import '@toast-ui/editor/dist/toastui-editor.css';
- import '@toast-ui/chart/dist/toastui-chart.css';
- import 'prismjs/themes/prism.css';
- import 'prismjs/components/prism-clojure.js';
- import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';
- import '@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css';
- const props = defineProps({
- modelValue: { type: String, default: '' },
- html: { type: String, default: '' },
- initialEditType: { type: String as PropType<EditorType>, default: 'markdown' },
- height: { type: String, default: '300px' },
- previewStyle: { type: String as PropType<PreviewStyle>, default: 'tab' },
- language: { type: String, default: 'en' },
- options: { type: Object, default: null },
- });
- const emit = defineEmits([...editorEvents, 'update:modelValue', 'update:html', 'different']);
- const { modelValue, html, initialEditType, height, previewStyle, language, options } = toRefs(props);
- const toastuiEditor = ref();
- let editor: Editor;
- const { formItem } = useFormItem();
- watch(previewStyle, () => {
- editor.changePreviewStyle(previewStyle.value);
- });
- watch(height, () => {
- editor.setHeight(height.value);
- });
- const eventOptions: any = {};
- // 内容为空时,默认生成以下HTML,应作为空串处理
- const emptyHtml = '<p><br class="ProseMirror-trailingBreak"></p>';
- editorEvents.forEach((event) => {
- eventOptions[event] = (...args: any[]) => {
- if (event === 'change') {
- const newHtml = editor.getHTML();
- if (newHtml !== html.value) {
- emit('update:html', newHtml !== emptyHtml ? newHtml : '');
- }
- const newMarkdown = editor.getMarkdown();
- if (newMarkdown !== modelValue.value) {
- emit('update:modelValue', newMarkdown);
- }
- formItem?.validate?.('change').catch((err: any) => {
- if (import.meta.env.MODE !== 'production') {
- console.warn(err);
- }
- });
- }
- emit(event, ...args);
- };
- });
- const createFullscreenButton = () => {
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'toastui-editor-toolbar-icons text-xl';
- button.style.backgroundImage = 'none';
- button.style.margin = '0';
- button.innerHTML = 'F';
- button.addEventListener('click', () => {
- toggleFullScreen(editor, toastuiEditor.value, height.value);
- });
- return button;
- };
- onMounted(() => {
- const chartOptions = {
- maxWidth: 800,
- maxHeight: 400,
- };
- const computedOptions: EditorOptions = {
- ...options?.value,
- initialValue: modelValue.value ?? '',
- initialEditType: initialEditType.value,
- height: height.value,
- previewStyle: previewStyle.value,
- language: language.value,
- autofocus: false,
- usageStatistics: false,
- el: toastuiEditor.value,
- events: eventOptions,
- hooks: { addImageBlobHook },
- plugins: [[chart, chartOptions], [codeSyntaxHighlight, { highlighter: Prism }], tableMergedCell, uml],
- toolbarItems: [
- [
- {
- name: 'fullscreen',
- el: createFullscreenButton(),
- tooltip: 'Fullscreen',
- },
- ],
- ['heading', 'bold', 'italic', 'strike'],
- ['hr', 'quote'],
- ['ul', 'ol', 'task', 'indent', 'outdent'],
- ['table', 'image', 'link'],
- ['code', 'codeblock'],
- ['scrollSync'],
- ],
- };
- editor = new Editor(computedOptions);
- // markdown无值,html有值,则用设置html
- if (!modelValue.value && html.value) {
- editor.setHTML(html.value);
- // 防止在切换编辑器时,因清空markdown值导致事件无效
- nextTick().then(() => {
- emit('update:modelValue', editor.getMarkdown());
- });
- return;
- }
- // 检查markdown生成的HTML和原HTML是否匹配
- const currHtml = editor.getHTML();
- if (modelValue.value && decodeHTML(html.value) !== currHtml) {
- // 触发不匹配事件
- emit('different', html.value, currHtml);
- emit('update:html', currHtml);
- }
- });
- onUnmounted(() => {
- editorEvents.forEach((event) => {
- editor.off(event);
- });
- editor.destroy();
- });
- const getHTML = () => editor.getHTML();
- const setHTML = (html: string): void => editor.setHTML(html);
- const getMarkdown = () => editor.getMarkdown();
- const setMarkdown = (markdown: string): void => editor.setMarkdown(markdown);
- const getRootElement = () => toastuiEditor.value;
- defineExpose({ getRootElement, getHTML, getMarkdown, setHTML, setMarkdown });
- </script>
- <template>
- <!-- 在ElementPlus的对话框中,“更多”工具条按钮点击后,点击其它地方不会关闭工具条 -->
- <div ref="toastuiEditor" v-on-click-outside="clickOutside"></div>
- </template>
- <style lang="scss" scoped>
- :deep(.ProseMirror),
- :deep(.toastui-editor-contents) {
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Hiragino Sans GB',
- 'Microsoft YaHei', 'WenQuanYi Micro Hei', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
- }
- </style>
|