mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-24 19:29:17 +08:00
feat(editor,stage): 新增鼠标悬停在组件上显示当前位置下所有组件菜单
This commit is contained in:
parent
e9eb47308a
commit
1c6c9ab3e8
@ -1,31 +1,42 @@
|
||||
<template>
|
||||
<div v-if="menuData.length" v-show="visible" class="magic-editor-content-menu" ref="menu" :style="menuStyle">
|
||||
<div>
|
||||
<ToolButton
|
||||
v-for="(item, index) in menuData"
|
||||
event-type="mouseup"
|
||||
ref="buttons"
|
||||
:data="item"
|
||||
:key="index"
|
||||
@mouseup="hide"
|
||||
@mouseenter="showSubMenu(item, index)"
|
||||
></ToolButton>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="visible"
|
||||
class="magic-editor-content-menu"
|
||||
ref="menu"
|
||||
:style="menuStyle"
|
||||
@mouseenter="mouseenterHandler()"
|
||||
>
|
||||
<slot name="title"></slot>
|
||||
<div>
|
||||
<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>
|
||||
<teleport to="body">
|
||||
<content-menu
|
||||
v-if="subMenuData.length"
|
||||
class="sub-menu"
|
||||
ref="subMenu"
|
||||
:menu-data="subMenuData"
|
||||
:is-sub-menu="true"
|
||||
@hide="hide"
|
||||
></content-menu>
|
||||
</teleport>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
@ -39,25 +50,38 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
menuData?: (MenuButton | MenuComponent)[];
|
||||
isSubMenu?: boolean;
|
||||
active?: string | number;
|
||||
autoHide?: boolean;
|
||||
}>(),
|
||||
{
|
||||
menuData: () => [],
|
||||
isSubMenu: false,
|
||||
autoHide: true,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['hide', 'show']);
|
||||
const emit = defineEmits<{
|
||||
hide: [];
|
||||
show: [];
|
||||
mouseenter: [];
|
||||
}>();
|
||||
|
||||
const menu = ref<HTMLDivElement>();
|
||||
const buttons = ref<InstanceType<typeof ToolButton>[]>();
|
||||
const subMenu = ref<any>();
|
||||
const visible = ref(false);
|
||||
const subMenuData = ref<(MenuButton | MenuComponent)[]>([]);
|
||||
const menuStyle = ref({
|
||||
left: '0',
|
||||
top: '0',
|
||||
|
||||
const menuPosition = ref({
|
||||
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 hide = () => {
|
||||
@ -69,7 +93,15 @@ const 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;
|
||||
if (!visible.value || !target) {
|
||||
return;
|
||||
@ -80,23 +112,31 @@ const hideHandler = (e: MouseEvent) => {
|
||||
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(() => {
|
||||
visible.value = true;
|
||||
|
||||
if (!e) {
|
||||
return;
|
||||
}
|
||||
|
||||
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`,
|
||||
};
|
||||
setPosition(e);
|
||||
|
||||
emit('show');
|
||||
});
|
||||
@ -126,22 +166,28 @@ const showSubMenu = (item: MenuButton | MenuComponent, index: number) => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const mouseenterHandler = () => {
|
||||
emit('mouseenter');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.addEventListener('mousedown', hideHandler, true);
|
||||
globalThis.addEventListener('mousedown', outsideClickhideHandler, true);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.removeEventListener('mousedown', hideHandler, true);
|
||||
globalThis.removeEventListener('mousedown', outsideClickhideHandler, true);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
menu,
|
||||
menuPosition,
|
||||
hide,
|
||||
show,
|
||||
contains,
|
||||
setPosition,
|
||||
});
|
||||
</script>
|
||||
|
@ -5,41 +5,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import Gesto from 'gesto';
|
||||
import { ref } from 'vue';
|
||||
import type { OnDrag } from 'gesto';
|
||||
|
||||
import { useGetSo } from '@editor/hooks/use-getso';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorResizer',
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
const emit = defineEmits<{
|
||||
change: [e: OnDrag];
|
||||
}>();
|
||||
|
||||
const target = ref<HTMLSpanElement>();
|
||||
const isDraging = ref(false);
|
||||
|
||||
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;
|
||||
});
|
||||
const { isDraging } = useGetSo(target, emit);
|
||||
</script>
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { OnDrag } from 'gesto';
|
||||
|
||||
import Resizer from './Resizer.vue';
|
||||
|
||||
@ -111,7 +112,7 @@ onUnmounted(() => {
|
||||
|
||||
const center = ref(0);
|
||||
|
||||
const changeLeft = (deltaX: number) => {
|
||||
const changeLeft = ({ deltaX }: OnDrag) => {
|
||||
if (typeof props.left === 'undefined') return;
|
||||
let left = Math.max(props.left + deltaX, props.minLeft) || 0;
|
||||
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;
|
||||
let right = Math.max(props.right - deltaX, props.minRight) || 0;
|
||||
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';
|
||||
|
||||
export type { OnDrag } from 'gesto';
|
||||
|
||||
export type { MoveableOptions } from '@tmagic/stage';
|
||||
export * from './type';
|
||||
export * from './hooks';
|
||||
|
@ -20,9 +20,9 @@ import { computed, inject } from 'vue';
|
||||
|
||||
import { MenuButton, MenuComponent, Services } from '@editor/type';
|
||||
|
||||
import MagicStage from './viewer/Stage.vue';
|
||||
import Breadcrumb from './Breadcrumb.vue';
|
||||
import PageBar from './PageBar.vue';
|
||||
import MagicStage from './Stage.vue';
|
||||
|
||||
defineOptions({
|
||||
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>
|
||||
<Teleport to="body">
|
||||
<ViewerMenu ref="menu" :is-multi-select="isMultiSelect" :stage-content-menu="stageContentMenu"></ViewerMenu>
|
||||
<NodeListMenu ref="nodeList" :is-multi-select="isMultiSelect"></NodeListMenu>
|
||||
</Teleport>
|
||||
</ScrollViewer>
|
||||
</template>
|
||||
@ -40,6 +41,7 @@ import { useStage } from '@editor/hooks/use-stage';
|
||||
import { Layout, MenuButton, MenuComponent, Services, StageOptions } from '@editor/type';
|
||||
import { getConfig } from '@editor/utils/config';
|
||||
|
||||
import NodeListMenu from './NodeListMenu.vue';
|
||||
import ViewerMenu from './ViewerMenu.vue';
|
||||
|
||||
defineOptions({
|
@ -3,12 +3,13 @@
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px 2px rgba(68, 73, 77, 0.16);
|
||||
z-index: 9999;
|
||||
z-index: 1000;
|
||||
transform-origin: 0% 0%;
|
||||
font-weight: 600;
|
||||
padding: 4px 0px;
|
||||
overflow: auto;
|
||||
max-height: 80%;
|
||||
min-width: 180px;
|
||||
|
||||
.menu-item {
|
||||
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 {
|
||||
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 "./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;
|
||||
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
|
||||
items?: MenuButton[];
|
||||
/** 唯一标识,用于高亮 */
|
||||
id?: string | number;
|
||||
}
|
||||
|
||||
export interface MenuComponent {
|
||||
|
@ -20,7 +20,7 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
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 ||
|
||||
|
@ -19,7 +19,7 @@ import EventEmitter from 'events';
|
||||
|
||||
import KeyController from 'keycon';
|
||||
import { throttle } from 'lodash-es';
|
||||
import type { MoveableOptions } from 'moveable';
|
||||
import type { MoveableOptions, OnDragStart } from 'moveable';
|
||||
|
||||
import { Env } from '@tmagic/core';
|
||||
import type { Id } from '@tmagic/schema';
|
||||
@ -88,6 +88,9 @@ export default class ActionManager extends EventEmitter {
|
||||
this.clearHighlight();
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('mousemove', event);
|
||||
|
||||
this.highlight(el);
|
||||
}, throttleTime);
|
||||
|
||||
@ -204,6 +207,8 @@ export default class ActionManager extends EventEmitter {
|
||||
public async getElementFromPoint(event: MouseEvent): Promise<HTMLElement | undefined> {
|
||||
const els = this.getElementsFromPoint(event as Point);
|
||||
|
||||
this.emit('get-elements-from-point', els);
|
||||
|
||||
let stopped = false;
|
||||
const stop = () => (stopped = true);
|
||||
for (const el of els) {
|
||||
@ -343,6 +348,10 @@ export default class ActionManager extends EventEmitter {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getDragStatus() {
|
||||
return this.dr.getDragStatus();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.container.removeEventListener('mousedown', this.mouseDownHandler);
|
||||
this.container.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||
@ -484,6 +493,9 @@ export default class ActionManager extends EventEmitter {
|
||||
data: [{ el: drTarget }],
|
||||
};
|
||||
this.emit('remove', data);
|
||||
})
|
||||
.on('drag-start', (e: OnDragStart) => {
|
||||
this.emit('drag-start', e);
|
||||
});
|
||||
|
||||
this.multiDr
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import type { MoveableOptions } from 'moveable';
|
||||
import type { MoveableOptions, OnDragStart } from 'moveable';
|
||||
|
||||
import type { Id } from '@tmagic/schema';
|
||||
|
||||
@ -217,6 +217,10 @@ export default class StageCore extends EventEmitter {
|
||||
return this.actionManager.getMoveableOption(key);
|
||||
}
|
||||
|
||||
public getDragStatus() {
|
||||
return this.actionManager.getDragStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
@ -292,6 +296,7 @@ export default class StageCore extends EventEmitter {
|
||||
this.initDrEvent();
|
||||
this.initMulDrEvent();
|
||||
this.initHighlightEvent();
|
||||
this.initMouseEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -356,4 +361,20 @@ export default class StageCore extends EventEmitter {
|
||||
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();
|
||||
}
|
||||
|
||||
public getDragStatus(): StageDragStatus {
|
||||
return this.dragStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
@ -187,6 +191,7 @@ export default class StageDragResize extends MoveableOptionsManager {
|
||||
this.dragStatus = StageDragStatus.START;
|
||||
|
||||
this.dragResizeHelper.onDragStart(e);
|
||||
this.emit('drag-start', e);
|
||||
})
|
||||
.on('drag', (e) => {
|
||||
if (!this.target || !this.dragResizeHelper.getShadowEl()) return;
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
import StageCore from './StageCore';
|
||||
|
||||
export type { MoveableOptions } from 'moveable';
|
||||
export type { MoveableOptions, OnDragStart } from 'moveable';
|
||||
export { default as StageRender } from './StageRender';
|
||||
export { default as StageMask } from './StageMask';
|
||||
export { default as StageDragResize } from './StageDragResize';
|
||||
|
Loading…
x
Reference in New Issue
Block a user