feat(editor,stage): 优化可选组件交互

This commit is contained in:
roymondchen 2023-12-07 19:40:40 +08:00
parent 5c6a3455b0
commit 258d2bc2ea
15 changed files with 307 additions and 257 deletions

View 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>

View File

@ -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);

View File

@ -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>

View File

@ -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;

View File

@ -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);
}, },
}; };

View File

@ -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,

View File

@ -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', () => {
// mouseleavemousemove
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) => {
// menumouseentermousemove 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;
menu.value?.setPosition({
clientY: menuPosition.top + deltaY,
clientX: menuPosition.left + deltaX,
}); });
};
const closeHandler = () => { watch(visible, async (visible) => {
menu.value?.hide(); if (!button.value || !visible) {
}; return;
}
watch(pinned, () => { await nextTick();
globalThis.localStorage.setItem(PINNED_STATUE_CACHE_KEY, pinned.value.toString());
const rect = button.value.getBoundingClientRect();
const height = box.value?.target?.clientHeight || 0;
menuPosition.value = {
left: rect.left + rect.width + 5,
top: rect.top - height / 2 + rect.height / 2,
};
}); });
</script> </script>

View File

@ -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>

View File

@ -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>

View 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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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]);
}); });
} }
}; };

View File

@ -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);
} }
}; };

View File

@ -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);
}); });
} }