feat(editor): 新增右键菜单移动至其他页面功能

This commit is contained in:
roymondchen 2023-07-05 14:14:53 +08:00
parent b0f2ed55f6
commit 434bf2ed70
6 changed files with 133 additions and 62 deletions

View File

@ -4,10 +4,11 @@
<ToolButton
v-for="(item, index) in menuData"
event-type="mouseup"
ref="buttons"
:data="item"
:key="index"
@mouseup="hide"
@mouseenter="showSubMenu(item)"
@mouseenter="showSubMenu(item, index)"
></ToolButton>
</div>
<teleport to="body">
@ -48,6 +49,7 @@ const props = withDefaults(
const emit = defineEmits(['hide', 'show']);
const menu = ref<HTMLDivElement>();
const buttons = ref<InstanceType<typeof ToolButton>[]>();
const subMenu = ref<any>();
const visible = ref(false);
const subMenuData = ref<(MenuButton | MenuComponent)[]>([]);
@ -101,7 +103,7 @@ const show = (e: MouseEvent) => {
}, 300);
};
const showSubMenu = (item: MenuButton | MenuComponent) => {
const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
const menuItem = item as MenuButton;
if (typeof item !== 'object' || !menuItem.items?.length) {
return;
@ -110,9 +112,15 @@ const showSubMenu = (item: MenuButton | MenuComponent) => {
subMenuData.value = menuItem.items || [];
setTimeout(() => {
if (menu.value) {
//
let y = menu.value.offsetTop;
if (buttons.value?.[index].$el) {
const rect = buttons.value?.[index].$el.getBoundingClientRect();
y = rect.top;
}
subMenu.value?.show({
clientX: menu.value.offsetLeft + menu.value.clientWidth,
clientY: menu.value.offsetTop,
clientY: y,
});
}
}, 0);

View File

@ -0,0 +1,13 @@
<template>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4H21V6H2V4Z" fill="black" fill-opacity="0.9" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11H18V13H5V11Z" fill="black" fill-opacity="0.9" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 18H21V20H2V18Z" fill="black" fill-opacity="0.9" />
</svg>
</template>
<script lang="ts" setup>
defineOptions({
name: 'MEditorCenterIcon',
});
</script>

View File

@ -4,12 +4,11 @@
<script lang="ts" setup>
import { computed, inject, markRaw, ref } from 'vue';
import { CopyDocument, Delete, Files, Plus } from '@element-plus/icons-vue';
import { NodeType } from '@tmagic/schema';
import { Files, Plus } from '@element-plus/icons-vue';
import ContentMenu from '@editor/components/ContentMenu.vue';
import type { ComponentGroup, MenuButton, MenuComponent, Services } from '@editor/type';
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
defineOptions({
name: 'MEditorLayerMenu',
@ -23,8 +22,6 @@ const services = inject<Services>('services');
const menu = ref<InstanceType<typeof ContentMenu>>();
const node = computed(() => services?.editorService.get('node'));
const nodes = computed(() => services?.editorService.get('nodes'));
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[] =>
@ -86,24 +83,10 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
display: () => node.value?.items && nodes.value?.length === 1,
items: getSubMenuData.value,
},
{
type: 'button',
text: '复制',
icon: markRaw(CopyDocument),
display: () => !isRoot.value,
handler: () => {
node.value && services?.editorService.copy(nodes.value || []);
},
},
{
type: 'button',
text: '删除',
icon: markRaw(Delete),
display: () => !isRoot.value && !isPage.value,
handler: () => {
node.value && services?.editorService.remove(nodes.value || []);
},
},
useCopyMenu(),
usePasteMenu(),
useDeleteMenu(),
useMoveToMenu(services),
...props.layerContentMenu,
]);

View File

@ -4,14 +4,16 @@
<script lang="ts" setup>
import { computed, inject, markRaw, ref, watch } from 'vue';
import { Bottom, CopyDocument, Delete, DocumentCopy, Top } from '@element-plus/icons-vue';
import { Bottom, Top } from '@element-plus/icons-vue';
import { NodeType } from '@tmagic/schema';
import { isPage } from '@tmagic/utils';
import ContentMenu from '@editor/components/ContentMenu.vue';
import CenterIcon from '@editor/icons/CenterIcon.vue';
import storageService from '@editor/services/storage';
import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
defineOptions({
@ -32,42 +34,20 @@ const canCenter = ref(false);
const node = computed(() => editorService?.get('node'));
const nodes = computed(() => editorService?.get('nodes'));
const parent = computed(() => editorService?.get('parent'));
const stage = computed(() => editorService?.get('stage'));
const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
{
type: 'button',
text: '水平居中',
icon: markRaw(CenterIcon),
display: () => canCenter.value,
handler: () => {
if (!nodes.value) return;
editorService?.alignCenter(nodes.value);
},
},
{
type: 'button',
text: '复制',
icon: markRaw(CopyDocument),
handler: () => {
nodes.value && editorService?.copy(nodes.value);
canPaste.value = true;
},
},
{
type: 'button',
text: '粘贴',
icon: markRaw(DocumentCopy),
display: () => canPaste.value,
handler: () => {
const rect = menu.value?.$el.getBoundingClientRect();
const parentRect = stage.value?.container?.getBoundingClientRect();
const initialLeft = (rect?.left || 0) - (parentRect?.left || 0);
const initialTop = (rect?.top || 0) - (parentRect?.top || 0);
if (!nodes.value || nodes.value.length === 0) return;
editorService?.paste({ left: initialLeft, top: initialTop });
},
},
useCopyMenu(),
usePasteMenu(menu),
{
type: 'divider',
direction: 'horizontal',
@ -97,6 +77,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
{
type: 'button',
text: '置顶',
icon: markRaw(Top),
display: () => !isPage(node.value) && !props.isMultiSelect,
handler: () => {
editorService?.moveLayer(LayerOffset.TOP);
@ -105,25 +86,19 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
{
type: 'button',
text: '置底',
icon: markRaw(Bottom),
display: () => !isPage(node.value) && !props.isMultiSelect,
handler: () => {
editorService?.moveLayer(LayerOffset.BOTTOM);
},
},
useMoveToMenu(services),
{
type: 'divider',
direction: 'horizontal',
display: () => !isPage(node.value) && !props.isMultiSelect,
},
{
type: 'button',
text: '删除',
icon: Delete,
display: () => !isPage(node.value),
handler: () => {
nodes.value && editorService?.remove(nodes.value);
},
},
useDeleteMenu(),
{
type: 'divider',
direction: 'horizontal',

View File

@ -8,7 +8,7 @@
font-weight: 600;
padding: 4px 0px;
overflow: auto;
max-height: 100%;
max-height: 80%;
.menu-item {
color: #333;

View File

@ -0,0 +1,92 @@
import { computed, markRaw, Ref, ref } from 'vue';
import { CopyDocument, Delete, DocumentCopy } from '@element-plus/icons-vue';
import { Id, MContainer, NodeType } from '@tmagic/schema';
import { isPage } from '@tmagic/utils';
import ContentMenu from '@editor/components/ContentMenu.vue';
import type { MenuButton, Services } from '@editor/type';
export const useDeleteMenu = (): MenuButton => ({
type: 'button',
text: '删除',
icon: Delete,
display: (services) => {
const node = services?.editorService?.get('node');
return node?.type !== NodeType.ROOT && !isPage(node);
},
handler: (services) => {
const nodes = services?.editorService?.get('nodes');
nodes && services?.editorService?.remove(nodes);
},
});
const canPaste = ref(false);
export const useCopyMenu = (): MenuButton => ({
type: 'button',
text: '复制',
icon: markRaw(CopyDocument),
handler: (services) => {
const nodes = services?.editorService?.get('nodes');
nodes && services?.editorService?.copy(nodes);
canPaste.value = true;
},
});
export const usePasteMenu = (menu?: Ref<InstanceType<typeof ContentMenu> | undefined>): MenuButton => ({
type: 'button',
text: '粘贴',
icon: markRaw(DocumentCopy),
display: () => canPaste.value,
handler: (services) => {
const nodes = services?.editorService?.get('nodes');
if (!nodes || nodes.length === 0) return;
if (menu?.value?.$el) {
const stage = services?.editorService?.get('stage');
const rect = menu.value.$el.getBoundingClientRect();
const parentRect = stage?.container?.getBoundingClientRect();
const initialLeft = (rect.left || 0) - (parentRect?.left || 0);
const initialTop = (rect.top || 0) - (parentRect?.top || 0);
services?.editorService?.paste({ left: initialLeft, top: initialTop });
} else {
services?.editorService?.paste();
}
},
});
const moveTo = (id: Id, services?: Services) => {
if (!services?.editorService) return;
const nodes = services.editorService.get('nodes') || [];
const parent = services.editorService.getNodeById(id) as MContainer;
if (!parent) return;
services?.editorService.add(nodes, parent);
services?.editorService.remove(nodes);
};
export const useMoveToMenu = (services?: Services): MenuButton => {
const root = computed(() => services?.editorService?.get('root'));
return {
type: 'button',
text: '移动至',
display: (services) => {
const node = services?.editorService?.get('node');
const pageLength = services?.editorService?.get('pageLength') || 0;
return !isPage(node) && pageLength > 1;
},
items: (root.value?.items || [])
.filter((page) => page.id !== services?.editorService?.get('page')?.id)
.map((page) => ({
text: `${page.name}(${page.id})`,
type: 'button',
handler: (services?: Services) => {
moveTo(page.id, services);
},
})),
};
};