mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat(editor,stage): 优化可选组件交互
This commit is contained in:
parent
5c6a3455b0
commit
258d2bc2ea
117
packages/editor/src/components/FloatingBox.vue
Normal file
117
packages/editor/src/components/FloatingBox.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<Teleport to="body" v-if="visible">
|
||||
<div ref="target" class="m-editor-float-box" :style="style">
|
||||
<div ref="dragTarget" class="m-editor-float-box-title">
|
||||
<slot name="title">
|
||||
<span>{{ title }}</span>
|
||||
</slot>
|
||||
<div>
|
||||
<TMagicButton text size="small" :icon="Close" @click="closeHandler"></TMagicButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-editor-float-box-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import VanillaMoveable from 'moveable';
|
||||
|
||||
import { TMagicButton } from '@tmagic/design';
|
||||
|
||||
interface Position {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
interface Rect {
|
||||
width: number | string;
|
||||
height: number | string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{ visible: boolean; position?: Position; rect?: Rect; title?: string }>(), {
|
||||
visible: false,
|
||||
title: '',
|
||||
position: () => ({ left: 0, top: 0 }),
|
||||
rect: () => ({ width: 'auto', height: 'auto' }),
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [boolean];
|
||||
}>();
|
||||
|
||||
const target = ref<HTMLDivElement>();
|
||||
const dragTarget = ref<HTMLDivElement>();
|
||||
|
||||
const style = computed(() => ({
|
||||
left: `${props.position.left}px`,
|
||||
top: `${props.position.top}px`,
|
||||
width: typeof props.rect.width === 'string' ? props.rect.width : `${props.rect.width}px`,
|
||||
height: typeof props.rect.height === 'string' ? props.rect.height : `${props.rect.height}px`,
|
||||
}));
|
||||
|
||||
let moveable: VanillaMoveable | null = null;
|
||||
|
||||
const initMoveable = () => {
|
||||
moveable = new VanillaMoveable(globalThis.document.body, {
|
||||
className: 'm-editor-floating-box-moveable',
|
||||
target: target.value,
|
||||
draggable: true,
|
||||
resizable: true,
|
||||
edge: true,
|
||||
keepRatio: false,
|
||||
origin: false,
|
||||
snappable: true,
|
||||
dragTarget: dragTarget.value,
|
||||
dragTargetSelf: false,
|
||||
linePadding: 10,
|
||||
controlPadding: 10,
|
||||
});
|
||||
|
||||
moveable.on('drag', (e) => {
|
||||
e.target.style.transform = e.transform;
|
||||
});
|
||||
|
||||
moveable.on('resize', (e) => {
|
||||
e.target.style.width = `${e.width}px`;
|
||||
e.target.style.height = `${e.height}px`;
|
||||
e.target.style.transform = e.drag.transform;
|
||||
});
|
||||
};
|
||||
|
||||
const destroyMoveable = () => {
|
||||
moveable?.destroy();
|
||||
moveable = null;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await nextTick();
|
||||
initMoveable();
|
||||
} else {
|
||||
destroyMoveable();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyMoveable();
|
||||
});
|
||||
|
||||
const closeHandler = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
target,
|
||||
});
|
||||
</script>
|
@ -108,7 +108,7 @@ const selected = computed(() => nodeStatus.value.selected);
|
||||
const visible = computed(() => nodeStatus.value.visible);
|
||||
const draggable = computed(() => nodeStatus.value.draggable);
|
||||
|
||||
const hasChilren = computed(() => props.data.items && props.data.items.length > 0);
|
||||
const hasChilren = computed(() => props.data.items?.some((item) => props.nodeStatusMap.get(item.id)?.visible));
|
||||
|
||||
const handleDragStart = (event: DragEvent) => {
|
||||
treeEmit?.('node-dragstart', event, props.data);
|
||||
|
@ -4,13 +4,13 @@
|
||||
<div
|
||||
class="m-editor-sidebar-header-item"
|
||||
v-for="(config, index) in sideBarItems"
|
||||
v-show="!floatBoxStates?.get(config.$key)?.status"
|
||||
draggable="true"
|
||||
:key="config.$key ?? index"
|
||||
:class="{ 'is-active': activeTabName === config.text }"
|
||||
@click="activeTabName = config.text || `${index}`"
|
||||
draggable="true"
|
||||
@dragstart="dragstartHandler"
|
||||
@dragend="dragendHandler(config.$key, $event)"
|
||||
v-show="!floatBoxStates?.get(config.$key)?.status"
|
||||
>
|
||||
<MIcon v-if="config.icon" :icon="config.icon"></MIcon>
|
||||
<div v-if="config.text" class="magic-editor-tab-panel-title">{{ config.text }}</div>
|
||||
|
@ -39,10 +39,11 @@
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import { TMagicScrollbar } from '@tmagic/design';
|
||||
import type { MNode } from '@tmagic/schema';
|
||||
|
||||
import SearchInput from '@editor/components/SearchInput.vue';
|
||||
import Tree from '@editor/components/Tree.vue';
|
||||
import { LayerPanelSlots, MenuButton, MenuComponent, Services } from '@editor/type';
|
||||
import type { LayerPanelSlots, MenuButton, MenuComponent, Services } from '@editor/type';
|
||||
|
||||
import LayerMenu from './LayerMenu.vue';
|
||||
import LayerNodeTool from './LayerNodeTool.vue';
|
||||
@ -69,10 +70,21 @@ const tree = ref<InstanceType<typeof Tree>>();
|
||||
|
||||
const page = computed(() => editorService?.get('page'));
|
||||
|
||||
const { nodeStatusMap } = useNodeStatus(services, page);
|
||||
const { nodeStatusMap } = useNodeStatus(services);
|
||||
const { isCtrlKeyDown } = useKeybinding(services, tree);
|
||||
|
||||
const { filterTextChangeHandler } = useFilter(nodeStatusMap, page);
|
||||
const filterNodeMethod = (v: string, data: MNode): boolean => {
|
||||
let name = '';
|
||||
if (data.name) {
|
||||
name = data.name;
|
||||
} else if (data.items) {
|
||||
name = 'container';
|
||||
}
|
||||
|
||||
return `${data.id}${name}${data.type}`.includes(v);
|
||||
};
|
||||
|
||||
const { filterTextChangeHandler } = useFilter(services, nodeStatusMap, filterNodeMethod);
|
||||
|
||||
const collapseAllHandler = () => {
|
||||
if (!page.value || !nodeStatusMap.value) return;
|
||||
|
@ -1,34 +1,42 @@
|
||||
import { type ComputedRef, ref } from 'vue';
|
||||
import { computed, type ComputedRef, ref } from 'vue';
|
||||
|
||||
import { Id, MNode, MPage } from '@tmagic/schema';
|
||||
import { Id, MNode } from '@tmagic/schema';
|
||||
|
||||
import { LayerNodeStatus } from '@editor/type';
|
||||
import { LayerNodeStatus, Services } from '@editor/type';
|
||||
import { traverseNode } from '@editor/utils';
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
|
||||
export const useFilter = (
|
||||
services: Services | undefined,
|
||||
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
|
||||
page: ComputedRef<MPage | null | undefined>,
|
||||
filterNodeMethod: (value: string, data: MNode) => boolean,
|
||||
) => {
|
||||
const page = computed(() => services?.editorService.get('page'));
|
||||
|
||||
// tree方法:对树节点进行筛选时执行的方法
|
||||
const filterIsMatch = (value: string, data: MNode): boolean => {
|
||||
if (!value) {
|
||||
const filterIsMatch = (value: string | string[], data: MNode): boolean => {
|
||||
const string = !Array.isArray(value) ? [value] : value;
|
||||
|
||||
if (!string.length) {
|
||||
return true;
|
||||
}
|
||||
let name = '';
|
||||
if (data.name) {
|
||||
name = data.name;
|
||||
} else if (data.items) {
|
||||
name = 'container';
|
||||
}
|
||||
return `${data.id}${name}${data.type}`.includes(value);
|
||||
|
||||
return string.some((v) => filterNodeMethod(v, data));
|
||||
};
|
||||
|
||||
const filterNode = (text: string) => (node: MNode, parents: MNode[]) => {
|
||||
const filter = (text: string | string[]) => {
|
||||
if (!page.value?.items?.length) return;
|
||||
|
||||
page.value.items.forEach((node) => {
|
||||
traverseNode(node, (node: MNode, parents: MNode[]) => {
|
||||
if (!nodeStatusMap.value) return;
|
||||
|
||||
const visible = filterIsMatch(text, node);
|
||||
if (visible && parents.length) {
|
||||
console.log(
|
||||
node.id,
|
||||
parents.map((a) => a.id),
|
||||
);
|
||||
parents.forEach((parent) => {
|
||||
updateStatus(nodeStatusMap.value!, parent.id, {
|
||||
visible,
|
||||
@ -40,19 +48,13 @@ export const useFilter = (
|
||||
updateStatus(nodeStatusMap.value, node.id, {
|
||||
visible,
|
||||
});
|
||||
};
|
||||
|
||||
const filter = (text: string) => {
|
||||
if (!page.value?.items?.length) return;
|
||||
|
||||
page.value.items.forEach((node) => {
|
||||
traverseNode(node, filterNode(text));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
filterText: ref(''),
|
||||
filterTextChangeHandler(text: string) {
|
||||
filterTextChangeHandler(text: string | string[]) {
|
||||
filter(text);
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { computed, type ComputedRef, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import type { Id, MNode, MPage } from '@tmagic/schema';
|
||||
import { getNodePath } from '@tmagic/utils';
|
||||
@ -7,21 +7,17 @@ import { LayerNodeStatus, Services } from '@editor/type';
|
||||
import { traverseNode } from '@editor/utils';
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
|
||||
const createPageNodeStatus = (
|
||||
services: Services | undefined,
|
||||
pageId: Id,
|
||||
initalLayerNodeStatus?: Map<Id, LayerNodeStatus>,
|
||||
) => {
|
||||
const createPageNodeStatus = (page: MPage, initalLayerNodeStatus?: Map<Id, LayerNodeStatus>) => {
|
||||
const map = new Map<Id, LayerNodeStatus>();
|
||||
|
||||
map.set(pageId, {
|
||||
map.set(page.id, {
|
||||
visible: true,
|
||||
expand: true,
|
||||
selected: true,
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) =>
|
||||
page.items.forEach((node: MNode) =>
|
||||
traverseNode(node, (node) => {
|
||||
map.set(
|
||||
node.id,
|
||||
@ -38,7 +34,8 @@ const createPageNodeStatus = (
|
||||
return map;
|
||||
};
|
||||
|
||||
export const useNodeStatus = (services: Services | undefined, page: ComputedRef<MPage | null | undefined>) => {
|
||||
export const useNodeStatus = (services: Services | undefined) => {
|
||||
const page = computed(() => services?.editorService.get('page'));
|
||||
const nodes = computed(() => services?.editorService.get('nodes') || []);
|
||||
|
||||
/** 所有页面的节点状态 */
|
||||
@ -57,7 +54,7 @@ export const useNodeStatus = (services: Services | undefined, page: ComputedRef<
|
||||
return;
|
||||
}
|
||||
// 生成节点状态
|
||||
nodeStatusMaps.value.set(page.id, createPageNodeStatus(services, page.id, nodeStatusMaps.value.get(page.id)));
|
||||
nodeStatusMaps.value.set(page.id, createPageNodeStatus(page, nodeStatusMaps.value.get(page.id)));
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
|
@ -1,112 +1,63 @@
|
||||
<template>
|
||||
<ContentMenu
|
||||
ref="menu"
|
||||
class="magic-editor-node-list-menu"
|
||||
style="max-width: 280px"
|
||||
:menu-data="menuData"
|
||||
:active="node?.id"
|
||||
:auto-hide="!pinned"
|
||||
@mouseenter="mouseenterHandler()"
|
||||
<TMagicTooltip v-if="page" content="点击查看当前位置下的组件">
|
||||
<div ref="button" class="m-editor-stage-float-button" @click="visible = true">可选组件</div>
|
||||
</TMagicTooltip>
|
||||
<FloatingBox
|
||||
v-if="page && nodeStatusMap"
|
||||
ref="box"
|
||||
v-model:visible="visible"
|
||||
title="当前位置下的组件"
|
||||
:position="menuPosition"
|
||||
>
|
||||
<template #title>
|
||||
<NodeListMenuTitle v-model:pinned="pinned" @change="dragMenuHandler" @close="closeHandler"></NodeListMenuTitle>
|
||||
<template #body>
|
||||
<Tree
|
||||
class="m-editor-node-list-menu magic-editor-layer-tree"
|
||||
:data="[page]"
|
||||
:node-status-map="nodeStatusMap"
|
||||
@node-click="clickHandler"
|
||||
></Tree>
|
||||
</template>
|
||||
</ContentMenu>
|
||||
</FloatingBox>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Component, computed, inject, ref, watch } from 'vue';
|
||||
import type { OnDrag } from 'gesto';
|
||||
import { computed, inject, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { TMagicTooltip } from '@tmagic/design';
|
||||
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 PINNED_STATUE_CACHE_KEY = 'tmagic-pinned-node-list-pinned-status';
|
||||
|
||||
const props = defineProps<{ isMultiSelect?: boolean }>();
|
||||
|
||||
const menu = ref<InstanceType<typeof ContentMenu>>();
|
||||
const nodeList = ref<MNode[]>([]);
|
||||
const pinned = ref(Boolean(globalThis.localStorage.getItem(PINNED_STATUE_CACHE_KEY)));
|
||||
const firstShow = ref(true);
|
||||
import FloatingBox from '@editor/components/FloatingBox.vue';
|
||||
import Tree from '@editor/components/Tree.vue';
|
||||
import { useFilter } from '@editor/layouts/sidebar/layer/use-filter';
|
||||
import { useNodeStatus } from '@editor/layouts/sidebar/layer/use-node-status';
|
||||
import type { Services, TreeNodeData } from '@editor/type';
|
||||
|
||||
const services = inject<Services>('services');
|
||||
const editorService = services?.editorService;
|
||||
const componentListService = services?.componentListService;
|
||||
|
||||
const visible = ref(false);
|
||||
const button = ref<HTMLDivElement>();
|
||||
const box = ref<InstanceType<typeof FloatingBox>>();
|
||||
|
||||
const stage = computed(() => editorService?.get('stage'));
|
||||
const page = computed(() => editorService?.get('page'));
|
||||
const node = computed(() => editorService?.get('node'));
|
||||
const nodes = computed(() => editorService?.get('nodes') || []);
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
const { nodeStatusMap } = useNodeStatus(services);
|
||||
|
||||
const cancel = () => {
|
||||
if (timeout) {
|
||||
globalThis.clearTimeout(timeout);
|
||||
}
|
||||
const filterNodeMethod = (value: string, data: MNode): boolean => data.id === value;
|
||||
|
||||
if (pinned.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodeList.value = [];
|
||||
menu.value?.hide();
|
||||
};
|
||||
|
||||
const clearTimeoutLazy = () => {
|
||||
globalThis.setTimeout(() => {
|
||||
if (timeout) {
|
||||
globalThis.clearTimeout(timeout);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
const { filterTextChangeHandler } = useFilter(services, nodeStatusMap, filterNodeMethod);
|
||||
|
||||
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(() => {
|
||||
stage.on('select', (el: HTMLElement, event: MouseEvent) => {
|
||||
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 && !firstShow.value ? undefined : event);
|
||||
firstShow.value = false;
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
stage.on('mouseleave', () => {
|
||||
// mouseleave后,大概率还有最后一个mousemove事件,这里延迟清除
|
||||
clearTimeoutLazy();
|
||||
const ids = els.map((el) => el.id).filter((id) => Boolean(id));
|
||||
filterTextChangeHandler(ids);
|
||||
});
|
||||
|
||||
unWatch();
|
||||
@ -116,60 +67,43 @@ const unWatch = watch(
|
||||
},
|
||||
);
|
||||
|
||||
const componentMap = computed(() => {
|
||||
const map: Record<string, ComponentItem> = {};
|
||||
componentListService?.getList().forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
map[item.type] = item;
|
||||
});
|
||||
});
|
||||
return map;
|
||||
});
|
||||
watch(
|
||||
nodes,
|
||||
(nodes) => {
|
||||
if (!nodeStatusMap.value) return;
|
||||
|
||||
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;
|
||||
for (const [id, status] of nodeStatusMap.value.entries()) {
|
||||
status.selected = nodes.some((node) => node.id === id);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'button',
|
||||
text,
|
||||
id: node.id,
|
||||
icon,
|
||||
handler: async () => {
|
||||
await editorService?.select(node);
|
||||
stage.value?.select(node.id);
|
||||
},
|
||||
};
|
||||
}),
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
const mouseenterHandler = () => {
|
||||
// menu的mouseenter后,大概率还有最后一个mousemove事件,这里延迟清除
|
||||
clearTimeoutLazy();
|
||||
const clickHandler = async (event: MouseEvent, data: TreeNodeData) => {
|
||||
await editorService?.select(data.id);
|
||||
stage.value?.select(data.id);
|
||||
};
|
||||
|
||||
const dragMenuHandler = ({ deltaY, deltaX }: OnDrag) => {
|
||||
if (!menu.value) return;
|
||||
const menuPosition = ref({
|
||||
left: 0,
|
||||
top: 0,
|
||||
});
|
||||
|
||||
const { menuPosition } = menu.value;
|
||||
watch(visible, async (visible) => {
|
||||
if (!button.value || !visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.value?.setPosition({
|
||||
clientY: menuPosition.top + deltaY,
|
||||
clientX: menuPosition.left + deltaX,
|
||||
});
|
||||
};
|
||||
await nextTick();
|
||||
|
||||
const closeHandler = () => {
|
||||
menu.value?.hide();
|
||||
};
|
||||
const rect = button.value.getBoundingClientRect();
|
||||
const height = box.value?.target?.clientHeight || 0;
|
||||
|
||||
watch(pinned, () => {
|
||||
globalThis.localStorage.setItem(PINNED_STATUE_CACHE_KEY, pinned.value.toString());
|
||||
menuPosition.value = {
|
||||
left: rect.left + rect.width + 5,
|
||||
top: rect.top - height / 2 + rect.height / 2,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -1,68 +0,0 @@
|
||||
<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>
|
@ -22,9 +22,10 @@
|
||||
@drop="dropHandler"
|
||||
@dragover="dragoverHandler"
|
||||
></div>
|
||||
<NodeListMenu></NodeListMenu>
|
||||
|
||||
<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>
|
||||
|
30
packages/editor/src/theme/floating-box.scss
Normal file
30
packages/editor/src/theme/floating-box.scss
Normal file
@ -0,0 +1,30 @@
|
||||
.m-editor-float-box {
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
z-index: 100;
|
||||
border: 1px solid $--border-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.m-editor-float-box-title {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 5px;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $--border-color;
|
||||
}
|
||||
|
||||
.m-editor-float-box-body {
|
||||
padding: 5px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor-floating-box-moveable {
|
||||
opacity: 0;
|
||||
}
|
@ -25,3 +25,27 @@
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor-stage-float-button {
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
width: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
padding: 5px;
|
||||
background-color: #ffffff;
|
||||
transition: background-color 0.2s;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.m-editor-node-list-menu {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
@ -24,3 +24,4 @@
|
||||
@import "./key-value.scss";
|
||||
@import "./floatbox.scss";
|
||||
@import "./tree.scss";
|
||||
@import "./floating-box.scss";
|
||||
|
@ -263,7 +263,7 @@ export const traverseNode = (node: MNode, cb: (node: MNode, parents: MNode[]) =>
|
||||
if (node.items?.length) {
|
||||
parents.push(node);
|
||||
node.items.forEach((item: MNode) => {
|
||||
traverseNode(item, cb, parents);
|
||||
traverseNode(item, cb, [...parents]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -561,13 +561,13 @@ export default class ActionManager extends EventEmitter {
|
||||
/**
|
||||
* 在up事件中负责对外通知选中事件,通知画布之外的编辑器更新
|
||||
*/
|
||||
private mouseUpHandler = (): void => {
|
||||
private mouseUpHandler = (event: MouseEvent): void => {
|
||||
getDocument().removeEventListener('mouseup', this.mouseUpHandler);
|
||||
this.container.addEventListener('mousemove', this.mouseMoveHandler);
|
||||
if (this.isMultiSelectStatus) {
|
||||
this.emit('multi-select', this.selectedElList);
|
||||
this.emit('multi-select', this.selectedElList, event);
|
||||
} else {
|
||||
this.emit('select', this.selectedEl);
|
||||
this.emit('select', this.selectedEl, event);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -307,14 +307,14 @@ export default class StageCore extends EventEmitter {
|
||||
.on('before-select', (idOrEl: Id | HTMLElement, event?: MouseEvent) => {
|
||||
this.select(idOrEl, event);
|
||||
})
|
||||
.on('select', (selectedEl: HTMLElement) => {
|
||||
this.emit('select', selectedEl);
|
||||
.on('select', (selectedEl: HTMLElement, event: MouseEvent) => {
|
||||
this.emit('select', selectedEl, event);
|
||||
})
|
||||
.on('before-multi-select', (idOrElList: HTMLElement[] | Id[]) => {
|
||||
this.multiSelect(idOrElList);
|
||||
})
|
||||
.on('multi-select', (selectedElList: HTMLElement[]) => {
|
||||
this.emit('multi-select', selectedElList);
|
||||
.on('multi-select', (selectedElList: HTMLElement[], event: MouseEvent) => {
|
||||
this.emit('multi-select', selectedElList, event);
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user