Vben
2021-02-25 8a9ca498d70a0a4f66c073fe869fc6d8a3e79a55
提交 | 用户 | age
c0e4c9 1 <template>
3ad1a4 2   <div :class="prefixCls" :style="{ width: containerWidth }">
V 3     <ImgUpload
4       @uploading="handleImageUploading"
5       @done="handleDone"
6       v-if="showImageUpload"
7       v-show="editorRef"
8     />
1093ec 9     <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea>
c0e4c9 10   </div>
J 11 </template>
12
13 <script lang="ts">
f75425 14   import {
V 15     defineComponent,
16     computed,
17     nextTick,
18     ref,
19     unref,
20     watch,
21     onUnmounted,
22     onDeactivated,
23   } from 'vue';
c0e4c9 24   import { basicProps } from './props';
J 25   import toolbar from './toolbar';
26   import plugins from './plugins';
f75425 27   import { getTinymce } from './getTinymce';
V 28   import { useScript } from '/@/hooks/web/useScript';
8a9ca4 29   import { shortUuid } from '/@/utils/uuid';
f75425 30   import { bindHandlers } from './helper';
7658f4 31   import lineHeight from './lineHeight';
73c8e0 32   import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
3ad1a4 33   import ImgUpload from './ImgUpload.vue';
V 34   import { useDesign } from '/@/hooks/web/useDesign';
c0e4c9 35
J 36   const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';
f75425 37
c0e4c9 38   const tinymceScriptSrc = `${CDN_URL}/tinymce.min.js`;
J 39
40   export default defineComponent({
41     name: 'Tinymce',
9edc28 42     components: { ImgUpload },
144ab5 43     inheritAttrs: false,
c0e4c9 44     props: basicProps,
f75425 45     emits: ['change', 'update:modelValue'],
V 46     setup(props, { emit, attrs }) {
47       const editorRef = ref<any>(null);
8a9ca4 48       const tinymceId = ref<string>(shortUuid('tiny-vue'));
f75425 49       const elRef = ref<Nullable<HTMLElement>>(null);
3ad1a4 50
V 51       const { prefixCls } = useDesign('tinymce-container');
f75425 52
V 53       const tinymceContent = computed(() => {
54         return props.modelValue;
55       });
56
c0e4c9 57       const containerWidth = computed(() => {
J 58         const width = props.width;
59         if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) {
60           return `${width}px`;
61         }
62         return width;
63       });
f75425 64
c0e4c9 65       const initOptions = computed(() => {
e034d1 66         const { height, options } = props;
c0e4c9 67         return {
f75425 68           selector: `#${unref(tinymceId)}`,
58f988 69           base_url: CDN_URL,
V 70           suffix: '.min',
c0e4c9 71           height: height,
J 72           toolbar: toolbar,
e034d1 73           menubar: 'file edit insert view format table',
c0e4c9 74           plugins: plugins,
J 75           // 语言包
76           language_url: 'resource/tinymce/langs/zh_CN.js',
77           // 中文
78           language: 'zh_CN',
f75425 79           default_link_target: '_blank',
V 80           link_title: false,
81           advlist_bullet_styles: 'square',
82           advlist_number_styles: 'default',
83           object_resizing: false,
adffef 84           fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px 36px 48px',
8a9822 85           lineheight_formats: '1 1.5 1.75 2.0 3.0 4.0 5.0',
e034d1 86           ...options,
f75425 87           setup: (editor: any) => {
V 88             editorRef.value = editor;
89             editor.on('init', (e: Event) => initSetup(e));
90           },
c0e4c9 91         };
J 92       });
f75425 93
V 94       const { toPromise } = useScript({
95         src: tinymceScriptSrc,
96       });
97
98       watch(
99         () => attrs.disabled,
100         () => {
101           const editor = unref(editorRef);
102           if (!editor) return;
103           editor.setMode(attrs.disabled ? 'readonly' : 'design');
104         }
105       );
73c8e0 106       onMountedOrActivated(() => {
8a9ca4 107         tinymceId.value = shortUuid('tiny-vue');
f75425 108         nextTick(() => {
V 109           init();
110         });
111       });
112
113       onUnmounted(() => {
114         destory();
115       });
116
117       onDeactivated(() => {
118         destory();
119       });
120
121       function destory() {
122         if (getTinymce() !== null) {
e6db0d 123           getTinymce()?.remove?.(unref(editorRef));
f75425 124         }
V 125       }
126
127       function init() {
128         toPromise().then(() => {
1093ec 129           setTimeout(() => {
V 130             initEditor();
131           }, 0);
f75425 132         });
V 133       }
134
135       function initEditor() {
7658f4 136         getTinymce().PluginManager.add('lineHeight', lineHeight(getTinymce()));
1093ec 137         const el = unref(elRef);
V 138         if (el) {
139           el.style.visibility = '';
140         }
f75425 141         getTinymce().init(unref(initOptions));
V 142       }
143
144       function initSetup(e: Event) {
145         const editor = unref(editorRef);
146         if (!editor) return;
147         const value = props.modelValue || '';
148
149         editor.setContent(value);
150         bindModelHandlers(editor);
151         bindHandlers(e, attrs, unref(editorRef));
152       }
153
3ad1a4 154       function setValue(editor: Recordable, val: string, prevVal?: string) {
58f988 155         if (
V 156           editor &&
157           typeof val === 'string' &&
158           val !== prevVal &&
159           val !== editor.getContent({ format: attrs.outputFormat })
160         ) {
161           editor.setContent(val);
162         }
163       }
164
f75425 165       function bindModelHandlers(editor: any) {
V 166         const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
167         const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
09c9f8 168
f75425 169         watch(
V 170           () => props.modelValue,
171           (val: string, prevVal: string) => {
58f988 172             setValue(editor, val, prevVal);
V 173           }
174         );
175
176         watch(
177           () => props.value,
178           (val: string, prevVal: string) => {
179             setValue(editor, val, prevVal);
180           },
181           {
182             immediate: true,
f75425 183           }
V 184         );
185
186         editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
58f988 187           const content = editor.getContent({ format: attrs.outputFormat });
V 188           emit('update:modelValue', content);
189           emit('change', content);
f75425 190         });
V 191       }
192
3ad1a4 193       function handleImageUploading(name: string) {
V 194         const editor = unref(editorRef);
195         if (!editor) return;
196         const content = editor?.getContent() ?? '';
197         setValue(editor, `${content}\n${getImgName(name)}`);
198       }
199
200       function handleDone(name: string, url: string) {
201         const editor = unref(editorRef);
202         if (!editor) return;
203
204         const content = editor?.getContent() ?? '';
205         const val = content?.replace(getImgName(name), `<img src="${url}"/>`) ?? '';
206         setValue(editor, val);
207       }
208
209       function getImgName(name: string) {
210         return `[uploading:${name}]`;
211       }
212
f75425 213       return {
3ad1a4 214         prefixCls,
f75425 215         containerWidth,
V 216         initOptions,
217         tinymceContent,
218         tinymceScriptSrc,
219         elRef,
220         tinymceId,
3ad1a4 221         handleImageUploading,
V 222         handleDone,
223         editorRef,
f75425 224       };
c0e4c9 225     },
J 226   });
227 </script>
228
3ad1a4 229 <style lang="less" scoped></style>
V 230
231 <style lang="less">
232   @prefix-cls: ~'@{namespace}-tinymce-container';
233
234   .@{prefix-cls} {
c0e4c9 235     position: relative;
J 236     line-height: normal;
237
3ad1a4 238     textarea {
V 239       z-index: -1;
240       visibility: hidden;
c0e4c9 241     }
J 242   }
243 </style>