Commit f75425d13bc9f6003021fd4b5d6451ae096c09b7

Authored by vben
1 parent 9c02d8ec

perf: review tinymce code

src/components/Tinymce/src/Editor.vue
1 <template> 1 <template>
2 <div class="tinymce-container" :style="{ width: containerWidth }"> 2 <div class="tinymce-container" :style="{ width: containerWidth }">
3 - <tinymce-editor  
4 - :id="id"  
5 - :init="initOptions"  
6 - :modelValue="tinymceContent"  
7 - @update:modelValue="handleChange"  
8 - :tinymceScriptSrc="tinymceScriptSrc"  
9 - ></tinymce-editor> 3 + <textarea :id="tinymceId" visibility="hidden" ref="elRef"></textarea>
10 </div> 4 </div>
11 </template> 5 </template>
12 6
13 <script lang="ts"> 7 <script lang="ts">
14 - import TinymceEditor from './lib'; // TinyMCE vue wrapper  
15 - import { defineComponent, computed } from 'vue'; 8 + import {
  9 + defineComponent,
  10 + computed,
  11 + onMounted,
  12 + nextTick,
  13 + ref,
  14 + unref,
  15 + watch,
  16 + onUnmounted,
  17 + onDeactivated,
  18 + } from 'vue';
16 import { basicProps } from './props'; 19 import { basicProps } from './props';
17 import toolbar from './toolbar'; 20 import toolbar from './toolbar';
18 import plugins from './plugins'; 21 import plugins from './plugins';
  22 + import { getTinymce } from './getTinymce';
  23 + import { useScript } from '/@/hooks/web/useScript';
  24 + import { snowUuid } from '/@/utils/uuid';
  25 + import { bindHandlers } from './helper';
19 26
20 const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1'; 27 const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';
  28 +
21 const tinymceScriptSrc = `${CDN_URL}/tinymce.min.js`; 29 const tinymceScriptSrc = `${CDN_URL}/tinymce.min.js`;
22 30
23 export default defineComponent({ 31 export default defineComponent({
24 name: 'Tinymce', 32 name: 'Tinymce',
25 - components: { TinymceEditor },  
26 props: basicProps, 33 props: basicProps,
27 - setup(props, { emit }) { 34 + emits: ['change', 'update:modelValue'],
  35 + setup(props, { emit, attrs }) {
  36 + const editorRef = ref<any>(null);
  37 + const elRef = ref<Nullable<HTMLElement>>(null);
  38 +
  39 + const tinymceId = computed(() => {
  40 + return snowUuid('tiny-vue');
  41 + });
  42 +
28 const tinymceContent = computed(() => { 43 const tinymceContent = computed(() => {
29 - return props.value; 44 + return props.modelValue;
30 }); 45 });
31 - function handleChange(value: string) {  
32 - emit('change', value);  
33 - } 46 +
34 const containerWidth = computed(() => { 47 const containerWidth = computed(() => {
35 const width = props.width; 48 const width = props.width;
36 - // Test matches `100`, `'100'`  
37 if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) { 49 if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) {
38 return `${width}px`; 50 return `${width}px`;
39 } 51 }
40 return width; 52 return width;
41 }); 53 });
  54 +
42 const initOptions = computed(() => { 55 const initOptions = computed(() => {
43 - const { id, height, menubar } = props; 56 + const { height, menubar } = props;
44 return { 57 return {
45 - selector: `#${id}`, 58 + selector: `#${unref(tinymceId)}`,
46 height: height, 59 height: height,
47 toolbar: toolbar, 60 toolbar: toolbar,
  61 + theme: 'silver',
48 menubar: menubar, 62 menubar: menubar,
49 plugins: plugins, 63 plugins: plugins,
50 // 语言包 64 // 语言包
51 language_url: 'resource/tinymce/langs/zh_CN.js', 65 language_url: 'resource/tinymce/langs/zh_CN.js',
52 // 中文 66 // 中文
53 language: 'zh_CN', 67 language: 'zh_CN',
  68 + default_link_target: '_blank',
  69 + link_title: false,
  70 + advlist_bullet_styles: 'square',
  71 + advlist_number_styles: 'default',
  72 + object_resizing: false,
  73 + setup: (editor: any) => {
  74 + editorRef.value = editor;
  75 + editor.on('init', (e: Event) => initSetup(e));
  76 + },
54 }; 77 };
55 }); 78 });
56 - return { containerWidth, initOptions, tinymceContent, handleChange, tinymceScriptSrc }; 79 +
  80 + const { toPromise } = useScript({
  81 + src: tinymceScriptSrc,
  82 + });
  83 +
  84 + watch(
  85 + () => attrs.disabled,
  86 + () => {
  87 + const editor = unref(editorRef);
  88 + if (!editor) return;
  89 + editor.setMode(attrs.disabled ? 'readonly' : 'design');
  90 + }
  91 + );
  92 +
  93 + onMounted(() => {
  94 + nextTick(() => {
  95 + init();
  96 + });
  97 + });
  98 +
  99 + onUnmounted(() => {
  100 + destory();
  101 + });
  102 +
  103 + onDeactivated(() => {
  104 + destory();
  105 + });
  106 +
  107 + function destory() {
  108 + if (getTinymce() !== null) {
  109 + getTinymce().remove(unref(editorRef));
  110 + }
  111 + }
  112 +
  113 + function init() {
  114 + toPromise().then(() => {
  115 + initEditor();
  116 + });
  117 + }
  118 +
  119 + function initEditor() {
  120 + getTinymce().init(unref(initOptions));
  121 + }
  122 +
  123 + function initSetup(e: Event) {
  124 + const editor = unref(editorRef);
  125 + if (!editor) return;
  126 + const value = props.modelValue || '';
  127 +
  128 + editor.setContent(value);
  129 + bindModelHandlers(editor);
  130 + bindHandlers(e, attrs, unref(editorRef));
  131 + }
  132 +
  133 + function bindModelHandlers(editor: any) {
  134 + const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
  135 + const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
  136 + watch(
  137 + () => props.modelValue,
  138 + (val: string, prevVal: string) => {
  139 + if (
  140 + editor &&
  141 + typeof val === 'string' &&
  142 + val !== prevVal &&
  143 + val !== editor.getContent({ format: attrs.outputFormat })
  144 + ) {
  145 + editor.setContent(val);
  146 + }
  147 + }
  148 + );
  149 +
  150 + editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
  151 + emit('update:modelValue', editor.getContent({ format: attrs.outputFormat }));
  152 + });
  153 + }
  154 +
  155 + function handleChange(value: string) {
  156 + emit('change', value);
  157 + }
  158 + return {
  159 + containerWidth,
  160 + initOptions,
  161 + tinymceContent,
  162 + handleChange,
  163 + tinymceScriptSrc,
  164 + elRef,
  165 + tinymceId,
  166 + };
57 }, 167 },
58 }); 168 });
59 </script> 169 </script>
src/components/Tinymce/src/lib/TinyMCE.ts renamed to src/components/Tinymce/src/getTinymce.ts
1 const getGlobal = (): any => (typeof window !== 'undefined' ? window : global); 1 const getGlobal = (): any => (typeof window !== 'undefined' ? window : global);
2 2
3 -const getTinymce = () => { 3 +export const getTinymce = () => {
4 const global = getGlobal(); 4 const global = getGlobal();
5 -  
6 return global && global.tinymce ? global.tinymce : null; 5 return global && global.tinymce ? global.tinymce : null;
7 }; 6 };
8 -  
9 -export { getTinymce };  
src/components/Tinymce/src/lib/Utils.ts renamed to src/components/Tinymce/src/helper.ts
1 -import { ComponentPublicInstance } from 'vue';  
2 -  
3 const validEvents = [ 1 const validEvents = [
4 'onActivate', 2 'onActivate',
5 'onAddUndo', 3 'onAddUndo',
@@ -62,12 +60,12 @@ const validEvents = [ @@ -62,12 +60,12 @@ const validEvents = [
62 'onShow', 60 'onShow',
63 'onSubmit', 61 'onSubmit',
64 'onUndo', 62 'onUndo',
65 - 'onVisualAid' 63 + 'onVisualAid',
66 ]; 64 ];
67 65
68 const isValidKey = (key: string) => validEvents.indexOf(key) !== -1; 66 const isValidKey = (key: string) => validEvents.indexOf(key) !== -1;
69 67
70 -const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => { 68 +export const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => {
71 Object.keys(listeners) 69 Object.keys(listeners)
72 .filter(isValidKey) 70 .filter(isValidKey)
73 .forEach((key: string) => { 71 .forEach((key: string) => {
@@ -81,71 +79,3 @@ const bindHandlers = (initEvent: Event, listeners: any, editor: any): void =&gt; { @@ -81,71 +79,3 @@ const bindHandlers = (initEvent: Event, listeners: any, editor: any): void =&gt; {
81 } 79 }
82 }); 80 });
83 }; 81 };
84 -  
85 -const bindModelHandlers = (ctx: ComponentPublicInstance, editor: any) => {  
86 - const modelEvents = ctx.$props.modelEvents ? ctx.$props.modelEvents : null;  
87 - const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;  
88 - // @ts-ignore  
89 - ctx.$watch('modelValue', (val: string, prevVal: string) => {  
90 - if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: ctx.$props.outputFormat })) {  
91 - editor.setContent(val);  
92 - }  
93 - });  
94 -  
95 - editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {  
96 - ctx.$emit('update:modelValue', editor.getContent({ format: ctx.$props.outputFormat }));  
97 - });  
98 -};  
99 -  
100 -const initEditor = (initEvent: Event, ctx: ComponentPublicInstance, editor: any) => {  
101 - const value = ctx.$props.modelValue ? ctx.$props.modelValue : '';  
102 - const initialValue = ctx.$props.initialValue ? ctx.$props.initialValue : '';  
103 -  
104 - editor.setContent(value || initialValue);  
105 -  
106 - // checks if the v-model shorthand is used (which sets an v-on:input listener) and then binds either  
107 - // specified the events or defaults to "change keyup" event and emits the editor content on that event  
108 - if (ctx.$attrs['onUpdate:modelValue']) {  
109 - bindModelHandlers(ctx, editor);  
110 - }  
111 -  
112 - bindHandlers(initEvent, ctx.$attrs, editor);  
113 -};  
114 -  
115 -let unique = 0;  
116 -  
117 -const uuid = (prefix: string): string => {  
118 - const time = Date.now();  
119 - const random = Math.floor(Math.random() * 1000000000);  
120 -  
121 - unique++;  
122 -  
123 - return prefix + '_' + random + unique + String(time);  
124 -};  
125 -  
126 -const isTextarea = (element: Element | null): element is HTMLTextAreaElement => {  
127 - return element !== null && element.tagName.toLowerCase() === 'textarea';  
128 -};  
129 -  
130 -const normalizePluginArray = (plugins?: string | string[]): string[] => {  
131 - if (typeof plugins === 'undefined' || plugins === '') {  
132 - return [];  
133 - }  
134 -  
135 - return Array.isArray(plugins) ? plugins : plugins.split(' ');  
136 -};  
137 -  
138 -const mergePlugins = (initPlugins: string | string[], inputPlugins?: string | string[]) =>  
139 - normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins));  
140 -  
141 -const isNullOrUndefined = (value: any): value is null | undefined => value === null || value === undefined;  
142 -  
143 -export {  
144 - bindHandlers,  
145 - bindModelHandlers,  
146 - initEditor,  
147 - uuid,  
148 - isTextarea,  
149 - mergePlugins,  
150 - isNullOrUndefined  
151 -};  
152 \ No newline at end of file 82 \ No newline at end of file
src/components/Tinymce/src/lib/ScriptLoader.ts deleted 100644 → 0
1 -import { uuid } from './Utils';  
2 -  
3 -export type callbackFn = () => void;  
4 -export interface IStateObj {  
5 - listeners: callbackFn[];  
6 - scriptId: string;  
7 - scriptLoaded: boolean;  
8 -}  
9 -  
10 -const createState = (): IStateObj => {  
11 - return {  
12 - listeners: [],  
13 - scriptId: uuid('tiny-script'),  
14 - scriptLoaded: false  
15 - };  
16 -};  
17 -  
18 -interface ScriptLoader {  
19 - load: (doc: Document, url: string, callback: callbackFn) => void;  
20 - reinitialize: () => void;  
21 -}  
22 -  
23 -const CreateScriptLoader = (): ScriptLoader => {  
24 - let state: IStateObj = createState();  
25 -  
26 - const injectScriptTag = (scriptId: string, doc: Document, url: string, callback: callbackFn) => {  
27 - const scriptTag = doc.createElement('script');  
28 - scriptTag.referrerPolicy = 'origin';  
29 - scriptTag.type = 'application/javascript';  
30 - scriptTag.id = scriptId;  
31 - scriptTag.src = url;  
32 -  
33 - const handler = () => {  
34 - scriptTag.removeEventListener('load', handler);  
35 - callback();  
36 - };  
37 - scriptTag.addEventListener('load', handler);  
38 - if (doc.head) {  
39 - doc.head.appendChild(scriptTag);  
40 - }  
41 - };  
42 -  
43 - const load = (doc: Document, url: string, callback: callbackFn) => {  
44 - if (state.scriptLoaded) {  
45 - callback();  
46 - } else {  
47 - state.listeners.push(callback);  
48 - if (!doc.getElementById(state.scriptId)) {  
49 - injectScriptTag(state.scriptId, doc, url, () => {  
50 - state.listeners.forEach((fn) => fn());  
51 - state.scriptLoaded = true;  
52 - });  
53 - }  
54 - }  
55 - };  
56 -  
57 - // Only to be used by tests.  
58 - const reinitialize = () => {  
59 - state = createState();  
60 - };  
61 -  
62 - return {  
63 - load,  
64 - reinitialize  
65 - };  
66 -};  
67 -  
68 -const ScriptLoader = CreateScriptLoader();  
69 -  
70 -export {  
71 - ScriptLoader  
72 -};  
73 \ No newline at end of file 0 \ No newline at end of file
src/components/Tinymce/src/lib/components/Editor.ts deleted 100644 → 0
1 -/**  
2 - * Copyright (c) 2018-present, Ephox, Inc.  
3 - *  
4 - * This source code is licensed under the Apache 2 license found in the  
5 - * LICENSE file in the root directory of this source tree.  
6 - *  
7 - */  
8 -  
9 -// import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options';  
10 -// import { CreateElement, Vue } from 'vue/types/vue';  
11 -  
12 -import { ScriptLoader } from '../ScriptLoader';  
13 -import { getTinymce } from '../TinyMCE';  
14 -import { initEditor, isTextarea, mergePlugins, uuid, isNullOrUndefined } from '../Utils';  
15 -import { editorProps, IPropTypes } from './EditorPropTypes';  
16 -import { h, defineComponent, ComponentPublicInstance } from 'vue'  
17 -  
18 -  
19 -export interface IEditor {  
20 - $props: Partial<IPropTypes>  
21 -}  
22 -  
23 -declare module '@vue/runtime-core' {  
24 - interface ComponentCustomProperties {  
25 - elementId: string;  
26 - element: Element | null;  
27 - editor: any;  
28 - inlineEditor: boolean;  
29 - $props: Partial<IPropTypes>;  
30 - }  
31 -}  
32 -  
33 -  
34 -const renderInline = (id: string, tagName?: string) => {  
35 - return h(tagName ? tagName : 'div', {  
36 - id  
37 - });  
38 -};  
39 -  
40 -const renderIframe = (id: string) => {  
41 - return h('textarea', {  
42 - id,  
43 - visibility: 'hidden'  
44 - });  
45 -};  
46 -  
47 -const initialise = (ctx: ComponentPublicInstance) => () => {  
48 - const finalInit = {  
49 - ...ctx.$props.init,  
50 - readonly: ctx.$props.disabled,  
51 - selector: `#${ctx.elementId}`,  
52 - plugins: mergePlugins(ctx.$props.init && ctx.$props.init.plugins, ctx.$props.plugins),  
53 - toolbar: ctx.$props.toolbar || (ctx.$props.init && ctx.$props.init.toolbar),  
54 - inline: ctx.inlineEditor,  
55 - setup: (editor: any) => {  
56 - ctx.editor = editor;  
57 - editor.on('init', (e: Event) => initEditor(e, ctx, editor));  
58 -  
59 - if (ctx.$props.init && typeof ctx.$props.init.setup === 'function') {  
60 - ctx.$props.init.setup(editor);  
61 - }  
62 - }  
63 - };  
64 -  
65 - if (isTextarea(ctx.element)) {  
66 - ctx.element.style.visibility = '';  
67 - }  
68 -  
69 - getTinymce().init(finalInit);  
70 -};  
71 -  
72 -export const Editor = defineComponent({  
73 - props: editorProps,  
74 - created() {  
75 - this.elementId = this.$props.id || uuid('tiny-vue');  
76 - this.inlineEditor = (this.$props.init && this.$props.init.inline) || this.$props.inline;  
77 - },  
78 - watch: {  
79 - disabled() {  
80 - (this as any).editor.setMode(this.disabled ? 'readonly' : 'design');  
81 - }  
82 - },  
83 - mounted() {  
84 - this.element = this.$el;  
85 -  
86 - if (getTinymce() !== null) {  
87 - initialise(this)();  
88 - } else if (this.element && this.element.ownerDocument) {  
89 - const channel = this.$props.cloudChannel ? this.$props.cloudChannel : '5';  
90 - const apiKey = this.$props.apiKey ? this.$props.apiKey : 'no-api-key';  
91 -  
92 - const scriptSrc = isNullOrUndefined(this.$props.tinymceScriptSrc) ?  
93 - `https://cdn.tiny.cloud/1/${apiKey}/tinymce/${channel}/tinymce.min.js` :  
94 - this.$props.tinymceScriptSrc;  
95 -  
96 - ScriptLoader.load(  
97 - this.element.ownerDocument,  
98 - scriptSrc,  
99 - initialise(this)  
100 - );  
101 - }  
102 - },  
103 - beforeUnmount() {  
104 - if (getTinymce() !== null) {  
105 - getTinymce().remove(this.editor);  
106 - }  
107 - },  
108 - render() {  
109 - return this.inlineEditor ? renderInline(this.elementId, this.$props.tagName) : renderIframe(this.elementId);  
110 - }  
111 -})  
src/components/Tinymce/src/lib/components/EditorPropTypes.ts deleted 100644 → 0
1 -/**  
2 - * Copyright (c) 2018-present, Ephox, Inc.  
3 - *  
4 - * This source code is licensed under the Apache 2 license found in the  
5 - * LICENSE file in the root directory of this source tree.  
6 - *  
7 - */  
8 -  
9 -export type CopyProps<T> = { [P in keyof T]: any };  
10 -  
11 -export interface IPropTypes {  
12 - apiKey: string;  
13 - cloudChannel: string;  
14 - id: string;  
15 - init: any;  
16 - initialValue: string;  
17 - outputFormat: 'html' | 'text';  
18 - inline: boolean;  
19 - modelEvents: string[] | string;  
20 - plugins: string[] | string;  
21 - tagName: string;  
22 - toolbar: string[] | string;  
23 - modelValue: string;  
24 - disabled: boolean;  
25 - tinymceScriptSrc: string;  
26 -}  
27 -  
28 -export const editorProps: CopyProps<IPropTypes> = {  
29 - apiKey: String,  
30 - cloudChannel: String,  
31 - id: String,  
32 - init: Object,  
33 - initialValue: String,  
34 - inline: Boolean,  
35 - modelEvents: [String, Array],  
36 - plugins: [String, Array],  
37 - tagName: String,  
38 - toolbar: [String, Array],  
39 - modelValue: String,  
40 - disabled: Boolean,  
41 - tinymceScriptSrc: String,  
42 - outputFormat: {  
43 - type: String,  
44 - validator: (prop: string) => prop === 'html' || prop === 'text'  
45 - },  
46 -};  
src/components/Tinymce/src/lib/global.d.ts deleted 100644 → 0
1 -// Global compile-time constants  
2 -declare var __DEV__: boolean  
3 -declare var __BROWSER__: boolean  
4 -declare var __CI__: boolean  
src/components/Tinymce/src/lib/index.ts deleted 100644 → 0
1 -import { Editor } from './components/Editor';  
2 -  
3 -export default Editor;  
src/components/Tinymce/src/props.ts
1 import { PropType } from 'vue'; 1 import { PropType } from 'vue';
2 2
3 export const basicProps = { 3 export const basicProps = {
4 - id: {  
5 - type: String as PropType<string>,  
6 - default: () => {  
7 - return `tinymce-${new Date().getTime()}${(Math.random() * 1000).toFixed(0)}`;  
8 - },  
9 - },  
10 menubar: { 4 menubar: {
11 type: String as PropType<string>, 5 type: String as PropType<string>,
12 default: 'file edit insert view format table', 6 default: 'file edit insert view format table',
@@ -15,6 +9,10 @@ export const basicProps = { @@ -15,6 +9,10 @@ export const basicProps = {
15 type: String as PropType<string>, 9 type: String as PropType<string>,
16 // default: '' 10 // default: ''
17 }, 11 },
  12 + modelValue: {
  13 + type: String as PropType<string>,
  14 + // default: ''
  15 + },
18 // 高度 16 // 高度
19 height: { 17 height: {
20 type: [Number, String] as PropType<string | number>, 18 type: [Number, String] as PropType<string | number>,
src/router/menus/modules/demo/charts.ts
@@ -6,23 +6,23 @@ const menu: MenuModule = { @@ -6,23 +6,23 @@ const menu: MenuModule = {
6 path: '/charts', 6 path: '/charts',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/apexChart', 9 + path: 'apexChart',
10 name: 'ApexChart', 10 name: 'ApexChart',
11 }, 11 },
12 { 12 {
13 - path: '/echarts', 13 + path: 'echarts',
14 name: 'Echarts', 14 name: 'Echarts',
15 children: [ 15 children: [
16 { 16 {
17 - path: '/map', 17 + path: 'map',
18 name: '地图', 18 name: '地图',
19 }, 19 },
20 { 20 {
21 - path: '/line', 21 + path: 'line',
22 name: '折线图', 22 name: '折线图',
23 }, 23 },
24 { 24 {
25 - path: '/pie', 25 + path: 'pie',
26 name: '饼图', 26 name: '饼图',
27 }, 27 },
28 ], 28 ],
src/router/menus/modules/demo/comp.ts
@@ -6,16 +6,16 @@ const menu: MenuModule = { @@ -6,16 +6,16 @@ const menu: MenuModule = {
6 path: '/comp', 6 path: '/comp',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/basic', 9 + path: 'basic',
10 name: '基础组件', 10 name: '基础组件',
11 }, 11 },
12 { 12 {
13 - path: '/countTo', 13 + path: 'countTo',
14 name: '数字动画', 14 name: '数字动画',
15 }, 15 },
16 16
17 { 17 {
18 - path: '/scroll', 18 + path: 'scroll',
19 name: '滚动组件', 19 name: '滚动组件',
20 children: [ 20 children: [
21 { 21 {
@@ -33,53 +33,39 @@ const menu: MenuModule = { @@ -33,53 +33,39 @@ const menu: MenuModule = {
33 ], 33 ],
34 }, 34 },
35 { 35 {
36 - path: '/modal', 36 + path: 'modal',
37 name: '弹窗扩展', 37 name: '弹窗扩展',
38 }, 38 },
39 { 39 {
40 - path: '/drawer', 40 + path: 'drawer',
41 name: '抽屉扩展', 41 name: '抽屉扩展',
42 }, 42 },
43 { 43 {
44 - path: '/desc', 44 + path: 'desc',
45 name: '详情组件', 45 name: '详情组件',
46 }, 46 },
47 { 47 {
48 - path: '/verify', 48 + path: 'verify',
49 name: '验证组件', 49 name: '验证组件',
50 children: [ 50 children: [
51 { 51 {
52 - path: '/drag', 52 + path: 'drag',
53 name: '拖拽校验', 53 name: '拖拽校验',
54 }, 54 },
55 { 55 {
56 - path: '/rotate', 56 + path: 'rotate',
57 name: '图片还原校验', 57 name: '图片还原校验',
58 }, 58 },
59 ], 59 ],
60 }, 60 },
61 { 61 {
62 - path: '/qrcode', 62 + path: 'qrcode',
63 name: '二维码组件', 63 name: '二维码组件',
64 }, 64 },
65 { 65 {
66 - path: '/strength-meter', 66 + path: 'strength-meter',
67 name: '密码强度组件', 67 name: '密码强度组件',
68 }, 68 },
69 - {  
70 - path: '/tinymce',  
71 - name: '富文本',  
72 - children: [  
73 - {  
74 - path: '/index',  
75 - name: '基础使用',  
76 - },  
77 - {  
78 - path: '/editor',  
79 - name: '嵌入form使用',  
80 - },  
81 - ],  
82 - },  
83 ], 69 ],
84 }, 70 },
85 }; 71 };
src/router/menus/modules/demo/editor.ts
@@ -6,9 +6,23 @@ const menu: MenuModule = { @@ -6,9 +6,23 @@ const menu: MenuModule = {
6 path: '/editor', 6 path: '/editor',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/markdown', 9 + path: 'markdown',
10 name: 'markdown编辑器', 10 name: 'markdown编辑器',
11 }, 11 },
  12 + {
  13 + path: 'tinymce',
  14 + name: '富文本',
  15 + children: [
  16 + {
  17 + path: 'index',
  18 + name: '基础使用',
  19 + },
  20 + // {
  21 + // path: 'editor',
  22 + // name: '嵌入form使用',
  23 + // },
  24 + ],
  25 + },
12 ], 26 ],
13 }, 27 },
14 }; 28 };
src/router/menus/modules/demo/excel.ts
@@ -6,23 +6,21 @@ const menu: MenuModule = { @@ -6,23 +6,21 @@ const menu: MenuModule = {
6 path: '/excel', 6 path: '/excel',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/customExport', 9 + path: 'customExport',
10 name: '选择导出格式', 10 name: '选择导出格式',
11 }, 11 },
12 { 12 {
13 - path: '/jsonExport', 13 + path: 'jsonExport',
14 name: 'JSON数据导出', 14 name: 'JSON数据导出',
15 }, 15 },
16 { 16 {
17 - path: '/arrayExport', 17 + path: 'arrayExport',
18 name: 'Array数据导出', 18 name: 'Array数据导出',
19 }, 19 },
20 { 20 {
21 - path: '/importExcel', 21 + path: 'importExcel',
22 name: '导入', 22 name: '导入',
23 }, 23 },
24 - // ],  
25 - // },  
26 ], 24 ],
27 }, 25 },
28 }; 26 };
src/router/menus/modules/demo/exception.ts
@@ -6,27 +6,27 @@ const menu: MenuModule = { @@ -6,27 +6,27 @@ const menu: MenuModule = {
6 path: '/exception', 6 path: '/exception',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/404', 9 + path: '404',
10 name: '404', 10 name: '404',
11 }, 11 },
12 { 12 {
13 - path: '/500', 13 + path: '500',
14 name: '500', 14 name: '500',
15 }, 15 },
16 { 16 {
17 - path: '/net-work-error', 17 + path: 'net-work-error',
18 name: '网络错误', 18 name: '网络错误',
19 }, 19 },
20 { 20 {
21 - path: '/page-time-out', 21 + path: 'page-time-out',
22 name: '页面超时', 22 name: '页面超时',
23 }, 23 },
24 { 24 {
25 - path: '/not-data', 25 + path: 'not-data',
26 name: '无数据', 26 name: '无数据',
27 }, 27 },
28 { 28 {
29 - path: '/error-log', 29 + path: 'error-log',
30 name: '错误日志', 30 name: '错误日志',
31 }, 31 },
32 ], 32 ],
src/router/menus/modules/demo/feat.ts
@@ -6,55 +6,55 @@ const menu: MenuModule = { @@ -6,55 +6,55 @@ const menu: MenuModule = {
6 path: '/feat', 6 path: '/feat',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/icon', 9 + path: 'icon',
10 name: '图标', 10 name: '图标',
11 }, 11 },
12 { 12 {
13 - path: '/tabs', 13 + path: 'tabs',
14 name: '标签页操作', 14 name: '标签页操作',
15 }, 15 },
16 { 16 {
17 - path: '/context-menu', 17 + path: 'context-menu',
18 name: '右键菜单', 18 name: '右键菜单',
19 }, 19 },
20 { 20 {
21 - path: '/click-out-side', 21 + path: 'click-out-side',
22 name: 'ClickOutSide', 22 name: 'ClickOutSide',
23 }, 23 },
24 { 24 {
25 - path: '/img-preview', 25 + path: 'img-preview',
26 name: '图片预览', 26 name: '图片预览',
27 }, 27 },
28 { 28 {
29 - path: '/i18n', 29 + path: 'i18n',
30 name: '国际化', 30 name: '国际化',
31 }, 31 },
32 { 32 {
33 - path: '/copy', 33 + path: 'copy',
34 name: '剪切板', 34 name: '剪切板',
35 }, 35 },
36 { 36 {
37 - path: '/msg', 37 + path: 'msg',
38 name: '消息提示', 38 name: '消息提示',
39 }, 39 },
40 { 40 {
41 - path: '/watermark', 41 + path: 'watermark',
42 name: '水印', 42 name: '水印',
43 }, 43 },
44 { 44 {
45 - path: '/full-screen', 45 + path: 'full-screen',
46 name: '全屏', 46 name: '全屏',
47 }, 47 },
48 { 48 {
49 - path: '/testTab', 49 + path: 'testTab',
50 name: '带参Tab', 50 name: '带参Tab',
51 children: [ 51 children: [
52 { 52 {
53 - path: '/id1', 53 + path: 'id1',
54 name: '带参tab1', 54 name: '带参tab1',
55 }, 55 },
56 { 56 {
57 - path: '/id2', 57 + path: 'id2',
58 name: '带参tab2', 58 name: '带参tab2',
59 }, 59 },
60 ], 60 ],
src/router/menus/modules/demo/form.ts
@@ -6,31 +6,31 @@ const menu: MenuModule = { @@ -6,31 +6,31 @@ const menu: MenuModule = {
6 name: 'Form', 6 name: 'Form',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/basic', 9 + path: 'basic',
10 name: '基础表单', 10 name: '基础表单',
11 }, 11 },
12 { 12 {
13 - path: '/useForm', 13 + path: 'useForm',
14 name: 'useForm', 14 name: 'useForm',
15 }, 15 },
16 { 16 {
17 - path: '/refForm', 17 + path: 'refForm',
18 name: 'RefForm', 18 name: 'RefForm',
19 }, 19 },
20 { 20 {
21 - path: '/advancedForm', 21 + path: 'advancedForm',
22 name: '可收缩表单', 22 name: '可收缩表单',
23 }, 23 },
24 { 24 {
25 - path: '/ruleForm', 25 + path: 'ruleForm',
26 name: '表单校验', 26 name: '表单校验',
27 }, 27 },
28 { 28 {
29 - path: '/dynamicForm', 29 + path: 'dynamicForm',
30 name: '动态表单', 30 name: '动态表单',
31 }, 31 },
32 { 32 {
33 - path: '/customerForm', 33 + path: 'customerForm',
34 name: '自定义组件', 34 name: '自定义组件',
35 }, 35 },
36 ], 36 ],
src/router/menus/modules/demo/iframe.ts
@@ -6,15 +6,15 @@ const menu: MenuModule = { @@ -6,15 +6,15 @@ const menu: MenuModule = {
6 path: '/frame', 6 path: '/frame',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/antv', 9 + path: 'antv',
10 name: 'antVue文档(内嵌)', 10 name: 'antVue文档(内嵌)',
11 }, 11 },
12 { 12 {
13 - path: '/doc', 13 + path: 'doc',
14 name: '项目文档(内嵌)', 14 name: '项目文档(内嵌)',
15 }, 15 },
16 { 16 {
17 - path: '/docExternal', 17 + path: 'docExternal',
18 name: '项目文档(外链)', 18 name: '项目文档(外链)',
19 }, 19 },
20 ], 20 ],
src/router/menus/modules/demo/permission.ts
@@ -6,37 +6,37 @@ const menu: MenuModule = { @@ -6,37 +6,37 @@ const menu: MenuModule = {
6 path: '/permission', 6 path: '/permission',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/front', 9 + path: 'front',
10 name: '基于前端', 10 name: '基于前端',
11 children: [ 11 children: [
12 { 12 {
13 - path: '/page', 13 + path: 'page',
14 name: '页面权限', 14 name: '页面权限',
15 }, 15 },
16 { 16 {
17 - path: '/btn', 17 + path: 'btn',
18 name: '按钮权限', 18 name: '按钮权限',
19 }, 19 },
20 { 20 {
21 - path: '/auth-pageA', 21 + path: 'auth-pageA',
22 name: '权限测试页A', 22 name: '权限测试页A',
23 }, 23 },
24 { 24 {
25 - path: '/auth-pageB', 25 + path: 'auth-pageB',
26 name: '权限测试页B', 26 name: '权限测试页B',
27 }, 27 },
28 ], 28 ],
29 }, 29 },
30 { 30 {
31 - path: '/back', 31 + path: 'back',
32 name: '基于后台', 32 name: '基于后台',
33 children: [ 33 children: [
34 { 34 {
35 - path: '/page', 35 + path: 'page',
36 name: '页面权限', 36 name: '页面权限',
37 }, 37 },
38 { 38 {
39 - path: '/btn', 39 + path: 'btn',
40 name: '按钮权限', 40 name: '按钮权限',
41 }, 41 },
42 ], 42 ],
src/router/menus/modules/demo/table.ts
@@ -6,59 +6,59 @@ const menu: MenuModule = { @@ -6,59 +6,59 @@ const menu: MenuModule = {
6 name: 'Table', 6 name: 'Table',
7 children: [ 7 children: [
8 { 8 {
9 - path: '/basic', 9 + path: 'basic',
10 name: '基础表格', 10 name: '基础表格',
11 }, 11 },
12 { 12 {
13 - path: '/treeTable', 13 + path: 'treeTable',
14 name: '树形表格', 14 name: '树形表格',
15 }, 15 },
16 { 16 {
17 - path: '/fetchTable', 17 + path: 'fetchTable',
18 name: '远程加载', 18 name: '远程加载',
19 }, 19 },
20 { 20 {
21 - path: '/fixedColumn', 21 + path: 'fixedColumn',
22 name: '固定列', 22 name: '固定列',
23 }, 23 },
24 { 24 {
25 - path: '/customerCell', 25 + path: 'customerCell',
26 name: '自定义列', 26 name: '自定义列',
27 }, 27 },
28 { 28 {
29 - path: '/formTable', 29 + path: 'formTable',
30 name: '开启搜索区域', 30 name: '开启搜索区域',
31 }, 31 },
32 { 32 {
33 - path: '/useTable', 33 + path: 'useTable',
34 name: 'UseTable', 34 name: 'UseTable',
35 }, 35 },
36 { 36 {
37 - path: '/refTable', 37 + path: 'refTable',
38 name: 'RefTable', 38 name: 'RefTable',
39 }, 39 },
40 { 40 {
41 - path: '/multipleHeader', 41 + path: 'multipleHeader',
42 name: '多级表头', 42 name: '多级表头',
43 }, 43 },
44 { 44 {
45 - path: '/mergeHeader', 45 + path: 'mergeHeader',
46 name: '合并单元格', 46 name: '合并单元格',
47 }, 47 },
48 { 48 {
49 - path: '/expandTable', 49 + path: 'expandTable',
50 name: '可展开表格', 50 name: '可展开表格',
51 }, 51 },
52 { 52 {
53 - path: '/fixedHeight', 53 + path: 'fixedHeight',
54 name: '定高/头部自定义', 54 name: '定高/头部自定义',
55 }, 55 },
56 { 56 {
57 - path: '/footerTable', 57 + path: 'footerTable',
58 name: '表尾行合计', 58 name: '表尾行合计',
59 }, 59 },
60 { 60 {
61 - path: '/editCellTable', 61 + path: 'editCellTable',
62 name: '可编辑单元格', 62 name: '可编辑单元格',
63 }, 63 },
64 ], 64 ],
src/router/routes/modules/demo/comp.ts
@@ -136,31 +136,5 @@ export default { @@ -136,31 +136,5 @@ export default {
136 title: '密码强度组件', 136 title: '密码强度组件',
137 }, 137 },
138 }, 138 },
139 - {  
140 - path: '/tinymce',  
141 - name: 'TinymceDemo',  
142 - meta: {  
143 - title: '富文本',  
144 - },  
145 - redirect: '/comp/tinymce/index',  
146 - children: [  
147 - {  
148 - path: 'index',  
149 - name: 'Tinymce',  
150 - component: () => import('/@/views/demo/comp/tinymce/index.vue'),  
151 - meta: {  
152 - title: '基础使用',  
153 - },  
154 - },  
155 - {  
156 - path: 'editor',  
157 - name: 'TinymceEditor',  
158 - component: () => import('/@/views/demo/comp/tinymce/Editor.vue'),  
159 - meta: {  
160 - title: '嵌入form使用',  
161 - },  
162 - },  
163 - ],  
164 - },  
165 ], 139 ],
166 } as AppRouteModule; 140 } as AppRouteModule;
src/router/routes/modules/demo/editor.ts
@@ -23,5 +23,32 @@ export default { @@ -23,5 +23,32 @@ export default {
23 title: 'markdown编辑器', 23 title: 'markdown编辑器',
24 }, 24 },
25 }, 25 },
  26 + {
  27 + path: '/tinymce',
  28 + name: 'TinymceDemo',
  29 + meta: {
  30 + title: '富文本',
  31 + },
  32 + redirect: '/editor/tinymce/index',
  33 + children: [
  34 + {
  35 + path: 'index',
  36 + name: 'TinymceBasicDemo',
  37 + component: () => import('/@/views/demo/editor/tinymce/index.vue'),
  38 + meta: {
  39 + title: '基础使用',
  40 + },
  41 + },
  42 + // TODO
  43 + // {
  44 + // path: 'editor',
  45 + // name: 'TinymceFormDemo',
  46 + // component: () => import('/@/views/demo/comp/tinymce/Editor.vue'),
  47 + // meta: {
  48 + // title: '嵌入form使用',
  49 + // },
  50 + // },
  51 + ],
  52 + },
26 ], 53 ],
27 } as AppRouteModule; 54 } as AppRouteModule;
src/utils/is.ts
@@ -67,3 +67,7 @@ export const isServer = typeof window === &#39;undefined&#39;; @@ -67,3 +67,7 @@ export const isServer = typeof window === &#39;undefined&#39;;
67 export function isImageDom(o: Element) { 67 export function isImageDom(o: Element) {
68 return o && ['IMAGE', 'IMG'].includes(o.tagName); 68 return o && ['IMAGE', 'IMG'].includes(o.tagName);
69 } 69 }
  70 +
  71 +export const isTextarea = (element: Element | null): element is HTMLTextAreaElement => {
  72 + return element !== null && element.tagName.toLowerCase() === 'textarea';
  73 +};
src/utils/uuid.ts
@@ -17,3 +17,11 @@ export function buildUUID(): string { @@ -17,3 +17,11 @@ export function buildUUID(): string {
17 } 17 }
18 return uuid.replace(/-/g, ''); 18 return uuid.replace(/-/g, '');
19 } 19 }
  20 +
  21 +let unique = 0;
  22 +export function snowUuid(prefix: string): string {
  23 + const time = Date.now();
  24 + const random = Math.floor(Math.random() * 1000000000);
  25 + unique++;
  26 + return prefix + '_' + random + unique + String(time);
  27 +}
src/views/demo/comp/tinymce/Editor.vue renamed to src/views/demo/editor/tinymce/Editor.vue
@@ -43,7 +43,7 @@ @@ -43,7 +43,7 @@
43 }, 43 },
44 ]; 44 ];
45 export default defineComponent({ 45 export default defineComponent({
46 - components: { BasicForm, CollapseContainer, Tinymce }, 46 + components: { BasicForm, CollapseContainer },
47 setup() { 47 setup() {
48 const { createMessage } = useMessage(); 48 const { createMessage } = useMessage();
49 49
src/views/demo/comp/tinymce/index.vue renamed to src/views/demo/editor/tinymce/index.vue
1 <template> 1 <template>
2 <div class="flex p-4"> 2 <div class="flex p-4">
3 - <Tinymce value="Hello, World!" @change="handleChange" width="100%" /> 3 + {{ value }}
  4 + <Tinymce v-model="value" @change="handleChange" width="100%" />
4 </div> 5 </div>
5 </template> 6 </template>
6 <script lang="ts"> 7 <script lang="ts">
7 - import { defineComponent } from 'vue'; 8 + import { defineComponent, ref } from 'vue';
8 import { Tinymce } from '/@/components/Tinymce/index'; 9 import { Tinymce } from '/@/components/Tinymce/index';
9 10
10 export default defineComponent({ 11 export default defineComponent({
11 components: { Tinymce }, 12 components: { Tinymce },
12 setup() { 13 setup() {
  14 + const value = ref('hello world!');
13 function handleChange(value: string) { 15 function handleChange(value: string) {
14 console.log(value); 16 console.log(value);
15 } 17 }
16 - return { handleChange }; 18 + // setTimeout(() => {
  19 + // value.value = '1233';
  20 + // }, 5000);
  21 + return { handleChange, value };
17 }, 22 },
18 }); 23 });
19 </script> 24 </script>