mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-19 04:01:33 +08:00
feat(editor): 新增右键菜单移动至其他页面功能
This commit is contained in:
parent
b0f2ed55f6
commit
434bf2ed70
@ -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);
|
||||
|
13
packages/editor/src/icons/CenterIcon.vue
Normal file
13
packages/editor/src/icons/CenterIcon.vue
Normal 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>
|
@ -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,
|
||||
]);
|
||||
|
||||
|
@ -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',
|
||||
|
@ -8,7 +8,7 @@
|
||||
font-weight: 600;
|
||||
padding: 4px 0px;
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
max-height: 80%;
|
||||
|
||||
.menu-item {
|
||||
color: #333;
|
||||
|
92
packages/editor/src/utils/content-menu.ts
Normal file
92
packages/editor/src/utils/content-menu.ts
Normal 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);
|
||||
},
|
||||
})),
|
||||
};
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user