Vben
2021-06-11 3b2c40bec818238bde165656dc17e885f242aa81
提交 | 用户 | age
c0e4c9 1 <template>
3ad1a4 2   <div :class="prefixCls" :style="{ width: containerWidth }">
V 3     <ImgUpload
39d629 4       :fullscreen="fullscreen"
3ad1a4 5       @uploading="handleImageUploading"
V 6       @done="handleDone"
7       v-if="showImageUpload"
8       v-show="editorRef"
966571 9       :disabled="disabled"
3ad1a4 10     />
1093ec 11     <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea>
c0e4c9 12   </div>
J 13 </template>
14
15 <script lang="ts">
d73d43 16   import type { RawEditorSettings } from 'tinymce';
a81268 17   import tinymce from 'tinymce/tinymce';
V 18   import 'tinymce/themes/silver';
19   import 'tinymce/icons/default/icons';
20   import 'tinymce/plugins/advlist';
21   import 'tinymce/plugins/anchor';
22   import 'tinymce/plugins/autolink';
23   import 'tinymce/plugins/autosave';
24   import 'tinymce/plugins/code';
25   import 'tinymce/plugins/codesample';
26   import 'tinymce/plugins/directionality';
27   import 'tinymce/plugins/fullscreen';
28   import 'tinymce/plugins/hr';
29   import 'tinymce/plugins/insertdatetime';
30   import 'tinymce/plugins/link';
31   import 'tinymce/plugins/lists';
32   import 'tinymce/plugins/media';
33   import 'tinymce/plugins/nonbreaking';
34   import 'tinymce/plugins/noneditable';
35   import 'tinymce/plugins/pagebreak';
36   import 'tinymce/plugins/paste';
37   import 'tinymce/plugins/preview';
38   import 'tinymce/plugins/print';
39   import 'tinymce/plugins/save';
40   import 'tinymce/plugins/searchreplace';
41   import 'tinymce/plugins/spellchecker';
42   import 'tinymce/plugins/tabfocus';
43   // import 'tinymce/plugins/table';
44   import 'tinymce/plugins/template';
45   import 'tinymce/plugins/textpattern';
46   import 'tinymce/plugins/visualblocks';
47   import 'tinymce/plugins/visualchars';
48   import 'tinymce/plugins/wordcount';
49
f75425 50   import {
V 51     defineComponent,
52     computed,
53     nextTick,
54     ref,
55     unref,
56     watch,
57     onUnmounted,
58     onDeactivated,
59   } from 'vue';
3576d0 60   import ImgUpload from './ImgUpload.vue';
a81268 61   import { toolbar, plugins } from './tinymce';
fcee7d 62   import { buildShortUUID } from '/@/utils/uuid';
f75425 63   import { bindHandlers } from './helper';
73c8e0 64   import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
3ad1a4 65   import { useDesign } from '/@/hooks/web/useDesign';
a9462f 66   import { isNumber } from '/@/utils/is';
83c9cd 67   import { useLocale } from '/@/locales/useLocale';
68   import { useAppStore } from '/@/store/modules/app';
f75425 69
39d629 70   const tinymceProps = {
V 71     options: {
d73d43 72       type: Object as PropType<Partial<RawEditorSettings>>,
39d629 73       default: {},
V 74     },
75     value: {
76       type: String,
77     },
78
79     toolbar: {
80       type: Array as PropType<string[]>,
81       default: toolbar,
82     },
83     plugins: {
84       type: Array as PropType<string[]>,
85       default: plugins,
86     },
87     modelValue: {
88       type: String,
89     },
90     height: {
91       type: [Number, String] as PropType<string | number>,
92       required: false,
93       default: 400,
94     },
95     width: {
96       type: [Number, String] as PropType<string | number>,
97       required: false,
98       default: 'auto',
99     },
100     showImageUpload: {
101       type: Boolean,
102       default: true,
103     },
104   };
c0e4c9 105
J 106   export default defineComponent({
107     name: 'Tinymce',
9edc28 108     components: { ImgUpload },
144ab5 109     inheritAttrs: false,
39d629 110     props: tinymceProps,
f75425 111     emits: ['change', 'update:modelValue'],
V 112     setup(props, { emit, attrs }) {
3576d0 113       const editorRef = ref();
39d629 114       const fullscreen = ref(false);
fcee7d 115       const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
f75425 116       const elRef = ref<Nullable<HTMLElement>>(null);
3ad1a4 117
V 118       const { prefixCls } = useDesign('tinymce-container');
f75425 119
83c9cd 120       const appStore = useAppStore();
121
3576d0 122       const tinymceContent = computed(() => props.modelValue);
f75425 123
c0e4c9 124       const containerWidth = computed(() => {
J 125         const width = props.width;
a9462f 126         if (isNumber(width)) {
c0e4c9 127           return `${width}px`;
J 128         }
129         return width;
83c9cd 130       });
131
132       const skinName = computed(() => {
133         return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark';
134       });
135
136       const langName = computed(() => {
137         const lang = useLocale().getLocale.value;
138         return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
c0e4c9 139       });
f75425 140
d73d43 141       const initOptions = computed((): RawEditorSettings => {
39d629 142         const { height, options, toolbar, plugins } = props;
a863ad 143         const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
c0e4c9 144         return {
f75425 145           selector: `#${unref(tinymceId)}`,
39d629 146           height,
V 147           toolbar,
e034d1 148           menubar: 'file edit insert view format table',
39d629 149           plugins,
83c9cd 150           language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
151           language: langName.value,
39d629 152           branding: false,
f75425 153           default_link_target: '_blank',
V 154           link_title: false,
155           object_resizing: false,
d73d43 156           auto_focus: true,
83c9cd 157           skin: skinName.value,
158           skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
159           content_css:
160             publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
e034d1 161           ...options,
d73d43 162           setup: (editor) => {
f75425 163             editorRef.value = editor;
d73d43 164             editor.on('init', (e) => initSetup(e));
f75425 165           },
c0e4c9 166         };
f75425 167       });
V 168
966571 169       const disabled = computed(() => {
L 170         const { options } = props;
171         const getdDisabled = options && Reflect.get(options, 'readonly');
efce48 172         const editor = unref(editorRef);
L 173         if (editor) {
174           editor.setMode(getdDisabled ? 'readonly' : 'design');
175         }
966571 176         return getdDisabled ?? false;
L 177       });
178
f75425 179       watch(
V 180         () => attrs.disabled,
181         () => {
182           const editor = unref(editorRef);
3576d0 183           if (!editor) {
V 184             return;
185           }
f75425 186           editor.setMode(attrs.disabled ? 'readonly' : 'design');
V 187         }
188       );
39d629 189
73c8e0 190       onMountedOrActivated(() => {
fcee7d 191         tinymceId.value = buildShortUUID('tiny-vue');
f75425 192         nextTick(() => {
39d629 193           setTimeout(() => {
V 194             initEditor();
195           }, 30);
f75425 196         });
V 197       });
198
199       onUnmounted(() => {
200         destory();
201       });
202
203       onDeactivated(() => {
204         destory();
205       });
206
207       function destory() {
39d629 208         if (tinymce !== null) {
V 209           tinymce?.remove?.(unref(editorRef));
f75425 210         }
V 211       }
212
213       function initEditor() {
1093ec 214         const el = unref(elRef);
V 215         if (el) {
216           el.style.visibility = '';
217         }
39d629 218         tinymce.init(unref(initOptions));
f75425 219       }
V 220
d73d43 221       function initSetup(e) {
f75425 222         const editor = unref(editorRef);
3576d0 223         if (!editor) {
V 224           return;
225         }
f75425 226         const value = props.modelValue || '';
V 227
228         editor.setContent(value);
229         bindModelHandlers(editor);
230         bindHandlers(e, attrs, unref(editorRef));
231       }
232
3ad1a4 233       function setValue(editor: Recordable, val: string, prevVal?: string) {
58f988 234         if (
V 235           editor &&
236           typeof val === 'string' &&
237           val !== prevVal &&
238           val !== editor.getContent({ format: attrs.outputFormat })
239         ) {
240           editor.setContent(val);
241         }
242       }
243
f75425 244       function bindModelHandlers(editor: any) {
V 245         const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
246         const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
09c9f8 247
f75425 248         watch(
V 249           () => props.modelValue,
250           (val: string, prevVal: string) => {
58f988 251             setValue(editor, val, prevVal);
V 252           }
253         );
254
255         watch(
256           () => props.value,
257           (val: string, prevVal: string) => {
258             setValue(editor, val, prevVal);
259           },
260           {
261             immediate: true,
f75425 262           }
V 263         );
264
265         editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
58f988 266           const content = editor.getContent({ format: attrs.outputFormat });
V 267           emit('update:modelValue', content);
268           emit('change', content);
f75425 269         });
39d629 270
V 271         editor.on('FullscreenStateChanged', (e) => {
272           fullscreen.value = e.state;
273         });
f75425 274       }
V 275
3ad1a4 276       function handleImageUploading(name: string) {
V 277         const editor = unref(editorRef);
3576d0 278         if (!editor) {
V 279           return;
280         }
3ad1a4 281         const content = editor?.getContent() ?? '';
3576d0 282         setValue(editor, `${content}\n${getUploadingImgName(name)}`);
3ad1a4 283       }
V 284
285       function handleDone(name: string, url: string) {
286         const editor = unref(editorRef);
3576d0 287         if (!editor) {
V 288           return;
289         }
3ad1a4 290         const content = editor?.getContent() ?? '';
3576d0 291         const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
3ad1a4 292         setValue(editor, val);
V 293       }
294
3576d0 295       function getUploadingImgName(name: string) {
3ad1a4 296         return `[uploading:${name}]`;
V 297       }
298
f75425 299       return {
3ad1a4 300         prefixCls,
f75425 301         containerWidth,
V 302         initOptions,
303         tinymceContent,
304         elRef,
305         tinymceId,
3ad1a4 306         handleImageUploading,
V 307         handleDone,
308         editorRef,
39d629 309         fullscreen,
966571 310         disabled,
f75425 311       };
c0e4c9 312     },
J 313   });
314 </script>
315
3ad1a4 316 <style lang="less" scoped></style>
V 317
318 <style lang="less">
319   @prefix-cls: ~'@{namespace}-tinymce-container';
320
321   .@{prefix-cls} {
c0e4c9 322     position: relative;
J 323     line-height: normal;
324
3ad1a4 325     textarea {
V 326       z-index: -1;
327       visibility: hidden;
c0e4c9 328     }
J 329   }
330 </style>