Completion-template-Vue3.0/src/components/Tinymce/src/Editor.vue

342 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div :class="prefixCls" :style="{ width: containerWidth }">
<ImgUpload
:fullscreen="fullscreen"
@uploading="handleImageUploading"
@done="handleDone"
v-if="showImageUpload"
v-show="editorRef"
:disabled="disabled"
/>
<Editor :id="tinymceId" ref="elRef" :disabled="disabled" :init="initOptions" :style="{ visibility: 'hidden' }" v-if="!initOptions.inline"></Editor>
<slot v-else></slot>
</div>
</template>
<script lang="ts">
import tinymce from 'tinymce/tinymce';
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver';
import 'tinymce/icons/default/icons';
import 'tinymce/models/dom';
// tinymce插件可按自己的需要进行导入
// 更多插件参考https://www.tiny.cloud/docs/plugins/
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/image';
import { defineComponent, computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount } from 'vue';
import ImgUpload from './ImgUpload.vue';
import {simpleToolbar, menubar, simplePlugins} from './tinymce';
import { buildShortUUID } from '/@/utils/uuid';
import { bindHandlers } from './helper';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useDesign } from '/@/hooks/web/useDesign';
import { isNumber } from '/@/utils/is';
import { useLocale } from '/@/locales/useLocale';
import { useAppStore } from '/@/store/modules/app';
import { uploadFile } from '/@/api/common/api';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
const tinymceProps = {
options: {
type: Object as PropType<Partial<RawEditorSettings>>,
default: {},
},
value: {
type: String,
},
toolbar: {
type: [Array as PropType<string[]>, String],
default: simpleToolbar,
},
plugins: {
type: [Array as PropType<string[]>, String],
default: simplePlugins,
},
menubar: {
type: [Object, String],
default: menubar,
},
modelValue: {
type: String,
},
height: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 400,
},
width: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 'auto',
},
showImageUpload: {
type: Boolean,
default: true,
},
};
export default defineComponent({
name: 'Tinymce',
components: { ImgUpload,Editor },
inheritAttrs: false,
props: tinymceProps,
emits: ['change', 'update:modelValue', 'inited', 'init-error'],
setup(props, { emit, attrs }) {
console.log("---Tinymce---初始化---")
const editorRef = ref<Nullable<any>>(null);
const fullscreen = ref(false);
const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
const elRef = ref<Nullable<HTMLElement>>(null);
const { prefixCls } = useDesign('tinymce-container');
const appStore = useAppStore();
const tinymceContent = computed(() => props.modelValue);
const containerWidth = computed(() => {
const width = props.width;
if (isNumber(width)) {
return `${width}px`;
}
return width;
});
const skinName = computed(() => {
return appStore.getDarkMode === 'light' ? 'jeecg' : 'oxide-dark';
});
const langName = computed(() => {
const lang = useLocale().getLocale.value;
return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
});
const initOptions = computed(() => {
const { height, options, toolbar, plugins, menubar } = props;
const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
return {
selector: `#${unref(tinymceId)}`,
height,
toolbar,
menubar: false,
plugins,
language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
language: langName.value,
branding: false,
default_link_target: '_blank',
link_title: false,
object_resizing: true,
toolbar_mode: 'sliding',
auto_focus: true,
// toolbar_groups: true,
skin: skinName.value,
skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
images_upload_handler: (blobInfo, process) =>
new Promise((resolve, reject) => {
let params = {
file: blobInfo.blob(),
filename: blobInfo.filename(),
data: { biz: 'jeditor', jeditor: '1' },
};
const uploadSuccess = (res) => {
if (res.success) {
if (res.message == 'local') {
const img = 'data:image/jpeg;base64,' + blobInfo.base64();
resolve(img);
} else {
let img = getFileAccessHttpUrl(res.message);
resolve(img);
}
} else {
reject('上传失败!');
}
};
uploadFile(params, uploadSuccess);
}),
content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
...options,
setup: (editor: any) => {
editorRef.value = editor;
editor.on('init', (e) => initSetup(e));
},
};
});
const disabled = computed(() => {
const { options } = props;
const getdDisabled = options && Reflect.get(options, 'readonly');
const editor = unref(editorRef);
// update-begin-author:taoyan date:20220407 for: 设置disabled图片上传没有被禁用
if (editor && editor?.setMode) {
editor.setMode(getdDisabled || attrs.disabled === true ? 'readonly' : 'design');
}
if (attrs.disabled === true) {
return true;
}
// update-end-author:taoyan date:20220407 for: 设置disabled图片上传没有被禁用
return getdDisabled ?? false;
});
watch(
() => attrs.disabled,
() => {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor?.setMode && editor.setMode(attrs.disabled ? 'readonly' : 'design');
}
);
onMountedOrActivated(() => {
if (!initOptions.value.inline) {
tinymceId.value = buildShortUUID('tiny-vue');
}
nextTick(() => {
setTimeout(() => {
initEditor();
}, 30);
});
});
onBeforeUnmount(() => {
destory();
});
onDeactivated(() => {
destory();
});
function destory() {
if (tinymce !== null) {
tinymce?.remove?.(unref(initOptions).selector!);
}
}
function initEditor() {
const el = unref(elRef);
if (el && el?.style && el?.style?.visibility) {
el.style.visibility = '';
}
tinymce
.init(unref(initOptions))
.then((editor) => {
emit('inited', editor);
})
.catch((err) => {
emit('init-error', err);
});
}
function initSetup(e) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const value = props.modelValue || '';
editor.setContent(value);
bindModelHandlers(editor);
bindHandlers(e, attrs, unref(editorRef));
}
function setValue(editor: Recordable, val: string, prevVal?: string) {
if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: attrs.outputFormat })) {
editor.setContent(val);
}
}
function bindModelHandlers(editor: any) {
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
watch(
() => props.modelValue,
(val: string, prevVal: string) => {
setValue(editor, val, prevVal);
}
);
watch(
() => props.value,
(val: string, prevVal: string) => {
setValue(editor, val, prevVal);
},
{
immediate: true,
}
);
editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
const content = editor.getContent({ format: attrs.outputFormat });
emit('update:modelValue', content);
emit('change', content);
});
editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state;
});
}
function handleImageUploading(name: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
const content = editor?.getContent() ?? '';
setValue(editor, content);
}
function handleDone(name: string, url: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const content = editor?.getContent() ?? '';
const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
setValue(editor, val);
}
function getUploadingImgName(name: string) {
return `[uploading:${name}]`;
}
return {
prefixCls,
containerWidth,
initOptions,
tinymceContent,
elRef,
tinymceId,
handleImageUploading,
handleDone,
editorRef,
fullscreen,
disabled,
};
},
});
</script>
<style lang="less" scoped></style>
<style lang="less">
@prefix-cls: ~'@{namespace}-tinymce-container';
.@{prefix-cls} {
position: relative;
line-height: normal;
textarea {
z-index: -1;
visibility: hidden;
}
}
</style>