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
|
<ToolButton
|
||||||
v-for="(item, index) in menuData"
|
v-for="(item, index) in menuData"
|
||||||
event-type="mouseup"
|
event-type="mouseup"
|
||||||
|
ref="buttons"
|
||||||
:data="item"
|
:data="item"
|
||||||
:key="index"
|
:key="index"
|
||||||
@mouseup="hide"
|
@mouseup="hide"
|
||||||
@mouseenter="showSubMenu(item)"
|
@mouseenter="showSubMenu(item, index)"
|
||||||
></ToolButton>
|
></ToolButton>
|
||||||
</div>
|
</div>
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
@ -48,6 +49,7 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits(['hide', 'show']);
|
const emit = defineEmits(['hide', 'show']);
|
||||||
|
|
||||||
const menu = ref<HTMLDivElement>();
|
const menu = ref<HTMLDivElement>();
|
||||||
|
const buttons = ref<InstanceType<typeof ToolButton>[]>();
|
||||||
const subMenu = ref<any>();
|
const subMenu = ref<any>();
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const subMenuData = ref<(MenuButton | MenuComponent)[]>([]);
|
const subMenuData = ref<(MenuButton | MenuComponent)[]>([]);
|
||||||
@ -101,7 +103,7 @@ const show = (e: MouseEvent) => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showSubMenu = (item: MenuButton | MenuComponent) => {
|
const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
|
||||||
const menuItem = item as MenuButton;
|
const menuItem = item as MenuButton;
|
||||||
if (typeof item !== 'object' || !menuItem.items?.length) {
|
if (typeof item !== 'object' || !menuItem.items?.length) {
|
||||||
return;
|
return;
|
||||||
@ -110,9 +112,15 @@ const showSubMenu = (item: MenuButton | MenuComponent) => {
|
|||||||
subMenuData.value = menuItem.items || [];
|
subMenuData.value = menuItem.items || [];
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (menu.value) {
|
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({
|
subMenu.value?.show({
|
||||||
clientX: menu.value.offsetLeft + menu.value.clientWidth,
|
clientX: menu.value.offsetLeft + menu.value.clientWidth,
|
||||||
clientY: menu.value.offsetTop,
|
clientY: y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 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>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, markRaw, ref } from 'vue';
|
import { computed, inject, markRaw, ref } from 'vue';
|
||||||
import { CopyDocument, Delete, Files, Plus } from '@element-plus/icons-vue';
|
import { Files, Plus } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { NodeType } from '@tmagic/schema';
|
|
||||||
|
|
||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
import type { ComponentGroup, MenuButton, MenuComponent, Services } from '@editor/type';
|
import type { ComponentGroup, MenuButton, MenuComponent, Services } from '@editor/type';
|
||||||
|
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MEditorLayerMenu',
|
name: 'MEditorLayerMenu',
|
||||||
@ -23,8 +22,6 @@ const services = inject<Services>('services');
|
|||||||
const menu = ref<InstanceType<typeof ContentMenu>>();
|
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||||
const node = computed(() => services?.editorService.get('node'));
|
const node = computed(() => services?.editorService.get('node'));
|
||||||
const nodes = computed(() => services?.editorService.get('nodes'));
|
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 componentList = computed(() => services?.componentListService.getList() || []);
|
||||||
|
|
||||||
const createMenuItems = (group: ComponentGroup): MenuButton[] =>
|
const createMenuItems = (group: ComponentGroup): MenuButton[] =>
|
||||||
@ -86,24 +83,10 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
|
|||||||
display: () => node.value?.items && nodes.value?.length === 1,
|
display: () => node.value?.items && nodes.value?.length === 1,
|
||||||
items: getSubMenuData.value,
|
items: getSubMenuData.value,
|
||||||
},
|
},
|
||||||
{
|
useCopyMenu(),
|
||||||
type: 'button',
|
usePasteMenu(),
|
||||||
text: '复制',
|
useDeleteMenu(),
|
||||||
icon: markRaw(CopyDocument),
|
useMoveToMenu(services),
|
||||||
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 || []);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...props.layerContentMenu,
|
...props.layerContentMenu,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -4,14 +4,16 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, markRaw, ref, watch } from 'vue';
|
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 { NodeType } from '@tmagic/schema';
|
||||||
import { isPage } from '@tmagic/utils';
|
import { isPage } from '@tmagic/utils';
|
||||||
|
|
||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
|
import CenterIcon from '@editor/icons/CenterIcon.vue';
|
||||||
import storageService from '@editor/services/storage';
|
import storageService from '@editor/services/storage';
|
||||||
import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
|
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';
|
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@ -32,42 +34,20 @@ const canCenter = ref(false);
|
|||||||
const node = computed(() => editorService?.get('node'));
|
const node = computed(() => editorService?.get('node'));
|
||||||
const nodes = computed(() => editorService?.get('nodes'));
|
const nodes = computed(() => editorService?.get('nodes'));
|
||||||
const parent = computed(() => editorService?.get('parent'));
|
const parent = computed(() => editorService?.get('parent'));
|
||||||
const stage = computed(() => editorService?.get('stage'));
|
|
||||||
|
|
||||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
|
const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: '水平居中',
|
text: '水平居中',
|
||||||
|
icon: markRaw(CenterIcon),
|
||||||
display: () => canCenter.value,
|
display: () => canCenter.value,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
if (!nodes.value) return;
|
if (!nodes.value) return;
|
||||||
editorService?.alignCenter(nodes.value);
|
editorService?.alignCenter(nodes.value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
useCopyMenu(),
|
||||||
type: 'button',
|
usePasteMenu(menu),
|
||||||
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 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
direction: 'horizontal',
|
direction: 'horizontal',
|
||||||
@ -97,6 +77,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
|
|||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: '置顶',
|
text: '置顶',
|
||||||
|
icon: markRaw(Top),
|
||||||
display: () => !isPage(node.value) && !props.isMultiSelect,
|
display: () => !isPage(node.value) && !props.isMultiSelect,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
editorService?.moveLayer(LayerOffset.TOP);
|
editorService?.moveLayer(LayerOffset.TOP);
|
||||||
@ -105,25 +86,19 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() => [
|
|||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: '置底',
|
text: '置底',
|
||||||
|
icon: markRaw(Bottom),
|
||||||
display: () => !isPage(node.value) && !props.isMultiSelect,
|
display: () => !isPage(node.value) && !props.isMultiSelect,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
editorService?.moveLayer(LayerOffset.BOTTOM);
|
editorService?.moveLayer(LayerOffset.BOTTOM);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
useMoveToMenu(services),
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
direction: 'horizontal',
|
direction: 'horizontal',
|
||||||
display: () => !isPage(node.value) && !props.isMultiSelect,
|
display: () => !isPage(node.value) && !props.isMultiSelect,
|
||||||
},
|
},
|
||||||
{
|
useDeleteMenu(),
|
||||||
type: 'button',
|
|
||||||
text: '删除',
|
|
||||||
icon: Delete,
|
|
||||||
display: () => !isPage(node.value),
|
|
||||||
handler: () => {
|
|
||||||
nodes.value && editorService?.remove(nodes.value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
direction: 'horizontal',
|
direction: 'horizontal',
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 4px 0px;
|
padding: 4px 0px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 100%;
|
max-height: 80%;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
color: #333;
|
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