| | |
| | | <template> |
| | | <div class="tinymce-container" :style="{ width: containerWidth }"> |
| | | <textarea :id="tinymceId" ref="elRef"></textarea> |
| | | <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' }"></textarea> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts"> |
| | | import type { 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, |
| | | onMounted, |
| | | nextTick, |
| | | ref, |
| | | unref, |
| | | watch, |
| | | onUnmounted, |
| | | onDeactivated, |
| | | watchEffect, |
| | | } from 'vue'; |
| | | import { basicProps } from './props'; |
| | | import toolbar from './toolbar'; |
| | | import plugins from './plugins'; |
| | | import { getTinymce } from './getTinymce'; |
| | | import { useScript } from '/@/hooks/web/useScript'; |
| | | import { snowUuid } from '/@/utils/uuid'; |
| | | |
| | | import ImgUpload from './ImgUpload.vue'; |
| | | |
| | | import { toolbar, plugins } from './tinymce'; |
| | | |
| | | import { buildShortUUID } from '/@/utils/uuid'; |
| | | import { bindHandlers } from './helper'; |
| | | import lineHeight from './lineHeight'; |
| | | 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 CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1'; |
| | | const tinymceProps = { |
| | | options: { |
| | | type: Object as PropType<Partial<RawEditorSettings>>, |
| | | default: {}, |
| | | }, |
| | | value: { |
| | | type: String, |
| | | }, |
| | | |
| | | const tinymceScriptSrc = `${CDN_URL}/tinymce.min.js`; |
| | | 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', |
| | | props: basicProps, |
| | | components: { ImgUpload }, |
| | | inheritAttrs: false, |
| | | props: tinymceProps, |
| | | emits: ['change', 'update:modelValue'], |
| | | setup(props, { emit, attrs }) { |
| | | const editorRef = ref<any>(null); |
| | | const editorRef = ref(); |
| | | const fullscreen = ref(false); |
| | | const tinymceId = ref<string>(buildShortUUID('tiny-vue')); |
| | | const elRef = ref<Nullable<HTMLElement>>(null); |
| | | |
| | | const tinymceId = computed(() => { |
| | | return snowUuid('tiny-vue'); |
| | | }); |
| | | const { prefixCls } = useDesign('tinymce-container'); |
| | | |
| | | const tinymceContent = computed(() => { |
| | | return props.modelValue; |
| | | }); |
| | | const appStore = useAppStore(); |
| | | |
| | | const tinymceContent = computed(() => props.modelValue); |
| | | |
| | | const containerWidth = computed(() => { |
| | | const width = props.width; |
| | | if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) { |
| | | if (isNumber(width)) { |
| | | return `${width}px`; |
| | | } |
| | | return width; |
| | | }); |
| | | |
| | | const initOptions = computed(() => { |
| | | const { height, options } = props; |
| | | 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)}`, |
| | | base_url: CDN_URL, |
| | | suffix: '.min', |
| | | height: height, |
| | | toolbar: toolbar, |
| | | height, |
| | | toolbar, |
| | | menubar: 'file edit insert view format table', |
| | | plugins: plugins, |
| | | // 语言包 |
| | | language_url: 'resource/tinymce/langs/zh_CN.js', |
| | | // 中文 |
| | | language: 'zh_CN', |
| | | plugins, |
| | | language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js', |
| | | language: langName.value, |
| | | branding: false, |
| | | default_link_target: '_blank', |
| | | link_title: false, |
| | | advlist_bullet_styles: 'square', |
| | | advlist_number_styles: 'default', |
| | | object_resizing: false, |
| | | fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px 36px 48px', |
| | | lineheight_formats: '1 1.5 1.75 2.0 3.0 4.0 5.0', |
| | | 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: any) => { |
| | | setup: (editor) => { |
| | | editorRef.value = editor; |
| | | editor.on('init', (e: Event) => initSetup(e)); |
| | | editor.on('init', (e) => initSetup(e)); |
| | | }, |
| | | }; |
| | | }); |
| | | |
| | | const { toPromise } = useScript({ |
| | | src: tinymceScriptSrc, |
| | | const disabled = computed(() => { |
| | | const { options } = props; |
| | | const getdDisabled = options && Reflect.get(options, 'readonly'); |
| | | return getdDisabled ?? false; |
| | | }); |
| | | |
| | | watch( |
| | | () => attrs.disabled, |
| | | () => { |
| | | const editor = unref(editorRef); |
| | | if (!editor) return; |
| | | if (!editor) { |
| | | return; |
| | | } |
| | | editor.setMode(attrs.disabled ? 'readonly' : 'design'); |
| | | } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | onMountedOrActivated(() => { |
| | | tinymceId.value = buildShortUUID('tiny-vue'); |
| | | nextTick(() => { |
| | | init(); |
| | | setTimeout(() => { |
| | | initEditor(); |
| | | }, 30); |
| | | }); |
| | | }); |
| | | |
| | |
| | | }); |
| | | |
| | | function destory() { |
| | | if (getTinymce() !== null) { |
| | | getTinymce().remove(unref(editorRef)); |
| | | if (tinymce !== null) { |
| | | tinymce?.remove?.(unref(editorRef)); |
| | | } |
| | | } |
| | | |
| | | function init() { |
| | | toPromise().then(() => { |
| | | initEditor(); |
| | | }); |
| | | } |
| | | |
| | | function initEditor() { |
| | | getTinymce().PluginManager.add('lineHeight', lineHeight(getTinymce())); |
| | | getTinymce().init(unref(initOptions)); |
| | | const el = unref(elRef); |
| | | if (el) { |
| | | el.style.visibility = ''; |
| | | } |
| | | tinymce.init(unref(initOptions)); |
| | | } |
| | | |
| | | function initSetup(e: Event) { |
| | | function initSetup(e) { |
| | | const editor = unref(editorRef); |
| | | if (!editor) return; |
| | | if (!editor) { |
| | | return; |
| | | } |
| | | const value = props.modelValue || ''; |
| | | |
| | | editor.setContent(value); |
| | |
| | | bindHandlers(e, attrs, unref(editorRef)); |
| | | } |
| | | |
| | | function setValue(editor: any, val: string, prevVal: string) { |
| | | function setValue(editor: Recordable, val: string, prevVal?: string) { |
| | | if ( |
| | | editor && |
| | | typeof val === 'string' && |
| | |
| | | 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) => { |
| | |
| | | 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; |
| | | } |
| | | const content = editor?.getContent() ?? ''; |
| | | setValue(editor, `${content}\n${getUploadingImgName(name)}`); |
| | | } |
| | | |
| | | 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, |
| | | tinymceScriptSrc, |
| | | elRef, |
| | | tinymceId, |
| | | handleImageUploading, |
| | | handleDone, |
| | | editorRef, |
| | | fullscreen, |
| | | disabled, |
| | | }; |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="less" scoped> |
| | | .tinymce-container { |
| | | <style lang="less" scoped></style> |
| | | |
| | | <style lang="less"> |
| | | @prefix-cls: ~'@{namespace}-tinymce-container'; |
| | | |
| | | .@{prefix-cls} { |
| | | position: relative; |
| | | line-height: normal; |
| | | |
| | | .mce-fullscreen { |
| | | z-index: 10000; |
| | | textarea { |
| | | z-index: -1; |
| | | visibility: hidden; |
| | | } |
| | | } |
| | | |
| | | .editor-custom-btn-container { |
| | | position: absolute; |
| | | top: 6px; |
| | | right: 6px; |
| | | |
| | | &.fullscreen { |
| | | position: fixed; |
| | | z-index: 10000; |
| | | } |
| | | } |
| | | |
| | | .editor-upload-btn { |
| | | display: inline-block; |
| | | } |
| | | |
| | | textarea { |
| | | z-index: -1; |
| | | visibility: hidden; |
| | | } |
| | | </style> |