@tmagic/editor 1.7.14-beta.1 → 1.7.14-beta.3
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.
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +11 -11
- package/dist/es/layouts/NavMenuColumn.js +5 -0
- package/dist/es/layouts/NavMenuColumn.vue_vue_type_script_setup_true_lang.js +201 -0
- package/dist/es/services/editor.js +88 -31
- package/dist/es/services/storage.js +5 -5
- package/dist/es/style.css +51 -0
- package/dist/es/utils/keybinding-config.js +19 -19
- package/dist/style.css +51 -0
- package/dist/tmagic-editor.umd.cjs +353 -93
- package/package.json +8 -8
- package/src/components/ToolButton.vue +7 -1
- package/src/layouts/CodeEditor.vue +18 -10
- package/src/layouts/NavMenu.vue +9 -4
- package/src/layouts/NavMenuColumn.vue +252 -0
- package/src/services/editor.ts +132 -40
- package/src/theme/nav-menu.scss +61 -0
- package/src/type.ts +42 -0
- package/src/utils/undo-redo.ts +2 -0
- package/types/index.d.ts +456 -437
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.7.14-beta.
|
|
2
|
+
"version": "1.7.14-beta.3",
|
|
3
3
|
"name": "@tmagic/editor",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"moveable": "^0.53.0",
|
|
59
59
|
"serialize-javascript": "^7.0.0",
|
|
60
60
|
"sortablejs": "^1.15.6",
|
|
61
|
-
"@tmagic/design": "1.7.14-beta.
|
|
62
|
-
"@tmagic/form": "1.7.14-beta.
|
|
63
|
-
"@tmagic/
|
|
64
|
-
"@tmagic/
|
|
65
|
-
"@tmagic/utils": "1.7.14-beta.
|
|
61
|
+
"@tmagic/design": "1.7.14-beta.3",
|
|
62
|
+
"@tmagic/form": "1.7.14-beta.3",
|
|
63
|
+
"@tmagic/stage": "1.7.14-beta.3",
|
|
64
|
+
"@tmagic/table": "1.7.14-beta.3",
|
|
65
|
+
"@tmagic/utils": "1.7.14-beta.3"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/events": "^3.0.3",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"monaco-editor": "^0.55.1 ",
|
|
76
76
|
"type-fest": "^5.2.0",
|
|
77
77
|
"typescript": "^6.0.3",
|
|
78
|
-
"vue": "^3.5.
|
|
79
|
-
"@tmagic/core": "1.7.14-beta.
|
|
78
|
+
"vue": "^3.5.34",
|
|
79
|
+
"@tmagic/core": "1.7.14-beta.3"
|
|
80
80
|
},
|
|
81
81
|
"peerDependenciesMeta": {
|
|
82
82
|
"typescript": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
v-if="display"
|
|
4
|
+
ref="rootEl"
|
|
4
5
|
class="menu-item"
|
|
5
6
|
:class="`${data.type} ${data.className || ''}`"
|
|
6
7
|
@click="clickHandler(data, $event)"
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
</template>
|
|
57
58
|
|
|
58
59
|
<script lang="ts" setup>
|
|
59
|
-
import { computed } from 'vue';
|
|
60
|
+
import { computed, useTemplateRef } from 'vue';
|
|
60
61
|
import { ArrowDown } from '@element-plus/icons-vue';
|
|
61
62
|
|
|
62
63
|
import {
|
|
@@ -93,6 +94,9 @@ const props = withDefaults(
|
|
|
93
94
|
);
|
|
94
95
|
const services = useServices();
|
|
95
96
|
|
|
97
|
+
const rootElRef = useTemplateRef<HTMLDivElement>('rootEl');
|
|
98
|
+
const getElRef = () => rootElRef;
|
|
99
|
+
|
|
96
100
|
const disabled = computed(() => {
|
|
97
101
|
if (typeof props.data === 'string') return false;
|
|
98
102
|
if (props.data.type === 'component') return false;
|
|
@@ -145,4 +149,6 @@ const mouseupHandler = (item: MenuButton | MenuComponent, event: MouseEvent) =>
|
|
|
145
149
|
buttonHandler(item, event);
|
|
146
150
|
}
|
|
147
151
|
};
|
|
152
|
+
|
|
153
|
+
defineExpose({ getElRef });
|
|
148
154
|
</script>
|
|
@@ -214,24 +214,32 @@ const setEditorValue = (v: string | any, m: string | any) => {
|
|
|
214
214
|
if (props.type === 'diff') {
|
|
215
215
|
const originalModel = monaco.editor.createModel(values.value, 'text/javascript');
|
|
216
216
|
const modifiedModel = monaco.editor.createModel(toString(m, props.language), 'text/javascript');
|
|
217
|
-
|
|
217
|
+
// 保存视图状态(光标、选区、滚动、折叠等)
|
|
218
|
+
const viewState = vsDiffEditor?.saveViewState();
|
|
218
219
|
const result = vsDiffEditor?.setModel({
|
|
219
220
|
original: originalModel,
|
|
220
221
|
modified: modifiedModel,
|
|
221
222
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0,这里也放到 nextTick 中
|
|
224
|
+
// 利用 Vue nextTick 队列的 FIFO 特性,保证恢复在重置之后执行
|
|
225
|
+
if (viewState) {
|
|
226
|
+
nextTick(() => {
|
|
227
|
+
vsDiffEditor?.restoreViewState(viewState);
|
|
228
|
+
vsDiffEditor?.focus();
|
|
229
|
+
});
|
|
225
230
|
}
|
|
226
231
|
return result;
|
|
227
232
|
}
|
|
228
|
-
//
|
|
229
|
-
const
|
|
233
|
+
// 保存视图状态(光标、选区、滚动、折叠等)
|
|
234
|
+
const viewState = vsEditor?.saveViewState();
|
|
230
235
|
const result = vsEditor?.setValue(values.value);
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
// setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0,这里也放到 nextTick 中
|
|
237
|
+
// 利用 Vue nextTick 队列的 FIFO 特性,保证恢复在重置之后执行
|
|
238
|
+
if (viewState) {
|
|
239
|
+
nextTick(() => {
|
|
240
|
+
vsEditor?.restoreViewState(viewState);
|
|
241
|
+
vsEditor?.focus();
|
|
242
|
+
});
|
|
235
243
|
}
|
|
236
244
|
return result;
|
|
237
245
|
};
|
package/src/layouts/NavMenu.vue
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="m-editor-nav-menu" :style="{ height: `${height}px` }" ref="navMenu">
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<NavMenuColumn
|
|
4
|
+
v-for="key in keys"
|
|
5
|
+
:key="key"
|
|
6
|
+
:column-key="key"
|
|
7
|
+
:items="buttons[key]"
|
|
8
|
+
:width="columnWidth?.[key]"
|
|
9
|
+
></NavMenuColumn>
|
|
6
10
|
</div>
|
|
7
11
|
</template>
|
|
8
12
|
|
|
@@ -12,10 +16,11 @@ import { Back, Delete, FullScreen, Grid, Memo, Right, ScaleToOriginal, ZoomIn, Z
|
|
|
12
16
|
|
|
13
17
|
import { NodeType } from '@tmagic/core';
|
|
14
18
|
|
|
15
|
-
import ToolButton from '@editor/components/ToolButton.vue';
|
|
16
19
|
import { useServices } from '@editor/hooks/use-services';
|
|
17
20
|
import { ColumnLayout, MenuBarData, MenuButton, MenuComponent, MenuItem } from '@editor/type';
|
|
18
21
|
|
|
22
|
+
import NavMenuColumn from './NavMenuColumn.vue';
|
|
23
|
+
|
|
19
24
|
defineOptions({
|
|
20
25
|
name: 'MEditorNavMenu',
|
|
21
26
|
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="`menu-${columnKey} m-editor-nav-menu-column`"
|
|
4
|
+
:style="width != null ? `width: ${width}px` : ''"
|
|
5
|
+
ref="columnEl"
|
|
6
|
+
>
|
|
7
|
+
<ToolButton
|
|
8
|
+
v-for="(item, index) in items"
|
|
9
|
+
:data="item"
|
|
10
|
+
:key="`item-${index}`"
|
|
11
|
+
:class="{ 'm-editor-nav-menu-slot-hidden': hiddenIndexSet.has(index) }"
|
|
12
|
+
:ref="(comp: any) => setItemRef(comp, index)"
|
|
13
|
+
></ToolButton>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="m-editor-nav-menu-more-wrapper"
|
|
17
|
+
:class="{ 'm-editor-nav-menu-more-wrapper-hidden': !hasOverflow }"
|
|
18
|
+
ref="moreWrapperEl"
|
|
19
|
+
>
|
|
20
|
+
<TMagicPopover
|
|
21
|
+
placement="bottom-end"
|
|
22
|
+
popper-class="m-editor-nav-menu-popover"
|
|
23
|
+
:width="popoverWidth"
|
|
24
|
+
:visible="popoverVisible"
|
|
25
|
+
>
|
|
26
|
+
<div class="m-editor-nav-menu-overflow-list">
|
|
27
|
+
<ToolButton v-for="(item, index) in overflowItems" :data="item" :key="`o-${index}`"></ToolButton>
|
|
28
|
+
</div>
|
|
29
|
+
<template #reference>
|
|
30
|
+
<div class="menu-item button m-editor-nav-menu-more" ref="referenceEl" @click="togglePopover">
|
|
31
|
+
<TMagicButton
|
|
32
|
+
size="small"
|
|
33
|
+
text
|
|
34
|
+
:icon="popoverVisible ? ArrowUp : ArrowDown"
|
|
35
|
+
:bg="popoverVisible"
|
|
36
|
+
:type="popoverVisible ? 'primary' : ''"
|
|
37
|
+
></TMagicButton>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
</TMagicPopover>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script lang="ts" setup>
|
|
46
|
+
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
|
47
|
+
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
|
48
|
+
|
|
49
|
+
import { TMagicButton, TMagicPopover } from '@tmagic/design';
|
|
50
|
+
|
|
51
|
+
import ToolButton from '@editor/components/ToolButton.vue';
|
|
52
|
+
import { MenuButton, MenuComponent } from '@editor/type';
|
|
53
|
+
|
|
54
|
+
defineOptions({
|
|
55
|
+
name: 'MEditorNavMenuColumn',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const props = withDefaults(
|
|
59
|
+
defineProps<{
|
|
60
|
+
columnKey: string;
|
|
61
|
+
items: (MenuButton | MenuComponent)[];
|
|
62
|
+
width?: number;
|
|
63
|
+
/** 子元素之间的间距,需与 SCSS gap 保持一致 */
|
|
64
|
+
gap?: number;
|
|
65
|
+
/** Popover 内容宽度 */
|
|
66
|
+
popoverWidth?: number;
|
|
67
|
+
}>(),
|
|
68
|
+
{
|
|
69
|
+
items: () => [],
|
|
70
|
+
gap: 3,
|
|
71
|
+
popoverWidth: 180,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const columnEl = useTemplateRef<HTMLDivElement>('columnEl');
|
|
76
|
+
const moreWrapperEl = useTemplateRef<HTMLDivElement>('moreWrapperEl');
|
|
77
|
+
|
|
78
|
+
const popoverVisible = ref(false);
|
|
79
|
+
const togglePopover = () => {
|
|
80
|
+
popoverVisible.value = !popoverVisible.value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const itemInstances = ref<(InstanceType<typeof ToolButton> | null)[]>([]);
|
|
84
|
+
let slotsRO: ResizeObserver | undefined;
|
|
85
|
+
const observedEls = new Set<HTMLElement>();
|
|
86
|
+
|
|
87
|
+
const setItemRef = (inst: InstanceType<typeof ToolButton> | null, index: number) => {
|
|
88
|
+
itemInstances.value[index] = inst ?? null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const itemEls = computed<(HTMLElement | null)[]>(() =>
|
|
92
|
+
itemInstances.value.map((inst) => inst?.getElRef?.().value ?? null),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const reobserveSlots = () => {
|
|
96
|
+
if (!slotsRO) return;
|
|
97
|
+
for (const el of observedEls) slotsRO.unobserve(el);
|
|
98
|
+
observedEls.clear();
|
|
99
|
+
for (const el of itemEls.value) {
|
|
100
|
+
if (el) {
|
|
101
|
+
slotsRO.observe(el);
|
|
102
|
+
observedEls.add(el);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const cachedWidths = ref<Map<number, number>>(new Map());
|
|
108
|
+
const moreWidth = ref(0);
|
|
109
|
+
const containerWidth = ref(0);
|
|
110
|
+
const hiddenIndexSet = ref<Set<number>>(new Set());
|
|
111
|
+
|
|
112
|
+
const hasOverflow = computed(() => hiddenIndexSet.value.size > 0);
|
|
113
|
+
const overflowItems = computed(() => props.items.filter((_, index) => hiddenIndexSet.value.has(index)));
|
|
114
|
+
|
|
115
|
+
const measureAndCompute = () => {
|
|
116
|
+
if (!columnEl.value) return;
|
|
117
|
+
|
|
118
|
+
containerWidth.value = columnEl.value.clientWidth;
|
|
119
|
+
|
|
120
|
+
const els = itemEls.value;
|
|
121
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
122
|
+
const el = els[i];
|
|
123
|
+
if (!el) {
|
|
124
|
+
cachedWidths.value.delete(i);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const w = el.getBoundingClientRect().width;
|
|
128
|
+
if (w > 0) {
|
|
129
|
+
cachedWidths.value.set(i, w);
|
|
130
|
+
} else {
|
|
131
|
+
cachedWidths.value.delete(i);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (moreWrapperEl.value) {
|
|
136
|
+
const w = moreWrapperEl.value.getBoundingClientRect().width;
|
|
137
|
+
if (w > 0) moreWidth.value = w;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const total = props.items.length;
|
|
141
|
+
if (total === 0 || containerWidth.value <= 0) {
|
|
142
|
+
if (hiddenIndexSet.value.size > 0) hiddenIndexSet.value = new Set();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let fullSum = 0;
|
|
147
|
+
let positive = 0;
|
|
148
|
+
for (let i = 0; i < total; i++) {
|
|
149
|
+
const w = cachedWidths.value.get(i) ?? 0;
|
|
150
|
+
if (w > 0) {
|
|
151
|
+
fullSum += w;
|
|
152
|
+
positive += 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
fullSum += props.gap * Math.max(0, positive - 1);
|
|
156
|
+
|
|
157
|
+
// more 按钮位置始终保留,参与"是否放得下"的判断,避免出现时再次挤压
|
|
158
|
+
const effectiveMoreWidth = moreWidth.value > 0 ? moreWidth.value : 32;
|
|
159
|
+
const reservedMore = effectiveMoreWidth + (positive > 0 ? props.gap : 0);
|
|
160
|
+
|
|
161
|
+
let nextHidden: Set<number> | null = null;
|
|
162
|
+
if (fullSum + reservedMore <= containerWidth.value + 0.5) {
|
|
163
|
+
if (hiddenIndexSet.value.size > 0) nextHidden = new Set();
|
|
164
|
+
} else {
|
|
165
|
+
const newHidden = new Set<number>();
|
|
166
|
+
let used = effectiveMoreWidth;
|
|
167
|
+
let cutoff = -1;
|
|
168
|
+
for (let i = 0; i < total; i++) {
|
|
169
|
+
const w = cachedWidths.value.get(i) ?? 0;
|
|
170
|
+
if (w === 0) continue;
|
|
171
|
+
const need = props.gap + w;
|
|
172
|
+
if (used + need > containerWidth.value) {
|
|
173
|
+
cutoff = i;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
used += need;
|
|
177
|
+
}
|
|
178
|
+
if (cutoff >= 0) {
|
|
179
|
+
for (let j = cutoff; j < total; j++) newHidden.add(j);
|
|
180
|
+
}
|
|
181
|
+
nextHidden = newHidden;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (nextHidden) {
|
|
185
|
+
const same =
|
|
186
|
+
nextHidden.size === hiddenIndexSet.value.size && [...nextHidden].every((v) => hiddenIndexSet.value.has(v));
|
|
187
|
+
if (!same) hiddenIndexSet.value = nextHidden;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let raf = 0;
|
|
192
|
+
const scheduleMeasure = () => {
|
|
193
|
+
if (raf) cancelAnimationFrame(raf);
|
|
194
|
+
raf = requestAnimationFrame(() => {
|
|
195
|
+
raf = 0;
|
|
196
|
+
measureAndCompute();
|
|
197
|
+
if (hasOverflow.value && moreWidth.value === 0) {
|
|
198
|
+
raf = requestAnimationFrame(() => {
|
|
199
|
+
raf = 0;
|
|
200
|
+
measureAndCompute();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
watch(hasOverflow, (value) => {
|
|
207
|
+
if (!value) popoverVisible.value = false;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
watch(
|
|
211
|
+
() => props.items,
|
|
212
|
+
() => {
|
|
213
|
+
cachedWidths.value = new Map();
|
|
214
|
+
hiddenIndexSet.value = new Set();
|
|
215
|
+
},
|
|
216
|
+
{ deep: true },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
watch(itemEls, () => {
|
|
220
|
+
cachedWidths.value = new Map();
|
|
221
|
+
reobserveSlots();
|
|
222
|
+
scheduleMeasure();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
watch(
|
|
226
|
+
() => props.width,
|
|
227
|
+
() => scheduleMeasure(),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
let ro: ResizeObserver | undefined;
|
|
231
|
+
|
|
232
|
+
onMounted(() => {
|
|
233
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
234
|
+
if (columnEl.value) {
|
|
235
|
+
ro = new ResizeObserver(() => scheduleMeasure());
|
|
236
|
+
ro.observe(columnEl.value);
|
|
237
|
+
}
|
|
238
|
+
slotsRO = new ResizeObserver(() => scheduleMeasure());
|
|
239
|
+
reobserveSlots();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
scheduleMeasure();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
onBeforeUnmount(() => {
|
|
246
|
+
if (raf) cancelAnimationFrame(raf);
|
|
247
|
+
ro?.disconnect();
|
|
248
|
+
slotsRO?.disconnect();
|
|
249
|
+
slotsRO = undefined;
|
|
250
|
+
observedEls.clear();
|
|
251
|
+
});
|
|
252
|
+
</script>
|