mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-23 16:35:51 +08:00
refactor(editor): 新增tree组件
This commit is contained in:
parent
8d8c8df82f
commit
7d251f04e8
59
packages/editor/src/components/Tree.vue
Normal file
59
packages/editor/src/components/Tree.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="m-editor-tree" @dragover="handleDragOver">
|
||||
<TreeNode v-for="item in data" :key="item.id" :data="item" :indent="indent" :node-status-map="nodeStatusMap">
|
||||
<template #tree-node-content="{ data: nodeData }">
|
||||
<slot name="tree-node-content" :data="nodeData"> </slot>
|
||||
</template>
|
||||
|
||||
<template #tree-node-tool="{ data: nodeData }">
|
||||
<slot name="tree-node-tool" :data="nodeData"> </slot>
|
||||
</template>
|
||||
</TreeNode>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide } from 'vue';
|
||||
|
||||
import type { Id } from '@tmagic/schema';
|
||||
|
||||
import type { LayerNodeStatus, TreeNodeData } from '@editor/type';
|
||||
|
||||
import TreeNode from './TreeNode.vue';
|
||||
|
||||
defineSlots<{
|
||||
'tree-node-content'(props: { data: TreeNodeData }): any;
|
||||
'tree-node-tool'(props: { data: TreeNodeData }): any;
|
||||
}>();
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorTree',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'node-dragover': [event: DragEvent];
|
||||
'node-dragstart': [event: DragEvent, data: TreeNodeData];
|
||||
'node-dragleave': [event: DragEvent, data: TreeNodeData];
|
||||
'node-dragend': [event: DragEvent, data: TreeNodeData];
|
||||
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
|
||||
'node-mouseenter': [event: MouseEvent, data: TreeNodeData];
|
||||
'node-click': [event: MouseEvent, data: TreeNodeData];
|
||||
}>();
|
||||
|
||||
provide('treeEmit', emit);
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
data: TreeNodeData[];
|
||||
nodeStatusMap: Map<Id, LayerNodeStatus>;
|
||||
indent?: number;
|
||||
}>(),
|
||||
{
|
||||
indent: 0,
|
||||
},
|
||||
);
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
emit('node-dragover', event);
|
||||
};
|
||||
</script>
|
142
packages/editor/src/components/TreeNode.vue
Normal file
142
packages/editor/src/components/TreeNode.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="visible"
|
||||
class="m-editor-tree-node"
|
||||
:draggable="draggable"
|
||||
:data-node-id="data.id"
|
||||
:data-is-container="Array.isArray(data.items)"
|
||||
@dragstart="handleDragStart"
|
||||
@dragleave="handleDragLeave"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<div
|
||||
class="tree-node"
|
||||
:class="{ selected, expanded }"
|
||||
:style="`padding-left: ${indent}px`"
|
||||
@contextmenu="nodeContentmenuHandler"
|
||||
@mouseenter="mouseenterHandler"
|
||||
>
|
||||
<MIcon
|
||||
class="expand-icon"
|
||||
:style="hasChilren ? '' : 'color: transparent; cursor: default'"
|
||||
:icon="expanded ? ArrowDown : ArrowRight"
|
||||
@click="expandHandler"
|
||||
></MIcon>
|
||||
|
||||
<div class="tree-node-content" @click="nodeClickHandler">
|
||||
<slot name="tree-node-content" :data="data">
|
||||
<div class="tree-node-label">{{ `${data.name} (${data.id})` }}</div>
|
||||
<div class="tree-node-tool">
|
||||
<slot name="tree-node-tool" :data="data"></slot>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasChilren && expanded" class="m-editor-tree-node-children">
|
||||
<TreeNode
|
||||
v-for="item in data.items"
|
||||
:key="item.id"
|
||||
:data="item"
|
||||
:node-status-map="nodeStatusMap"
|
||||
:indent="indent + 11"
|
||||
>
|
||||
<template #tree-node-content="{ data: nodeData }">
|
||||
<slot name="tree-node-content" :data="nodeData"> </slot>
|
||||
</template>
|
||||
<template #tree-node-tool="{ data: nodeData }">
|
||||
<slot name="tree-node-tool" :data="nodeData"> </slot>
|
||||
</template>
|
||||
</TreeNode>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
import type { Id } from '@tmagic/schema';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import type { LayerNodeStatus, TreeNodeData } from '@editor/type';
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
|
||||
defineSlots<{
|
||||
'tree-node-tool'(props: { data: TreeNodeData }): any;
|
||||
'tree-node-content'(props: { data: TreeNodeData }): any;
|
||||
}>();
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorTreeNode',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'node-dragstart': [event: DragEvent, data: TreeNodeData];
|
||||
'node-dragleave': [event: DragEvent, data: TreeNodeData];
|
||||
'node-dragend': [event: DragEvent, data: TreeNodeData];
|
||||
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
|
||||
'node-mouseenter': [event: MouseEvent, data: TreeNodeData];
|
||||
'node-click': [event: MouseEvent, data: TreeNodeData];
|
||||
}>();
|
||||
|
||||
const treeEmit = inject<typeof emit>('treeEmit');
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: TreeNodeData;
|
||||
nodeStatusMap: Map<Id, LayerNodeStatus>;
|
||||
indent?: number;
|
||||
}>(),
|
||||
{
|
||||
indent: 0,
|
||||
},
|
||||
);
|
||||
|
||||
const nodeStatus = computed(
|
||||
() =>
|
||||
props.nodeStatusMap?.get(props.data.id) || {
|
||||
selected: false,
|
||||
expand: false,
|
||||
visible: false,
|
||||
draggable: false,
|
||||
},
|
||||
);
|
||||
|
||||
const expanded = computed(() => nodeStatus.value.expand);
|
||||
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 handleDragStart = (event: DragEvent) => {
|
||||
treeEmit?.('node-dragstart', event, props.data);
|
||||
};
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
treeEmit?.('node-dragleave', event, props.data);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEvent) => {
|
||||
treeEmit?.('node-dragend', event, props.data);
|
||||
};
|
||||
|
||||
const nodeContentmenuHandler = (event: MouseEvent) => {
|
||||
treeEmit?.('node-contextmenu', event, props.data);
|
||||
};
|
||||
|
||||
const mouseenterHandler = (event: MouseEvent) => {
|
||||
treeEmit?.('node-mouseenter', event, props.data);
|
||||
};
|
||||
|
||||
const expandHandler = () => {
|
||||
updateStatus(props.nodeStatusMap, props.data.id, {
|
||||
expand: !expanded.value,
|
||||
});
|
||||
};
|
||||
|
||||
const nodeClickHandler = (event: MouseEvent) => {
|
||||
treeEmit?.('node-click', event, props.data);
|
||||
};
|
||||
</script>
|
@ -1,208 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="visible"
|
||||
class="magic-editor-layer-node"
|
||||
ref="nodeEl"
|
||||
:draggable="!isPage(data)"
|
||||
:data-node-id="data.id"
|
||||
:data-is-container="Array.isArray(data.items)"
|
||||
@dragstart="handleDragStart"
|
||||
@dragleave="handleDragLeave"
|
||||
@dragend="handleDragEnd($event, data)"
|
||||
>
|
||||
<div
|
||||
class="layer-node"
|
||||
:class="{ selected, expanded }"
|
||||
:style="`padding-left: ${indent}px`"
|
||||
@contextmenu="contextmenuHandler"
|
||||
@mouseenter="highlightHandler()"
|
||||
>
|
||||
<MIcon
|
||||
class="expand-icon"
|
||||
:style="hasChilren ? '' : 'color: transparent; cursor: default'"
|
||||
:icon="expanded ? ArrowDown : ArrowRight"
|
||||
@click="expandHandler"
|
||||
></MIcon>
|
||||
|
||||
<div class="layer-node-content" @click="nodeClickHandler">
|
||||
<slot name="layer-node-content" :data="data">
|
||||
<div class="layer-node-label">{{ `${data.name} (${data.id})` }}</div>
|
||||
<div class="layer-node-tool">
|
||||
<LayerNodeTool :data="data"></LayerNodeTool>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasChilren && expanded" class="magic-editor-layer-node-children">
|
||||
<LayerNode
|
||||
v-for="item in data.items"
|
||||
:data="item"
|
||||
:parent="(data as MContainer)"
|
||||
:key="item.id"
|
||||
:indent="indent + 11"
|
||||
:filter-text="filterText"
|
||||
:is-ctrl-key-down="isCtrlKeyDown"
|
||||
@node-contextmenu="nodeContentmenuHandler"
|
||||
>
|
||||
<template #layer-node-content="{ data: nodeData }">
|
||||
<slot name="layer-node-content" :data="nodeData"> </slot>
|
||||
</template>
|
||||
</LayerNode>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, type ComputedRef, inject, nextTick, ref } from 'vue';
|
||||
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import type { Id, MContainer, MNode } from '@tmagic/schema';
|
||||
import { isPage } from '@tmagic/utils';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { LayerNodeSlots, LayerNodeStatus, Services, UI_SELECT_MODE_EVENT_NAME } from '@editor/type';
|
||||
|
||||
import LayerNodeTool from './LayerNodeTool.vue';
|
||||
import { useDrag } from './use-drag';
|
||||
import { updateStatus } from './use-filter';
|
||||
|
||||
defineSlots<LayerNodeSlots>();
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorLayerNode',
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: MNode;
|
||||
parent?: MContainer;
|
||||
indent?: number;
|
||||
filterText?: string;
|
||||
isCtrlKeyDown?: boolean;
|
||||
}>(),
|
||||
{
|
||||
indent: 0,
|
||||
filterText: '',
|
||||
isCtrlKeyDown: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'node-contextmenu': [event: MouseEvent, node: MNode];
|
||||
}>();
|
||||
|
||||
const services = inject<Services>('services');
|
||||
const nodeStatusMap = inject<ComputedRef<Map<Id, LayerNodeStatus>>>('nodeStatusMap');
|
||||
const editorService = services?.editorService;
|
||||
const uiService = services?.uiService;
|
||||
|
||||
const nodeStatus = computed(
|
||||
() =>
|
||||
nodeStatusMap?.value?.get(props.data.id) || {
|
||||
selected: false,
|
||||
expand: false,
|
||||
visible: false,
|
||||
},
|
||||
);
|
||||
|
||||
const expanded = computed(() => nodeStatus.value.expand);
|
||||
const selected = computed(() => nodeStatus.value.selected);
|
||||
const visible = computed(() => nodeStatus.value.visible);
|
||||
|
||||
const hasChilren = computed(() => props.data.items?.length > 0);
|
||||
|
||||
const nodeEl = ref<HTMLDivElement>();
|
||||
const { handleDragStart, handleDragEnd, handleDragLeave } = useDrag(services);
|
||||
|
||||
const expandHandler = () => {
|
||||
if (!nodeStatusMap?.value) return;
|
||||
|
||||
updateStatus(nodeStatusMap.value, props.data.id, {
|
||||
expand: !expanded.value,
|
||||
});
|
||||
};
|
||||
|
||||
const nodeClickHandler = () => {
|
||||
if (!nodeStatusMap?.value) return;
|
||||
|
||||
if (uiService?.get('uiSelectMode')) {
|
||||
document.dispatchEvent(new CustomEvent(UI_SELECT_MODE_EVENT_NAME, { detail: props.data }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasChilren.value && !props.isCtrlKeyDown) {
|
||||
updateStatus(nodeStatusMap.value, props.data.id, {
|
||||
expand: true,
|
||||
});
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
select(props.data);
|
||||
});
|
||||
};
|
||||
|
||||
const contextmenuHandler = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
const nodes = editorService?.get('nodes') || [];
|
||||
if (nodes.length < 2 || !nodes.includes(props.data)) {
|
||||
nodeClickHandler();
|
||||
}
|
||||
|
||||
emit('node-contextmenu', event, props.data);
|
||||
};
|
||||
|
||||
const nodeContentmenuHandler = (event: MouseEvent, node: MNode) => {
|
||||
emit('node-contextmenu', event, node);
|
||||
};
|
||||
|
||||
// 触发画布选中
|
||||
const select = async (data: MNode) => {
|
||||
if (!data.id) {
|
||||
throw new Error('没有id');
|
||||
}
|
||||
|
||||
if (props.isCtrlKeyDown) {
|
||||
multiSelect(data);
|
||||
} else {
|
||||
await editorService?.select(data);
|
||||
editorService?.get('stage')?.select(data.id);
|
||||
}
|
||||
};
|
||||
|
||||
const multiSelect = async (data: MNode) => {
|
||||
const nodes = editorService?.get('nodes') || [];
|
||||
|
||||
const newNodes: Id[] = [];
|
||||
let isCancel = false;
|
||||
nodes.forEach((node) => {
|
||||
if (node.id === data.id) {
|
||||
isCancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
newNodes.push(node.id);
|
||||
});
|
||||
|
||||
// 只剩一个不能取消选中
|
||||
if (!isCancel || newNodes.length === 0) {
|
||||
newNodes.push(data.id);
|
||||
}
|
||||
|
||||
await editorService?.multiSelect(newNodes);
|
||||
editorService?.get('stage')?.multiSelect(newNodes);
|
||||
};
|
||||
|
||||
const throttleTime = 300;
|
||||
// 鼠标在组件树移动触发高亮
|
||||
const highlightHandler = throttle(() => {
|
||||
highlight();
|
||||
}, throttleTime);
|
||||
|
||||
// 触发画布高亮
|
||||
const highlight = () => {
|
||||
editorService?.highlight(props.data);
|
||||
editorService?.get('stage')?.highlight(props.data.id);
|
||||
};
|
||||
</script>
|
@ -1,22 +1,31 @@
|
||||
<template>
|
||||
<TMagicScrollbar class="magic-editor-layer-panel">
|
||||
<TMagicScrollbar class="m-editor-layer-panel">
|
||||
<slot name="layer-panel-header"></slot>
|
||||
|
||||
<SearchInput @search="filterTextChangeHandler"></SearchInput>
|
||||
|
||||
<div class="magic-editor-layer-tree" ref="layerTreeContainer" tabindex="-1" @dragover="handleDragOver">
|
||||
<LayerNode
|
||||
v-if="page && root"
|
||||
:data="page"
|
||||
:filter-text="filterText"
|
||||
:is-ctrl-key-down="isCtrlKeyDown"
|
||||
@node-contextmenu="contextmenu"
|
||||
>
|
||||
<template #layer-node-content="{ data: nodeData }">
|
||||
<slot name="layer-node-content" :data="nodeData"> </slot>
|
||||
</template>
|
||||
</LayerNode>
|
||||
</div>
|
||||
<Tree
|
||||
v-if="page && nodeStatusMap"
|
||||
tabindex="-1"
|
||||
ref="tree"
|
||||
:data="[page]"
|
||||
:node-status-map="nodeStatusMap"
|
||||
@node-dragover="handleDragOver"
|
||||
@node-dragstart="handleDragStart"
|
||||
@node-dragleave="handleDragLeave"
|
||||
@node-dragend="handleDragEnd"
|
||||
@node-contextmenu="nodeContentmenuHandler"
|
||||
@node-mouseenter="mouseenterHandler"
|
||||
@node-click="nodeClickHandler"
|
||||
>
|
||||
<template #tree-node-tool="{ data: nodeData }">
|
||||
<LayerNodeTool :data="nodeData"></LayerNodeTool>
|
||||
</template>
|
||||
|
||||
<template #tree-node-content="{ data: nodeData }">
|
||||
<slot name="layer-node-content" :data="nodeData"> </slot>
|
||||
</template>
|
||||
</Tree>
|
||||
|
||||
<Teleport to="body">
|
||||
<LayerMenu ref="menu" :layer-content-menu="layerContentMenu" @collapse-all="collapseAllHandler"></LayerMenu>
|
||||
@ -25,15 +34,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, provide, ref } from 'vue';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import { TMagicScrollbar } from '@tmagic/design';
|
||||
|
||||
import SearchInput from '@editor/components/SearchInput.vue';
|
||||
import type { LayerPanelSlots, MenuButton, MenuComponent, Services } from '@editor/type';
|
||||
import Tree from '@editor/components/Tree.vue';
|
||||
import { LayerPanelSlots, MenuButton, MenuComponent, Services } from '@editor/type';
|
||||
|
||||
import LayerMenu from './LayerMenu.vue';
|
||||
import LayerNode from './LayerNode.vue';
|
||||
import LayerNodeTool from './LayerNodeTool.vue';
|
||||
import { useClick } from './use-click';
|
||||
import { useDrag } from './use-drag';
|
||||
import { useFilter } from './use-filter';
|
||||
import { useKeybinding } from './use-keybinding';
|
||||
@ -52,17 +63,14 @@ defineProps<{
|
||||
const services = inject<Services>('services');
|
||||
const editorService = services?.editorService;
|
||||
|
||||
const layerTreeContainer = ref<HTMLDivElement>();
|
||||
const tree = ref<InstanceType<typeof Tree>>();
|
||||
|
||||
const page = computed(() => editorService?.get('page'));
|
||||
const root = computed(() => editorService?.get('root'));
|
||||
|
||||
const { nodeStatusMap } = useNodeStatus(services, page);
|
||||
const { isCtrlKeyDown } = useKeybinding(services, layerTreeContainer);
|
||||
const { isCtrlKeyDown } = useKeybinding(services, tree);
|
||||
|
||||
provide('nodeStatusMap', nodeStatusMap);
|
||||
|
||||
const { filterText, filterTextChangeHandler } = useFilter(nodeStatusMap, page);
|
||||
const { filterTextChangeHandler } = useFilter(nodeStatusMap, page);
|
||||
|
||||
const collapseAllHandler = () => {
|
||||
if (!page.value || !nodeStatusMap.value) return;
|
||||
@ -75,13 +83,12 @@ const collapseAllHandler = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 右键菜单
|
||||
const menu = ref<InstanceType<typeof LayerMenu>>();
|
||||
const contextmenu = (event: MouseEvent): void => {
|
||||
event.preventDefault();
|
||||
const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useDrag(services);
|
||||
|
||||
menu.value?.show(event);
|
||||
};
|
||||
|
||||
const { handleDragOver } = useDrag(services);
|
||||
const {
|
||||
menu,
|
||||
nodeClickHandler,
|
||||
nodeContentmenuHandler,
|
||||
highlightHandler: mouseenterHandler,
|
||||
} = useClick(services, isCtrlKeyDown, nodeStatusMap);
|
||||
</script>
|
||||
|
105
packages/editor/src/layouts/sidebar/layer/use-click.ts
Normal file
105
packages/editor/src/layouts/sidebar/layer/use-click.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { type ComputedRef, nextTick, type Ref, ref } from 'vue';
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import { Id, MNode } from '@tmagic/schema';
|
||||
|
||||
import { LayerNodeStatus, Services, TreeNodeData, UI_SELECT_MODE_EVENT_NAME } from '@editor/type';
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
|
||||
import LayerMenu from './LayerMenu.vue';
|
||||
|
||||
export const useClick = (
|
||||
services: Services | undefined,
|
||||
isCtrlKeyDown: Ref<boolean>,
|
||||
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
|
||||
) => {
|
||||
// 触发画布选中
|
||||
const select = async (data: MNode) => {
|
||||
if (!data.id) {
|
||||
throw new Error('没有id');
|
||||
}
|
||||
|
||||
if (isCtrlKeyDown.value) {
|
||||
multiSelect(data);
|
||||
} else {
|
||||
await services?.editorService.select(data);
|
||||
services?.editorService.get('stage')?.select(data.id);
|
||||
}
|
||||
};
|
||||
|
||||
const multiSelect = async (data: MNode) => {
|
||||
const nodes = services?.editorService.get('nodes') || [];
|
||||
|
||||
const newNodes: Id[] = [];
|
||||
let isCancel = false;
|
||||
nodes.forEach((node) => {
|
||||
if (node.id === data.id) {
|
||||
isCancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
newNodes.push(node.id);
|
||||
});
|
||||
|
||||
// 只剩一个不能取消选中
|
||||
if (!isCancel || newNodes.length === 0) {
|
||||
newNodes.push(data.id);
|
||||
}
|
||||
|
||||
await services?.editorService.multiSelect(newNodes);
|
||||
services?.editorService.get('stage')?.multiSelect(newNodes);
|
||||
};
|
||||
|
||||
const throttleTime = 300;
|
||||
// 鼠标在组件树移动触发高亮
|
||||
const highlightHandler = throttle((event: MouseEvent, data: TreeNodeData) => {
|
||||
highlight(data);
|
||||
}, throttleTime);
|
||||
|
||||
// 触发画布高亮
|
||||
const highlight = (data: TreeNodeData) => {
|
||||
services?.editorService?.highlight(data);
|
||||
services?.editorService?.get('stage')?.highlight(data.id);
|
||||
};
|
||||
|
||||
const nodeClickHandler = (event: MouseEvent, data: TreeNodeData) => {
|
||||
if (!nodeStatusMap?.value) return;
|
||||
|
||||
if (services?.uiService.get('uiSelectMode')) {
|
||||
document.dispatchEvent(new CustomEvent(UI_SELECT_MODE_EVENT_NAME, { detail: data }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.items && data.items.length > 0 && !isCtrlKeyDown.value) {
|
||||
updateStatus(nodeStatusMap.value, data.id, {
|
||||
expand: true,
|
||||
});
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
select(data);
|
||||
});
|
||||
};
|
||||
|
||||
// 右键菜单
|
||||
const menu = ref<InstanceType<typeof LayerMenu>>();
|
||||
|
||||
return {
|
||||
menu,
|
||||
|
||||
nodeClickHandler,
|
||||
|
||||
nodeContentmenuHandler(event: MouseEvent, data: TreeNodeData) {
|
||||
event.preventDefault();
|
||||
|
||||
const nodes = services?.editorService.get('nodes') || [];
|
||||
if (nodes.length < 2 || !nodes.includes(data)) {
|
||||
nodeClickHandler(event, data);
|
||||
}
|
||||
|
||||
menu.value?.show(event);
|
||||
},
|
||||
|
||||
highlightHandler,
|
||||
};
|
||||
};
|
@ -1,21 +1,10 @@
|
||||
import { type ComputedRef, ref } from 'vue';
|
||||
|
||||
import { Id, MNode, MPage } from '@tmagic/schema';
|
||||
import { getKeys } from '@tmagic/utils';
|
||||
|
||||
import { LayerNodeStatus } from '@editor/type';
|
||||
import { traverseNode } from '@editor/utils';
|
||||
|
||||
export const updateStatus = (nodeStatusMap: Map<Id, LayerNodeStatus>, id: Id, status: Partial<LayerNodeStatus>) => {
|
||||
const nodeStatus = nodeStatusMap.get(id);
|
||||
|
||||
if (!nodeStatus) return;
|
||||
getKeys(status).forEach((key) => {
|
||||
if (nodeStatus[key] !== undefined && status[key] !== undefined) {
|
||||
nodeStatus[key] = Boolean(status[key]);
|
||||
}
|
||||
});
|
||||
};
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
|
||||
export const useFilter = (
|
||||
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { type Ref, ref, watchEffect } from 'vue';
|
||||
|
||||
import Tree from '@editor/components/Tree.vue';
|
||||
import type { Services } from '@editor/type';
|
||||
import { KeyBindingContainerKey } from '@editor/utils/keybinding-config';
|
||||
|
||||
export const useKeybinding = (services: Services | undefined, contianer: Ref<HTMLDivElement | undefined>) => {
|
||||
export const useKeybinding = (
|
||||
services: Services | undefined,
|
||||
contianer: Ref<InstanceType<typeof Tree> | undefined>,
|
||||
) => {
|
||||
const keybindingService = services?.keybindingService;
|
||||
|
||||
// 是否多选
|
||||
@ -37,7 +41,7 @@ export const useKeybinding = (services: Services | undefined, contianer: Ref<HTM
|
||||
watchEffect(() => {
|
||||
if (contianer.value) {
|
||||
globalThis.addEventListener('blur', windowBlurHandler);
|
||||
keybindingService?.registeEl(KeyBindingContainerKey.LAYER_PANEL, contianer.value);
|
||||
keybindingService?.registeEl(KeyBindingContainerKey.LAYER_PANEL, contianer.value.$el);
|
||||
} else {
|
||||
globalThis.removeEventListener('blur', windowBlurHandler);
|
||||
keybindingService?.unregisteEl(KeyBindingContainerKey.LAYER_PANEL);
|
||||
|
@ -5,8 +5,7 @@ import { getNodePath } from '@tmagic/utils';
|
||||
|
||||
import { LayerNodeStatus, Services } from '@editor/type';
|
||||
import { traverseNode } from '@editor/utils';
|
||||
|
||||
import { updateStatus } from './use-filter';
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
|
||||
const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
||||
const map = new Map<Id, LayerNodeStatus>();
|
||||
@ -15,6 +14,7 @@ const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
||||
visible: true,
|
||||
expand: true,
|
||||
selected: true,
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) =>
|
||||
@ -23,6 +23,7 @@ const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
||||
visible: true,
|
||||
expand: false,
|
||||
selected: false,
|
||||
draggable: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
@ -87,6 +88,7 @@ export const useNodeStatus = (services: Services | undefined, page: ComputedRef<
|
||||
visible: true,
|
||||
expand: Array.isArray(node.items),
|
||||
selected: true,
|
||||
draggable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,79 +1,7 @@
|
||||
.magic-editor-layer-panel {
|
||||
$--node-height: 22px;
|
||||
|
||||
.m-editor-layer-panel {
|
||||
background: $--sidebar-content-background-color;
|
||||
|
||||
.magic-editor-layer-tree {
|
||||
.m-editor-tree {
|
||||
padding-top: 48px;
|
||||
background-color: $--sidebar-content-background-color;
|
||||
color: $--font-color;
|
||||
font-size: 13px;
|
||||
|
||||
.magic-editor-layer-node {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
.layer-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: $--hover-color;
|
||||
color: $--font-color;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $--theme-color;
|
||||
color: $--hover-color;
|
||||
}
|
||||
|
||||
&.drag-inner {
|
||||
.layer-node-content {
|
||||
background-color: rgba($color: $--theme-color, $alpha: 0.5);
|
||||
color: $--hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-before {
|
||||
.layer-node-content {
|
||||
border-top-color: rgba($color: $--theme-color, $alpha: 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-after {
|
||||
.layer-node-content {
|
||||
border-bottom-color: rgba($color: $--theme-color, $alpha: 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
padding: 4px;
|
||||
box-sizing: content-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.layer-node-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
height: $--node-height;
|
||||
border-top: 2px solid transparent;
|
||||
border-bottom: 2px solid transparent;
|
||||
.layer-node-label {
|
||||
line-height: $--node-height;
|
||||
flex: 1;
|
||||
width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-node-tool {
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,3 +22,4 @@
|
||||
@import "./data-source-methods.scss";
|
||||
@import "./data-source-input.scss";
|
||||
@import "./key-value.scss";
|
||||
@import "./tree.scss";
|
||||
|
72
packages/editor/src/theme/tree.scss
Normal file
72
packages/editor/src/theme/tree.scss
Normal file
@ -0,0 +1,72 @@
|
||||
.m-editor-tree {
|
||||
$--node-height: 22px;
|
||||
color: $--font-color;
|
||||
font-size: 13px;
|
||||
|
||||
.m-editor-tree-node {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: $--hover-color;
|
||||
color: $--font-color;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $--theme-color;
|
||||
color: $--hover-color;
|
||||
}
|
||||
|
||||
&.drag-inner {
|
||||
.tree-node-content {
|
||||
background-color: rgba($color: $--theme-color, $alpha: 0.5);
|
||||
color: $--hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-before {
|
||||
.tree-node-content {
|
||||
border-top-color: rgba($color: $--theme-color, $alpha: 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-after {
|
||||
.tree-node-content {
|
||||
border-bottom-color: rgba($color: $--theme-color, $alpha: 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
padding: 4px;
|
||||
box-sizing: content-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
height: $--node-height;
|
||||
border-top: 2px solid transparent;
|
||||
border-bottom: 2px solid transparent;
|
||||
.tree-node-label {
|
||||
line-height: $--node-height;
|
||||
flex: 1;
|
||||
width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node-tool {
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -595,6 +595,8 @@ export interface LayerNodeStatus {
|
||||
expand: boolean;
|
||||
/** 选中 */
|
||||
selected: boolean;
|
||||
/** 是否可拖拽 */
|
||||
draggable: boolean;
|
||||
}
|
||||
|
||||
/** 拖拽类型 */
|
||||
@ -607,3 +609,9 @@ export enum DragType {
|
||||
|
||||
/** 当uiService.get('uiSelectMode')为true,点击组件(包括任何形式,组件树/画布)时触发的事件名 */
|
||||
export const UI_SELECT_MODE_EVENT_NAME = 'ui-select';
|
||||
|
||||
export interface TreeNodeData {
|
||||
id: Id;
|
||||
name?: string;
|
||||
items?: TreeNodeData[];
|
||||
}
|
||||
|
15
packages/editor/src/utils/tree.ts
Normal file
15
packages/editor/src/utils/tree.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { Id } from '@tmagic/schema';
|
||||
import { getKeys } from '@tmagic/utils';
|
||||
|
||||
import type { LayerNodeStatus } from '@editor/type';
|
||||
|
||||
export const updateStatus = (nodeStatusMap: Map<Id, LayerNodeStatus>, id: Id, status: Partial<LayerNodeStatus>) => {
|
||||
const nodeStatus = nodeStatusMap.get(id);
|
||||
|
||||
if (!nodeStatus) return;
|
||||
getKeys(status).forEach((key) => {
|
||||
if (nodeStatus[key] !== undefined && status[key] !== undefined) {
|
||||
nodeStatus[key] = Boolean(status[key]);
|
||||
}
|
||||
});
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user