Commit f75425d13bc9f6003021fd4b5d6451ae096c09b7
1 parent
9c02d8ec
perf: review tinymce code
Showing
25 changed files
with
270 additions
and
455 deletions
src/components/Tinymce/src/Editor.vue
1 | 1 | <template> |
2 | 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 | 4 | </div> |
11 | 5 | </template> |
12 | 6 | |
13 | 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 | 19 | import { basicProps } from './props'; |
17 | 20 | import toolbar from './toolbar'; |
18 | 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 | 27 | const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1'; |
28 | + | |
21 | 29 | const tinymceScriptSrc = `${CDN_URL}/tinymce.min.js`; |
22 | 30 | |
23 | 31 | export default defineComponent({ |
24 | 32 | name: 'Tinymce', |
25 | - components: { TinymceEditor }, | |
26 | 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 | 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 | 47 | const containerWidth = computed(() => { |
35 | 48 | const width = props.width; |
36 | - // Test matches `100`, `'100'` | |
37 | 49 | if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) { |
38 | 50 | return `${width}px`; |
39 | 51 | } |
40 | 52 | return width; |
41 | 53 | }); |
54 | + | |
42 | 55 | const initOptions = computed(() => { |
43 | - const { id, height, menubar } = props; | |
56 | + const { height, menubar } = props; | |
44 | 57 | return { |
45 | - selector: `#${id}`, | |
58 | + selector: `#${unref(tinymceId)}`, | |
46 | 59 | height: height, |
47 | 60 | toolbar: toolbar, |
61 | + theme: 'silver', | |
48 | 62 | menubar: menubar, |
49 | 63 | plugins: plugins, |
50 | 64 | // 语言包 |
51 | 65 | language_url: 'resource/tinymce/langs/zh_CN.js', |
52 | 66 | // 中文 |
53 | 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 | 169 | </script> | ... | ... |
src/components/Tinymce/src/lib/TinyMCE.ts renamed to src/components/Tinymce/src/getTinymce.ts
src/components/Tinymce/src/lib/Utils.ts renamed to src/components/Tinymce/src/helper.ts
1 | -import { ComponentPublicInstance } from 'vue'; | |
2 | - | |
3 | 1 | const validEvents = [ |
4 | 2 | 'onActivate', |
5 | 3 | 'onAddUndo', |
... | ... | @@ -62,12 +60,12 @@ const validEvents = [ |
62 | 60 | 'onShow', |
63 | 61 | 'onSubmit', |
64 | 62 | 'onUndo', |
65 | - 'onVisualAid' | |
63 | + 'onVisualAid', | |
66 | 64 | ]; |
67 | 65 | |
68 | 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 | 69 | Object.keys(listeners) |
72 | 70 | .filter(isValidKey) |
73 | 71 | .forEach((key: string) => { |
... | ... | @@ -81,71 +79,3 @@ const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => { |
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 | 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 | 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
src/components/Tinymce/src/lib/index.ts deleted
100644 → 0
src/components/Tinymce/src/props.ts
1 | 1 | import { PropType } from 'vue'; |
2 | 2 | |
3 | 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 | 4 | menubar: { |
11 | 5 | type: String as PropType<string>, |
12 | 6 | default: 'file edit insert view format table', |
... | ... | @@ -15,6 +9,10 @@ export const basicProps = { |
15 | 9 | type: String as PropType<string>, |
16 | 10 | // default: '' |
17 | 11 | }, |
12 | + modelValue: { | |
13 | + type: String as PropType<string>, | |
14 | + // default: '' | |
15 | + }, | |
18 | 16 | // 高度 |
19 | 17 | height: { |
20 | 18 | type: [Number, String] as PropType<string | number>, | ... | ... |
src/router/menus/modules/demo/charts.ts
... | ... | @@ -6,23 +6,23 @@ const menu: MenuModule = { |
6 | 6 | path: '/charts', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/apexChart', | |
9 | + path: 'apexChart', | |
10 | 10 | name: 'ApexChart', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/echarts', | |
13 | + path: 'echarts', | |
14 | 14 | name: 'Echarts', |
15 | 15 | children: [ |
16 | 16 | { |
17 | - path: '/map', | |
17 | + path: 'map', | |
18 | 18 | name: '地图', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/line', | |
21 | + path: 'line', | |
22 | 22 | name: '折线图', |
23 | 23 | }, |
24 | 24 | { |
25 | - path: '/pie', | |
25 | + path: 'pie', | |
26 | 26 | name: '饼图', |
27 | 27 | }, |
28 | 28 | ], | ... | ... |
src/router/menus/modules/demo/comp.ts
... | ... | @@ -6,16 +6,16 @@ const menu: MenuModule = { |
6 | 6 | path: '/comp', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/basic', | |
9 | + path: 'basic', | |
10 | 10 | name: '基础组件', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/countTo', | |
13 | + path: 'countTo', | |
14 | 14 | name: '数字动画', |
15 | 15 | }, |
16 | 16 | |
17 | 17 | { |
18 | - path: '/scroll', | |
18 | + path: 'scroll', | |
19 | 19 | name: '滚动组件', |
20 | 20 | children: [ |
21 | 21 | { |
... | ... | @@ -33,53 +33,39 @@ const menu: MenuModule = { |
33 | 33 | ], |
34 | 34 | }, |
35 | 35 | { |
36 | - path: '/modal', | |
36 | + path: 'modal', | |
37 | 37 | name: '弹窗扩展', |
38 | 38 | }, |
39 | 39 | { |
40 | - path: '/drawer', | |
40 | + path: 'drawer', | |
41 | 41 | name: '抽屉扩展', |
42 | 42 | }, |
43 | 43 | { |
44 | - path: '/desc', | |
44 | + path: 'desc', | |
45 | 45 | name: '详情组件', |
46 | 46 | }, |
47 | 47 | { |
48 | - path: '/verify', | |
48 | + path: 'verify', | |
49 | 49 | name: '验证组件', |
50 | 50 | children: [ |
51 | 51 | { |
52 | - path: '/drag', | |
52 | + path: 'drag', | |
53 | 53 | name: '拖拽校验', |
54 | 54 | }, |
55 | 55 | { |
56 | - path: '/rotate', | |
56 | + path: 'rotate', | |
57 | 57 | name: '图片还原校验', |
58 | 58 | }, |
59 | 59 | ], |
60 | 60 | }, |
61 | 61 | { |
62 | - path: '/qrcode', | |
62 | + path: 'qrcode', | |
63 | 63 | name: '二维码组件', |
64 | 64 | }, |
65 | 65 | { |
66 | - path: '/strength-meter', | |
66 | + path: 'strength-meter', | |
67 | 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 | 6 | path: '/editor', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/markdown', | |
9 | + path: 'markdown', | |
10 | 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 | 6 | path: '/excel', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/customExport', | |
9 | + path: 'customExport', | |
10 | 10 | name: '选择导出格式', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/jsonExport', | |
13 | + path: 'jsonExport', | |
14 | 14 | name: 'JSON数据导出', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/arrayExport', | |
17 | + path: 'arrayExport', | |
18 | 18 | name: 'Array数据导出', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/importExcel', | |
21 | + path: 'importExcel', | |
22 | 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 | 6 | path: '/exception', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/404', | |
9 | + path: '404', | |
10 | 10 | name: '404', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/500', | |
13 | + path: '500', | |
14 | 14 | name: '500', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/net-work-error', | |
17 | + path: 'net-work-error', | |
18 | 18 | name: '网络错误', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/page-time-out', | |
21 | + path: 'page-time-out', | |
22 | 22 | name: '页面超时', |
23 | 23 | }, |
24 | 24 | { |
25 | - path: '/not-data', | |
25 | + path: 'not-data', | |
26 | 26 | name: '无数据', |
27 | 27 | }, |
28 | 28 | { |
29 | - path: '/error-log', | |
29 | + path: 'error-log', | |
30 | 30 | name: '错误日志', |
31 | 31 | }, |
32 | 32 | ], | ... | ... |
src/router/menus/modules/demo/feat.ts
... | ... | @@ -6,55 +6,55 @@ const menu: MenuModule = { |
6 | 6 | path: '/feat', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/icon', | |
9 | + path: 'icon', | |
10 | 10 | name: '图标', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/tabs', | |
13 | + path: 'tabs', | |
14 | 14 | name: '标签页操作', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/context-menu', | |
17 | + path: 'context-menu', | |
18 | 18 | name: '右键菜单', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/click-out-side', | |
21 | + path: 'click-out-side', | |
22 | 22 | name: 'ClickOutSide', |
23 | 23 | }, |
24 | 24 | { |
25 | - path: '/img-preview', | |
25 | + path: 'img-preview', | |
26 | 26 | name: '图片预览', |
27 | 27 | }, |
28 | 28 | { |
29 | - path: '/i18n', | |
29 | + path: 'i18n', | |
30 | 30 | name: '国际化', |
31 | 31 | }, |
32 | 32 | { |
33 | - path: '/copy', | |
33 | + path: 'copy', | |
34 | 34 | name: '剪切板', |
35 | 35 | }, |
36 | 36 | { |
37 | - path: '/msg', | |
37 | + path: 'msg', | |
38 | 38 | name: '消息提示', |
39 | 39 | }, |
40 | 40 | { |
41 | - path: '/watermark', | |
41 | + path: 'watermark', | |
42 | 42 | name: '水印', |
43 | 43 | }, |
44 | 44 | { |
45 | - path: '/full-screen', | |
45 | + path: 'full-screen', | |
46 | 46 | name: '全屏', |
47 | 47 | }, |
48 | 48 | { |
49 | - path: '/testTab', | |
49 | + path: 'testTab', | |
50 | 50 | name: '带参Tab', |
51 | 51 | children: [ |
52 | 52 | { |
53 | - path: '/id1', | |
53 | + path: 'id1', | |
54 | 54 | name: '带参tab1', |
55 | 55 | }, |
56 | 56 | { |
57 | - path: '/id2', | |
57 | + path: 'id2', | |
58 | 58 | name: '带参tab2', |
59 | 59 | }, |
60 | 60 | ], | ... | ... |
src/router/menus/modules/demo/form.ts
... | ... | @@ -6,31 +6,31 @@ const menu: MenuModule = { |
6 | 6 | name: 'Form', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/basic', | |
9 | + path: 'basic', | |
10 | 10 | name: '基础表单', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/useForm', | |
13 | + path: 'useForm', | |
14 | 14 | name: 'useForm', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/refForm', | |
17 | + path: 'refForm', | |
18 | 18 | name: 'RefForm', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/advancedForm', | |
21 | + path: 'advancedForm', | |
22 | 22 | name: '可收缩表单', |
23 | 23 | }, |
24 | 24 | { |
25 | - path: '/ruleForm', | |
25 | + path: 'ruleForm', | |
26 | 26 | name: '表单校验', |
27 | 27 | }, |
28 | 28 | { |
29 | - path: '/dynamicForm', | |
29 | + path: 'dynamicForm', | |
30 | 30 | name: '动态表单', |
31 | 31 | }, |
32 | 32 | { |
33 | - path: '/customerForm', | |
33 | + path: 'customerForm', | |
34 | 34 | name: '自定义组件', |
35 | 35 | }, |
36 | 36 | ], | ... | ... |
src/router/menus/modules/demo/iframe.ts
... | ... | @@ -6,15 +6,15 @@ const menu: MenuModule = { |
6 | 6 | path: '/frame', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/antv', | |
9 | + path: 'antv', | |
10 | 10 | name: 'antVue文档(内嵌)', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/doc', | |
13 | + path: 'doc', | |
14 | 14 | name: '项目文档(内嵌)', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/docExternal', | |
17 | + path: 'docExternal', | |
18 | 18 | name: '项目文档(外链)', |
19 | 19 | }, |
20 | 20 | ], | ... | ... |
src/router/menus/modules/demo/permission.ts
... | ... | @@ -6,37 +6,37 @@ const menu: MenuModule = { |
6 | 6 | path: '/permission', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/front', | |
9 | + path: 'front', | |
10 | 10 | name: '基于前端', |
11 | 11 | children: [ |
12 | 12 | { |
13 | - path: '/page', | |
13 | + path: 'page', | |
14 | 14 | name: '页面权限', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/btn', | |
17 | + path: 'btn', | |
18 | 18 | name: '按钮权限', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/auth-pageA', | |
21 | + path: 'auth-pageA', | |
22 | 22 | name: '权限测试页A', |
23 | 23 | }, |
24 | 24 | { |
25 | - path: '/auth-pageB', | |
25 | + path: 'auth-pageB', | |
26 | 26 | name: '权限测试页B', |
27 | 27 | }, |
28 | 28 | ], |
29 | 29 | }, |
30 | 30 | { |
31 | - path: '/back', | |
31 | + path: 'back', | |
32 | 32 | name: '基于后台', |
33 | 33 | children: [ |
34 | 34 | { |
35 | - path: '/page', | |
35 | + path: 'page', | |
36 | 36 | name: '页面权限', |
37 | 37 | }, |
38 | 38 | { |
39 | - path: '/btn', | |
39 | + path: 'btn', | |
40 | 40 | name: '按钮权限', |
41 | 41 | }, |
42 | 42 | ], | ... | ... |
src/router/menus/modules/demo/table.ts
... | ... | @@ -6,59 +6,59 @@ const menu: MenuModule = { |
6 | 6 | name: 'Table', |
7 | 7 | children: [ |
8 | 8 | { |
9 | - path: '/basic', | |
9 | + path: 'basic', | |
10 | 10 | name: '基础表格', |
11 | 11 | }, |
12 | 12 | { |
13 | - path: '/treeTable', | |
13 | + path: 'treeTable', | |
14 | 14 | name: '树形表格', |
15 | 15 | }, |
16 | 16 | { |
17 | - path: '/fetchTable', | |
17 | + path: 'fetchTable', | |
18 | 18 | name: '远程加载', |
19 | 19 | }, |
20 | 20 | { |
21 | - path: '/fixedColumn', | |
21 | + path: 'fixedColumn', | |
22 | 22 | name: '固定列', |
23 | 23 | }, |
24 | 24 | { |
25 | - path: '/customerCell', | |
25 | + path: 'customerCell', | |
26 | 26 | name: '自定义列', |
27 | 27 | }, |
28 | 28 | { |
29 | - path: '/formTable', | |
29 | + path: 'formTable', | |
30 | 30 | name: '开启搜索区域', |
31 | 31 | }, |
32 | 32 | { |
33 | - path: '/useTable', | |
33 | + path: 'useTable', | |
34 | 34 | name: 'UseTable', |
35 | 35 | }, |
36 | 36 | { |
37 | - path: '/refTable', | |
37 | + path: 'refTable', | |
38 | 38 | name: 'RefTable', |
39 | 39 | }, |
40 | 40 | { |
41 | - path: '/multipleHeader', | |
41 | + path: 'multipleHeader', | |
42 | 42 | name: '多级表头', |
43 | 43 | }, |
44 | 44 | { |
45 | - path: '/mergeHeader', | |
45 | + path: 'mergeHeader', | |
46 | 46 | name: '合并单元格', |
47 | 47 | }, |
48 | 48 | { |
49 | - path: '/expandTable', | |
49 | + path: 'expandTable', | |
50 | 50 | name: '可展开表格', |
51 | 51 | }, |
52 | 52 | { |
53 | - path: '/fixedHeight', | |
53 | + path: 'fixedHeight', | |
54 | 54 | name: '定高/头部自定义', |
55 | 55 | }, |
56 | 56 | { |
57 | - path: '/footerTable', | |
57 | + path: 'footerTable', | |
58 | 58 | name: '表尾行合计', |
59 | 59 | }, |
60 | 60 | { |
61 | - path: '/editCellTable', | |
61 | + path: 'editCellTable', | |
62 | 62 | name: '可编辑单元格', |
63 | 63 | }, |
64 | 64 | ], | ... | ... |
src/router/routes/modules/demo/comp.ts
... | ... | @@ -136,31 +136,5 @@ export default { |
136 | 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 | 140 | } as AppRouteModule; | ... | ... |
src/router/routes/modules/demo/editor.ts
... | ... | @@ -23,5 +23,32 @@ export default { |
23 | 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 | 54 | } as AppRouteModule; | ... | ... |
src/utils/is.ts
... | ... | @@ -67,3 +67,7 @@ export const isServer = typeof window === 'undefined'; |
67 | 67 | export function isImageDom(o: Element) { |
68 | 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 | 17 | } |
18 | 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
src/views/demo/comp/tinymce/index.vue renamed to src/views/demo/editor/tinymce/index.vue
1 | 1 | <template> |
2 | 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 | 5 | </div> |
5 | 6 | </template> |
6 | 7 | <script lang="ts"> |
7 | - import { defineComponent } from 'vue'; | |
8 | + import { defineComponent, ref } from 'vue'; | |
8 | 9 | import { Tinymce } from '/@/components/Tinymce/index'; |
9 | 10 | |
10 | 11 | export default defineComponent({ |
11 | 12 | components: { Tinymce }, |
12 | 13 | setup() { |
14 | + const value = ref('hello world!'); | |
13 | 15 | function handleChange(value: string) { |
14 | 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 | 24 | </script> | ... | ... |