mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-26 19:46:42 +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 visible = computed(() => nodeStatus.value.visible);
|
||||||
const draggable = computed(() => nodeStatus.value.draggable);
|
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) => {
|
const handleDragStart = (event: DragEvent) => {
|
||||||
treeEmit?.('node-dragstart', event, props.data);
|
treeEmit?.('node-dragstart', event, props.data);
|
||||||
|
@ -4,13 +4,13 @@
|
|||||||
<div
|
<div
|
||||||
class="m-editor-sidebar-header-item"
|
class="m-editor-sidebar-header-item"
|
||||||
v-for="(config, index) in sideBarItems"
|
v-for="(config, index) in sideBarItems"
|
||||||
|
v-show="!floatBoxStates?.get(config.$key)?.status"
|
||||||
|
draggable="true"
|
||||||
:key="config.$key ?? index"
|
:key="config.$key ?? index"
|
||||||
:class="{ 'is-active': activeTabName === config.text }"
|
:class="{ 'is-active': activeTabName === config.text }"
|
||||||
@click="activeTabName = config.text || `${index}`"
|
@click="activeTabName = config.text || `${index}`"
|
||||||
draggable="true"
|
|
||||||
@dragstart="dragstartHandler"
|
@dragstart="dragstartHandler"
|
||||||
@dragend="dragendHandler(config.$key, $event)"
|
@dragend="dragendHandler(config.$key, $event)"
|
||||||
v-show="!floatBoxStates?.get(config.$key)?.status"
|
|
||||||
>
|
>
|
||||||
<MIcon v-if="config.icon" :icon="config.icon"></MIcon>
|
<MIcon v-if="config.icon" :icon="config.icon"></MIcon>
|
||||||
<div v-if="config.text" class="magic-editor-tab-panel-title">{{ config.text }}</div>
|
<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 { computed, inject, ref } from 'vue';
|
||||||
|
|
||||||
import { TMagicScrollbar } from '@tmagic/design';
|
import { TMagicScrollbar } from '@tmagic/design';
|
||||||
|
import type { MNode } from '@tmagic/schema';
|
||||||
|
|
||||||
import SearchInput from '@editor/components/SearchInput.vue';
|
import SearchInput from '@editor/components/SearchInput.vue';
|
||||||
import Tree from '@editor/components/Tree.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 LayerMenu from './LayerMenu.vue';
|
||||||
import LayerNodeTool from './LayerNodeTool.vue';
|
import LayerNodeTool from './LayerNodeTool.vue';
|
||||||
@ -69,10 +70,21 @@ const tree = ref<InstanceType<typeof Tree>>();
|
|||||||
|
|
||||||
const page = computed(() => editorService?.get('page'));
|
const page = computed(() => editorService?.get('page'));
|
||||||
|
|
||||||
const { nodeStatusMap } = useNodeStatus(services, page);
|
const { nodeStatusMap } = useNodeStatus(services);
|
||||||
const { isCtrlKeyDown } = useKeybinding(services, tree);
|
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 = () => {
|
const collapseAllHandler = () => {
|
||||||
if (!page.value || !nodeStatusMap.value) return;
|
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 { traverseNode } from '@editor/utils';
|
||||||
import { updateStatus } from '@editor/utils/tree';
|
import { updateStatus } from '@editor/utils/tree';
|
||||||
|
|
||||||
export const useFilter = (
|
export const useFilter = (
|
||||||
|
services: Services | undefined,
|
||||||
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | 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方法:对树节点进行筛选时执行的方法
|
// tree方法:对树节点进行筛选时执行的方法
|
||||||
const filterIsMatch = (value: string, data: MNode): boolean => {
|
const filterIsMatch = (value: string | string[], data: MNode): boolean => {
|
||||||
if (!value) {
|
const string = !Array.isArray(value) ? [value] : value;
|
||||||
|
|
||||||
|
if (!string.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let name = '';
|
|
||||||
if (data.name) {
|
return string.some((v) => filterNodeMethod(v, data));
|
||||||
name = data.name;
|
|
||||||
} else if (data.items) {
|
|
||||||
name = 'container';
|
|
||||||
}
|
|
||||||
return `${data.id}${name}${data.type}`.includes(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
if (!nodeStatusMap.value) return;
|
||||||
|
|
||||||
const visible = filterIsMatch(text, node);
|
const visible = filterIsMatch(text, node);
|
||||||
if (visible && parents.length) {
|
if (visible && parents.length) {
|
||||||
|
console.log(
|
||||||
|
node.id,
|
||||||
|
parents.map((a) => a.id),
|
||||||
|
);
|
||||||
parents.forEach((parent) => {
|
parents.forEach((parent) => {
|
||||||
updateStatus(nodeStatusMap.value!, parent.id, {
|
updateStatus(nodeStatusMap.value!, parent.id, {
|
||||||
visible,
|
visible,
|
||||||
@ -40,19 +48,13 @@ export const useFilter = (
|
|||||||
updateStatus(nodeStatusMap.value, node.id, {
|
updateStatus(nodeStatusMap.value, node.id, {
|
||||||
visible,
|
visible,
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
const filter = (text: string) => {
|
|
||||||
if (!page.value?.items?.length) return;
|
|
||||||
|
|
||||||
page.value.items.forEach((node) => {
|
|
||||||
traverseNode(node, filterNode(text));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterText: ref(''),
|
filterText: ref(''),
|
||||||
filterTextChangeHandler(text: string) {
|
filterTextChangeHandler(text: string | string[]) {
|
||||||
filter(text);
|
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 type { Id, MNode, MPage } from '@tmagic/schema';
|
||||||
import { getNodePath } from '@tmagic/utils';
|
import { getNodePath } from '@tmagic/utils';
|
||||||
@ -7,21 +7,17 @@ import { LayerNodeStatus, Services } from '@editor/type';
|
|||||||
import { traverseNode } from '@editor/utils';
|
import { traverseNode } from '@editor/utils';
|
||||||
import { updateStatus } from '@editor/utils/tree';
|
import { updateStatus } from '@editor/utils/tree';
|
||||||
|
|
||||||
const createPageNodeStatus = (
|
const createPageNodeStatus = (page: MPage, initalLayerNodeStatus?: Map<Id, LayerNodeStatus>) => {
|
||||||
services: Services | undefined,
|
|
||||||
pageId: Id,
|
|
||||||
initalLayerNodeStatus?: Map<Id, LayerNodeStatus>,
|
|
||||||
) => {
|
|
||||||
const map = new Map<Id, LayerNodeStatus>();
|
const map = new Map<Id, LayerNodeStatus>();
|
||||||
|
|
||||||
map.set(pageId, {
|
map.set(page.id, {
|
||||||
visible: true,
|
visible: true,
|
||||||
expand: true,
|
expand: true,
|
||||||
selected: true,
|
selected: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) =>
|
page.items.forEach((node: MNode) =>
|
||||||
traverseNode(node, (node) => {
|
traverseNode(node, (node) => {
|
||||||
map.set(
|
map.set(
|
||||||
node.id,
|
node.id,
|
||||||
@ -38,7 +34,8 @@ const createPageNodeStatus = (
|
|||||||
return map;
|
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') || []);
|
const nodes = computed(() => services?.editorService.get('nodes') || []);
|
||||||
|
|
||||||
/** 所有页面的节点状态 */
|
/** 所有页面的节点状态 */
|
||||||
@ -57,7 +54,7 @@ export const useNodeStatus = (services: Services | undefined, page: ComputedRef<
|
|||||||
return;
|
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,
|
immediate: true,
|
||||||
|
@ -1,112 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<ContentMenu
|
<TMagicTooltip v-if="page" content="点击查看当前位置下的组件">
|
||||||
ref="menu"
|
<div ref="button" class="m-editor-stage-float-button" @click="visible = true">可选组件</div>
|
||||||
class="magic-editor-node-list-menu"
|
</TMagicTooltip>
|
||||||
style="max-width: 280px"
|
<FloatingBox
|
||||||
:menu-data="menuData"
|
v-if="page && nodeStatusMap"
|
||||||
:active="node?.id"
|
ref="box"
|
||||||
:auto-hide="!pinned"
|
v-model:visible="visible"
|
||||||
@mouseenter="mouseenterHandler()"
|
title="当前位置下的组件"
|
||||||
|
:position="menuPosition"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #body>
|
||||||
<NodeListMenuTitle v-model:pinned="pinned" @change="dragMenuHandler" @close="closeHandler"></NodeListMenuTitle>
|
<Tree
|
||||||
|
class="m-editor-node-list-menu magic-editor-layer-tree"
|
||||||
|
:data="[page]"
|
||||||
|
:node-status-map="nodeStatusMap"
|
||||||
|
@node-click="clickHandler"
|
||||||
|
></Tree>
|
||||||
</template>
|
</template>
|
||||||
</ContentMenu>
|
</FloatingBox>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Component, computed, inject, ref, watch } from 'vue';
|
import { computed, inject, nextTick, ref, watch } from 'vue';
|
||||||
import type { OnDrag } from 'gesto';
|
|
||||||
|
|
||||||
|
import { TMagicTooltip } from '@tmagic/design';
|
||||||
import type { MNode } from '@tmagic/schema';
|
import type { MNode } from '@tmagic/schema';
|
||||||
import { StageDragStatus } from '@tmagic/stage';
|
|
||||||
import { getNodes } from '@tmagic/utils';
|
|
||||||
|
|
||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
import FloatingBox from '@editor/components/FloatingBox.vue';
|
||||||
import type { ComponentItem, MenuButton, Services } from '@editor/type';
|
import Tree from '@editor/components/Tree.vue';
|
||||||
|
import { useFilter } from '@editor/layouts/sidebar/layer/use-filter';
|
||||||
import NodeListMenuTitle from './NodeListMenuTitle.vue';
|
import { useNodeStatus } from '@editor/layouts/sidebar/layer/use-node-status';
|
||||||
|
import type { Services, TreeNodeData } from '@editor/type';
|
||||||
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);
|
|
||||||
|
|
||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
const editorService = services?.editorService;
|
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 stage = computed(() => editorService?.get('stage'));
|
||||||
const page = computed(() => editorService?.get('page'));
|
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 = () => {
|
const filterNodeMethod = (value: string, data: MNode): boolean => data.id === value;
|
||||||
if (timeout) {
|
|
||||||
globalThis.clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinned.value) {
|
const { filterTextChangeHandler } = useFilter(services, nodeStatusMap, filterNodeMethod);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeList.value = [];
|
|
||||||
menu.value?.hide();
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearTimeoutLazy = () => {
|
|
||||||
globalThis.setTimeout(() => {
|
|
||||||
if (timeout) {
|
|
||||||
globalThis.clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const unWatch = watch(
|
const unWatch = watch(
|
||||||
stage,
|
stage,
|
||||||
(stage) => {
|
(stage) => {
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
|
|
||||||
stage.on('drag-start', () => {
|
stage.on('select', (el: HTMLElement, event: MouseEvent) => {
|
||||||
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 els = stage.renderer.getElementsFromPoint(event) || [];
|
||||||
|
const ids = els.map((el) => el.id).filter((id) => Boolean(id));
|
||||||
const nodes = getNodes(
|
filterTextChangeHandler(ids);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
unWatch();
|
unWatch();
|
||||||
@ -116,60 +67,43 @@ const unWatch = watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const componentMap = computed(() => {
|
watch(
|
||||||
const map: Record<string, ComponentItem> = {};
|
nodes,
|
||||||
componentListService?.getList().forEach((group) => {
|
(nodes) => {
|
||||||
group.items.forEach((item) => {
|
if (!nodeStatusMap.value) return;
|
||||||
map[item.type] = item;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuData = computed<MenuButton[]>(() =>
|
for (const [id, status] of nodeStatusMap.value.entries()) {
|
||||||
nodeList.value.map((node: MNode) => {
|
status.selected = nodes.some((node) => node.id === id);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
}),
|
immediate: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseenterHandler = () => {
|
const clickHandler = async (event: MouseEvent, data: TreeNodeData) => {
|
||||||
// menu的mouseenter后,大概率还有最后一个mousemove事件,这里延迟清除
|
await editorService?.select(data.id);
|
||||||
clearTimeoutLazy();
|
stage.value?.select(data.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragMenuHandler = ({ deltaY, deltaX }: OnDrag) => {
|
const menuPosition = ref({
|
||||||
if (!menu.value) return;
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const { menuPosition } = menu.value;
|
watch(visible, async (visible) => {
|
||||||
|
if (!button.value || !visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
menu.value?.setPosition({
|
await nextTick();
|
||||||
clientY: menuPosition.top + deltaY,
|
|
||||||
clientX: menuPosition.left + deltaX,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeHandler = () => {
|
const rect = button.value.getBoundingClientRect();
|
||||||
menu.value?.hide();
|
const height = box.value?.target?.clientHeight || 0;
|
||||||
};
|
|
||||||
|
|
||||||
watch(pinned, () => {
|
menuPosition.value = {
|
||||||
globalThis.localStorage.setItem(PINNED_STATUE_CACHE_KEY, pinned.value.toString());
|
left: rect.left + rect.width + 5,
|
||||||
|
top: rect.top - height / 2 + rect.height / 2,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</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"
|
@drop="dropHandler"
|
||||||
@dragover="dragoverHandler"
|
@dragover="dragoverHandler"
|
||||||
></div>
|
></div>
|
||||||
|
<NodeListMenu></NodeListMenu>
|
||||||
|
|
||||||
<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>
|
||||||
|
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;
|
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 "./key-value.scss";
|
||||||
@import "./floatbox.scss";
|
@import "./floatbox.scss";
|
||||||
@import "./tree.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) {
|
if (node.items?.length) {
|
||||||
parents.push(node);
|
parents.push(node);
|
||||||
node.items.forEach((item: MNode) => {
|
node.items.forEach((item: MNode) => {
|
||||||
traverseNode(item, cb, parents);
|
traverseNode(item, cb, [...parents]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -561,13 +561,13 @@ export default class ActionManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* 在up事件中负责对外通知选中事件,通知画布之外的编辑器更新
|
* 在up事件中负责对外通知选中事件,通知画布之外的编辑器更新
|
||||||
*/
|
*/
|
||||||
private mouseUpHandler = (): void => {
|
private mouseUpHandler = (event: MouseEvent): void => {
|
||||||
getDocument().removeEventListener('mouseup', this.mouseUpHandler);
|
getDocument().removeEventListener('mouseup', this.mouseUpHandler);
|
||||||
this.container.addEventListener('mousemove', this.mouseMoveHandler);
|
this.container.addEventListener('mousemove', this.mouseMoveHandler);
|
||||||
if (this.isMultiSelectStatus) {
|
if (this.isMultiSelectStatus) {
|
||||||
this.emit('multi-select', this.selectedElList);
|
this.emit('multi-select', this.selectedElList, event);
|
||||||
} else {
|
} 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) => {
|
.on('before-select', (idOrEl: Id | HTMLElement, event?: MouseEvent) => {
|
||||||
this.select(idOrEl, event);
|
this.select(idOrEl, event);
|
||||||
})
|
})
|
||||||
.on('select', (selectedEl: HTMLElement) => {
|
.on('select', (selectedEl: HTMLElement, event: MouseEvent) => {
|
||||||
this.emit('select', selectedEl);
|
this.emit('select', selectedEl, event);
|
||||||
})
|
})
|
||||||
.on('before-multi-select', (idOrElList: HTMLElement[] | Id[]) => {
|
.on('before-multi-select', (idOrElList: HTMLElement[] | Id[]) => {
|
||||||
this.multiSelect(idOrElList);
|
this.multiSelect(idOrElList);
|
||||||
})
|
})
|
||||||
.on('multi-select', (selectedElList: HTMLElement[]) => {
|
.on('multi-select', (selectedElList: HTMLElement[], event: MouseEvent) => {
|
||||||
this.emit('multi-select', selectedElList);
|
this.emit('multi-select', selectedElList, event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user