refactor: menu
parent
24efefe16d
commit
8862f604e2
|
@ -10,6 +10,7 @@ import {
|
||||||
watch,
|
watch,
|
||||||
reactive,
|
reactive,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
toRaw,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import shallowEqual from '../../_util/shallowequal';
|
import shallowEqual from '../../_util/shallowequal';
|
||||||
import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext';
|
import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext';
|
||||||
|
@ -24,6 +25,7 @@ import {
|
||||||
} from './interface';
|
} from './interface';
|
||||||
import devWarning from 'ant-design-vue/es/vc-util/devWarning';
|
import devWarning from 'ant-design-vue/es/vc-util/devWarning';
|
||||||
import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition';
|
import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition';
|
||||||
|
import uniq from 'lodash-es/uniq';
|
||||||
|
|
||||||
export const menuProps = {
|
export const menuProps = {
|
||||||
prefixCls: String,
|
prefixCls: String,
|
||||||
|
@ -32,8 +34,8 @@ export const menuProps = {
|
||||||
overflowDisabled: Boolean,
|
overflowDisabled: Boolean,
|
||||||
openKeys: Array,
|
openKeys: Array,
|
||||||
selectedKeys: Array,
|
selectedKeys: Array,
|
||||||
selectable: Boolean,
|
selectable: { type: Boolean, default: true },
|
||||||
multiple: Boolean,
|
multiple: { type: Boolean, default: false },
|
||||||
|
|
||||||
motion: Object as PropType<CSSMotionProps>,
|
motion: Object as PropType<CSSMotionProps>,
|
||||||
|
|
||||||
|
@ -56,7 +58,7 @@ export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'AMenu',
|
name: 'AMenu',
|
||||||
props: menuProps,
|
props: menuProps,
|
||||||
emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys'],
|
emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys', 'click'],
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
const { prefixCls, direction } = useConfigInject('menu', props);
|
const { prefixCls, direction } = useConfigInject('menu', props);
|
||||||
const store = reactive<Record<string, StoreMenuInfo>>({});
|
const store = reactive<Record<string, StoreMenuInfo>>({});
|
||||||
|
@ -78,13 +80,13 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
devWarning(
|
devWarning(
|
||||||
!('inlineCollapsed' in props && props.mode !== 'inline'),
|
!(props.inlineCollapsed === true && props.mode !== 'inline'),
|
||||||
'Menu',
|
'Menu',
|
||||||
'`inlineCollapsed` should only be used when `mode` is inline.',
|
'`inlineCollapsed` should only be used when `mode` is inline.',
|
||||||
);
|
);
|
||||||
|
|
||||||
devWarning(
|
devWarning(
|
||||||
!(siderCollapsed.value !== undefined && 'inlineCollapsed' in props),
|
!(siderCollapsed.value !== undefined && props.inlineCollapsed === true),
|
||||||
'Menu',
|
'Menu',
|
||||||
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
|
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
|
||||||
);
|
);
|
||||||
|
@ -101,18 +103,37 @@ export default defineComponent({
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectedSubMenuEventKeys = ref([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[store, mergedSelectedKeys],
|
||||||
|
() => {
|
||||||
|
let subMenuParentEventKeys = [];
|
||||||
|
(Object.values(toRaw(store)) as any).forEach((menuInfo: StoreMenuInfo) => {
|
||||||
|
if (mergedSelectedKeys.value.includes(menuInfo.key)) {
|
||||||
|
subMenuParentEventKeys.push(...menuInfo.parentEventKeys.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
subMenuParentEventKeys = uniq(subMenuParentEventKeys);
|
||||||
|
if (!shallowEqual(selectedSubMenuEventKeys.value, subMenuParentEventKeys)) {
|
||||||
|
selectedSubMenuEventKeys.value = subMenuParentEventKeys;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
// >>>>> Trigger select
|
// >>>>> Trigger select
|
||||||
const triggerSelection = (info: MenuInfo) => {
|
const triggerSelection = (info: MenuInfo) => {
|
||||||
if (!props.selectable) {
|
if (!props.selectable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert or Remove
|
// Insert or Remove
|
||||||
const { key: targetKey } = info;
|
const { key: targetKey } = info;
|
||||||
const exist = mergedSelectedKeys.value.includes(targetKey);
|
const exist = mergedSelectedKeys.value.includes(targetKey);
|
||||||
let newSelectedKeys: Key[];
|
let newSelectedKeys: Key[];
|
||||||
|
|
||||||
if (exist) {
|
if (exist && props.multiple) {
|
||||||
newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey);
|
newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey);
|
||||||
} else if (props.multiple) {
|
} else if (props.multiple) {
|
||||||
newSelectedKeys = [...mergedSelectedKeys.value, targetKey];
|
newSelectedKeys = [...mergedSelectedKeys.value, targetKey];
|
||||||
|
@ -120,17 +141,21 @@ export default defineComponent({
|
||||||
newSelectedKeys = [targetKey];
|
newSelectedKeys = [targetKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
mergedSelectedKeys.value = newSelectedKeys;
|
|
||||||
// Trigger event
|
// Trigger event
|
||||||
const selectInfo: SelectInfo = {
|
const selectInfo: SelectInfo = {
|
||||||
...info,
|
...info,
|
||||||
selectedKeys: newSelectedKeys,
|
selectedKeys: newSelectedKeys,
|
||||||
};
|
};
|
||||||
|
if (!('selectedKeys' in props)) {
|
||||||
if (exist) {
|
mergedSelectedKeys.value = newSelectedKeys;
|
||||||
emit('deselect', selectInfo);
|
}
|
||||||
} else {
|
if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) {
|
||||||
emit('select', selectInfo);
|
emit('update:selectedKeys', newSelectedKeys);
|
||||||
|
if (exist && props.multiple) {
|
||||||
|
emit('deselect', selectInfo);
|
||||||
|
} else {
|
||||||
|
emit('select', selectInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -212,7 +237,7 @@ export default defineComponent({
|
||||||
|
|
||||||
useProvideFirstLevel(true);
|
useProvideFirstLevel(true);
|
||||||
|
|
||||||
const getChildrenKeys = (eventKeys: string[]): Key[] => {
|
const getChildrenKeys = (eventKeys: string[] = []): Key[] => {
|
||||||
const keys = [];
|
const keys = [];
|
||||||
eventKeys.forEach(eventKey => {
|
eventKeys.forEach(eventKey => {
|
||||||
const { key, childrenEventKeys } = store[eventKey] as any;
|
const { key, childrenEventKeys } = store[eventKey] as any;
|
||||||
|
@ -221,6 +246,15 @@ export default defineComponent({
|
||||||
return keys;
|
return keys;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========================= Open =========================
|
||||||
|
/**
|
||||||
|
* Click for item. SubMenu do not have selection status
|
||||||
|
*/
|
||||||
|
const onInternalClick = (info: MenuInfo) => {
|
||||||
|
emit('click', info);
|
||||||
|
triggerSelection(info);
|
||||||
|
};
|
||||||
|
|
||||||
const onInternalOpenChange = (eventKey: Key, open: boolean) => {
|
const onInternalOpenChange = (eventKey: Key, open: boolean) => {
|
||||||
const { key, childrenEventKeys } = store[eventKey] as any;
|
const { key, childrenEventKeys } = store[eventKey] as any;
|
||||||
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
|
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
|
||||||
|
@ -270,9 +304,10 @@ export default defineComponent({
|
||||||
motion: computed(() => (isMounted.value ? props.motion : null)),
|
motion: computed(() => (isMounted.value ? props.motion : null)),
|
||||||
overflowDisabled: computed(() => props.overflowDisabled),
|
overflowDisabled: computed(() => props.overflowDisabled),
|
||||||
onOpenChange: onInternalOpenChange,
|
onOpenChange: onInternalOpenChange,
|
||||||
onItemClick: triggerSelection,
|
onItemClick: onInternalClick,
|
||||||
registerMenuInfo,
|
registerMenuInfo,
|
||||||
unRegisterMenuInfo,
|
unRegisterMenuInfo,
|
||||||
|
selectedSubMenuEventKeys,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
return <ul class={className.value}>{slots.default?.()}</ul>;
|
return <ul class={className.value}>{slots.default?.()}</ul>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
|
import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
|
||||||
import PropTypes from '../../_util/vue-types';
|
import PropTypes from '../../_util/vue-types';
|
||||||
import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue';
|
import { computed, defineComponent, getCurrentInstance, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useInjectKeyPath } from './hooks/useKeyPath';
|
import { useInjectKeyPath } from './hooks/useKeyPath';
|
||||||
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
|
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
|
||||||
import { cloneElement } from '../../_util/vnode';
|
import { cloneElement } from '../../_util/vnode';
|
||||||
|
@ -26,7 +26,6 @@ export default defineComponent({
|
||||||
const key = instance.vnode.key;
|
const key = instance.vnode.key;
|
||||||
const eventKey = `menu_item_${++indexGuid}_$$_${key}`;
|
const eventKey = `menu_item_${++indexGuid}_$$_${key}`;
|
||||||
const { parentEventKeys } = useInjectKeyPath();
|
const { parentEventKeys } = useInjectKeyPath();
|
||||||
console.log(parentEventKeys.value);
|
|
||||||
const {
|
const {
|
||||||
prefixCls,
|
prefixCls,
|
||||||
activeKeys,
|
activeKeys,
|
||||||
|
@ -36,9 +35,30 @@ export default defineComponent({
|
||||||
inlineCollapsed,
|
inlineCollapsed,
|
||||||
siderCollapsed,
|
siderCollapsed,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
|
selectedKeys,
|
||||||
|
store,
|
||||||
|
registerMenuInfo,
|
||||||
|
unRegisterMenuInfo,
|
||||||
} = useInjectMenu();
|
} = useInjectMenu();
|
||||||
const firstLevel = useInjectFirstLevel();
|
const firstLevel = useInjectFirstLevel();
|
||||||
const isActive = ref(false);
|
const isActive = ref(false);
|
||||||
|
const keyPath = computed(() => {
|
||||||
|
return [...parentEventKeys.value.map(eK => store[eK].key), key];
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuInfo = {
|
||||||
|
eventKey,
|
||||||
|
key,
|
||||||
|
parentEventKeys,
|
||||||
|
isLeaf: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
registerMenuInfo(eventKey, menuInfo);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unRegisterMenuInfo(eventKey);
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
activeKeys,
|
activeKeys,
|
||||||
() => {
|
() => {
|
||||||
|
@ -47,7 +67,7 @@ export default defineComponent({
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
const mergedDisabled = computed(() => disabled.value || props.disabled);
|
const mergedDisabled = computed(() => disabled.value || props.disabled);
|
||||||
const selected = computed(() => false);
|
const selected = computed(() => selectedKeys.value.includes(key));
|
||||||
const classNames = computed(() => {
|
const classNames = computed(() => {
|
||||||
const itemCls = `${prefixCls.value}-item`;
|
const itemCls = `${prefixCls.value}-item`;
|
||||||
return {
|
return {
|
||||||
|
@ -63,7 +83,8 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
eventKey: eventKey,
|
eventKey: eventKey,
|
||||||
eventKeyPath: [...parentEventKeys.value, key],
|
keyPath: keyPath.value,
|
||||||
|
eventKeyPath: [...parentEventKeys.value, eventKey],
|
||||||
domEvent: e,
|
domEvent: e,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -74,6 +74,7 @@ export default defineComponent({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
registerMenuInfo,
|
registerMenuInfo,
|
||||||
unRegisterMenuInfo,
|
unRegisterMenuInfo,
|
||||||
|
selectedSubMenuEventKeys,
|
||||||
} = useInjectMenu();
|
} = useInjectMenu();
|
||||||
|
|
||||||
registerMenuInfo(eventKey, menuInfo);
|
registerMenuInfo(eventKey, menuInfo);
|
||||||
|
@ -96,7 +97,9 @@ export default defineComponent({
|
||||||
const open = computed(() => !overflowDisabled.value && originOpen.value);
|
const open = computed(() => !overflowDisabled.value && originOpen.value);
|
||||||
|
|
||||||
// =============================== Select ===============================
|
// =============================== Select ===============================
|
||||||
const childrenSelected = ref(true); // isSubPathKey(selectedKeys, eventKey);
|
const childrenSelected = computed(() => {
|
||||||
|
return selectedSubMenuEventKeys.value.includes(eventKey);
|
||||||
|
});
|
||||||
|
|
||||||
const isActive = ref(false);
|
const isActive = ref(false);
|
||||||
watch(
|
watch(
|
||||||
|
@ -225,8 +228,6 @@ export default defineComponent({
|
||||||
if (!overflowDisabled.value) {
|
if (!overflowDisabled.value) {
|
||||||
const triggerMode = triggerModeRef.value;
|
const triggerMode = triggerModeRef.value;
|
||||||
|
|
||||||
// Still wrap with Trigger here since we need avoid react re-mount dom node
|
|
||||||
// Which makes motion failed
|
|
||||||
titleNode = (
|
titleNode = (
|
||||||
<PopupTrigger
|
<PopupTrigger
|
||||||
mode={triggerMode}
|
mode={triggerMode}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export interface StoreMenuInfo {
|
||||||
eventKey: string;
|
eventKey: string;
|
||||||
key: Key;
|
key: Key;
|
||||||
parentEventKeys: ComputedRef<Key[]>;
|
parentEventKeys: ComputedRef<Key[]>;
|
||||||
childrenEventKeys: Ref<Key[]>;
|
childrenEventKeys?: Ref<Key[]>;
|
||||||
isLeaf?: boolean;
|
isLeaf?: boolean;
|
||||||
}
|
}
|
||||||
export interface MenuContextProps {
|
export interface MenuContextProps {
|
||||||
|
@ -32,6 +32,8 @@ export interface MenuContextProps {
|
||||||
prefixCls: ComputedRef<string>;
|
prefixCls: ComputedRef<string>;
|
||||||
openKeys: Ref<Key[]>;
|
openKeys: Ref<Key[]>;
|
||||||
selectedKeys: Ref<Key[]>;
|
selectedKeys: Ref<Key[]>;
|
||||||
|
|
||||||
|
selectedSubMenuEventKeys: Ref<string[]>;
|
||||||
rtl?: ComputedRef<boolean>;
|
rtl?: ComputedRef<boolean>;
|
||||||
|
|
||||||
locked?: Ref<boolean>;
|
locked?: Ref<boolean>;
|
||||||
|
|
|
@ -21,7 +21,7 @@ export type RenderIconType = (props: RenderIconInfo) => any;
|
||||||
export interface MenuInfo {
|
export interface MenuInfo {
|
||||||
key: Key;
|
key: Key;
|
||||||
eventKey: string;
|
eventKey: string;
|
||||||
keyPath?: string[];
|
keyPath?: Key[];
|
||||||
eventKeyPath: Key[];
|
eventKeyPath: Key[];
|
||||||
domEvent: MouseEvent | KeyboardEvent;
|
domEvent: MouseEvent | KeyboardEvent;
|
||||||
}
|
}
|
||||||
|
|
100
examples/App.vue
100
examples/App.vue
|
@ -1,89 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<a-menu
|
<a-menu v-model:selectedKeys="current" mode="horizontal">
|
||||||
id="dddddd"
|
<a-menu-item key="mail">
|
||||||
v-model:openKeys="openKeys"
|
<mail-outlined />
|
||||||
v-model:selectedKeys="selectedKeys"
|
Navigation One
|
||||||
style="width: 256px"
|
</a-menu-item>
|
||||||
mode="inline"
|
<a-menu-item key="app" disabled>
|
||||||
@click="handleClick"
|
<appstore-outlined />
|
||||||
>
|
Navigation Two
|
||||||
<a-menu-item key="0">Option 0</a-menu-item>
|
</a-menu-item>
|
||||||
<a-sub-menu key="sub1" @titleClick="titleClick">
|
<a-sub-menu>
|
||||||
<template #title>
|
<template #title>
|
||||||
<span>
|
<span class="submenu-title-wrapper">
|
||||||
<MailOutlined />
|
<setting-outlined />
|
||||||
<span>Navigation One</span>
|
Navigation Three - Submenu
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item key="1">
|
<a-menu-item-group title="Item 1">
|
||||||
<template #icon><QqOutlined /></template>
|
<a-menu-item key="setting:1">Option 1</a-menu-item>
|
||||||
Option 1
|
<a-menu-item key="setting:2">Option 2</a-menu-item>
|
||||||
</a-menu-item>
|
</a-menu-item-group>
|
||||||
<a-menu-item key="2">Option 2</a-menu-item>
|
<a-menu-item-group title="Item 2">
|
||||||
<a-menu-item-group key="g2" title="Item 2">
|
<a-menu-item key="setting:3">Option 3</a-menu-item>
|
||||||
<a-menu-item key="3">Option 3</a-menu-item>
|
<a-menu-item key="setting:4">Option 4</a-menu-item>
|
||||||
<a-menu-item key="4">Option 4</a-menu-item>
|
|
||||||
</a-menu-item-group>
|
</a-menu-item-group>
|
||||||
</a-sub-menu>
|
</a-sub-menu>
|
||||||
<!-- <a-sub-menu key="sub2" @titleClick="titleClick">
|
<a-menu-item key="alipay">
|
||||||
<template #title>
|
<a href="https://antdv.com" target="_blank" rel="noopener noreferrer">
|
||||||
<span>
|
Navigation Four - Link
|
||||||
<AppstoreOutlined />
|
</a>
|
||||||
<span>Navigation Two</span>
|
</a-menu-item>
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<a-menu-item key="5">Option 5</a-menu-item>
|
|
||||||
<a-menu-item key="6">Option 6</a-menu-item>
|
|
||||||
<a-sub-menu key="sub3" title="Submenu">
|
|
||||||
<a-menu-item key="7">Option 7</a-menu-item>
|
|
||||||
<a-menu-item key="8">Option 8</a-menu-item>
|
|
||||||
</a-sub-menu>
|
|
||||||
</a-sub-menu>
|
|
||||||
<a-sub-menu key="sub4">
|
|
||||||
<template #title>
|
|
||||||
<span>
|
|
||||||
<SettingOutlined />
|
|
||||||
<span>Navigation Three</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<a-menu-item key="9">Option 9</a-menu-item>
|
|
||||||
<a-menu-item key="10">Option 10</a-menu-item>
|
|
||||||
<a-menu-item key="11">Option 11</a-menu-item>
|
|
||||||
<a-menu-item key="12">Option 12</a-menu-item>
|
|
||||||
</a-sub-menu> -->
|
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script>
|
||||||
import { defineComponent, ref, watch } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import { MailOutlined, QqOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
import { MailOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MailOutlined,
|
MailOutlined,
|
||||||
QqOutlined,
|
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const selectedKeys = ref<string[]>(['1']);
|
const current = ref(['mail']);
|
||||||
const openKeys = ref<string[]>(['sub1']);
|
|
||||||
const handleClick = (e: Event) => {
|
|
||||||
// console.log('click', e);
|
|
||||||
};
|
|
||||||
const titleClick = (e: Event) => {
|
|
||||||
// console.log('titleClick', e);
|
|
||||||
};
|
|
||||||
watch(
|
|
||||||
() => openKeys,
|
|
||||||
val => {
|
|
||||||
// console.log('openKeys', val);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
selectedKeys,
|
current,
|
||||||
openKeys,
|
|
||||||
|
|
||||||
handleClick,
|
|
||||||
titleClick,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
||||||
Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2
|
Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557
|
Loading…
Reference in New Issue