mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-24 00:40:05 +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>
|
<template>
|
||||||
<TMagicScrollbar class="magic-editor-layer-panel">
|
<TMagicScrollbar class="m-editor-layer-panel">
|
||||||
<slot name="layer-panel-header"></slot>
|
<slot name="layer-panel-header"></slot>
|
||||||
|
|
||||||
<SearchInput @search="filterTextChangeHandler"></SearchInput>
|
<SearchInput @search="filterTextChangeHandler"></SearchInput>
|
||||||
|
|
||||||
<div class="magic-editor-layer-tree" ref="layerTreeContainer" tabindex="-1" @dragover="handleDragOver">
|
<Tree
|
||||||
<LayerNode
|
v-if="page && nodeStatusMap"
|
||||||
v-if="page && root"
|
tabindex="-1"
|
||||||
:data="page"
|
ref="tree"
|
||||||
:filter-text="filterText"
|
:data="[page]"
|
||||||
:is-ctrl-key-down="isCtrlKeyDown"
|
:node-status-map="nodeStatusMap"
|
||||||
@node-contextmenu="contextmenu"
|
@node-dragover="handleDragOver"
|
||||||
>
|
@node-dragstart="handleDragStart"
|
||||||
<template #layer-node-content="{ data: nodeData }">
|
@node-dragleave="handleDragLeave"
|
||||||
<slot name="layer-node-content" :data="nodeData"> </slot>
|
@node-dragend="handleDragEnd"
|
||||||
</template>
|
@node-contextmenu="nodeContentmenuHandler"
|
||||||
</LayerNode>
|
@node-mouseenter="mouseenterHandler"
|
||||||
</div>
|
@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">
|
<Teleport to="body">
|
||||||
<LayerMenu ref="menu" :layer-content-menu="layerContentMenu" @collapse-all="collapseAllHandler"></LayerMenu>
|
<LayerMenu ref="menu" :layer-content-menu="layerContentMenu" @collapse-all="collapseAllHandler"></LayerMenu>
|
||||||
@ -25,15 +34,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, provide, ref } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
|
|
||||||
import { TMagicScrollbar } from '@tmagic/design';
|
import { TMagicScrollbar } from '@tmagic/design';
|
||||||
|
|
||||||
import SearchInput from '@editor/components/SearchInput.vue';
|
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 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 { useDrag } from './use-drag';
|
||||||
import { useFilter } from './use-filter';
|
import { useFilter } from './use-filter';
|
||||||
import { useKeybinding } from './use-keybinding';
|
import { useKeybinding } from './use-keybinding';
|
||||||
@ -52,17 +63,14 @@ defineProps<{
|
|||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
const editorService = services?.editorService;
|
const editorService = services?.editorService;
|
||||||
|
|
||||||
const layerTreeContainer = ref<HTMLDivElement>();
|
const tree = ref<InstanceType<typeof Tree>>();
|
||||||
|
|
||||||
const page = computed(() => editorService?.get('page'));
|
const page = computed(() => editorService?.get('page'));
|
||||||
const root = computed(() => editorService?.get('root'));
|
|
||||||
|
|
||||||
const { nodeStatusMap } = useNodeStatus(services, page);
|
const { nodeStatusMap } = useNodeStatus(services, page);
|
||||||
const { isCtrlKeyDown } = useKeybinding(services, layerTreeContainer);
|
const { isCtrlKeyDown } = useKeybinding(services, tree);
|
||||||
|
|
||||||
provide('nodeStatusMap', nodeStatusMap);
|
const { filterTextChangeHandler } = useFilter(nodeStatusMap, page);
|
||||||
|
|
||||||
const { filterText, filterTextChangeHandler } = useFilter(nodeStatusMap, page);
|
|
||||||
|
|
||||||
const collapseAllHandler = () => {
|
const collapseAllHandler = () => {
|
||||||
if (!page.value || !nodeStatusMap.value) return;
|
if (!page.value || !nodeStatusMap.value) return;
|
||||||
@ -75,13 +83,12 @@ const collapseAllHandler = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 右键菜单
|
const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useDrag(services);
|
||||||
const menu = ref<InstanceType<typeof LayerMenu>>();
|
|
||||||
const contextmenu = (event: MouseEvent): void => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
menu.value?.show(event);
|
const {
|
||||||
};
|
menu,
|
||||||
|
nodeClickHandler,
|
||||||
const { handleDragOver } = useDrag(services);
|
nodeContentmenuHandler,
|
||||||
|
highlightHandler: mouseenterHandler,
|
||||||
|
} = useClick(services, isCtrlKeyDown, nodeStatusMap);
|
||||||
</script>
|
</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 { type ComputedRef, ref } from 'vue';
|
||||||
|
|
||||||
import { Id, MNode, MPage } from '@tmagic/schema';
|
import { Id, MNode, MPage } from '@tmagic/schema';
|
||||||
import { getKeys } from '@tmagic/utils';
|
|
||||||
|
|
||||||
import { LayerNodeStatus } from '@editor/type';
|
import { LayerNodeStatus } from '@editor/type';
|
||||||
import { traverseNode } from '@editor/utils';
|
import { traverseNode } from '@editor/utils';
|
||||||
|
import { updateStatus } from '@editor/utils/tree';
|
||||||
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]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFilter = (
|
export const useFilter = (
|
||||||
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
|
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { type Ref, ref, watchEffect } from 'vue';
|
import { type Ref, ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
import Tree from '@editor/components/Tree.vue';
|
||||||
import type { Services } from '@editor/type';
|
import type { Services } from '@editor/type';
|
||||||
import { KeyBindingContainerKey } from '@editor/utils/keybinding-config';
|
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;
|
const keybindingService = services?.keybindingService;
|
||||||
|
|
||||||
// 是否多选
|
// 是否多选
|
||||||
@ -37,7 +41,7 @@ export const useKeybinding = (services: Services | undefined, contianer: Ref<HTM
|
|||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (contianer.value) {
|
if (contianer.value) {
|
||||||
globalThis.addEventListener('blur', windowBlurHandler);
|
globalThis.addEventListener('blur', windowBlurHandler);
|
||||||
keybindingService?.registeEl(KeyBindingContainerKey.LAYER_PANEL, contianer.value);
|
keybindingService?.registeEl(KeyBindingContainerKey.LAYER_PANEL, contianer.value.$el);
|
||||||
} else {
|
} else {
|
||||||
globalThis.removeEventListener('blur', windowBlurHandler);
|
globalThis.removeEventListener('blur', windowBlurHandler);
|
||||||
keybindingService?.unregisteEl(KeyBindingContainerKey.LAYER_PANEL);
|
keybindingService?.unregisteEl(KeyBindingContainerKey.LAYER_PANEL);
|
||||||
|
@ -5,8 +5,7 @@ import { getNodePath } from '@tmagic/utils';
|
|||||||
|
|
||||||
import { LayerNodeStatus, Services } 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 './use-filter';
|
|
||||||
|
|
||||||
const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
||||||
const map = new Map<Id, LayerNodeStatus>();
|
const map = new Map<Id, LayerNodeStatus>();
|
||||||
@ -15,6 +14,7 @@ const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
|||||||
visible: true,
|
visible: true,
|
||||||
expand: true,
|
expand: true,
|
||||||
selected: true,
|
selected: true,
|
||||||
|
draggable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) =>
|
services?.editorService.getNodeById(pageId)?.items.forEach((node: MNode) =>
|
||||||
@ -23,6 +23,7 @@ const createPageNodeStatus = (services: Services | undefined, pageId: Id) => {
|
|||||||
visible: true,
|
visible: true,
|
||||||
expand: false,
|
expand: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
draggable: true,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -87,6 +88,7 @@ export const useNodeStatus = (services: Services | undefined, page: ComputedRef<
|
|||||||
visible: true,
|
visible: true,
|
||||||
expand: Array.isArray(node.items),
|
expand: Array.isArray(node.items),
|
||||||
selected: true,
|
selected: true,
|
||||||
|
draggable: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,79 +1,7 @@
|
|||||||
.magic-editor-layer-panel {
|
.m-editor-layer-panel {
|
||||||
$--node-height: 22px;
|
|
||||||
|
|
||||||
background: $--sidebar-content-background-color;
|
background: $--sidebar-content-background-color;
|
||||||
|
|
||||||
.magic-editor-layer-tree {
|
.m-editor-tree {
|
||||||
padding-top: 48px;
|
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-methods.scss";
|
||||||
@import "./data-source-input.scss";
|
@import "./data-source-input.scss";
|
||||||
@import "./key-value.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;
|
expand: boolean;
|
||||||
/** 选中 */
|
/** 选中 */
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
/** 是否可拖拽 */
|
||||||
|
draggable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拖拽类型 */
|
/** 拖拽类型 */
|
||||||
@ -607,3 +609,9 @@ export enum DragType {
|
|||||||
|
|
||||||
/** 当uiService.get('uiSelectMode')为true,点击组件(包括任何形式,组件树/画布)时触发的事件名 */
|
/** 当uiService.get('uiSelectMode')为true,点击组件(包括任何形式,组件树/画布)时触发的事件名 */
|
||||||
export const UI_SELECT_MODE_EVENT_NAME = 'ui-select';
|
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