mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-26 19:19:59 +08:00
feat(editor,stage): 新增鼠标悬停在组件上显示当前位置下所有组件菜单
This commit is contained in:
parent
e9eb47308a
commit
1c6c9ab3e8
@ -1,31 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="menuData.length" v-show="visible" class="magic-editor-content-menu" ref="menu" :style="menuStyle">
|
<transition name="fade">
|
||||||
<div>
|
<div
|
||||||
<ToolButton
|
v-show="visible"
|
||||||
v-for="(item, index) in menuData"
|
class="magic-editor-content-menu"
|
||||||
event-type="mouseup"
|
ref="menu"
|
||||||
ref="buttons"
|
:style="menuStyle"
|
||||||
:data="item"
|
@mouseenter="mouseenterHandler()"
|
||||||
:key="index"
|
>
|
||||||
@mouseup="hide"
|
<slot name="title"></slot>
|
||||||
@mouseenter="showSubMenu(item, index)"
|
<div>
|
||||||
></ToolButton>
|
<ToolButton
|
||||||
|
v-for="(item, index) in menuData"
|
||||||
|
event-type="mouseup"
|
||||||
|
ref="buttons"
|
||||||
|
:class="{ active: active && item.id === active }"
|
||||||
|
:data="item"
|
||||||
|
:key="index"
|
||||||
|
@mouseup="clickHandler"
|
||||||
|
@mouseenter="showSubMenu(item, index)"
|
||||||
|
></ToolButton>
|
||||||
|
</div>
|
||||||
|
<teleport to="body">
|
||||||
|
<content-menu
|
||||||
|
v-if="subMenuData.length"
|
||||||
|
class="sub-menu"
|
||||||
|
ref="subMenu"
|
||||||
|
:active="active"
|
||||||
|
:menu-data="subMenuData"
|
||||||
|
:is-sub-menu="true"
|
||||||
|
@hide="hide"
|
||||||
|
></content-menu>
|
||||||
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
<teleport to="body">
|
</transition>
|
||||||
<content-menu
|
|
||||||
v-if="subMenuData.length"
|
|
||||||
class="sub-menu"
|
|
||||||
ref="subMenu"
|
|
||||||
:menu-data="subMenuData"
|
|
||||||
:is-sub-menu="true"
|
|
||||||
@hide="hide"
|
|
||||||
></content-menu>
|
|
||||||
</teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
import { MenuButton, MenuComponent } from '@editor/type';
|
import { MenuButton, MenuComponent } from '@editor/type';
|
||||||
|
|
||||||
@ -39,25 +50,38 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
menuData?: (MenuButton | MenuComponent)[];
|
menuData?: (MenuButton | MenuComponent)[];
|
||||||
isSubMenu?: boolean;
|
isSubMenu?: boolean;
|
||||||
|
active?: string | number;
|
||||||
|
autoHide?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
menuData: () => [],
|
menuData: () => [],
|
||||||
isSubMenu: false,
|
isSubMenu: false,
|
||||||
|
autoHide: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits(['hide', 'show']);
|
const emit = defineEmits<{
|
||||||
|
hide: [];
|
||||||
|
show: [];
|
||||||
|
mouseenter: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const menu = ref<HTMLDivElement>();
|
const menu = ref<HTMLDivElement>();
|
||||||
const buttons = ref<InstanceType<typeof ToolButton>[]>();
|
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)[]>([]);
|
||||||
const menuStyle = ref({
|
|
||||||
left: '0',
|
const menuPosition = ref({
|
||||||
top: '0',
|
left: 0,
|
||||||
|
top: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const menuStyle = computed(() => ({
|
||||||
|
top: `${menuPosition.value.top}px`,
|
||||||
|
left: `${menuPosition.value.left}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
const contains = (el: HTMLElement) => menu.value?.contains(el) || subMenu.value?.contains(el);
|
const contains = (el: HTMLElement) => menu.value?.contains(el) || subMenu.value?.contains(el);
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
@ -69,7 +93,15 @@ const hide = () => {
|
|||||||
emit('hide');
|
emit('hide');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideHandler = (e: MouseEvent) => {
|
const clickHandler = () => {
|
||||||
|
if (!props.autoHide) return;
|
||||||
|
|
||||||
|
hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const outsideClickhideHandler = (e: MouseEvent) => {
|
||||||
|
if (!props.autoHide) return;
|
||||||
|
|
||||||
const target = e.target as HTMLElement | undefined;
|
const target = e.target as HTMLElement | undefined;
|
||||||
if (!visible.value || !target) {
|
if (!visible.value || !target) {
|
||||||
return;
|
return;
|
||||||
@ -80,23 +112,31 @@ const hideHandler = (e: MouseEvent) => {
|
|||||||
hide();
|
hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
const show = (e: MouseEvent) => {
|
const setPosition = (e: { clientY: number; clientX: number }) => {
|
||||||
|
const menuHeight = menu.value?.clientHeight || 0;
|
||||||
|
|
||||||
|
let top = e.clientY;
|
||||||
|
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||||
|
top = document.body.clientHeight - menuHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuPosition.value = {
|
||||||
|
top,
|
||||||
|
left: e.clientX,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const show = (e?: MouseEvent) => {
|
||||||
// 加settimeout是以为,如果菜单中的按钮监听的是mouseup,那么菜单显示出来时鼠标如果正好在菜单上就会马上触发按钮的mouseup
|
// 加settimeout是以为,如果菜单中的按钮监听的是mouseup,那么菜单显示出来时鼠标如果正好在菜单上就会马上触发按钮的mouseup
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
|
|
||||||
|
if (!e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const menuHeight = menu.value?.clientHeight || 0;
|
setPosition(e);
|
||||||
|
|
||||||
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`,
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('show');
|
emit('show');
|
||||||
});
|
});
|
||||||
@ -126,22 +166,28 @@ const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mouseenterHandler = () => {
|
||||||
|
emit('mouseenter');
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.isSubMenu) return;
|
if (props.isSubMenu) return;
|
||||||
|
|
||||||
globalThis.addEventListener('mousedown', hideHandler, true);
|
globalThis.addEventListener('mousedown', outsideClickhideHandler, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (props.isSubMenu) return;
|
if (props.isSubMenu) return;
|
||||||
|
|
||||||
globalThis.removeEventListener('mousedown', hideHandler, true);
|
globalThis.removeEventListener('mousedown', outsideClickhideHandler, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
menu,
|
menu,
|
||||||
|
menuPosition,
|
||||||
hide,
|
hide,
|
||||||
show,
|
show,
|
||||||
contains,
|
contains,
|
||||||
|
setPosition,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,41 +5,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Gesto from 'gesto';
|
import type { OnDrag } from 'gesto';
|
||||||
|
|
||||||
|
import { useGetSo } from '@editor/hooks/use-getso';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MEditorResizer',
|
name: 'MEditorResizer',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits<{
|
||||||
|
change: [e: OnDrag];
|
||||||
|
}>();
|
||||||
|
|
||||||
const target = ref<HTMLSpanElement>();
|
const target = ref<HTMLSpanElement>();
|
||||||
const isDraging = ref(false);
|
const { isDraging } = useGetSo(target, emit);
|
||||||
|
|
||||||
let getso: Gesto;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!target.value) return;
|
|
||||||
getso = new Gesto(target.value, {
|
|
||||||
container: window,
|
|
||||||
pinchOutside: true,
|
|
||||||
})
|
|
||||||
.on('drag', (e) => {
|
|
||||||
if (!target.value) return;
|
|
||||||
|
|
||||||
emit('change', e.deltaX);
|
|
||||||
})
|
|
||||||
.on('dragStart', () => {
|
|
||||||
isDraging.value = true;
|
|
||||||
})
|
|
||||||
.on('dragEnd', () => {
|
|
||||||
isDraging.value = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
getso?.unset();
|
|
||||||
isDraging.value = false;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { OnDrag } from 'gesto';
|
||||||
|
|
||||||
import Resizer from './Resizer.vue';
|
import Resizer from './Resizer.vue';
|
||||||
|
|
||||||
@ -111,7 +112,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
const center = ref(0);
|
const center = ref(0);
|
||||||
|
|
||||||
const changeLeft = (deltaX: number) => {
|
const changeLeft = ({ deltaX }: OnDrag) => {
|
||||||
if (typeof props.left === 'undefined') return;
|
if (typeof props.left === 'undefined') return;
|
||||||
let left = Math.max(props.left + deltaX, props.minLeft) || 0;
|
let left = Math.max(props.left + deltaX, props.minLeft) || 0;
|
||||||
emit('update:left', left);
|
emit('update:left', left);
|
||||||
@ -131,7 +132,7 @@ const changeLeft = (deltaX: number) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeRight = (deltaX: number) => {
|
const changeRight = ({ deltaX }: OnDrag) => {
|
||||||
if (typeof props.right === 'undefined') return;
|
if (typeof props.right === 'undefined') return;
|
||||||
let right = Math.max(props.right - deltaX, props.minRight) || 0;
|
let right = Math.max(props.right - deltaX, props.minRight) || 0;
|
||||||
emit('update:right', right);
|
emit('update:right', right);
|
||||||
|
35
packages/editor/src/hooks/use-getso.ts
Normal file
35
packages/editor/src/hooks/use-getso.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { onMounted, onUnmounted, type Ref, ref } from 'vue';
|
||||||
|
import Gesto, { type OnDrag } from 'gesto';
|
||||||
|
|
||||||
|
export const useGetSo = (target: Ref<HTMLElement | undefined>, emit: (evt: 'change', e: OnDrag<Gesto>) => void) => {
|
||||||
|
let getso: Gesto;
|
||||||
|
const isDraging = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!target.value) return;
|
||||||
|
getso = new Gesto(target.value, {
|
||||||
|
container: window,
|
||||||
|
pinchOutside: true,
|
||||||
|
})
|
||||||
|
.on('drag', (e) => {
|
||||||
|
if (!target.value) return;
|
||||||
|
|
||||||
|
emit('change', e);
|
||||||
|
})
|
||||||
|
.on('dragStart', () => {
|
||||||
|
isDraging.value = true;
|
||||||
|
})
|
||||||
|
.on('dragEnd', () => {
|
||||||
|
isDraging.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
getso?.unset();
|
||||||
|
isDraging.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDraging,
|
||||||
|
};
|
||||||
|
};
|
25
packages/editor/src/icons/PinIcon.vue
Normal file
25
packages/editor/src/icons/PinIcon.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-pin"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M15 4.5l-4 4l-4 1.5l-1.5 1.5l7 7l1.5 -1.5l1.5 -4l4 -4" />
|
||||||
|
<line x1="9" y1="15" x2="4.5" y2="19.5" />
|
||||||
|
<line x1="14.5" y1="4" x2="20" y2="9.5" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: 'MEditorPinIcon',
|
||||||
|
});
|
||||||
|
</script>
|
25
packages/editor/src/icons/PinnedIcon.vue
Normal file
25
packages/editor/src/icons/PinnedIcon.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-pinned"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M9 4v6l-2 4v2h10v-2l-2 -4v-6" />
|
||||||
|
<line x1="12" y1="16" x2="12" y2="21" />
|
||||||
|
<line x1="8" y1="4" x2="16" y2="4" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: 'MEditorPinnedIcon',
|
||||||
|
});
|
||||||
|
</script>
|
@ -37,6 +37,8 @@ import type { InstallOptions } from './type';
|
|||||||
|
|
||||||
import './theme/index.scss';
|
import './theme/index.scss';
|
||||||
|
|
||||||
|
export type { OnDrag } from 'gesto';
|
||||||
|
|
||||||
export type { MoveableOptions } from '@tmagic/stage';
|
export type { MoveableOptions } from '@tmagic/stage';
|
||||||
export * from './type';
|
export * from './type';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
@ -20,9 +20,9 @@ import { computed, inject } from 'vue';
|
|||||||
|
|
||||||
import { MenuButton, MenuComponent, Services } from '@editor/type';
|
import { MenuButton, MenuComponent, Services } from '@editor/type';
|
||||||
|
|
||||||
|
import MagicStage from './viewer/Stage.vue';
|
||||||
import Breadcrumb from './Breadcrumb.vue';
|
import Breadcrumb from './Breadcrumb.vue';
|
||||||
import PageBar from './PageBar.vue';
|
import PageBar from './PageBar.vue';
|
||||||
import MagicStage from './Stage.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MEditorWorkspace',
|
name: 'MEditorWorkspace',
|
||||||
|
159
packages/editor/src/layouts/workspace/viewer/NodeListMenu.vue
Normal file
159
packages/editor/src/layouts/workspace/viewer/NodeListMenu.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<content-menu
|
||||||
|
ref="menu"
|
||||||
|
:menu-data="menuData"
|
||||||
|
:active="node?.id"
|
||||||
|
:auto-hide="!pinned"
|
||||||
|
@mouseenter="mouseenterHandler()"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<NodeListMenuTitle v-model:pinned="pinned" @change="dragMenuHandler" @close="closeHandler"></NodeListMenuTitle>
|
||||||
|
</template>
|
||||||
|
</content-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Component, computed, inject, ref, watch } from 'vue';
|
||||||
|
import type { OnDrag } from 'gesto';
|
||||||
|
|
||||||
|
import type { MNode } from '@tmagic/schema';
|
||||||
|
import { StageDragStatus } from '@tmagic/stage';
|
||||||
|
import { getNodes } from '@tmagic/utils';
|
||||||
|
|
||||||
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||||
|
import type { ComponentItem, MenuButton, Services } from '@editor/type';
|
||||||
|
|
||||||
|
import NodeListMenuTitle from './NodeListMenuTitle.vue';
|
||||||
|
const props = defineProps<{ isMultiSelect?: boolean }>();
|
||||||
|
|
||||||
|
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||||
|
const nodeList = ref<MNode[]>([]);
|
||||||
|
const pinned = ref(false);
|
||||||
|
|
||||||
|
const services = inject<Services>('services');
|
||||||
|
const editorService = services?.editorService;
|
||||||
|
const componentListService = services?.componentListService;
|
||||||
|
|
||||||
|
const stage = computed(() => editorService?.get('stage'));
|
||||||
|
const page = computed(() => editorService?.get('page'));
|
||||||
|
const node = computed(() => editorService?.get('node'));
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
if (timeout) {
|
||||||
|
globalThis.clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pinned.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeList.value = [];
|
||||||
|
menu.value?.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const unWatch = watch(
|
||||||
|
stage,
|
||||||
|
(stage) => {
|
||||||
|
if (!stage) return;
|
||||||
|
|
||||||
|
stage.on('drag-start', () => {
|
||||||
|
cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
stage.on('mousemove', (event: MouseEvent) => {
|
||||||
|
cancel();
|
||||||
|
|
||||||
|
if (props.isMultiSelect || stage.getDragStatus() !== StageDragStatus.END) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = globalThis.setTimeout(() => {
|
||||||
|
const els = stage?.renderer.getElementsFromPoint(event);
|
||||||
|
|
||||||
|
const nodes = getNodes(
|
||||||
|
els.map((el) => el.id),
|
||||||
|
page.value?.items,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pinned.value && nodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeList.value = nodes;
|
||||||
|
|
||||||
|
if (nodeList.value.length > 1) {
|
||||||
|
menu.value?.show(pinned.value ? undefined : event);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
stage.on('mouseleave', () => {
|
||||||
|
cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
unWatch();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const componentMap = computed(() => {
|
||||||
|
const map: Record<string, ComponentItem> = {};
|
||||||
|
componentListService?.getList().forEach((group) => {
|
||||||
|
group.items.forEach((item) => {
|
||||||
|
map[item.type] = item;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuData = computed<MenuButton[]>(() =>
|
||||||
|
nodeList.value.map((node: MNode) => {
|
||||||
|
let text = node.name;
|
||||||
|
let icon: string | Component<{}, {}, any> | undefined;
|
||||||
|
if (node.type) {
|
||||||
|
const item = componentMap.value[node.type];
|
||||||
|
text += ` (${item?.text})`;
|
||||||
|
icon = item?.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'button',
|
||||||
|
text,
|
||||||
|
id: node.id,
|
||||||
|
icon,
|
||||||
|
handler: async () => {
|
||||||
|
await editorService?.select(node);
|
||||||
|
stage.value?.select(node.id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mouseenterHandler = () => {
|
||||||
|
// menu的mouseenter后,大概率还有最后一个mousemove事件,这里延迟清除
|
||||||
|
globalThis.setTimeout(() => {
|
||||||
|
if (timeout) {
|
||||||
|
globalThis.clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragMenuHandler = ({ deltaY, deltaX }: OnDrag) => {
|
||||||
|
if (!menu.value) return;
|
||||||
|
|
||||||
|
const { menuPosition } = menu.value;
|
||||||
|
|
||||||
|
menu.value?.setPosition({
|
||||||
|
clientY: menuPosition.top + deltaY,
|
||||||
|
clientX: menuPosition.left + deltaX,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeHandler = () => {
|
||||||
|
menu.value?.hide();
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: move;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
ref="target"
|
||||||
|
>
|
||||||
|
<TMagicTooltip placement="top" :content="pinned ? '取消置于顶层自动隐藏' : '置于顶层不消失'">
|
||||||
|
<MIcon
|
||||||
|
style="margin-left: 10px; cursor: pointer"
|
||||||
|
:icon="pinned ? PinnedIcon : PinIcon"
|
||||||
|
@click="pinHandler"
|
||||||
|
></MIcon>
|
||||||
|
</TMagicTooltip>
|
||||||
|
|
||||||
|
<span>可选组件</span>
|
||||||
|
|
||||||
|
<div style="margin-right: 10px">
|
||||||
|
<TMagicButton text size="small" @click="closeHandler">
|
||||||
|
<MIcon :icon="Close"></MIcon>
|
||||||
|
</TMagicButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Close } from '@element-plus/icons-vue';
|
||||||
|
import type { OnDrag } from 'gesto';
|
||||||
|
|
||||||
|
import { TMagicButton, TMagicTooltip } from '@tmagic/design';
|
||||||
|
|
||||||
|
import MIcon from '@editor/components/Icon.vue';
|
||||||
|
import { useGetSo } from '@editor/hooks/use-getso';
|
||||||
|
import PinIcon from '@editor/icons/PinIcon.vue';
|
||||||
|
import PinnedIcon from '@editor/icons/PinnedIcon.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MEditorNodeListMenuTitle',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
pinned: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
'update:pinned': [pinned: boolean];
|
||||||
|
change: [e: OnDrag];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pinHandler = () => {
|
||||||
|
emit('update:pinned', !props.pinned);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeHandler = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = ref<HTMLDivElement>();
|
||||||
|
useGetSo(target, emit);
|
||||||
|
</script>
|
@ -24,6 +24,7 @@
|
|||||||
></div>
|
></div>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<ViewerMenu ref="menu" :is-multi-select="isMultiSelect" :stage-content-menu="stageContentMenu"></ViewerMenu>
|
<ViewerMenu ref="menu" :is-multi-select="isMultiSelect" :stage-content-menu="stageContentMenu"></ViewerMenu>
|
||||||
|
<NodeListMenu ref="nodeList" :is-multi-select="isMultiSelect"></NodeListMenu>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</template>
|
</template>
|
||||||
@ -40,6 +41,7 @@ import { useStage } from '@editor/hooks/use-stage';
|
|||||||
import { Layout, MenuButton, MenuComponent, Services, StageOptions } from '@editor/type';
|
import { Layout, MenuButton, MenuComponent, Services, StageOptions } from '@editor/type';
|
||||||
import { getConfig } from '@editor/utils/config';
|
import { getConfig } from '@editor/utils/config';
|
||||||
|
|
||||||
|
import NodeListMenu from './NodeListMenu.vue';
|
||||||
import ViewerMenu from './ViewerMenu.vue';
|
import ViewerMenu from './ViewerMenu.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
@ -3,12 +3,13 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
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: 1000;
|
||||||
transform-origin: 0% 0%;
|
transform-origin: 0% 0%;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 4px 0px;
|
padding: 4px 0px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
|
min-width: 180px;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -43,8 +44,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $--theme-color;
|
||||||
|
.tmagic-design-button,
|
||||||
|
.tmagic-design-button:active,
|
||||||
|
.tmagic-design-button:focus {
|
||||||
|
color: #fff;
|
||||||
|
background-color: $--theme-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-item i {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $--hover-color;
|
background-color: $--hover-color;
|
||||||
|
.tmagic-design-button,
|
||||||
|
.tmagic-design-button:active,
|
||||||
|
.tmagic-design-button:focus {
|
||||||
|
color: $--font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-item i {
|
||||||
|
color: $--font-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,10 @@
|
|||||||
@import "./common/var.scss";
|
@import "./common/var.scss";
|
||||||
@import "./theme.scss";
|
@import "./theme.scss";
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
@ -201,6 +201,8 @@ export interface MenuButton {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
|
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
|
||||||
items?: MenuButton[];
|
items?: MenuButton[];
|
||||||
|
/** 唯一标识,用于高亮 */
|
||||||
|
id?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuComponent {
|
export interface MenuComponent {
|
||||||
|
@ -20,7 +20,7 @@ import { mount } from '@vue/test-utils';
|
|||||||
|
|
||||||
import { NodeType } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
|
|
||||||
import Stage from '@editor/layouts/workspace/Stage.vue';
|
import Stage from '@editor/layouts/workspace/viewer/Stage.vue';
|
||||||
|
|
||||||
globalThis.ResizeObserver =
|
globalThis.ResizeObserver =
|
||||||
globalThis.ResizeObserver ||
|
globalThis.ResizeObserver ||
|
||||||
|
@ -19,7 +19,7 @@ import EventEmitter from 'events';
|
|||||||
|
|
||||||
import KeyController from 'keycon';
|
import KeyController from 'keycon';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
import type { MoveableOptions } from 'moveable';
|
import type { MoveableOptions, OnDragStart } from 'moveable';
|
||||||
|
|
||||||
import { Env } from '@tmagic/core';
|
import { Env } from '@tmagic/core';
|
||||||
import type { Id } from '@tmagic/schema';
|
import type { Id } from '@tmagic/schema';
|
||||||
@ -88,6 +88,9 @@ export default class ActionManager extends EventEmitter {
|
|||||||
this.clearHighlight();
|
this.clearHighlight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('mousemove', event);
|
||||||
|
|
||||||
this.highlight(el);
|
this.highlight(el);
|
||||||
}, throttleTime);
|
}, throttleTime);
|
||||||
|
|
||||||
@ -204,6 +207,8 @@ export default class ActionManager extends EventEmitter {
|
|||||||
public async getElementFromPoint(event: MouseEvent): Promise<HTMLElement | undefined> {
|
public async getElementFromPoint(event: MouseEvent): Promise<HTMLElement | undefined> {
|
||||||
const els = this.getElementsFromPoint(event as Point);
|
const els = this.getElementsFromPoint(event as Point);
|
||||||
|
|
||||||
|
this.emit('get-elements-from-point', els);
|
||||||
|
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
const stop = () => (stopped = true);
|
const stop = () => (stopped = true);
|
||||||
for (const el of els) {
|
for (const el of els) {
|
||||||
@ -343,6 +348,10 @@ export default class ActionManager extends EventEmitter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDragStatus() {
|
||||||
|
return this.dr.getDragStatus();
|
||||||
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.container.removeEventListener('mousedown', this.mouseDownHandler);
|
this.container.removeEventListener('mousedown', this.mouseDownHandler);
|
||||||
this.container.removeEventListener('mousemove', this.mouseMoveHandler);
|
this.container.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||||
@ -484,6 +493,9 @@ export default class ActionManager extends EventEmitter {
|
|||||||
data: [{ el: drTarget }],
|
data: [{ el: drTarget }],
|
||||||
};
|
};
|
||||||
this.emit('remove', data);
|
this.emit('remove', data);
|
||||||
|
})
|
||||||
|
.on('drag-start', (e: OnDragStart) => {
|
||||||
|
this.emit('drag-start', e);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.multiDr
|
this.multiDr
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import type { MoveableOptions } from 'moveable';
|
import type { MoveableOptions, OnDragStart } from 'moveable';
|
||||||
|
|
||||||
import type { Id } from '@tmagic/schema';
|
import type { Id } from '@tmagic/schema';
|
||||||
|
|
||||||
@ -217,6 +217,10 @@ export default class StageCore extends EventEmitter {
|
|||||||
return this.actionManager.getMoveableOption(key);
|
return this.actionManager.getMoveableOption(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDragStatus() {
|
||||||
|
return this.actionManager.getDragStatus();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁实例
|
* 销毁实例
|
||||||
*/
|
*/
|
||||||
@ -292,6 +296,7 @@ export default class StageCore extends EventEmitter {
|
|||||||
this.initDrEvent();
|
this.initDrEvent();
|
||||||
this.initMulDrEvent();
|
this.initMulDrEvent();
|
||||||
this.initHighlightEvent();
|
this.initHighlightEvent();
|
||||||
|
this.initMouseEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -356,4 +361,20 @@ export default class StageCore extends EventEmitter {
|
|||||||
this.emit('highlight', highlightEl);
|
this.emit('highlight', highlightEl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化Highlight类通过ActionManager抛出来的事件监听
|
||||||
|
*/
|
||||||
|
private initMouseEvent(): void {
|
||||||
|
this.actionManager
|
||||||
|
.on('mousemove', async (event: MouseEvent) => {
|
||||||
|
this.emit('mousemove', event);
|
||||||
|
})
|
||||||
|
.on('mouseleave', async (event: MouseEvent) => {
|
||||||
|
this.emit('mouseleave', event);
|
||||||
|
})
|
||||||
|
.on('drag-start', (e: OnDragStart) => {
|
||||||
|
this.emit('drag-start', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,10 @@ export default class StageDragResize extends MoveableOptionsManager {
|
|||||||
this.moveable.updateRect();
|
this.moveable.updateRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDragStatus(): StageDragStatus {
|
||||||
|
return this.dragStatus;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁实例
|
* 销毁实例
|
||||||
*/
|
*/
|
||||||
@ -187,6 +191,7 @@ export default class StageDragResize extends MoveableOptionsManager {
|
|||||||
this.dragStatus = StageDragStatus.START;
|
this.dragStatus = StageDragStatus.START;
|
||||||
|
|
||||||
this.dragResizeHelper.onDragStart(e);
|
this.dragResizeHelper.onDragStart(e);
|
||||||
|
this.emit('drag-start', e);
|
||||||
})
|
})
|
||||||
.on('drag', (e) => {
|
.on('drag', (e) => {
|
||||||
if (!this.target || !this.dragResizeHelper.getShadowEl()) return;
|
if (!this.target || !this.dragResizeHelper.getShadowEl()) return;
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import StageCore from './StageCore';
|
import StageCore from './StageCore';
|
||||||
|
|
||||||
export type { MoveableOptions } from 'moveable';
|
export type { MoveableOptions, OnDragStart } from 'moveable';
|
||||||
export { default as StageRender } from './StageRender';
|
export { default as StageRender } from './StageRender';
|
||||||
export { default as StageMask } from './StageMask';
|
export { default as StageMask } from './StageMask';
|
||||||
export { default as StageDragResize } from './StageDragResize';
|
export { default as StageDragResize } from './StageDragResize';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user