@tmagic/editor 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/style.css +505 -0
  2. package/dist/tmagic-editor.es.js +4233 -0
  3. package/dist/tmagic-editor.es.js.map +1 -0
  4. package/dist/tmagic-editor.umd.js +4276 -0
  5. package/dist/tmagic-editor.umd.js.map +1 -0
  6. package/dist/types/src/Editor.vue.d.ts +136 -0
  7. package/dist/types/src/components/Icon.vue.d.ts +13 -0
  8. package/dist/types/src/components/ToolButton.vue.d.ts +35 -0
  9. package/dist/types/src/fields/Code.vue.d.ts +24 -0
  10. package/dist/types/src/fields/CodeLink.vue.d.ts +54 -0
  11. package/dist/types/src/fields/UISelect.vue.d.ts +32 -0
  12. package/dist/types/src/index.d.ts +19 -0
  13. package/dist/types/src/layouts/AddPageBox.vue.d.ts +5 -0
  14. package/dist/types/src/layouts/CodeEditor.vue.d.ts +46 -0
  15. package/dist/types/src/layouts/Framework.vue.d.ts +11 -0
  16. package/dist/types/src/layouts/NavMenu.vue.d.ts +24 -0
  17. package/dist/types/src/layouts/PropsPanel.vue.d.ts +13 -0
  18. package/dist/types/src/layouts/Resizer.vue.d.ts +13 -0
  19. package/dist/types/src/layouts/sidebar/ComponentListPanel.vue.d.ts +12 -0
  20. package/dist/types/src/layouts/sidebar/LayerMenu.vue.d.ts +31 -0
  21. package/dist/types/src/layouts/sidebar/LayerPanel.vue.d.ts +973 -0
  22. package/dist/types/src/layouts/sidebar/Sidebar.vue.d.ts +27 -0
  23. package/dist/types/src/layouts/workspace/PageBar.vue.d.ts +11 -0
  24. package/dist/types/src/layouts/workspace/Stage.vue.d.ts +153 -0
  25. package/dist/types/src/layouts/workspace/ViewerMenu.vue.d.ts +18 -0
  26. package/dist/types/src/layouts/workspace/Workspace.vue.d.ts +37 -0
  27. package/dist/types/src/services/BaseService.d.ts +56 -0
  28. package/dist/types/src/services/editor.d.ts +122 -0
  29. package/dist/types/src/services/events.d.ts +16 -0
  30. package/dist/types/src/services/history.d.ts +51 -0
  31. package/dist/types/src/services/props.d.ts +36 -0
  32. package/dist/types/src/services/ui.d.ts +26 -0
  33. package/dist/types/src/shims-vue.d.ts +6 -0
  34. package/dist/types/src/type.d.ts +183 -0
  35. package/dist/types/src/utils/compose.d.ts +5 -0
  36. package/dist/types/src/utils/config.d.ts +4 -0
  37. package/dist/types/src/utils/editor.d.ts +45 -0
  38. package/dist/types/src/utils/index.d.ts +4 -0
  39. package/dist/types/src/utils/logger.d.ts +5 -0
  40. package/dist/types/src/utils/props.d.ts +49 -0
  41. package/dist/types/src/utils/undo-redo.d.ts +12 -0
  42. package/dist/types/src/vite-env.d.ts +1 -0
  43. package/package.json +57 -0
  44. package/src/Editor.vue +234 -0
  45. package/src/components/Icon.vue +29 -0
  46. package/src/components/ToolButton.vue +188 -0
  47. package/src/fields/Code.vue +35 -0
  48. package/src/fields/CodeLink.vue +81 -0
  49. package/src/fields/UISelect.vue +100 -0
  50. package/src/index.ts +63 -0
  51. package/src/layouts/AddPageBox.vue +41 -0
  52. package/src/layouts/CodeEditor.vue +188 -0
  53. package/src/layouts/Framework.vue +73 -0
  54. package/src/layouts/NavMenu.vue +37 -0
  55. package/src/layouts/PropsPanel.vue +81 -0
  56. package/src/layouts/Resizer.vue +61 -0
  57. package/src/layouts/sidebar/ComponentListPanel.vue +69 -0
  58. package/src/layouts/sidebar/LayerMenu.vue +117 -0
  59. package/src/layouts/sidebar/LayerPanel.vue +209 -0
  60. package/src/layouts/sidebar/Sidebar.vue +86 -0
  61. package/src/layouts/workspace/PageBar.vue +83 -0
  62. package/src/layouts/workspace/Stage.vue +221 -0
  63. package/src/layouts/workspace/ViewerMenu.vue +110 -0
  64. package/src/layouts/workspace/Workspace.vue +98 -0
  65. package/src/services/BaseService.ts +138 -0
  66. package/src/services/componentList.ts +48 -0
  67. package/src/services/editor.ts +521 -0
  68. package/src/services/events.ts +82 -0
  69. package/src/services/history.ts +124 -0
  70. package/src/services/props.ts +110 -0
  71. package/src/services/ui.ts +101 -0
  72. package/src/shims-vue.d.ts +6 -0
  73. package/src/theme/code-editor.scss +7 -0
  74. package/src/theme/common/var.scss +13 -0
  75. package/src/theme/component-list-panel.scss +102 -0
  76. package/src/theme/content-menu.scss +39 -0
  77. package/src/theme/framework.scss +72 -0
  78. package/src/theme/index.scss +13 -0
  79. package/src/theme/layer-panel.scss +68 -0
  80. package/src/theme/nav-menu.scss +69 -0
  81. package/src/theme/page-bar.scss +31 -0
  82. package/src/theme/props-panel.scss +17 -0
  83. package/src/theme/resizer.scss +51 -0
  84. package/src/theme/ruler.scss +38 -0
  85. package/src/theme/sidebar.scss +97 -0
  86. package/src/theme/stage.scss +22 -0
  87. package/src/theme/workspace.scss +5 -0
  88. package/src/type.ts +236 -0
  89. package/src/utils/compose.ts +33 -0
  90. package/src/utils/config.ts +29 -0
  91. package/src/utils/editor.ts +244 -0
  92. package/src/utils/index.ts +22 -0
  93. package/src/utils/logger.ts +47 -0
  94. package/src/utils/props.ts +218 -0
  95. package/src/utils/undo-redo.ts +76 -0
  96. package/src/vite-env.d.ts +1 -0
  97. package/tsconfig.json +9 -0
  98. package/vite.config.ts +30 -0
@@ -0,0 +1,188 @@
1
+ <template>
2
+ <div v-if="display" class="menu-item">
3
+ <el-divider v-if="item.type === 'divider'" direction="vertical"></el-divider>
4
+ <div v-else-if="item.type === 'text'" class="menu-item-text">{{ item.text }}</div>
5
+
6
+ <template v-else-if="item.type === 'zoom'">
7
+ <m-icon :icon="ZoomIn" @click="zoomInHandler"></m-icon>
8
+ <span class="menu-item-text" style="margin: 0 5px">{{ parseInt(`${zoom * 100}`, 10) }}%</span>
9
+ <m-icon :icon="ZoomOut" @click="zoomOutHandler"></m-icon>
10
+ </template>
11
+
12
+ <el-tooltip
13
+ v-else-if="item.type === 'button'"
14
+ effect="dark"
15
+ placement="bottom-start"
16
+ :content="item.tooltip || item.text"
17
+ >
18
+ <el-button size="small" type="text" :disabled="disabled" @click="buttonHandler(item)"
19
+ ><m-icon :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
20
+ >
21
+ </el-tooltip>
22
+
23
+ <el-dropdown v-else-if="item.type === 'dropdown'" trigger="click" :disabled="disabled" @command="dropdownHandler">
24
+ <span class="el-dropdown-link menubar-menu-button">
25
+ {{ item.text }}<el-icon class="el-icon--right"><arrow-down></arrow-down></el-icon>
26
+ </span>
27
+ <template #dropdown>
28
+ <el-dropdown-menu v-if="item.items && item.items.length">
29
+ <el-dropdown-item v-for="(subItem, index) in item.items" :key="index" :command="{ item, subItem }">{{
30
+ subItem.text
31
+ }}</el-dropdown-item>
32
+ </el-dropdown-menu>
33
+ </template>
34
+ </el-dropdown>
35
+
36
+ <component v-else-if="item.type === 'component'" v-bind="item.props || {}" :is="item.component"></component>
37
+ </div>
38
+ </template>
39
+
40
+ <script lang="ts">
41
+ import { computed, defineComponent, inject, PropType } from 'vue';
42
+ import { ArrowDown, Back, Delete, Grid, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons';
43
+
44
+ import MIcon from '@editor/components/Icon.vue';
45
+ import type { MenuButton, MenuComponent, MenuItem, Services } from '@editor/type';
46
+
47
+ export default defineComponent({
48
+ components: { MIcon, ArrowDown },
49
+
50
+ props: {
51
+ data: {
52
+ type: [Object, String] as PropType<MenuItem | string>,
53
+ require: true,
54
+ default: () => ({
55
+ type: 'text',
56
+ display: false,
57
+ }),
58
+ },
59
+ },
60
+
61
+ setup(props) {
62
+ const services = inject<Services>('services');
63
+ const uiService = services?.uiService;
64
+
65
+ const zoomInHandler = () => uiService?.set('zoom', zoom.value + 0.1);
66
+ const zoomOutHandler = () => uiService?.set('zoom', zoom.value - 0.1);
67
+
68
+ const zoom = computed((): number => uiService?.get<number>('zoom') ?? 1);
69
+ const showGuides = computed((): boolean => uiService?.get<boolean>('showGuides') ?? true);
70
+ const showRule = computed((): boolean => uiService?.get<boolean>('showRule') ?? true);
71
+
72
+ const item = computed((): MenuButton | MenuComponent => {
73
+ if (typeof props.data !== 'string') {
74
+ return props.data;
75
+ }
76
+ switch (props.data) {
77
+ case '/':
78
+ return {
79
+ type: 'divider',
80
+ };
81
+ case 'zoom':
82
+ return {
83
+ type: 'zoom',
84
+ };
85
+ case 'delete':
86
+ return {
87
+ type: 'button',
88
+ icon: Delete,
89
+ tooltip: '刪除',
90
+ disabled: () => services?.editorService.get('node')?.type === 'page',
91
+ handler: () => services?.editorService.remove(services?.editorService.get('node')),
92
+ };
93
+ case 'undo':
94
+ return {
95
+ type: 'button',
96
+ icon: Back,
97
+ tooltip: '后退',
98
+ disabled: () => !services?.historyService.state.canUndo,
99
+ handler: () => services?.editorService.undo(),
100
+ };
101
+ case 'redo':
102
+ return {
103
+ type: 'button',
104
+ icon: Right,
105
+ tooltip: '前进',
106
+ disabled: () => !services?.historyService.state.canRedo,
107
+ handler: () => services?.editorService.redo(),
108
+ };
109
+ case 'zoom-in':
110
+ return {
111
+ type: 'button',
112
+ icon: ZoomIn,
113
+ tooltip: '放大',
114
+ handler: zoomInHandler,
115
+ };
116
+ case 'zoom-out':
117
+ return {
118
+ type: 'button',
119
+ icon: ZoomOut,
120
+ tooltip: '縮小',
121
+ handler: zoomOutHandler,
122
+ };
123
+ case 'rule':
124
+ return {
125
+ type: 'button',
126
+ icon: ScaleToOriginal,
127
+ tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
128
+ handler: () => uiService?.set('showRule', !showRule.value),
129
+ };
130
+ case 'guides':
131
+ return {
132
+ type: 'button',
133
+ icon: Grid,
134
+ tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
135
+ handler: () => uiService?.set('showGuides', !showGuides.value),
136
+ };
137
+ default:
138
+ return {
139
+ type: 'text',
140
+ text: props.data,
141
+ };
142
+ }
143
+ });
144
+
145
+ const disabled = computed(() => {
146
+ if (typeof item.value === 'string') return false;
147
+ if (item.value.type === 'component') return false;
148
+ if (typeof item.value.disabled === 'function') {
149
+ return item.value.disabled(services);
150
+ }
151
+ return item.value.disabled;
152
+ });
153
+
154
+ return {
155
+ ZoomIn,
156
+ ZoomOut,
157
+
158
+ item,
159
+ zoom,
160
+ disabled,
161
+ display: computed(() => {
162
+ if (!item.value) return false;
163
+ if (typeof item.value === 'string') return true;
164
+ if (typeof item.value.display === 'function') {
165
+ return item.value.display(services);
166
+ }
167
+ return item.value.display ?? true;
168
+ }),
169
+
170
+ zoomInHandler,
171
+ zoomOutHandler,
172
+
173
+ dropdownHandler(command: any) {
174
+ if (command.item.handler) {
175
+ command.item.handler(services);
176
+ }
177
+ },
178
+
179
+ buttonHandler(item: MenuButton | MenuComponent) {
180
+ if (disabled.value) return;
181
+ if (typeof (item as MenuButton).handler === 'function') {
182
+ (item as MenuButton).handler?.(services);
183
+ }
184
+ },
185
+ };
186
+ },
187
+ });
188
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <magic-code-editor
3
+ :style="`height: ${height}`"
4
+ :init-values="model[name]"
5
+ :language="language"
6
+ @save="save"
7
+ ></magic-code-editor>
8
+ </template>
9
+
10
+ <script lang="ts">
11
+ import { computed, defineComponent } from 'vue';
12
+
13
+ export default defineComponent({
14
+ name: 'm-fields-vs-code',
15
+
16
+ props: ['model', 'name', 'config', 'prop'],
17
+
18
+ emits: ['change'],
19
+
20
+ setup(props, { emit }) {
21
+ const language = computed(() => props.config.language || 'javascript');
22
+ const height = computed(() => `${document.body.clientHeight - 168}px`);
23
+
24
+ return {
25
+ height,
26
+ language,
27
+
28
+ save(v: string) {
29
+ props.model[props.name] = v;
30
+ emit('change', v);
31
+ },
32
+ };
33
+ },
34
+ });
35
+ </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <m-fields-link :config="formConfig" :model="modelValue" name="form" @change="changeHandler"></m-fields-link>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { computed, defineComponent, PropType, ref, watchEffect } from 'vue';
7
+ import serialize from 'serialize-javascript';
8
+
9
+ interface CodeLinkConfig {
10
+ type: 'code-link';
11
+ name: string;
12
+ text?: string;
13
+ formTitle?: string;
14
+ }
15
+
16
+ export default defineComponent({
17
+ name: 'm-fields-code-link',
18
+
19
+ props: {
20
+ config: {
21
+ type: Object as PropType<CodeLinkConfig>,
22
+ },
23
+
24
+ model: {
25
+ type: Object,
26
+ },
27
+
28
+ name: {
29
+ type: String,
30
+ },
31
+
32
+ prop: {
33
+ type: String,
34
+ },
35
+ },
36
+
37
+ emits: ['change'],
38
+
39
+ setup(props, { emit }) {
40
+ const modelValue = ref<{ form: Record<string, any> }>({
41
+ form: {},
42
+ });
43
+
44
+ watchEffect(() => {
45
+ if (!props.model || !props.name) return;
46
+ modelValue.value.form[props.name] = serialize(props.model[props.name], {
47
+ space: 2,
48
+ unsafe: true,
49
+ }).replace(/"(\w+)":\s/g, '$1: ');
50
+ });
51
+
52
+ return {
53
+ modelValue,
54
+
55
+ formConfig: computed(() => ({
56
+ ...props.config,
57
+ text: '',
58
+ type: 'link',
59
+ form: [
60
+ {
61
+ name: props.name,
62
+ type: 'vs-code',
63
+ },
64
+ ],
65
+ })),
66
+
67
+ changeHandler(v: Record<string, any>) {
68
+ if (!props.name || !props.model) return;
69
+
70
+ try {
71
+ // eslint-disable-next-line no-eval
72
+ props.model[props.name] = eval(`${v[props.name]}`);
73
+ emit('change', props.model[props.name]);
74
+ } catch (e) {
75
+ console.error(e);
76
+ }
77
+ },
78
+ };
79
+ },
80
+ });
81
+ </script>
@@ -0,0 +1,100 @@
1
+ <template>
2
+ <div class="m-fields-ui-select" v-if="uiSelectMode" @click="cancelHandler">
3
+ <i class="el-icon-delete" style="color: rgb(221, 75, 57)">取消</i>
4
+ </div>
5
+ <div class="m-fields-ui-select" v-else @click="startSelect">
6
+ <i class="el-icon-thumb"></i>
7
+ <span>{{ val ? toName + '_' + val : '点击此处选择' }}</span>
8
+ <i class="el-icon-delete" @click.stop="deleteHandler" v-if="val"></i>
9
+ </div>
10
+ </template>
11
+
12
+ <script lang="ts">
13
+ import { computed, defineComponent, inject, ref } from 'vue';
14
+
15
+ import { FormState } from '@tmagic/form';
16
+
17
+ import { Services } from '@editor/type';
18
+
19
+ export default defineComponent({
20
+ name: 'm-fields-ui-select',
21
+
22
+ props: {
23
+ labelWidth: String,
24
+ config: Object,
25
+ model: Object,
26
+ prop: {
27
+ type: String,
28
+ default() {
29
+ return '';
30
+ },
31
+ },
32
+ name: String,
33
+ },
34
+
35
+ emits: ['change'],
36
+
37
+ setup(props: any, { emit }) {
38
+ const services = inject<Services>('services');
39
+ const mForm = inject<FormState>('mForm');
40
+ const val = computed(() => props.model[props.name]);
41
+ const uiSelectMode = ref(false);
42
+
43
+ const cancelHandler = () => {
44
+ if (!services?.uiService) return;
45
+ services.uiService.set<boolean>('uiSelectMode', false);
46
+ uiSelectMode.value = false;
47
+ globalThis.document.removeEventListener('ui-select', clickHandler as EventListener);
48
+ };
49
+
50
+ const clickHandler = ({ detail }: Event & { detail: any }) => {
51
+ if (detail.id) {
52
+ props.model[props.name] = detail.id;
53
+ emit('change', detail.id);
54
+ mForm?.$emit('field-change', props.prop, detail.id);
55
+ }
56
+
57
+ if (cancelHandler) {
58
+ cancelHandler();
59
+ }
60
+ };
61
+
62
+ return {
63
+ val,
64
+ uiSelectMode,
65
+ toName: computed(() => {
66
+ const config = services?.editorService.getNodeById(val.value);
67
+ return config?.name || '';
68
+ }),
69
+
70
+ startSelect() {
71
+ if (!services?.uiService) return;
72
+ services.uiService.set<boolean>('uiSelectMode', true);
73
+ uiSelectMode.value = true;
74
+ globalThis.document.addEventListener('ui-select', clickHandler as EventListener);
75
+ },
76
+
77
+ cancelHandler,
78
+
79
+ deleteHandler() {
80
+ if (props.model) {
81
+ props.model[props.name] = '';
82
+ emit('change', '');
83
+ mForm?.$emit('field-change', props.prop, '');
84
+ }
85
+ },
86
+ };
87
+ },
88
+ });
89
+ </script>
90
+ <style lang="scss">
91
+ .m-fields-ui-select {
92
+ cursor: pointer;
93
+ i {
94
+ margin-right: 3px;
95
+ }
96
+ span {
97
+ color: #2882e0;
98
+ }
99
+ }
100
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import { App } from 'vue';
20
+
21
+ import Code from './fields/Code.vue';
22
+ import CodeLink from './fields/CodeLink.vue';
23
+ import uiSelect from './fields/UISelect.vue';
24
+ import CodeEditor from './layouts/CodeEditor.vue';
25
+ import { setConfig } from './utils/config';
26
+ import Editor from './Editor.vue';
27
+ import type { InstallOptions } from './type';
28
+
29
+ import './theme/index.scss';
30
+
31
+ export type { MoveableOptions } from '@tmagic/stage';
32
+ export * from './type';
33
+ export * from './utils';
34
+ export { default as TMagicEditor } from './Editor.vue';
35
+ export { default as TMagicCodeEditor } from './layouts/CodeEditor.vue';
36
+ export { default as editorService } from './services/editor';
37
+ export { default as propsService } from './services/props';
38
+ export { default as historyService } from './services/history';
39
+ export { default as eventsService } from './services/events';
40
+ export { default as uiService } from './services/ui';
41
+ export { default as ComponentListPanel } from './layouts/sidebar/ComponentListPanel.vue';
42
+ export { default as LayerPanel } from './layouts/sidebar/LayerPanel.vue';
43
+ export { default as PropsPanel } from './layouts/PropsPanel.vue';
44
+
45
+ const defaultInstallOpt: InstallOptions = {
46
+ // @todo, 自定义图片上传方法等编辑器依赖的外部选项
47
+ };
48
+
49
+ export default {
50
+ install: (app: App, opt?: InstallOptions): void => {
51
+ const option = Object.assign(defaultInstallOpt, opt || {});
52
+
53
+ // eslint-disable-next-line no-param-reassign
54
+ app.config.globalProperties.$TMAGIC_EDITOR = option;
55
+ setConfig(option);
56
+
57
+ app.component(Editor.name, Editor);
58
+ app.component(uiSelect.name, uiSelect);
59
+ app.component(CodeLink.name, CodeLink);
60
+ app.component(Code.name, Code);
61
+ app.component(CodeEditor.name, CodeEditor);
62
+ },
63
+ };
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="m-editor-empty-panel">
3
+ <div class="m-editor-empty-content">
4
+ <div class="m-editor-empty-button" @click="clickHandler">
5
+ <div>
6
+ <el-icon><plus /></el-icon>
7
+ </div>
8
+ <p>新增页面</p>
9
+ </div>
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts">
15
+ import { defineComponent, inject, toRaw } from 'vue';
16
+ import { Plus } from '@element-plus/icons';
17
+
18
+ import { Services } from '@editor/type';
19
+ import { generatePageNameByApp } from '@editor/utils';
20
+
21
+ export default defineComponent({
22
+ components: { Plus },
23
+
24
+ setup() {
25
+ const services = inject<Services>('services');
26
+
27
+ return {
28
+ clickHandler() {
29
+ const { editorService } = services || {};
30
+
31
+ if (!editorService) return;
32
+
33
+ editorService.add({
34
+ type: 'page',
35
+ name: generatePageNameByApp(toRaw(editorService.get('root'))),
36
+ });
37
+ },
38
+ };
39
+ },
40
+ });
41
+ </script>
@@ -0,0 +1,188 @@
1
+ <template>
2
+ <div ref="codeEditor" class="magic-code-editor"></div>
3
+ </template>
4
+
5
+ <script lang="ts">
6
+ import { defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
7
+ import serialize from 'serialize-javascript';
8
+
9
+ import { asyncLoadJs } from '@tmagic/utils';
10
+
11
+ const initEditor = () => {
12
+ if ((globalThis as any).monaco) {
13
+ Promise.resolve((globalThis as any).monaco);
14
+ }
15
+
16
+ return asyncLoadJs(`https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.26.1/min/vs/loader.min.js`).then(() => {
17
+ (globalThis as any).require.config({
18
+ paths: { vs: `https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.26.1/min/vs` },
19
+ });
20
+ return new Promise((resolve) => {
21
+ (globalThis as any).require(['vs/editor/editor.main'], () => {
22
+ resolve((globalThis as any).monaco);
23
+ });
24
+ });
25
+ });
26
+ };
27
+
28
+ const toString = (v: any, language: string): string => {
29
+ let value = '';
30
+ if (typeof v !== 'string') {
31
+ value = serialize(v, {
32
+ space: 2,
33
+ unsafe: true,
34
+ }).replace(/"(\w+)":\s/g, '$1: ');
35
+ } else {
36
+ value = v;
37
+ }
38
+ if (language === 'javascript' && value.startsWith('{') && value.endsWith('}')) {
39
+ value = `(${value})`;
40
+ }
41
+ return value;
42
+ };
43
+
44
+ export default defineComponent({
45
+ name: 'magic-code-editor',
46
+
47
+ props: {
48
+ initValues: {
49
+ type: [String, Object],
50
+ },
51
+
52
+ modifiedValues: {
53
+ type: [String, Object],
54
+ },
55
+
56
+ type: {
57
+ type: [String],
58
+ default: () => '',
59
+ },
60
+
61
+ language: {
62
+ type: [String],
63
+ default: () => 'javascript',
64
+ },
65
+ },
66
+
67
+ emits: ['inited', 'save'],
68
+
69
+ setup(props, { emit }) {
70
+ let vsEditor: any = null;
71
+ const values = ref('');
72
+ const loading = ref(false);
73
+ const codeEditor = ref<HTMLDivElement>();
74
+
75
+ const setEditorValue = (v: any, m: any) => {
76
+ values.value = toString(v, props.language);
77
+
78
+ if (props.type === 'diff') {
79
+ const originalModel = (globalThis as any).monaco.editor.createModel(values.value, 'text/javascript');
80
+ const modifiedModel = (globalThis as any).monaco.editor.createModel(
81
+ toString(m, props.language),
82
+ 'text/javascript',
83
+ );
84
+
85
+ return vsEditor.setModel({
86
+ original: originalModel,
87
+ modified: modifiedModel,
88
+ });
89
+ }
90
+
91
+ return vsEditor.setValue?.(values.value);
92
+ };
93
+
94
+ const resizeHandler = () => {
95
+ vsEditor?.layout();
96
+ };
97
+
98
+ const getEditorValue = () => vsEditor.getValue?.() || '';
99
+
100
+ const init = async () => {
101
+ if (!codeEditor.value) return;
102
+
103
+ vsEditor = (globalThis as any).monaco.editor[props.type === 'diff' ? 'createDiffEditor' : 'create'](
104
+ codeEditor.value,
105
+ {
106
+ value: values.value,
107
+ language: props.language,
108
+ tabSize: 2,
109
+ theme: 'vs-dark',
110
+ fontFamily: 'dm, Menlo, Monaco, "Courier New", monospace',
111
+ fontSize: 15,
112
+ formatOnPaste: true,
113
+ },
114
+ );
115
+
116
+ setEditorValue(props.initValues, props.modifiedValues);
117
+
118
+ loading.value = false;
119
+
120
+ emit('inited', vsEditor);
121
+
122
+ codeEditor.value.addEventListener('keydown', (e) => {
123
+ if (e.keyCode === 83 && (navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)) {
124
+ e.preventDefault();
125
+ emit('save', getEditorValue());
126
+ }
127
+ });
128
+
129
+ if (props.type !== 'diff') {
130
+ vsEditor.onDidBlurEditorWidget(() => {
131
+ emit('save', getEditorValue());
132
+ });
133
+ }
134
+
135
+ globalThis.addEventListener('resize', resizeHandler);
136
+ };
137
+
138
+ watch(
139
+ () => props.initValues,
140
+ (v, preV) => {
141
+ if (vsEditor && v !== preV) {
142
+ setEditorValue(props.initValues, props.modifiedValues);
143
+ }
144
+ },
145
+ {
146
+ deep: true,
147
+ immediate: true,
148
+ },
149
+ );
150
+
151
+ onMounted(async () => {
152
+ loading.value = true;
153
+
154
+ await initEditor();
155
+ if (!(globalThis as any).monaco) {
156
+ const interval = setInterval(() => {
157
+ if ((globalThis as any).monaco) {
158
+ clearInterval(interval);
159
+ init();
160
+ }
161
+ }, 300);
162
+ } else {
163
+ init();
164
+ }
165
+ });
166
+
167
+ onUnmounted(() => {
168
+ globalThis.removeEventListener('resize', resizeHandler);
169
+ });
170
+
171
+ return {
172
+ values,
173
+ loading,
174
+ codeEditor,
175
+
176
+ getEditor() {
177
+ return vsEditor;
178
+ },
179
+
180
+ setEditorValue,
181
+
182
+ focus() {
183
+ vsEditor.focus();
184
+ },
185
+ };
186
+ },
187
+ });
188
+ </script>