123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- <template>
- <div :class="prefixCls" :style="{ width: containerWidth }">
- <ImgUpload
- :fullscreen="fullscreen"
- @uploading="handleImageUploading"
- @done="handleDone"
- v-if="showImageUpload"
- v-show="editorRef"
- :disabled="disabled"
- />
- <textarea
- :id="tinymceId"
- ref="elRef"
- :style="{ visibility: 'hidden' }"
- v-if="!initOptions.inline"
- ></textarea>
- <slot v-else></slot>
- </div>
- </template>
- <script lang="ts">
- import type { Editor, RawEditorSettings } from 'tinymce';
- import tinymce from 'tinymce/tinymce';
- import 'tinymce/themes/silver';
- import 'tinymce/icons/default/icons';
- import 'tinymce/plugins/advlist';
- import 'tinymce/plugins/anchor';
- import 'tinymce/plugins/autolink';
- import 'tinymce/plugins/autosave';
- import 'tinymce/plugins/code';
- import 'tinymce/plugins/codesample';
- import 'tinymce/plugins/directionality';
- import 'tinymce/plugins/fullscreen';
- import 'tinymce/plugins/hr';
- import 'tinymce/plugins/insertdatetime';
- import 'tinymce/plugins/link';
- import 'tinymce/plugins/lists';
- import 'tinymce/plugins/media';
- import 'tinymce/plugins/nonbreaking';
- import 'tinymce/plugins/noneditable';
- import 'tinymce/plugins/pagebreak';
- import 'tinymce/plugins/paste';
- import 'tinymce/plugins/preview';
- import 'tinymce/plugins/print';
- import 'tinymce/plugins/save';
- import 'tinymce/plugins/searchreplace';
- import 'tinymce/plugins/spellchecker';
- import 'tinymce/plugins/tabfocus';
- // import 'tinymce/plugins/table';
- import 'tinymce/plugins/template';
- import 'tinymce/plugins/textpattern';
- import 'tinymce/plugins/visualblocks';
- import 'tinymce/plugins/visualchars';
- import 'tinymce/plugins/wordcount';
- import {
- defineComponent,
- computed,
- nextTick,
- ref,
- unref,
- watch,
- onDeactivated,
- onBeforeUnmount,
- PropType,
- } from 'vue';
- import ImgUpload from './ImgUpload.vue';
- import { toolbar, plugins } 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';
- const tinymceProps = {
- options: {
- type: Object as PropType<Partial<RawEditorSettings>>,
- default: () => ({}),
- },
- value: {
- type: String,
- },
- toolbar: {
- type: Array as PropType<string[]>,
- default: toolbar,
- },
- plugins: {
- type: Array as PropType<string[]>,
- default: plugins,
- },
- 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 },
- inheritAttrs: false,
- props: tinymceProps,
- emits: ['change', 'update:modelValue', 'inited', 'init-error'],
- setup(props, { emit, attrs }) {
- const editorRef = ref<Editor | null>(null);
- const fullscreen = ref(false);
- const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
- const elRef = ref<HTMLElement | null>(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' ? 'oxide' : 'oxide-dark';
- });
- const langName = computed(() => {
- const lang = useLocale().getLocale.value;
- return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
- });
- const initOptions = computed((): RawEditorSettings => {
- const { height, options, toolbar, plugins } = props;
- const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
- return {
- selector: `#${unref(tinymceId)}`,
- height,
- toolbar,
- menubar: 'file edit insert view format table',
- plugins,
- language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
- language: langName.value,
- branding: false,
- default_link_target: '_blank',
- link_title: false,
- object_resizing: false,
- auto_focus: true,
- skin: skinName.value,
- skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
- content_css:
- publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
- ...options,
- setup: (editor: Editor) => {
- 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);
- if (editor) {
- editor.setMode(getdDisabled ? 'readonly' : 'design');
- }
- return getdDisabled ?? false;
- });
- watch(
- () => attrs.disabled,
- () => {
- const editor = unref(editorRef);
- if (!editor) {
- return;
- }
- 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.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: Record<string, any>, 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 {
- visibility: hidden;
- z-index: -1;
- }
- }
- </style>
|