@tmagic/editor 1.4.8 → 1.4.9

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 (143) hide show
  1. package/package.json +11 -11
  2. package/src/Editor.vue +211 -0
  3. package/src/components/CodeBlockEditor.vue +268 -0
  4. package/src/components/CodeParams.vue +60 -0
  5. package/src/components/ContentMenu.vue +193 -0
  6. package/src/components/FloatingBox.vue +178 -0
  7. package/src/components/Icon.vue +27 -0
  8. package/src/components/Resizer.vue +23 -0
  9. package/src/components/ScrollBar.vue +151 -0
  10. package/src/components/ScrollViewer.vue +128 -0
  11. package/src/components/SearchInput.vue +37 -0
  12. package/src/components/SplitView.vue +186 -0
  13. package/src/components/ToolButton.vue +138 -0
  14. package/src/components/Tree.vue +71 -0
  15. package/src/components/TreeNode.vue +150 -0
  16. package/src/editorProps.ts +117 -0
  17. package/src/fields/Code.vue +50 -0
  18. package/src/fields/CodeLink.vue +80 -0
  19. package/src/fields/CodeSelect.vue +132 -0
  20. package/src/fields/CodeSelectCol.vue +139 -0
  21. package/src/fields/DataSourceFieldSelect.vue +149 -0
  22. package/src/fields/DataSourceFields.vue +323 -0
  23. package/src/fields/DataSourceInput.vue +339 -0
  24. package/src/fields/DataSourceMethodSelect.vue +153 -0
  25. package/src/fields/DataSourceMethods.vue +104 -0
  26. package/src/fields/DataSourceMocks.vue +255 -0
  27. package/src/fields/DataSourceSelect.vue +98 -0
  28. package/src/fields/EventSelect.vue +375 -0
  29. package/src/fields/KeyValue.vue +137 -0
  30. package/src/fields/PageFragmentSelect.vue +63 -0
  31. package/src/fields/UISelect.vue +135 -0
  32. package/src/hooks/index.ts +24 -0
  33. package/src/hooks/use-code-block-edit.ts +83 -0
  34. package/src/hooks/use-data-source-edit.ts +46 -0
  35. package/src/hooks/use-data-source-method.ts +100 -0
  36. package/src/hooks/use-editor-content-height.ts +26 -0
  37. package/src/hooks/use-filter.ts +55 -0
  38. package/src/hooks/use-float-box.ts +76 -0
  39. package/src/hooks/use-getso.ts +35 -0
  40. package/src/hooks/use-next-float-box-position.ts +29 -0
  41. package/src/hooks/use-node-status.ts +48 -0
  42. package/src/hooks/use-stage.ts +129 -0
  43. package/src/hooks/use-window-rect.ts +20 -0
  44. package/src/icons/AppManageIcon.vue +15 -0
  45. package/src/icons/CenterIcon.vue +13 -0
  46. package/src/icons/CodeIcon.vue +28 -0
  47. package/src/icons/FolderMinusIcon.vue +22 -0
  48. package/src/icons/PinIcon.vue +25 -0
  49. package/src/icons/PinnedIcon.vue +25 -0
  50. package/src/index.ts +118 -0
  51. package/src/initService.ts +448 -0
  52. package/src/layouts/AddPageBox.vue +55 -0
  53. package/src/layouts/CodeEditor.vue +226 -0
  54. package/src/layouts/Framework.vue +163 -0
  55. package/src/layouts/NavMenu.vue +200 -0
  56. package/src/layouts/PropsPanel.vue +131 -0
  57. package/src/layouts/page-bar/AddButton.vue +48 -0
  58. package/src/layouts/page-bar/PageBar.vue +159 -0
  59. package/src/layouts/page-bar/PageBarScrollContainer.vue +156 -0
  60. package/src/layouts/page-bar/SwitchTypeButton.vue +45 -0
  61. package/src/layouts/sidebar/ComponentListPanel.vue +134 -0
  62. package/src/layouts/sidebar/Sidebar.vue +278 -0
  63. package/src/layouts/sidebar/code-block/CodeBlockList.vue +160 -0
  64. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +70 -0
  65. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +84 -0
  66. package/src/layouts/sidebar/data-source/DataSourceList.vue +172 -0
  67. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +106 -0
  68. package/src/layouts/sidebar/layer/LayerMenu.vue +128 -0
  69. package/src/layouts/sidebar/layer/LayerNodeTool.vue +32 -0
  70. package/src/layouts/sidebar/layer/LayerPanel.vue +119 -0
  71. package/src/layouts/sidebar/layer/use-click.ts +113 -0
  72. package/src/layouts/sidebar/layer/use-drag.ts +167 -0
  73. package/src/layouts/sidebar/layer/use-keybinding.ts +54 -0
  74. package/src/layouts/sidebar/layer/use-node-status.ts +114 -0
  75. package/src/layouts/workspace/Breadcrumb.vue +35 -0
  76. package/src/layouts/workspace/Workspace.vue +46 -0
  77. package/src/layouts/workspace/viewer/NodeListMenu.vue +115 -0
  78. package/src/layouts/workspace/viewer/Stage.vue +246 -0
  79. package/src/layouts/workspace/viewer/StageOverlay.vue +68 -0
  80. package/src/layouts/workspace/viewer/ViewerMenu.vue +145 -0
  81. package/src/services/BaseService.ts +230 -0
  82. package/src/services/codeBlock.ts +327 -0
  83. package/src/services/componentList.ts +58 -0
  84. package/src/services/dataSource.ts +216 -0
  85. package/src/services/dep.ts +152 -0
  86. package/src/services/editor.ts +1135 -0
  87. package/src/services/events.ts +93 -0
  88. package/src/services/history.ts +126 -0
  89. package/src/services/keybinding.ts +220 -0
  90. package/src/services/props.ts +286 -0
  91. package/src/services/stageOverlay.ts +212 -0
  92. package/src/services/storage.ts +155 -0
  93. package/src/services/ui.ts +156 -0
  94. package/src/shims-vue.d.ts +6 -0
  95. package/src/theme/breadcrumb.scss +6 -0
  96. package/src/theme/code-block.scss +6 -0
  97. package/src/theme/code-editor.scss +38 -0
  98. package/src/theme/common/var.scss +14 -0
  99. package/src/theme/component-list-panel.scss +95 -0
  100. package/src/theme/content-menu.scss +76 -0
  101. package/src/theme/data-source-field.scss +12 -0
  102. package/src/theme/data-source-fields.scss +13 -0
  103. package/src/theme/data-source-input.scss +18 -0
  104. package/src/theme/data-source-methods.scss +13 -0
  105. package/src/theme/data-source.scss +28 -0
  106. package/src/theme/event.scss +37 -0
  107. package/src/theme/floating-box.scss +32 -0
  108. package/src/theme/framework.scss +69 -0
  109. package/src/theme/icon.scss +12 -0
  110. package/src/theme/index.scss +10 -0
  111. package/src/theme/key-value.scss +20 -0
  112. package/src/theme/layer-panel.scss +26 -0
  113. package/src/theme/layout.scss +9 -0
  114. package/src/theme/nav-menu.scss +78 -0
  115. package/src/theme/page-bar.scss +89 -0
  116. package/src/theme/page-fragment-select.scss +14 -0
  117. package/src/theme/props-panel.scss +55 -0
  118. package/src/theme/resizer.scss +66 -0
  119. package/src/theme/ruler.scss +38 -0
  120. package/src/theme/search-input.scss +14 -0
  121. package/src/theme/sidebar.scss +79 -0
  122. package/src/theme/stage.scss +76 -0
  123. package/src/theme/theme.scss +27 -0
  124. package/src/theme/tree.scss +89 -0
  125. package/src/theme/workspace.scss +9 -0
  126. package/src/type.ts +749 -0
  127. package/src/utils/compose.ts +52 -0
  128. package/src/utils/config.ts +29 -0
  129. package/src/utils/content-menu.ts +95 -0
  130. package/src/utils/data-source/formConfigs/base.ts +30 -0
  131. package/src/utils/data-source/formConfigs/http.ts +60 -0
  132. package/src/utils/data-source/index.ts +243 -0
  133. package/src/utils/editor.ts +313 -0
  134. package/src/utils/idle-task.ts +72 -0
  135. package/src/utils/index.ts +24 -0
  136. package/src/utils/keybinding-config.ts +125 -0
  137. package/src/utils/logger.ts +47 -0
  138. package/src/utils/monaco-editor.ts +8 -0
  139. package/src/utils/operator.ts +106 -0
  140. package/src/utils/props.ts +504 -0
  141. package/src/utils/scroll-viewer.ts +165 -0
  142. package/src/utils/tree.ts +15 -0
  143. package/src/utils/undo-redo.ts +76 -0
@@ -0,0 +1,55 @@
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(NodeType.PAGE)">
5
+ <div>
6
+ <MIcon :icon="Plus"></MIcon>
7
+ </div>
8
+ <p>新增页面</p>
9
+ </div>
10
+
11
+ <div v-if="!disabledPageFragment" class="m-editor-empty-button" @click="clickHandler(NodeType.PAGE_FRAGMENT)">
12
+ <div>
13
+ <MIcon :icon="Plus"></MIcon>
14
+ </div>
15
+ <p>新增页面片</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script lang="ts" setup>
22
+ import { inject, toRaw } from 'vue';
23
+ import { Plus } from '@element-plus/icons-vue';
24
+
25
+ import { NodeType } from '@tmagic/schema';
26
+
27
+ import MIcon from '@editor/components/Icon.vue';
28
+ import type { Services } from '@editor/type';
29
+ import { generatePageNameByApp } from '@editor/utils';
30
+
31
+ defineOptions({
32
+ name: 'MEditorAddPageBox',
33
+ });
34
+
35
+ defineProps<{
36
+ disabledPageFragment: boolean;
37
+ }>();
38
+
39
+ const services = inject<Services>('services');
40
+
41
+ const clickHandler = (type: NodeType.PAGE | NodeType.PAGE_FRAGMENT) => {
42
+ const { editorService } = services || {};
43
+
44
+ if (!editorService) return;
45
+
46
+ const root = toRaw(editorService.get('root'));
47
+ if (!root) throw new Error('root 不能为空');
48
+
49
+ editorService.add({
50
+ type,
51
+ name: generatePageNameByApp(root, type),
52
+ items: [],
53
+ });
54
+ };
55
+ </script>
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <div :class="`magic-code-editor`">
3
+ <Teleport to="body" :disabled="!fullScreen">
4
+ <div
5
+ :class="`magic-code-editor-wrapper${fullScreen ? ' full-screen' : ''}`"
6
+ :style="!fullScreen && height ? `height: ${height}` : '100%'"
7
+ >
8
+ <TMagicButton class="magic-code-editor-full-screen-icon" circle size="small" @click="fullScreenHandler"
9
+ ><MIcon :icon="FullScreen"></MIcon
10
+ ></TMagicButton>
11
+ <div ref="codeEditor" class="magic-code-editor-content"></div>
12
+ </div>
13
+ </Teleport>
14
+ </div>
15
+ </template>
16
+
17
+ <script lang="ts" setup>
18
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
19
+ import { FullScreen } from '@element-plus/icons-vue';
20
+ import { throttle } from 'lodash-es';
21
+ import serialize from 'serialize-javascript';
22
+
23
+ import { TMagicButton } from '@tmagic/design';
24
+
25
+ import MIcon from '@editor/components/Icon.vue';
26
+ import { getConfig } from '@editor/utils/config';
27
+ import monaco from '@editor/utils/monaco-editor';
28
+
29
+ defineOptions({
30
+ name: 'MEditorCodeEditor',
31
+ });
32
+
33
+ const props = withDefaults(
34
+ defineProps<{
35
+ initValues?: any;
36
+ modifiedValues?: any;
37
+ type?: 'diff';
38
+ language?: string;
39
+ options?: {
40
+ [key: string]: any;
41
+ };
42
+ height?: string;
43
+ autoSave?: boolean;
44
+ parse?: boolean;
45
+ }>(),
46
+ {
47
+ initValues: '',
48
+ autoSave: true,
49
+ language: 'javascript',
50
+ options: () => ({
51
+ tabSize: 2,
52
+ }),
53
+ parse: false,
54
+ },
55
+ );
56
+
57
+ const emit = defineEmits(['initd', 'save']);
58
+
59
+ const toString = (v: string | any, language: string): string => {
60
+ let value = '';
61
+ if (typeof v !== 'string') {
62
+ if (language === 'json') {
63
+ value = JSON.stringify(v, null, 2);
64
+ } else {
65
+ value = serialize(v, {
66
+ space: 2,
67
+ unsafe: true,
68
+ }).replace(/"(\w+)":\s/g, '$1: ');
69
+ }
70
+ } else {
71
+ value = v;
72
+ }
73
+ if (language === 'javascript' && value.startsWith('{') && value.endsWith('}')) {
74
+ value = `(${value})`;
75
+ }
76
+ return value;
77
+ };
78
+
79
+ const parse = (v: string | any, language: string): any => {
80
+ if (typeof v !== 'string') {
81
+ return v;
82
+ }
83
+
84
+ if (language === 'json') {
85
+ return JSON.parse(v);
86
+ }
87
+
88
+ return getConfig('parseDSL')(v);
89
+ };
90
+
91
+ let vsEditor: monaco.editor.IStandaloneCodeEditor | null = null;
92
+ let vsDiffEditor: monaco.editor.IStandaloneDiffEditor | null = null;
93
+
94
+ const values = ref('');
95
+ const loading = ref(false);
96
+ const codeEditor = ref<HTMLDivElement>();
97
+
98
+ const resizeObserver = new globalThis.ResizeObserver(
99
+ throttle((): void => {
100
+ vsEditor?.layout();
101
+ vsDiffEditor?.layout();
102
+ }, 300),
103
+ );
104
+
105
+ const setEditorValue = (v: string | any, m: string | any) => {
106
+ values.value = toString(v, props.language.toLocaleLowerCase());
107
+
108
+ if (props.type === 'diff') {
109
+ const originalModel = monaco.editor.createModel(values.value, 'text/javascript');
110
+ const modifiedModel = monaco.editor.createModel(toString(m, props.language), 'text/javascript');
111
+
112
+ return vsDiffEditor?.setModel({
113
+ original: originalModel,
114
+ modified: modifiedModel,
115
+ });
116
+ }
117
+
118
+ return vsEditor?.setValue(values.value);
119
+ };
120
+
121
+ const getEditorValue = () =>
122
+ (props.type === 'diff' ? vsDiffEditor?.getModifiedEditor().getValue() : vsEditor?.getValue()) || '';
123
+
124
+ const init = async () => {
125
+ if (!codeEditor.value) return;
126
+
127
+ const options = {
128
+ value: values.value,
129
+ language: props.language,
130
+ theme: 'vs-dark',
131
+ ...props.options,
132
+ };
133
+
134
+ if (props.type === 'diff') {
135
+ vsDiffEditor = monaco.editor.createDiffEditor(codeEditor.value, options);
136
+ } else {
137
+ vsEditor = monaco.editor.create(codeEditor.value, options);
138
+ }
139
+
140
+ setEditorValue(props.initValues, props.modifiedValues);
141
+
142
+ loading.value = false;
143
+
144
+ emit('initd', vsEditor);
145
+
146
+ codeEditor.value.addEventListener('keydown', (e) => {
147
+ if (e.keyCode === 83 && (navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)) {
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ const newValue = getEditorValue();
151
+ values.value = newValue;
152
+ emit('save', props.parse ? parse(newValue, props.language) : newValue);
153
+ }
154
+ });
155
+
156
+ if (props.type !== 'diff' && props.autoSave) {
157
+ vsEditor?.onDidBlurEditorWidget(() => {
158
+ const newValue = getEditorValue();
159
+ if (values.value !== newValue) {
160
+ values.value = newValue;
161
+ emit('save', props.parse ? parse(newValue, props.language) : newValue);
162
+ }
163
+ });
164
+ }
165
+
166
+ resizeObserver.observe(codeEditor.value);
167
+ };
168
+
169
+ watch(
170
+ () => props.initValues,
171
+ (v, preV) => {
172
+ if (v !== preV) {
173
+ setEditorValue(props.initValues, props.modifiedValues);
174
+ }
175
+ },
176
+ {
177
+ deep: true,
178
+ immediate: true,
179
+ },
180
+ );
181
+
182
+ onMounted(async () => {
183
+ loading.value = true;
184
+
185
+ init();
186
+ });
187
+
188
+ onBeforeUnmount(() => {
189
+ resizeObserver.disconnect();
190
+ });
191
+
192
+ const fullScreen = ref(false);
193
+ const fullScreenHandler = () => {
194
+ fullScreen.value = !fullScreen.value;
195
+ setTimeout(() => {
196
+ vsEditor?.focus();
197
+ vsEditor?.layout();
198
+ vsDiffEditor?.focus();
199
+ vsDiffEditor?.layout();
200
+ });
201
+ };
202
+
203
+ defineExpose({
204
+ values,
205
+
206
+ getEditor() {
207
+ return vsEditor || vsDiffEditor;
208
+ },
209
+
210
+ getVsEditor() {
211
+ return vsEditor;
212
+ },
213
+
214
+ getVsDiffEditor() {
215
+ return vsDiffEditor;
216
+ },
217
+
218
+ setEditorValue,
219
+ getEditorValue,
220
+
221
+ focus() {
222
+ vsEditor?.focus();
223
+ vsDiffEditor?.focus();
224
+ },
225
+ });
226
+ </script>
@@ -0,0 +1,163 @@
1
+ <template>
2
+ <div class="m-editor" ref="content" style="min-width: 180px">
3
+ <slot name="header"></slot>
4
+
5
+ <slot name="nav"></slot>
6
+
7
+ <slot name="content-before"></slot>
8
+
9
+ <slot name="src-code" v-if="showSrc">
10
+ <CodeEditor class="m-editor-content" :init-values="root" :options="codeOptions" @save="saveCode"></CodeEditor>
11
+ </slot>
12
+
13
+ <SplitView
14
+ v-loading="stageLoading"
15
+ element-loading-text="Runtime 加载中..."
16
+ v-else
17
+ ref="splitView"
18
+ class="m-editor-content"
19
+ left-class="m-editor-framework-left"
20
+ center-class="m-editor-framework-center"
21
+ right-class="m-editor-framework-right"
22
+ v-model:left="columnWidth.left"
23
+ v-model:right="columnWidth.right"
24
+ :min-left="65"
25
+ :min-right="20"
26
+ :min-center="100"
27
+ :width="frameworkRect?.width || 0"
28
+ @change="columnWidthChange"
29
+ >
30
+ <template #left>
31
+ <slot name="sidebar"></slot>
32
+ </template>
33
+
34
+ <template #center>
35
+ <slot v-if="page" name="workspace"></slot>
36
+ <slot v-else name="empty">
37
+ <AddPageBox :disabled-page-fragment="disabledPageFragment"></AddPageBox>
38
+ </slot>
39
+
40
+ <slot name="page-bar">
41
+ <PageBar :disabled-page-fragment="disabledPageFragment">
42
+ <template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template>
43
+ <template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template>
44
+ </PageBar>
45
+ </slot>
46
+ </template>
47
+
48
+ <template v-if="page" #right>
49
+ <TMagicScrollbar>
50
+ <slot name="props-panel"></slot>
51
+ </TMagicScrollbar>
52
+ </template>
53
+ </SplitView>
54
+
55
+ <slot name="content-after"></slot>
56
+ <slot name="footer"></slot>
57
+ </div>
58
+ </template>
59
+
60
+ <script lang="ts" setup>
61
+ import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue';
62
+
63
+ import { TMagicScrollbar } from '@tmagic/design';
64
+
65
+ import SplitView from '@editor/components/SplitView.vue';
66
+ import type { FrameworkSlots, GetColumnWidth, Services } from '@editor/type';
67
+ import { getConfig } from '@editor/utils/config';
68
+
69
+ import PageBar from './page-bar/PageBar.vue';
70
+ import AddPageBox from './AddPageBox.vue';
71
+ import CodeEditor from './CodeEditor.vue';
72
+
73
+ defineSlots<FrameworkSlots>();
74
+
75
+ defineOptions({
76
+ name: 'MEditorFramework',
77
+ });
78
+
79
+ defineProps<{
80
+ disabledPageFragment: boolean;
81
+ }>();
82
+
83
+ const DEFAULT_LEFT_COLUMN_WIDTH = 310;
84
+ const DEFAULT_RIGHT_COLUMN_WIDTH = 480;
85
+
86
+ const codeOptions = inject('codeOptions', {});
87
+ const { editorService, uiService } = inject<Services>('services') || {};
88
+
89
+ const content = ref<HTMLDivElement>();
90
+ const splitView = ref<InstanceType<typeof SplitView>>();
91
+
92
+ const root = computed(() => editorService?.get('root'));
93
+ const page = computed(() => editorService?.get('page'));
94
+
95
+ const pageLength = computed(() => editorService?.get('pageLength') || 0);
96
+ const stageLoading = computed(() => editorService?.get('stageLoading') || false);
97
+ const showSrc = computed(() => uiService?.get('showSrc'));
98
+
99
+ const LEFT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorLeftColumnWidthData';
100
+ const RIGHT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorRightColumnWidthData';
101
+
102
+ const getLeftColumnWidthCacheData = () =>
103
+ Number(globalThis.localStorage.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_LEFT_COLUMN_WIDTH;
104
+
105
+ const getRightColumnWidthCacheData = () =>
106
+ Number(globalThis.localStorage.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_RIGHT_COLUMN_WIDTH;
107
+
108
+ const columnWidth = ref<Partial<GetColumnWidth>>({
109
+ left: getLeftColumnWidthCacheData(),
110
+ center: 0,
111
+ right: getRightColumnWidthCacheData(),
112
+ });
113
+
114
+ watch(pageLength, () => {
115
+ splitView.value?.updateWidth();
116
+ });
117
+
118
+ watch(
119
+ () => uiService?.get('hideSlideBar'),
120
+ (hideSlideBar) => {
121
+ columnWidth.value.left = hideSlideBar ? 0 : getLeftColumnWidthCacheData();
122
+ },
123
+ );
124
+
125
+ const columnWidthChange = (columnW: GetColumnWidth) => {
126
+ columnWidth.value = columnW;
127
+
128
+ globalThis.localStorage.setItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, `${columnW.left}`);
129
+ globalThis.localStorage.setItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, `${columnW.right}`);
130
+ uiService?.set('columnWidth', columnW);
131
+ };
132
+
133
+ const frameworkRect = computed(() => uiService?.get('frameworkRect'));
134
+
135
+ const resizerObserver = new ResizeObserver((entries) => {
136
+ const { contentRect } = entries[0];
137
+ uiService?.set('frameworkRect', {
138
+ width: contentRect.width,
139
+ height: contentRect.height,
140
+ left: contentRect.left,
141
+ top: contentRect.top,
142
+ });
143
+ });
144
+
145
+ onMounted(() => {
146
+ if (content.value) {
147
+ resizerObserver.observe(content.value);
148
+ }
149
+ });
150
+
151
+ onBeforeUnmount(() => {
152
+ resizerObserver.disconnect();
153
+ });
154
+
155
+ const saveCode = (value: string) => {
156
+ try {
157
+ const parseDSL = getConfig('parseDSL');
158
+ editorService?.set('root', parseDSL(value));
159
+ } catch (e: any) {
160
+ console.error(e);
161
+ }
162
+ };
163
+ </script>
@@ -0,0 +1,200 @@
1
+ <template>
2
+ <div class="m-editor-nav-menu" :style="{ height: `${height}px` }" ref="navMenu">
3
+ <div v-for="key in keys" :class="`menu-${key}`" :key="key" :style="`width: ${columnWidth?.[key]}px`">
4
+ <ToolButton :data="item" v-for="(item, index) in buttons[key]" :key="index"></ToolButton>
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import { computed, inject, markRaw, onBeforeUnmount, onMounted, ref } from 'vue';
11
+ import { Back, Delete, FullScreen, Grid, Memo, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons-vue';
12
+
13
+ import { NodeType } from '@tmagic/schema';
14
+
15
+ import ToolButton from '@editor/components/ToolButton.vue';
16
+ import { ColumnLayout, MenuBarData, MenuButton, MenuComponent, MenuItem, Services } from '@editor/type';
17
+
18
+ defineOptions({
19
+ name: 'MEditorNavMenu',
20
+ });
21
+
22
+ const props = withDefaults(
23
+ defineProps<{
24
+ data?: MenuBarData;
25
+ height?: number;
26
+ }>(),
27
+ {
28
+ data: () => ({}),
29
+ height: 35,
30
+ },
31
+ );
32
+
33
+ const services = inject<Services>('services');
34
+ const uiService = services?.uiService;
35
+
36
+ const columnWidth = computed(() => services?.uiService.get('columnWidth'));
37
+ const keys = Object.values(ColumnLayout);
38
+
39
+ const showGuides = computed((): boolean => uiService?.get('showGuides') ?? true);
40
+ const showRule = computed((): boolean => uiService?.get('showRule') ?? true);
41
+ const zoom = computed((): number => uiService?.get('zoom') ?? 1);
42
+
43
+ const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
44
+ const ctrl = isMac ? 'Command' : 'Ctrl';
45
+
46
+ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
47
+ if (typeof item !== 'string') {
48
+ return [item];
49
+ }
50
+ const config: (MenuButton | MenuComponent)[] = [];
51
+ switch (item) {
52
+ case '/':
53
+ config.push({
54
+ type: 'divider',
55
+ className: 'divider',
56
+ });
57
+ break;
58
+ case 'zoom':
59
+ config.push(
60
+ ...getConfig('zoom-out'),
61
+ ...getConfig(`${parseInt(`${zoom.value * 100}`, 10)}%`),
62
+ ...getConfig('zoom-in'),
63
+ ...getConfig('scale-to-original'),
64
+ ...getConfig('scale-to-fit'),
65
+ );
66
+ break;
67
+ case 'delete':
68
+ config.push({
69
+ type: 'button',
70
+ className: 'delete',
71
+ icon: markRaw(Delete),
72
+ tooltip: `刪除(Delete)`,
73
+ disabled: () => services?.editorService.get('node')?.type === NodeType.PAGE,
74
+ handler: () => {
75
+ const node = services?.editorService.get('node');
76
+ node && services?.editorService.remove(node);
77
+ },
78
+ });
79
+ break;
80
+ case 'undo':
81
+ config.push({
82
+ type: 'button',
83
+ className: 'undo',
84
+ icon: markRaw(Back),
85
+ tooltip: `后退(${ctrl}+z)`,
86
+ disabled: () => !services?.historyService.state.canUndo,
87
+ handler: () => services?.editorService.undo(),
88
+ });
89
+ break;
90
+ case 'redo':
91
+ config.push({
92
+ type: 'button',
93
+ className: 'redo',
94
+ icon: markRaw(Right),
95
+ tooltip: `前进(${ctrl}+Shift+z)`,
96
+ disabled: () => !services?.historyService.state.canRedo,
97
+ handler: () => services?.editorService.redo(),
98
+ });
99
+ break;
100
+ case 'zoom-in':
101
+ config.push({
102
+ type: 'button',
103
+ className: 'zoom-in',
104
+ icon: markRaw(ZoomIn),
105
+ tooltip: `放大(${ctrl}+=)`,
106
+ handler: () => uiService?.zoom(0.1),
107
+ });
108
+ break;
109
+ case 'zoom-out':
110
+ config.push({
111
+ type: 'button',
112
+ className: 'zoom-out',
113
+ icon: markRaw(ZoomOut),
114
+ tooltip: `縮小(${ctrl}+-)`,
115
+ handler: () => uiService?.zoom(-0.1),
116
+ });
117
+ break;
118
+ case 'scale-to-original':
119
+ config.push({
120
+ type: 'button',
121
+ className: 'scale-to-original',
122
+ icon: markRaw(ScaleToOriginal),
123
+ tooltip: `缩放到实际大小(${ctrl}+1)`,
124
+ handler: () => uiService?.set('zoom', 1),
125
+ });
126
+ break;
127
+ case 'scale-to-fit':
128
+ config.push({
129
+ type: 'button',
130
+ className: 'scale-to-fit',
131
+ icon: markRaw(FullScreen),
132
+ tooltip: `缩放以适应(${ctrl}+0)`,
133
+ handler: async () => uiService?.set('zoom', await uiService.calcZoom()),
134
+ });
135
+ break;
136
+ case 'rule':
137
+ config.push({
138
+ type: 'button',
139
+ className: 'rule',
140
+ icon: markRaw(Memo),
141
+ tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
142
+ handler: () => uiService?.set('showRule', !showRule.value),
143
+ });
144
+ break;
145
+ case 'guides':
146
+ config.push({
147
+ type: 'button',
148
+ className: 'guides',
149
+ icon: markRaw(Grid),
150
+ tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
151
+ handler: () => uiService?.set('showGuides', !showGuides.value),
152
+ });
153
+ break;
154
+ default:
155
+ config.push({
156
+ type: 'text',
157
+ text: item,
158
+ });
159
+ }
160
+ return config;
161
+ };
162
+
163
+ const buttons = computed(() => {
164
+ const data: {
165
+ [ColumnLayout.LEFT]: (MenuButton | MenuComponent)[];
166
+ [ColumnLayout.CENTER]: (MenuButton | MenuComponent)[];
167
+ [ColumnLayout.RIGHT]: (MenuButton | MenuComponent)[];
168
+ } = {
169
+ [ColumnLayout.LEFT]: [],
170
+ [ColumnLayout.CENTER]: [],
171
+ [ColumnLayout.RIGHT]: [],
172
+ };
173
+ keys.forEach((key) => {
174
+ const items = props.data[key] || [];
175
+ items.forEach((item) => {
176
+ data[key].push(...getConfig(item));
177
+ });
178
+ });
179
+ return data;
180
+ });
181
+
182
+ const resizeObserver = new ResizeObserver(() => {
183
+ const rect = navMenu.value?.getBoundingClientRect();
184
+ if (rect) {
185
+ uiService?.set('navMenuRect', {
186
+ left: rect.left,
187
+ top: rect.top,
188
+ width: rect.width,
189
+ height: rect.height,
190
+ });
191
+ }
192
+ });
193
+ const navMenu = ref<HTMLDivElement>();
194
+ onMounted(() => {
195
+ navMenu.value && resizeObserver.observe(navMenu.value);
196
+ });
197
+ onBeforeUnmount(() => {
198
+ resizeObserver.disconnect();
199
+ });
200
+ </script>