feat(editor): 支持通过左侧组件树进行组件多选

* feat(layerpanel): 支持通过组件树对组件进行多选

* feat(editor): 重构LayerPanel为ts setup语法形式

* feat(editor): 组件列表选中节点与高亮节点时的逻辑优化,两种形态互斥处理

* fix(editor): 修复按住ctrl不放但鼠标移出的layerpanel时选择模式无法复原的问题,修复点击组件树index多选框可进行选择的问题

* fix(editor): 修复多选场景点击组件树节点取消多选时,节点高亮样式冲突的问题

close #404 

Co-authored-by: parisma <parisma@tencent.com>
This commit is contained in:
khuntoriia 2022-10-10 13:57:24 +08:00 committed by GitHub
parent 6ec67438d2
commit e3b7f587ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 246 additions and 156 deletions

View File

@ -1,5 +1,9 @@
<template> <template>
<el-scrollbar class="magic-editor-layer-panel"> <el-scrollbar
class="magic-editor-layer-panel"
@mouseenter="addSelectModeListener"
@mouseleave="removeSelectModeListener"
>
<slot name="layer-panel-header"></slot> <slot name="layer-panel-header"></slot>
<el-input <el-input
@ -17,7 +21,7 @@
node-key="id" node-key="id"
empty-text="页面空荡荡的" empty-text="页面空荡荡的"
draggable draggable
:default-expanded-keys="expandedKeys" :default-expanded-keys="defaultExpandedKeys"
:load="loadItems" :load="loadItems"
:data="values" :data="values"
:expand-on-click-node="false" :expand-on-click-node="false"
@ -27,20 +31,22 @@
}" }"
:filter-node-method="filterNode" :filter-node-method="filterNode"
:allow-drop="allowDrop" :allow-drop="allowDrop"
:show-checkbox="isMultiSelectStatus || selectedIds.length > 1"
@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"
@mousedown="toggleClickFlag"
@mouseup="toggleClickFlag"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<div <div
:id="data.id" :id="data.id"
class="cus-tree-node" class="cus-tree-node"
@mousedown="toggleClickFlag"
@mouseup="toggleClickFlag"
@mouseenter="highlightHandler(data)" @mouseenter="highlightHandler(data)"
:class="{ 'cus-tree-node-hover': canHighlight && data.id === highlightNode?.id }" :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>
@ -57,24 +63,48 @@
</el-scrollbar> </el-scrollbar>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent, inject, Ref, ref, watchEffect } from 'vue'; import { computed, inject, ref, watch } from 'vue';
import type { ElTree } from 'element-plus'; import type { ElTree } from 'element-plus';
import KeyController from 'keycon';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
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 type { EditorService } from '../../services/editor';
import type { Services } from '../../type'; import type { Services } from '../../type';
import { Layout } from '../../type'; import { Layout } from '../../type';
import LayerMenu from './LayerMenu.vue'; import LayerMenu from './LayerMenu.vue';
const throttleTime = 150; const throttleTime = 150;
const services = inject<Services>('services');
const tree = ref<InstanceType<typeof ElTree>>();
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
const selectedIds = computed(() => selectedNodes.value.map((node: MNode) => node.id));
//
const isMultiSelectStatus = ref(false);
// id
const spliceNodeKey = ref<Id>();
const filterText = ref('');
//
const defaultExpandedKeys = computed(() => (selectedIds.value.length > 0 ? selectedIds.value : []));
const select = async (data: MNode, editorService?: EditorService) => { editorService?.on('remove', () => {
setTimeout(() => {
tree.value?.getNode(editorService.get('node').id)?.updateChildren();
}, 0);
});
//
const select = async (data: MNode) => {
if (!data.id) { if (!data.id) {
throw new Error('没有id'); throw new Error('没有id');
} }
@ -83,7 +113,14 @@ const select = async (data: MNode, editorService?: EditorService) => {
editorService?.get<StageCore>('stage')?.select(data.id); editorService?.get<StageCore>('stage')?.select(data.id);
}; };
const highlight = (data: MNode, editorService?: EditorService) => { //
const multiSelect = async (data: Id[]) => {
await editorService?.multiSelect(data);
editorService?.get<StageCore>('stage')?.multiSelect(data);
};
//
const highlight = (data: MNode) => {
if (!data?.id) { if (!data?.id) {
throw new Error('没有id'); throw new Error('没有id');
} }
@ -91,58 +128,97 @@ const highlight = (data: MNode, editorService?: EditorService) => {
editorService?.get<StageCore>('stage')?.highlight(data.id); editorService?.get<StageCore>('stage')?.highlight(data.id);
}; };
const useDrop = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorService?: EditorService) => ({ const expandedKeys = new Map<Id, Id>();
allowDrop: (draggingNode: any, dropNode: any, type: string): boolean => {
const { data } = dropNode || {};
const { data: ingData } = draggingNode;
const { type: ingType } = ingData; // tree
const allowDrop = (draggingNode: any, dropNode: any, type: string): boolean => {
const { data } = dropNode || {};
const { data: ingData } = draggingNode;
if (ingType !== NodeType.PAGE && data.type === NodeType.PAGE) return false; const { type: ingType } = ingData;
if (ingType === NodeType.PAGE && data.type !== NodeType.PAGE) return false;
if (!data || !data.type) return false;
if (['prev', 'next'].includes(type)) return true;
if (data.items || data.type === 'container') return true;
return false; if (ingType !== NodeType.PAGE && data.type === NodeType.PAGE) return false;
}, if (ingType === NodeType.PAGE && data.type !== NodeType.PAGE) return false;
if (!data || !data.type) return false;
if (['prev', 'next'].includes(type)) return true;
if (data.items || data.type === 'container') return true;
async handleDragEnd(e: any) { return false;
};
// tree
const handleDragEnd = async (e: any) => {
if (!tree.value) return;
const { data: node } = e;
const parent = editorService?.getParentById(node.id, false) as MContainer;
const layout = await editorService?.getLayout(parent);
node.style.position = layout;
if (layout === Layout.RELATIVE) {
node.style.top = 0;
node.style.left = 0;
}
const { data } = tree.value;
const [page] = data as [MPage];
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
const handleCollapse = (data: MNode) => {
expandedKeys.delete(data.id);
};
// tree
const handleExpand = (data: MNode) => {
const parent = editorService?.getParentById(data.id);
if (!parent?.id) return;
expandedKeys.set(parent.id, parent.id);
};
// tree
const filterNode = (value: string, data: MNode): boolean => {
if (!value) {
return true;
}
let name = '';
if (data.name) {
name = data.name;
} else if (data.items) {
name = 'container';
}
return `${data.id}${name}${data.type}`.indexOf(value) !== -1;
};
//
const filterTextChangeHandler = (val: string) => {
tree.value?.filter(val);
};
//
const expandNodes = () => {
expandedKeys.forEach((key) => {
if (!tree.value) return; if (!tree.value) return;
const { data: node } = e; tree.value.getNode(key)?.expand();
const parent = editorService?.getParentById(node.id, false) as MContainer; });
const layout = await editorService?.getLayout(parent); };
node.style.position = layout;
if (layout === Layout.RELATIVE) {
node.style.top = 0;
node.style.left = 0;
}
const { data } = tree.value;
const [page] = data as [MPage];
editorService?.update(page);
},
});
const useStatus = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorService?: EditorService) => { watch(
const node = ref<MNode>(); () => editorService?.get('nodes'),
const page = computed(() => editorService?.get('page')); (nodes) => {
const expandedKeys = new Map<Id, Id>();
const expandNodes = () => {
expandedKeys.forEach((key) => {
if (!tree.value) return;
tree.value.getNode(key)?.expand();
});
};
watchEffect(() => {
if (!tree.value) return; if (!tree.value) return;
if (!editorService) return; if (!editorService) return;
node.value = editorService.get('node'); if (!nodes) return;
selectedNodes.value = nodes as unknown as MNode[];
if (!node.value) return;
tree.value.setCurrentKey(node.value.id, true);
const parent = editorService.get('parent'); const parent = editorService.get('parent');
if (!parent?.id) return; if (!parent?.id) return;
@ -159,114 +235,128 @@ const useStatus = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorSer
}); });
expandNodes(); expandNodes();
}); });
}); },
);
return { //
values: computed(() => (page.value ? [page.value] : [])), const setTreeKeyStatus = () => {
if (!tree.value) return;
loadItems: (node: any, resolve: Function) => { if (selectedIds.value.length === 0) {
if (Array.isArray(node.data)) { tree.value.setCheckedKeys([]);
return resolve(node.data); tree.value.setCurrentKey();
} } else if (selectedIds.value.length === 1 && !isMultiSelectStatus.value) {
if (Array.isArray(node.data?.items)) { // 1
return resolve(node.data?.items); tree.value.setCurrentKey(selectedIds.value[0], true);
} tree.value.setCheckedKeys([]);
resolve([]); } else {
}, //
tree.value.setCheckedKeys(selectedIds.value);
highlightNode: computed(() => editorService?.get('highlightNode')), tree.value.setCurrentKey();
clickNode: node, }
expandedKeys: computed(() => (node.value ? [node.value.id] : [])),
handleCollapse: (data: MNode) => {
expandedKeys.delete(data.id);
},
handleExpand: (data: MNode) => {
const parent = editorService?.getParentById(data.id);
if (!parent?.id) return;
expandedKeys.set(parent.id, parent.id);
},
};
}; };
const useFilter = (tree: Ref<InstanceType<typeof ElTree> | undefined>) => ({ watch(selectedIds, () => {
filterText: ref(''), setTreeKeyStatus();
filterNode: (value: string, data: MNode): boolean => {
if (!value) {
return true;
}
let name = '';
if (data.name) {
name = data.name;
} else if (data.items) {
name = 'container';
}
return `${data.id}${name}${data.type}`.indexOf(value) !== -1;
},
filterTextChangeHandler(val: string) {
tree.value?.filter(val);
},
}); });
export default defineComponent({ const keycon = ref<KeyController>();
name: 'magic-editor-layer-panel', //
const addSelectModeListener = () => {
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
const ctrl = isMac ? 'meta' : 'ctrl';
keycon.value = new KeyController();
keycon.value.keydown(ctrl, (e) => {
e.inputEvent.preventDefault();
isMultiSelectStatus.value = true;
});
// ctrl+tabfalse
keycon.value.on('blur', () => {
isMultiSelectStatus.value = false;
});
keycon.value.keyup(ctrl, (e) => {
e.inputEvent.preventDefault();
isMultiSelectStatus.value = false;
});
};
//
const removeSelectModeListener = () => {
keycon.value?.destroy();
// (ctrl)
if (selectedIds.value.length === 1) isMultiSelectStatus.value = false;
};
components: { LayerMenu }, //
const clicked = ref(false);
//
const highlightNode = computed(() => editorService?.get('highlightNode'));
setup() { //
const services = inject<Services>('services'); const highlightHandler = throttle((data: MNode) => {
const tree = ref<InstanceType<typeof ElTree>>(); highlight(data);
const menu = ref<InstanceType<typeof LayerMenu>>(); }, throttleTime);
const clicked = ref(false);
const editorService = services?.editorService;
const highlightHandler = throttle((data: MNode) => {
highlight(data, editorService);
}, throttleTime);
const toggleClickFlag = () => { const toggleClickFlag = () => {
clicked.value = !clicked.value; clicked.value = !clicked.value;
}; };
const statusData = useStatus(tree, editorService); //
const canHighlight = computed( const canHighlight = (data: MNode) => {
() => statusData.highlightNode.value?.id !== statusData.clickNode.value?.id && !clicked.value, if (clicked.value) return false;
); return (
data.id === highlightNode?.value?.id && !selectedIds.value.includes(data.id) && spliceNodeKey.value !== data.id
);
};
editorService?.on('remove', () => { //
setTimeout(() => { watch(isMultiSelectStatus, () => {
tree.value?.getNode(editorService.get('node').id)?.updateChildren(); // (magic-ui-page)
}, 0); if (isMultiSelectStatus.value && selectedNodes.value.length === 1 && selectedNodes.value[0].type === NodeType.PAGE) {
}); selectedNodes.value = [];
}
return {
tree,
menu,
...statusData,
...useDrop(tree, editorService),
...useFilter(tree),
highlightHandler,
toggleClickFlag,
canHighlight,
clickHandler(data: MNode): void {
if (services?.uiService.get<boolean>('uiSelectMode')) {
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
return;
}
tree.value?.setCurrentKey(data.id);
select(data, editorService);
},
async contextmenu(event: MouseEvent, data: MNode) {
event.preventDefault();
await select(data, editorService);
menu.value?.show(event);
},
};
},
}); });
//
const multiClickHandler = (data: MNode): void => {
if (!data?.id) {
throw new Error('没有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 {
selectedNodes.value = [...selectedNodes.value, data];
}
tree.value?.setCheckedKeys(selectedIds.value);
multiSelect(selectedIds.value);
};
//
const clickHandler = (data: MNode): void => {
if (!isMultiSelectStatus.value) {
if (services?.uiService.get<boolean>('uiSelectMode')) {
document.dispatchEvent(new CustomEvent('ui-select', { detail: data }));
return;
}
tree.value?.setCurrentKey(data.id);
select(data);
} else {
multiClickHandler(data);
}
};
//
const contextmenu = async (event: MouseEvent, data: MNode): Promise<void> => {
event.preventDefault();
await select(data);
menu.value?.show(event);
};
</script> </script>

View File

@ -134,6 +134,7 @@ export default class StageCore extends EventEmitter {
this.selectedDomList.push(el); this.selectedDomList.push(el);
} }
this.multiSelect(this.selectedDomList); this.multiSelect(this.selectedDomList);
this.emit('multiSelect', this.selectedDomList);
}); });
// 要先触发select在触发update // 要先触发select在触发update
@ -238,9 +239,8 @@ export default class StageCore extends EventEmitter {
*/ */
public async multiSelect(idOrElList: HTMLElement[] | Id[]): Promise<void> { public async multiSelect(idOrElList: HTMLElement[] | Id[]): Promise<void> {
this.clearSelectStatus('select'); this.clearSelectStatus('select');
const elList = await Promise.all(idOrElList.map(async (idOrEl) => await this.getTargetElement(idOrEl))); this.selectedDomList = await Promise.all(idOrElList.map(async (idOrEl) => await this.getTargetElement(idOrEl)));
this.multiDr.multiSelect(elList); this.multiDr.multiSelect(this.selectedDomList);
this.emit('multiSelect', elList);
} }
/** /**