mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-05-06 10:35:15 +08:00
refactor(editor): 重构右键菜单
This commit is contained in:
parent
34eb57b37a
commit
56375b0fb0
107
packages/editor/src/components/ContentMenu.vue
Normal file
107
packages/editor/src/components/ContentMenu.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="menuData.length" v-show="visible" class="magic-editor-content-menu" ref="menu" :style="menuStyle">
|
||||||
|
<div>
|
||||||
|
<tool-button
|
||||||
|
v-for="(item, index) in menuData"
|
||||||
|
event-type="mouseup"
|
||||||
|
:data="item"
|
||||||
|
:key="index"
|
||||||
|
@mouseup="hide"
|
||||||
|
@mouseenter="showSubMenu(item)"
|
||||||
|
></tool-button>
|
||||||
|
</div>
|
||||||
|
<teleport to="body">
|
||||||
|
<content-menu class="sub-menu" ref="subMenu" :menu-data="subMenuData"></content-menu>
|
||||||
|
</teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, nextTick, onMounted, PropType, ref } from 'vue';
|
||||||
|
|
||||||
|
import { MenuButton, MenuItem } from '@editor/type';
|
||||||
|
|
||||||
|
import ToolButton from './ToolButton.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { ToolButton },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
menuData: {
|
||||||
|
type: Array as PropType<MenuItem[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const menu = ref<HTMLDivElement>();
|
||||||
|
const subMenu = ref<any>();
|
||||||
|
const visible = ref(false);
|
||||||
|
const subMenuData = ref<MenuItem[]>([]);
|
||||||
|
const menuStyle = ref({
|
||||||
|
left: '0',
|
||||||
|
top: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
visible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
globalThis.addEventListener(
|
||||||
|
'mousedown',
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!visible.value || (e.target && menu.value?.contains(e.target as HTMLElement))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hide();
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
menu,
|
||||||
|
subMenu,
|
||||||
|
visible,
|
||||||
|
menuStyle,
|
||||||
|
subMenuData,
|
||||||
|
|
||||||
|
hide,
|
||||||
|
|
||||||
|
show(e: MouseEvent) {
|
||||||
|
visible.value = true;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const menuHeight = menu.value?.clientHeight || 0;
|
||||||
|
|
||||||
|
let top = e.clientY;
|
||||||
|
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||||
|
top = document.body.clientHeight - menuHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuStyle.value = {
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${e.clientX}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showSubMenu(item: MenuItem) {
|
||||||
|
const menuItem = item as MenuButton;
|
||||||
|
if (typeof item !== 'object' || !menuItem.items?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subMenuData.value = menuItem.items;
|
||||||
|
if (menu.value) {
|
||||||
|
subMenu.value.show({
|
||||||
|
clientX: menu.value.offsetLeft + menu.value.clientWidth,
|
||||||
|
clientY: menu.value.offsetTop,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,24 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="display" class="menu-item">
|
<div
|
||||||
<el-divider v-if="item.type === 'divider'" direction="vertical"></el-divider>
|
v-if="display"
|
||||||
|
class="menu-item"
|
||||||
|
:class="item.type"
|
||||||
|
@click="clickHandler(item, $event)"
|
||||||
|
@mousedown="mousedownHandler(item, $event)"
|
||||||
|
@mouseup="mouseupHandler(item, $event)"
|
||||||
|
>
|
||||||
|
<el-divider v-if="item.type === 'divider'" :direction="item.direction || 'vertical'"></el-divider>
|
||||||
<div v-else-if="item.type === 'text'" class="menu-item-text">{{ item.text }}</div>
|
<div v-else-if="item.type === 'text'" class="menu-item-text">{{ item.text }}</div>
|
||||||
|
|
||||||
<template v-else-if="item.type === 'zoom'">
|
<template v-else-if="item.type === 'zoom'">
|
||||||
<el-button size="small" type="text"><m-icon :icon="ZoomIn" @click="zoomInHandler"></m-icon></el-button>
|
<tool-button
|
||||||
|
:data="{ type: 'button', icon: ZoomIn, handler: zoomInHandler, tooltip: '放大' }"
|
||||||
|
:event-type="eventType"
|
||||||
|
></tool-button>
|
||||||
<span class="menu-item-text" style="margin: 0 5px">{{ parseInt(`${zoom * 100}`, 10) }}%</span>
|
<span class="menu-item-text" style="margin: 0 5px">{{ parseInt(`${zoom * 100}`, 10) }}%</span>
|
||||||
<el-button size="small" type="text"><m-icon :icon="ZoomOut" @click="zoomOutHandler"></m-icon></el-button>
|
<tool-button
|
||||||
|
:data="{ type: 'button', icon: ZoomOut, handler: zoomOutHandler, tooltip: '缩小' }"
|
||||||
|
:event-type="eventType"
|
||||||
|
></tool-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-tooltip
|
<template v-else-if="item.type === 'button'">
|
||||||
v-else-if="item.type === 'button'"
|
<el-tooltip v-if="item.tooltip" effect="dark" placement="bottom-start" :content="item.tooltip">
|
||||||
effect="dark"
|
<el-button size="small" type="text" :disabled="disabled"
|
||||||
placement="bottom-start"
|
><m-icon v-if="item.icon" :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
|
||||||
:content="item.tooltip || item.text"
|
>
|
||||||
>
|
</el-tooltip>
|
||||||
<el-button size="small" type="text" :disabled="disabled" @click="buttonHandler(item)"
|
<el-button v-else size="small" type="text" :disabled="disabled"
|
||||||
><m-icon :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
|
><m-icon v-if="item.icon" :icon="item.icon"></m-icon><span>{{ item.text }}</span></el-button
|
||||||
>
|
>
|
||||||
</el-tooltip>
|
</template>
|
||||||
|
|
||||||
<el-dropdown v-else-if="item.type === 'dropdown'" trigger="click" :disabled="disabled" @command="dropdownHandler">
|
<el-dropdown v-else-if="item.type === 'dropdown'" trigger="click" :disabled="disabled" @command="dropdownHandler">
|
||||||
<span class="el-dropdown-link menubar-menu-button">
|
<span class="el-dropdown-link menubar-menu-button">
|
||||||
@ -38,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject, PropType } from 'vue';
|
import { computed, defineComponent, inject, markRaw, PropType } from 'vue';
|
||||||
import { ArrowDown, Back, Delete, Grid, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons';
|
import { ArrowDown, Back, Delete, Grid, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons';
|
||||||
|
|
||||||
import { NodeType } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
@ -58,6 +71,11 @@ export default defineComponent({
|
|||||||
display: false,
|
display: false,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
eventType: {
|
||||||
|
type: String as PropType<'mousedown' | 'mouseup' | 'click'>,
|
||||||
|
default: 'click',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
@ -87,7 +105,7 @@ export default defineComponent({
|
|||||||
case 'delete':
|
case 'delete':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: Delete,
|
icon: markRaw(Delete),
|
||||||
tooltip: '刪除',
|
tooltip: '刪除',
|
||||||
disabled: () => services?.editorService.get('node')?.type === NodeType.PAGE,
|
disabled: () => services?.editorService.get('node')?.type === NodeType.PAGE,
|
||||||
handler: () => services?.editorService.remove(services?.editorService.get('node')),
|
handler: () => services?.editorService.remove(services?.editorService.get('node')),
|
||||||
@ -95,7 +113,7 @@ export default defineComponent({
|
|||||||
case 'undo':
|
case 'undo':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: Back,
|
icon: markRaw(Back),
|
||||||
tooltip: '后退',
|
tooltip: '后退',
|
||||||
disabled: () => !services?.historyService.state.canUndo,
|
disabled: () => !services?.historyService.state.canUndo,
|
||||||
handler: () => services?.editorService.undo(),
|
handler: () => services?.editorService.undo(),
|
||||||
@ -103,7 +121,7 @@ export default defineComponent({
|
|||||||
case 'redo':
|
case 'redo':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: Right,
|
icon: markRaw(Right),
|
||||||
tooltip: '前进',
|
tooltip: '前进',
|
||||||
disabled: () => !services?.historyService.state.canRedo,
|
disabled: () => !services?.historyService.state.canRedo,
|
||||||
handler: () => services?.editorService.redo(),
|
handler: () => services?.editorService.redo(),
|
||||||
@ -111,28 +129,28 @@ export default defineComponent({
|
|||||||
case 'zoom-in':
|
case 'zoom-in':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: ZoomIn,
|
icon: markRaw(ZoomIn),
|
||||||
tooltip: '放大',
|
tooltip: '放大',
|
||||||
handler: zoomInHandler,
|
handler: zoomInHandler,
|
||||||
};
|
};
|
||||||
case 'zoom-out':
|
case 'zoom-out':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: ZoomOut,
|
icon: markRaw(ZoomOut),
|
||||||
tooltip: '縮小',
|
tooltip: '縮小',
|
||||||
handler: zoomOutHandler,
|
handler: zoomOutHandler,
|
||||||
};
|
};
|
||||||
case 'rule':
|
case 'rule':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: ScaleToOriginal,
|
icon: markRaw(ScaleToOriginal),
|
||||||
tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
|
tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
|
||||||
handler: () => uiService?.set('showRule', !showRule.value),
|
handler: () => uiService?.set('showRule', !showRule.value),
|
||||||
};
|
};
|
||||||
case 'guides':
|
case 'guides':
|
||||||
return {
|
return {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: Grid,
|
icon: markRaw(Grid),
|
||||||
tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
|
tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
|
||||||
handler: () => uiService?.set('showGuides', !showGuides.value),
|
handler: () => uiService?.set('showGuides', !showGuides.value),
|
||||||
};
|
};
|
||||||
@ -153,9 +171,16 @@ export default defineComponent({
|
|||||||
return item.value.disabled;
|
return item.value.disabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buttonHandler = (item: MenuButton | MenuComponent, event: MouseEvent) => {
|
||||||
|
if (disabled.value) return;
|
||||||
|
if (typeof (item as MenuButton).handler === 'function' && services) {
|
||||||
|
(item as MenuButton).handler?.(services, event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ZoomIn,
|
ZoomIn: markRaw(ZoomIn),
|
||||||
ZoomOut,
|
ZoomOut: markRaw(ZoomOut),
|
||||||
|
|
||||||
item,
|
item,
|
||||||
zoom,
|
zoom,
|
||||||
@ -178,10 +203,24 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buttonHandler(item: MenuButton | MenuComponent) {
|
clickHandler(item: MenuButton | MenuComponent, event: MouseEvent) {
|
||||||
if (disabled.value) return;
|
if (props.eventType !== 'click') return;
|
||||||
if (typeof (item as MenuButton).handler === 'function') {
|
if (item.type === 'button') {
|
||||||
(item as MenuButton).handler?.(services);
|
buttonHandler(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mousedownHandler(item: MenuButton | MenuComponent, event: MouseEvent) {
|
||||||
|
if (props.eventType !== 'mousedown') return;
|
||||||
|
if (item.type === 'button') {
|
||||||
|
buttonHandler(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mouseupHandler(item: MenuButton | MenuComponent, event: MouseEvent) {
|
||||||
|
if (props.eventType !== 'mouseup') return;
|
||||||
|
if (item.type === 'button') {
|
||||||
|
buttonHandler(item, event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,91 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="node" class="magic-editor-content-menu">
|
<content-menu :menu-data="menuData" ref="menu" style="overflow: initial"></content-menu>
|
||||||
<div
|
|
||||||
v-if="node.items"
|
|
||||||
class="magic-editor-content-menu-item"
|
|
||||||
@mouseenter="setSubVisitable(true)"
|
|
||||||
@mouseleave="setSubVisitable(false)"
|
|
||||||
>
|
|
||||||
新增
|
|
||||||
</div>
|
|
||||||
<div v-if="node.type !== 'app'" class="magic-editor-content-menu-item" @click="() => copy(node)">复制</div>
|
|
||||||
<div
|
|
||||||
v-if="node.type !== 'app' && node.type !== 'page'"
|
|
||||||
class="magic-editor-content-menu-item"
|
|
||||||
@click="() => remove()"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</div>
|
|
||||||
<div class="subMenu" v-show="subVisible" @mouseenter="setSubVisitable(true)" @mouseleave="setSubVisitable(false)">
|
|
||||||
<el-scrollbar>
|
|
||||||
<template v-if="node.type === 'tabs'">
|
|
||||||
<div
|
|
||||||
class="magic-editor-content-menu-item"
|
|
||||||
@click="
|
|
||||||
() =>
|
|
||||||
append({
|
|
||||||
type: 'tab-pane',
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
标签
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="node.items">
|
|
||||||
<div v-for="list in componentGroupList" :key="list.title">
|
|
||||||
<template v-for="item in list.items">
|
|
||||||
<div class="magic-editor-content-menu-item" v-if="item" :key="item.type" @click="() => append(item)">
|
|
||||||
{{ item.text }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="separation"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject, ref } from 'vue';
|
import { computed, defineComponent, inject, ref } from 'vue';
|
||||||
|
|
||||||
import type { MNode } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
|
|
||||||
import type { AddMNode, Services } from '@editor/type';
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
|
import type { ComponentGroup, MenuButton, MenuItem, Services } from '@editor/type';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'magic-editor-content-menu',
|
components: { ContentMenu },
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
const subVisible = ref(false);
|
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||||
const node = computed(() => services?.editorService.get('node'));
|
const node = computed(() => services?.editorService.get('node'));
|
||||||
|
const isRoot = computed(() => node.value?.type === NodeType.ROOT);
|
||||||
|
const isPage = computed(() => node.value?.type === NodeType.PAGE);
|
||||||
|
const componentList = computed(() => services?.componentListService.getList() || []);
|
||||||
|
|
||||||
|
const createMenuItems = (group: ComponentGroup): MenuButton[] =>
|
||||||
|
group.items.map((component) => ({
|
||||||
|
text: component.text,
|
||||||
|
type: 'button',
|
||||||
|
handler: () => {
|
||||||
|
services?.editorService.add({
|
||||||
|
name: component.text,
|
||||||
|
type: component.type,
|
||||||
|
...(component.data || {}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getSubMenuData = computed<MenuButton[]>(() => {
|
||||||
|
if (node.value?.type === 'tabs') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: '标签页',
|
||||||
|
type: 'button',
|
||||||
|
handler: () => {
|
||||||
|
services?.editorService.add({
|
||||||
|
type: 'tab-pane',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (node.value?.items) {
|
||||||
|
return (
|
||||||
|
componentList.value.reduce(
|
||||||
|
(subMenuData: MenuButton[], group: ComponentGroup, index) =>
|
||||||
|
subMenuData.concat(
|
||||||
|
createMenuItems(group),
|
||||||
|
index < componentList.value.length - 1
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
direction: 'horizontal',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subVisible,
|
menu,
|
||||||
|
menuData: computed<MenuItem[]>(() => [
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '新增',
|
||||||
|
display: () => node.value?.items?.length > 0,
|
||||||
|
items: getSubMenuData.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '复制',
|
||||||
|
display: () => !isRoot.value,
|
||||||
|
handler: () => {
|
||||||
|
node.value && services?.editorService.copy(node.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '删除',
|
||||||
|
display: () => !isRoot.value && !isPage.value,
|
||||||
|
handler: () => {
|
||||||
|
node.value && services?.editorService.remove(node.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
node,
|
show(e: MouseEvent) {
|
||||||
|
menu.value?.show(e);
|
||||||
componentGroupList: computed(() => services?.componentListService.getList()),
|
|
||||||
|
|
||||||
append(config: AddMNode) {
|
|
||||||
services?.editorService.add({
|
|
||||||
name: config.text,
|
|
||||||
type: config.type,
|
|
||||||
...(config.data || {}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
node.value && services?.editorService.remove(node.value);
|
|
||||||
},
|
|
||||||
|
|
||||||
copy(node?: MNode) {
|
|
||||||
node && services?.editorService.copy(node);
|
|
||||||
},
|
|
||||||
|
|
||||||
setSubVisitable(v: boolean) {
|
|
||||||
subVisible.value = v;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -33,8 +33,8 @@
|
|||||||
<div
|
<div
|
||||||
:id="data.id"
|
:id="data.id"
|
||||||
class="cus-tree-node"
|
class="cus-tree-node"
|
||||||
@mousedown="toogleClickFlag"
|
@mousedown="toggleClickFlag"
|
||||||
@mouseup="toogleClickFlag"
|
@mouseup="toggleClickFlag"
|
||||||
@mouseenter="highlightHandler(data)"
|
@mouseenter="highlightHandler(data)"
|
||||||
:class="{ 'cus-tree-node-hover': canHighlight && data.id === highlightNode?.id }"
|
:class="{ 'cus-tree-node-hover': canHighlight && data.id === highlightNode?.id }"
|
||||||
>
|
>
|
||||||
@ -48,13 +48,13 @@
|
|||||||
</el-tree>
|
</el-tree>
|
||||||
|
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<layer-menu :style="menuStyle"></layer-menu>
|
<layer-menu ref="menu"></layer-menu>
|
||||||
</teleport>
|
</teleport>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject, onMounted, Ref, ref, watchEffect } from 'vue';
|
import { computed, defineComponent, inject, Ref, ref, watchEffect } from 'vue';
|
||||||
import type { ElTree } from 'element-plus';
|
import type { ElTree } from 'element-plus';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
|
|
||||||
@ -169,46 +169,6 @@ const useFilter = (tree: Ref<InstanceType<typeof ElTree> | undefined>) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const useContentMenu = (editorService?: EditorService) => {
|
|
||||||
const menuStyle = ref({
|
|
||||||
position: 'absolute',
|
|
||||||
left: '0',
|
|
||||||
top: '0',
|
|
||||||
display: 'none',
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
menuStyle.value.display = 'none';
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
menuStyle,
|
|
||||||
|
|
||||||
contextmenu(event: MouseEvent, data: MNode) {
|
|
||||||
const bodyHeight = globalThis.document.body.clientHeight;
|
|
||||||
|
|
||||||
const left = `${event.clientX + 20}px`;
|
|
||||||
let top = `${event.clientY - 10}px`;
|
|
||||||
|
|
||||||
if (event.clientY + 300 > bodyHeight) {
|
|
||||||
top = `${bodyHeight - 300}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
menuStyle.value.left = left;
|
|
||||||
menuStyle.value.top = top;
|
|
||||||
menuStyle.value.display = '';
|
|
||||||
|
|
||||||
select(data, editorService);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'magic-editor-layer-panel',
|
name: 'magic-editor-layer-panel',
|
||||||
|
|
||||||
@ -217,13 +177,14 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
const tree = ref<InstanceType<typeof ElTree>>();
|
const tree = ref<InstanceType<typeof ElTree>>();
|
||||||
|
const menu = ref<InstanceType<typeof LayerMenu>>();
|
||||||
const clicked = ref(false);
|
const clicked = ref(false);
|
||||||
const editorService = services?.editorService;
|
const editorService = services?.editorService;
|
||||||
const highlightHandler = throttle((data: MNode) => {
|
const highlightHandler = throttle((data: MNode) => {
|
||||||
highlight(data, editorService);
|
highlight(data, editorService);
|
||||||
}, throttleTime);
|
}, throttleTime);
|
||||||
|
|
||||||
const toogleClickFlag = () => {
|
const toggleClickFlag = () => {
|
||||||
clicked.value = !clicked.value;
|
clicked.value = !clicked.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -234,10 +195,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
tree,
|
tree,
|
||||||
|
menu,
|
||||||
...statusData,
|
...statusData,
|
||||||
...useDrop(tree, editorService),
|
...useDrop(tree, editorService),
|
||||||
...useFilter(tree),
|
...useFilter(tree),
|
||||||
...useContentMenu(editorService),
|
|
||||||
|
highlightHandler,
|
||||||
|
toggleClickFlag,
|
||||||
|
canHighlight,
|
||||||
|
|
||||||
clickHandler(data: MNode): void {
|
clickHandler(data: MNode): void {
|
||||||
if (services?.uiService.get<boolean>('uiSelectMode')) {
|
if (services?.uiService.get<boolean>('uiSelectMode')) {
|
||||||
@ -247,9 +212,12 @@ export default defineComponent({
|
|||||||
tree.value?.setCurrentKey(data.id);
|
tree.value?.setCurrentKey(data.id);
|
||||||
select(data, editorService);
|
select(data, editorService);
|
||||||
},
|
},
|
||||||
highlightHandler,
|
|
||||||
toogleClickFlag,
|
async contextmenu(event: MouseEvent, data: MNode) {
|
||||||
canHighlight,
|
event.preventDefault();
|
||||||
|
await select(data, editorService);
|
||||||
|
menu.value?.show(event);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
:style="`transform: scale(${zoom})`"
|
:style="`transform: scale(${zoom})`"
|
||||||
></div>
|
></div>
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<viewer-menu ref="menu" :style="menuStyle"></viewer-menu>
|
<viewer-menu ref="menu"></viewer-menu>
|
||||||
</teleport>
|
</teleport>
|
||||||
</scroll-viewer>
|
</scroll-viewer>
|
||||||
</template>
|
</template>
|
||||||
@ -43,45 +43,6 @@ import type { Services, StageRect } from '@editor/type';
|
|||||||
|
|
||||||
import ViewerMenu from './ViewerMenu.vue';
|
import ViewerMenu from './ViewerMenu.vue';
|
||||||
|
|
||||||
const useMenu = () => {
|
|
||||||
const menu = ref<InstanceType<typeof ViewerMenu>>();
|
|
||||||
const menuStyle = ref({
|
|
||||||
display: 'none',
|
|
||||||
left: '0',
|
|
||||||
top: '0',
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener(
|
|
||||||
'click',
|
|
||||||
() => {
|
|
||||||
menuStyle.value.display = 'none';
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
menu,
|
|
||||||
menuStyle,
|
|
||||||
|
|
||||||
contextmenuHandler(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const menuHeight = menu.value?.$el.clientHeight;
|
|
||||||
let top = e.clientY;
|
|
||||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
|
||||||
top = document.body.clientHeight - menuHeight;
|
|
||||||
}
|
|
||||||
menuStyle.value = {
|
|
||||||
display: 'block',
|
|
||||||
top: `${top}px`,
|
|
||||||
left: `${e.clientX}px`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'magic-stage',
|
name: 'magic-stage',
|
||||||
|
|
||||||
@ -115,12 +76,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
const stageWrap = ref<InstanceType<typeof ScrollViewer>>();
|
const stageWrap = ref<InstanceType<typeof ScrollViewer>>();
|
||||||
const stageContainer = ref<HTMLDivElement>();
|
const stageContainer = ref<HTMLDivElement>();
|
||||||
|
const menu = ref<InstanceType<typeof ViewerMenu>>();
|
||||||
|
|
||||||
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
|
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
|
||||||
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
|
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
|
||||||
const root = computed(() => services?.editorService.get<MApp>('root'));
|
const root = computed(() => services?.editorService.get<MApp>('root'));
|
||||||
const page = computed(() => services?.editorService.get<MPage>('page'));
|
const page = computed(() => services?.editorService.get<MPage>('page'));
|
||||||
const zoom = computed(() => services?.uiService.get<number>('zoom'));
|
const zoom = computed(() => services?.uiService.get<number>('zoom') || 1);
|
||||||
const node = computed(() => services?.editorService.get<MNode>('node'));
|
const node = computed(() => services?.editorService.get<MNode>('node'));
|
||||||
|
|
||||||
let stage: StageCore | null = null;
|
let stage: StageCore | null = null;
|
||||||
@ -218,9 +180,14 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
stageWrap,
|
stageWrap,
|
||||||
stageContainer,
|
stageContainer,
|
||||||
|
menu,
|
||||||
stageRect,
|
stageRect,
|
||||||
zoom,
|
zoom,
|
||||||
...useMenu(),
|
|
||||||
|
contextmenuHandler(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
menu.value?.show(e);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,47 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="magic-editor-content-menu" ref="menu">
|
<content-menu :menu-data="menuData" ref="menu"></content-menu>
|
||||||
<div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="() => center()" v-if="canCenter">水平居中</div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="() => copy()">复制</div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="paste" v-if="canPaste">粘贴</div>
|
|
||||||
<template v-if="canMoveZPos">
|
|
||||||
<div class="separation"></div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="topItem">上移一层</div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="bottomItem">下移一层</div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="top">置顶</div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="bottom">置底</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="canDelete">
|
|
||||||
<div class="separation"></div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="() => remove()">删除</div>
|
|
||||||
</template>
|
|
||||||
<div class="separation"></div>
|
|
||||||
<div class="magic-editor-content-menu-item" @click="clearGuides">清空参考线</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject, onMounted, ref, watch } from 'vue';
|
import { computed, defineComponent, inject, markRaw, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { Bottom, Delete, DocumentCopy, Top } from '@element-plus/icons';
|
||||||
|
|
||||||
import { NodeType } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
import type StageCore from '@tmagic/stage';
|
import type StageCore from '@tmagic/stage';
|
||||||
|
|
||||||
import { LayerOffset, Layout, Services } from '@editor/type';
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
|
import { LayerOffset, Layout, MenuItem, Services } from '@editor/type';
|
||||||
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
|
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'magic-editor-ui-viewer-menu',
|
components: { ContentMenu },
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
const editorService = services?.editorService;
|
const editorService = services?.editorService;
|
||||||
const menu = ref<HTMLDivElement>();
|
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||||
const canPaste = ref(false);
|
const canPaste = ref(false);
|
||||||
const canCenter = ref(false);
|
const canCenter = ref(false);
|
||||||
|
|
||||||
const node = computed(() => editorService?.get('node'));
|
const node = computed(() => editorService?.get('node'));
|
||||||
const parent = computed(() => editorService?.get('parent'));
|
const parent = computed(() => editorService?.get('parent'));
|
||||||
|
const isPage = computed(() => node.value?.type === NodeType.PAGE);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const data = globalThis.localStorage.getItem(COPY_STORAGE_KEY);
|
const data = globalThis.localStorage.getItem(COPY_STORAGE_KEY);
|
||||||
@ -62,49 +46,102 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
menu,
|
menu,
|
||||||
canPaste,
|
menuData: reactive<MenuItem[]>([
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '水平居中',
|
||||||
|
display: () => canCenter.value,
|
||||||
|
handler: () => {
|
||||||
|
node.value && editorService?.alignCenter(node.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '复制',
|
||||||
|
icon: markRaw(DocumentCopy),
|
||||||
|
handler: () => {
|
||||||
|
node.value && editorService?.copy(node.value);
|
||||||
|
canPaste.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '粘贴',
|
||||||
|
display: () => canPaste.value,
|
||||||
|
handler: () => {
|
||||||
|
const top = menu.value?.$el.offsetTop || 0;
|
||||||
|
const left = menu.value?.$el.offsetLeft || 0;
|
||||||
|
editorService?.paste({ left, top });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
direction: 'horizontal',
|
||||||
|
display: () => !isPage.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '上移一层',
|
||||||
|
icon: markRaw(Top),
|
||||||
|
display: () => !isPage.value,
|
||||||
|
handler: () => {
|
||||||
|
editorService?.moveLayer(1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '下移一层',
|
||||||
|
icon: markRaw(Bottom),
|
||||||
|
display: () => !isPage.value,
|
||||||
|
handler: () => {
|
||||||
|
editorService?.moveLayer(-1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '置顶',
|
||||||
|
display: () => !isPage.value,
|
||||||
|
handler: () => {
|
||||||
|
editorService?.moveLayer(LayerOffset.TOP);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '置底',
|
||||||
|
display: () => !isPage.value,
|
||||||
|
handler: () => {
|
||||||
|
editorService?.moveLayer(LayerOffset.BOTTOM);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
direction: 'horizontal',
|
||||||
|
display: () => !isPage.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '删除',
|
||||||
|
icon: Delete,
|
||||||
|
display: () => !isPage.value,
|
||||||
|
handler: () => {
|
||||||
|
node.value && editorService?.remove(node.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
direction: 'horizontal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '清空参考线',
|
||||||
|
handler: () => {
|
||||||
|
editorService?.get<StageCore>('stage').clearGuides();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
canDelete: computed(() => node.value?.type !== NodeType.PAGE),
|
show(e: MouseEvent) {
|
||||||
canMoveZPos: computed(() => node.value?.type !== NodeType.PAGE),
|
menu.value?.show(e);
|
||||||
canCenter,
|
|
||||||
|
|
||||||
center() {
|
|
||||||
node.value && editorService?.alignCenter(node.value);
|
|
||||||
},
|
|
||||||
|
|
||||||
copy() {
|
|
||||||
node.value && editorService?.copy(node.value);
|
|
||||||
canPaste.value = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
paste() {
|
|
||||||
const top = menu.value?.offsetTop || 0;
|
|
||||||
const left = menu.value?.offsetLeft || 0;
|
|
||||||
editorService?.paste({ left, top });
|
|
||||||
},
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
node.value && editorService?.remove(node.value);
|
|
||||||
},
|
|
||||||
|
|
||||||
top() {
|
|
||||||
editorService?.moveLayer(LayerOffset.TOP);
|
|
||||||
},
|
|
||||||
|
|
||||||
bottom() {
|
|
||||||
editorService?.moveLayer(LayerOffset.BOTTOM);
|
|
||||||
},
|
|
||||||
|
|
||||||
topItem() {
|
|
||||||
editorService?.moveLayer(1);
|
|
||||||
},
|
|
||||||
|
|
||||||
bottomItem() {
|
|
||||||
editorService?.moveLayer(-1);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearGuides() {
|
|
||||||
editorService?.get<StageCore>('stage').clearGuides();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -2,38 +2,45 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #333;
|
|
||||||
box-shadow: 0 2px 8px 2px rgba(68, 73, 77, 0.16);
|
box-shadow: 0 2px 8px 2px rgba(68, 73, 77, 0.16);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transform-origin: 0% 0%;
|
transform-origin: 0% 0%;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 4px 0px;
|
padding: 4px 0px;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
|
||||||
.separation {
|
.menu-item {
|
||||||
width: 100%;
|
|
||||||
height: 0;
|
|
||||||
border-color: rgba(155, 155, 155, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subMenu {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
transform: translate(100%, 0);
|
|
||||||
background-color: #fff;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
box-shadow: 0 2px 8px 2px rgba(68, 73, 77, 0.16);
|
display: flex;
|
||||||
top: 0;
|
-webkit-box-align: center;
|
||||||
height: 223px;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 140px;
|
||||||
|
transition: all 0.2s ease 0s;
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--text,
|
||||||
|
i {
|
||||||
|
color: $--font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.divider {
|
||||||
|
padding: 0 14px;
|
||||||
|
|
||||||
|
.el-divider {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f3f5f9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.magic-editor-content-menu-item {
|
|
||||||
display: flex;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 140px;
|
|
||||||
transition: all 0.2s ease 0s;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-left: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
@ -125,6 +125,8 @@ export interface MenuButton {
|
|||||||
* zoom: 放大缩小
|
* zoom: 放大缩小
|
||||||
*/
|
*/
|
||||||
type: 'button' | 'dropdown' | 'text' | 'divider' | 'zoom';
|
type: 'button' | 'dropdown' | 'text' | 'divider' | 'zoom';
|
||||||
|
/** 当type为divider时有效,分割线方向, 默认vertical */
|
||||||
|
direction?: 'horizontal' | 'vertical';
|
||||||
/** 展示的文案 */
|
/** 展示的文案 */
|
||||||
text?: string;
|
text?: string;
|
||||||
/** 鼠标悬浮是显示的气泡中的文案 */
|
/** 鼠标悬浮是显示的气泡中的文案 */
|
||||||
@ -136,14 +138,9 @@ export interface MenuButton {
|
|||||||
/** 是否显示,默认为true */
|
/** 是否显示,默认为true */
|
||||||
display?: boolean | ((data?: Services) => boolean);
|
display?: boolean | ((data?: Services) => boolean);
|
||||||
/** type为button/dropdown时点击运行的方法 */
|
/** type为button/dropdown时点击运行的方法 */
|
||||||
handler?: (data?: Services) => Promise<any> | any;
|
handler?: (data: Services, event: MouseEvent) => Promise<any> | any;
|
||||||
/** type为dropdown时,下拉的菜单列表 */
|
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
|
||||||
items?: {
|
items?: MenuButton[];
|
||||||
/** 展示的文案 */
|
|
||||||
text: string;
|
|
||||||
/** 点击运行的方法 */
|
|
||||||
handler(data: Services): any;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuComponent {
|
export interface MenuComponent {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user