refactor(editor): 优化组件树逻辑

This commit is contained in:
roymondchen 2022-12-21 16:19:08 +08:00
parent 87a19c9bae
commit 3934ebd030

View File

@ -14,42 +14,33 @@
<TMagicTree <TMagicTree
v-if="values.length" v-if="values.length"
tabindex="-1"
class="magic-editor-layer-tree" class="magic-editor-layer-tree"
ref="tree" ref="tree"
node-key="id" node-key="id"
empty-text="页面空荡荡的" empty-text="页面空荡荡的"
draggable draggable
:default-expanded-keys="defaultExpandedKeys" :default-expanded-keys="expandedKeys"
:load="loadItems" :default-checked-keys="checkedKeys"
:current-node-key="currentNodeKey"
:data="values" :data="values"
:default-expand-all="true"
:expand-on-click-node="false" :expand-on-click-node="false"
:highlight-current="true" :highlight-current="!isMultiSelect"
:props="{ :check-on-click-node="true"
children: 'items', :props="treeProps"
}"
:filter-node-method="filterNode" :filter-node-method="filterNode"
:allow-drop="allowDrop" :allow-drop="allowDrop"
:show-checkbox="isMultiSelectStatus || selectedIds.length > 1" :show-checkbox="isMultiSelect"
@node-click="clickHandler" @node-click="clickHandler"
@node-contextmenu="contextmenu" @node-contextmenu="contextmenu"
@node-drag-end="handleDragEnd" @node-drag-end="handleDragEnd"
@node-collapse="handleCollapse" @node-collapse="handleCollapse"
@node-expand="handleExpand" @node-expand="handleExpand"
@check="multiClickHandler" @check="checkHandler"
@mousedown="toggleClickFlag" @mousedown="toggleClickFlag"
@mouseup="toggleClickFlag" @mouseup="toggleClickFlag"
@mouseenter="mouseenterHandler"
@mouseleave="mouseleaveHandler"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<div <div class="cus-tree-node" :id="data.id" @mouseenter="highlightHandler(data)">
:id="data.id"
class="cus-tree-node"
@mouseenter="highlightHandler(data)"
:class="{ 'cus-tree-node-hover': canHighlight(data) }"
>
<slot name="layer-node-content" :node="node" :data="data"> <slot name="layer-node-content" :node="node" :data="data">
<span> <span>
{{ `${data.name} (${data.id})` }} {{ `${data.name} (${data.id})` }}
@ -66,15 +57,16 @@
</template> </template>
<script lang="ts" setup name="MEditorLayerPanel"> <script lang="ts" setup name="MEditorLayerPanel">
import { computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue';
import { Search } from '@element-plus/icons-vue'; import { Search } from '@element-plus/icons-vue';
import KeyController from 'keycon'; import KeyController from 'keycon';
import { throttle } from 'lodash-es'; import { difference, throttle, union } from 'lodash-es';
import { TMagicInput, TMagicScrollbar, TMagicTree } from '@tmagic/design'; import { TMagicInput, TMagicScrollbar, TMagicTree } from '@tmagic/design';
import type { Id, MNode, MPage } from '@tmagic/schema'; import type { Id, MNode, MPage } from '@tmagic/schema';
import { MContainer, NodeType } from '@tmagic/schema'; import { MContainer, NodeType } from '@tmagic/schema';
import StageCore from '@tmagic/stage'; import StageCore from '@tmagic/stage';
import { getNodePath, isPage } from '@tmagic/utils';
import type { MenuButton, MenuComponent, Services } from '../../type'; import type { MenuButton, MenuComponent, Services } from '../../type';
import { Layout } from '../../type'; import { Layout } from '../../type';
@ -87,22 +79,40 @@ defineProps<{
const throttleTime = 150; const throttleTime = 150;
const services = inject<Services>('services'); const services = inject<Services>('services');
const editorService = services?.editorService;
const tree = ref<InstanceType<typeof TMagicTree>>(); const tree = ref<InstanceType<typeof TMagicTree>>();
const menu = ref<InstanceType<typeof LayerMenu>>(); const menu = ref<InstanceType<typeof LayerMenu>>();
const editorService = services?.editorService;
const page = computed(() => editorService?.get('page'));
const values = computed(() => (page.value ? [page.value] : []));
//
const selectedNodes = ref<MNode[]>([]);
// id // id
const selectedIds = computed(() => selectedNodes.value.map((node: MNode) => node.id)); const checkedKeys = ref<Id[]>([]);
// //
const isMultiSelectStatus = ref(false); const isCtrlKeyDown = ref(false);
// id
const spliceNodeKey = ref<Id>();
const filterText = ref(''); const filterText = ref('');
// //
const defaultExpandedKeys = computed(() => (selectedIds.value.length > 0 ? selectedIds.value : [])); const expandedKeys = ref<Id[]>([]);
const currentNodeKey = ref<Id>();
//
const clicked = ref(false);
const treeProps = {
children: 'items',
disabled: (data: MNode) => Boolean(data.items?.length),
class: (data: MNode) => {
if (clicked.value || isPage(data)) return '';
if (data.id === highlightNode?.value?.id && !checkedKeys.value.includes(data.id)) {
return 'cus-tree-node-hover';
}
},
};
const isMultiSelect = computed(() => isCtrlKeyDown.value || checkedKeys.value.length > 1);
const nodes = computed(() => editorService?.get<MNode[]>('nodes') || []);
const page = computed(() => editorService?.get<MPage>('page'));
const values = computed(() => (page.value ? [page.value] : []));
//
const highlightNode = computed(() => editorService?.get('highlightNode'));
// //
const select = async (data: MNode) => { const select = async (data: MNode) => {
@ -129,8 +139,6 @@ const highlight = (data: MNode) => {
editorService?.get<StageCore>('stage')?.highlight(data.id); editorService?.get<StageCore>('stage')?.highlight(data.id);
}; };
const expandedKeys = new Map<Id, Id>();
// tree // tree
const allowDrop = (draggingNode: any, dropNode: any, type: string): boolean => { const allowDrop = (draggingNode: any, dropNode: any, type: string): boolean => {
const { data } = dropNode || {}; const { data } = dropNode || {};
@ -163,27 +171,22 @@ const handleDragEnd = async (e: any) => {
editorService?.update(page); editorService?.update(page);
}; };
// tree
const loadItems = (node: any, resolve: Function) => {
if (Array.isArray(node.data)) {
return resolve(node.data);
}
if (Array.isArray(node.data?.items)) {
return resolve(node.data?.items);
}
resolve([]);
};
// tree // tree
const handleCollapse = (data: MNode) => { const handleCollapse = (data: MNode) => {
expandedKeys.delete(data.id); expandedKeys.value = expandedKeys.value.filter((id) => id !== data.id);
}; };
// tree // tree
const handleExpand = (data: MNode) => { const handleExpand = (data: MNode) => {
const parent = editorService?.getParentById(data.id); if (!page.value) {
if (!parent?.id) return; expandedKeys.value = [];
expandedKeys.set(parent.id, parent.id); return;
}
expandedKeys.value = union(
expandedKeys.value,
getNodePath(data.id, [page.value]).map((node) => node.id),
);
}; };
// tree // tree
@ -205,67 +208,32 @@ const filterTextChangeHandler = (val: string) => {
tree.value?.filter(val); tree.value?.filter(val);
}; };
// watch(nodes, (nodes) => {
const expandNodes = async () => { const ids = nodes?.map((node) => node.id) || [];
if (!tree.value) return;
await nextTick();
tree.value &&
Object.entries(tree.value.getStore().nodesMap).forEach(([id, node]: [string, any]) => {
if (node.expanded && node.data.items) {
expandedKeys.set(id, id);
}
});
expandedKeys.forEach((key) => {
if (!tree.value) return;
tree.value.getNode(key)?.expand();
});
};
watch( const idsLength = ids.length;
() => editorService?.get<MNode[]>('nodes'), const checkedKeysLength = checkedKeys.value.length;
(nodes) => {
selectedNodes.value = nodes ?? [];
},
);
// if (
const setTreeKeyStatus = () => { difference(
if (!tree.value) return; idsLength > checkedKeysLength ? ids : checkedKeys.value,
if (selectedIds.value.length === 0) { idsLength > checkedKeysLength ? checkedKeys.value : ids,
tree.value.setCheckedKeys([]); ).length
tree.value.setCurrentKey(); ) {
} else if (selectedIds.value.length === 1 && !isMultiSelectStatus.value) { tree.value?.setCheckedKeys([], false);
// 1
tree.value.setCurrentKey(selectedIds.value[0], true); [currentNodeKey.value] = ids;
tree.value.setCheckedKeys([]); checkedKeys.value = ids.filter((id) => id !== page.value?.id);
} else { expandedKeys.value = union(expandedKeys.value, ids);
//
tree.value.setCheckedKeys(selectedIds.value);
tree.value.setCurrentKey();
} }
};
watch([selectedIds, tree], async () => {
if (!tree.value || !editorService) return;
const parent = editorService.get('parent');
if (!parent?.id) return;
const treeNode = tree.value.getNode(parent.id);
treeNode?.updateChildren();
await expandNodes();
//
setTreeKeyStatus();
}); });
const mouseenterHandler = () => { watch(isMultiSelect, (isMultiSelect) => {
tree.value?.$el.focus(); if (!isMultiSelect) {
}; currentNodeKey.value = editorService?.get<MNode>('node').id;
tree.value?.setCurrentKey(currentNodeKey.value);
const mouseleaveHandler = () => { }
tree.value?.$el.blur(); });
isMultiSelectStatus.value = false;
};
const editorServiceRemoveHandler = () => { const editorServiceRemoveHandler = () => {
setTimeout(() => { setTimeout(() => {
@ -273,37 +241,42 @@ const editorServiceRemoveHandler = () => {
}, 0); }, 0);
}; };
const windowBlurHandler = () => {
isCtrlKeyDown.value = false;
};
let keycon: KeyController; let keycon: KeyController;
onMounted(() => { onMounted(() => {
editorService?.on('remove', editorServiceRemoveHandler); editorService?.on('remove', editorServiceRemoveHandler);
keycon = new KeyController(tree.value?.$el); keycon = new KeyController();
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase()); const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
const ctrl = isMac ? 'meta' : 'ctrl'; const ctrl = isMac ? 'meta' : 'ctrl';
keycon keycon
.keydown(ctrl, (e) => { .keydown((e) => {
e.inputEvent.preventDefault(); if (e.key !== ctrl) {
isMultiSelectStatus.value = true; isCtrlKeyDown.value = false;
}
}) })
.keyup(ctrl, (e) => { .keydown(ctrl, () => {
e.inputEvent.preventDefault(); isCtrlKeyDown.value = true;
isMultiSelectStatus.value = false; })
.keyup(ctrl, () => {
isCtrlKeyDown.value = false;
}); });
globalThis.addEventListener('blur', windowBlurHandler);
}); });
onUnmounted(() => { onUnmounted(() => {
keycon.destroy(); keycon.destroy();
editorService?.off('remove', editorServiceRemoveHandler); editorService?.off('remove', editorServiceRemoveHandler);
globalThis.removeEventListener('blur', windowBlurHandler);
}); });
//
const clicked = ref(false);
//
const highlightNode = computed(() => editorService?.get('highlightNode'));
// //
const highlightHandler = throttle((data: MNode) => { const highlightHandler = throttle((data: MNode) => {
highlight(data); highlight(data);
@ -313,58 +286,25 @@ const toggleClickFlag = () => {
clicked.value = !clicked.value; clicked.value = !clicked.value;
}; };
//
const canHighlight = (data: MNode) => {
if (clicked.value) return false;
return (
data.id === highlightNode?.value?.id && !selectedIds.value.includes(data.id) && spliceNodeKey.value !== data.id
);
};
//
watch(isMultiSelectStatus, () => {
// (magic-ui-page)
if (isMultiSelectStatus.value && selectedNodes.value.length === 1 && selectedNodes.value[0].type === NodeType.PAGE) {
selectedNodes.value = [];
}
});
// //
const multiClickHandler = (data: MNode): void => { const checkHandler = (data: MNode, { checkedNodes }: any): void => {
if (!data?.id) { if (checkedNodes.length > 0) {
throw new Error('没有id'); multiSelect(checkedNodes.map((node: MNode) => node.id));
}
// (magic-ui-page)
if (data.type === NodeType.PAGE) {
tree.value?.setCheckedKeys([]);
return;
}
const index = selectedNodes.value.findIndex((node) => node.id === data.id);
if (index !== -1) {
//
selectedNodes.value.splice(index, 1);
spliceNodeKey.value = data.id;
} else { } else {
selectedNodes.value = [...selectedNodes.value, data]; multiSelect(nodes.value.map((node: MNode) => node.id));
} }
tree.value?.setCheckedKeys(selectedIds.value);
multiSelect(selectedIds.value);
}; };
// //
const clickHandler = (data: MNode): void => { const clickHandler = (data: MNode): void => {
if (!isMultiSelectStatus.value) { if (isCtrlKeyDown.value) {
if (services?.uiService.get<boolean>('uiSelectMode')) { return;
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
return;
}
tree.value?.setCurrentKey(data.id);
select(data);
} else {
multiClickHandler(data);
} }
if (services?.uiService.get<boolean>('uiSelectMode')) {
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
return;
}
select(data);
}; };
// //