feat: menu add itemsType
parent
0244e7f5b4
commit
c4a61f210f
|
@ -1,6 +1,7 @@
|
||||||
import type { Key } from '../../_util/type';
|
import type { Key } from '../../_util/type';
|
||||||
import type { ExtractPropTypes, PropType, VNode } from 'vue';
|
import type { ExtractPropTypes, PropType, VNode } from 'vue';
|
||||||
import {
|
import {
|
||||||
|
shallowRef,
|
||||||
Teleport,
|
Teleport,
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
|
@ -38,10 +39,14 @@ import { cloneElement } from '../../_util/vnode';
|
||||||
import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath';
|
import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath';
|
||||||
import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInterface';
|
import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInterface';
|
||||||
import collapseMotion from '../../_util/collapseMotion';
|
import collapseMotion from '../../_util/collapseMotion';
|
||||||
|
import type { ItemType } from './hooks/useItems';
|
||||||
|
import useItems from './hooks/useItems';
|
||||||
|
|
||||||
export const menuProps = () => ({
|
export const menuProps = () => ({
|
||||||
id: String,
|
id: String,
|
||||||
prefixCls: String,
|
prefixCls: String,
|
||||||
|
// donot use items, now only support inner use
|
||||||
|
items: Array as PropType<ItemType[]>,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
inlineCollapsed: Boolean,
|
inlineCollapsed: Boolean,
|
||||||
disabledOverflow: Boolean,
|
disabledOverflow: Boolean,
|
||||||
|
@ -90,7 +95,7 @@ export default defineComponent({
|
||||||
slots: ['expandIcon', 'overflowedIndicator'],
|
slots: ['expandIcon', 'overflowedIndicator'],
|
||||||
setup(props, { slots, emit, attrs }) {
|
setup(props, { slots, emit, attrs }) {
|
||||||
const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props);
|
const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props);
|
||||||
const store = ref<Record<string, StoreMenuInfo>>({});
|
const store = shallowRef<Map<string, StoreMenuInfo>>(new Map());
|
||||||
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
|
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
|
||||||
const inlineCollapsed = computed(() => {
|
const inlineCollapsed = computed(() => {
|
||||||
if (siderCollapsed.value !== undefined) {
|
if (siderCollapsed.value !== undefined) {
|
||||||
|
@ -98,7 +103,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
return props.inlineCollapsed;
|
return props.inlineCollapsed;
|
||||||
});
|
});
|
||||||
|
const { itemsNodes } = useItems(props);
|
||||||
const isMounted = ref(false);
|
const isMounted = ref(false);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isMounted.value = true;
|
isMounted.value = true;
|
||||||
|
@ -115,6 +120,11 @@ export default defineComponent({
|
||||||
'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.',
|
||||||
);
|
);
|
||||||
|
// devWarning(
|
||||||
|
// !!props.items && !slots.default,
|
||||||
|
// 'Menu',
|
||||||
|
// '`children` will be removed in next major version. Please use `items` instead.',
|
||||||
|
// );
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeKeys = ref([]);
|
const activeKeys = ref([]);
|
||||||
|
@ -124,7 +134,7 @@ export default defineComponent({
|
||||||
store,
|
store,
|
||||||
() => {
|
() => {
|
||||||
const newKeyMapStore = {};
|
const newKeyMapStore = {};
|
||||||
for (const menuInfo of Object.values(store.value)) {
|
for (const menuInfo of store.value.values()) {
|
||||||
newKeyMapStore[menuInfo.key] = menuInfo;
|
newKeyMapStore[menuInfo.key] = menuInfo;
|
||||||
}
|
}
|
||||||
keyMapStore.value = newKeyMapStore;
|
keyMapStore.value = newKeyMapStore;
|
||||||
|
@ -322,8 +332,8 @@ export default defineComponent({
|
||||||
const keys = [];
|
const keys = [];
|
||||||
const storeValue = store.value;
|
const storeValue = store.value;
|
||||||
eventKeys.forEach(eventKey => {
|
eventKeys.forEach(eventKey => {
|
||||||
const { key, childrenEventKeys } = storeValue[eventKey];
|
const { key, childrenEventKeys } = storeValue.get(eventKey);
|
||||||
keys.push(key, ...getChildrenKeys(childrenEventKeys));
|
keys.push(key, ...getChildrenKeys(unref(childrenEventKeys)));
|
||||||
});
|
});
|
||||||
return keys;
|
return keys;
|
||||||
};
|
};
|
||||||
|
@ -355,11 +365,12 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
|
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
|
||||||
store.value = { ...store.value, [key]: info as any };
|
store.value.set(key, info);
|
||||||
|
store.value = new Map(store.value);
|
||||||
};
|
};
|
||||||
const unRegisterMenuInfo = (key: string) => {
|
const unRegisterMenuInfo = (key: string) => {
|
||||||
delete store.value[key];
|
store.value.delete(key);
|
||||||
store.value = { ...store.value };
|
store.value = new Map(store.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastVisibleIndex = ref(0);
|
const lastVisibleIndex = ref(0);
|
||||||
|
@ -379,7 +390,6 @@ export default defineComponent({
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
useProvideMenu({
|
useProvideMenu({
|
||||||
store,
|
|
||||||
prefixCls,
|
prefixCls,
|
||||||
activeKeys,
|
activeKeys,
|
||||||
openKeys: mergedOpenKeys,
|
openKeys: mergedOpenKeys,
|
||||||
|
@ -408,9 +418,10 @@ export default defineComponent({
|
||||||
isRootMenu: ref(true),
|
isRootMenu: ref(true),
|
||||||
expandIcon,
|
expandIcon,
|
||||||
forceSubMenuRender: computed(() => props.forceSubMenuRender),
|
forceSubMenuRender: computed(() => props.forceSubMenuRender),
|
||||||
|
rootClassName: computed(() => ''),
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
const childList = flattenChildren(slots.default?.());
|
const childList = itemsNodes.value || flattenChildren(slots.default?.());
|
||||||
const allVisible =
|
const allVisible =
|
||||||
lastVisibleIndex.value >= childList.length - 1 ||
|
lastVisibleIndex.value >= childList.length - 1 ||
|
||||||
mergedMode.value !== 'horizontal' ||
|
mergedMode.value !== 'horizontal' ||
|
||||||
|
|
|
@ -42,6 +42,7 @@ export default defineComponent({
|
||||||
forceSubMenuRender,
|
forceSubMenuRender,
|
||||||
motion,
|
motion,
|
||||||
defaultMotions,
|
defaultMotions,
|
||||||
|
rootClassName,
|
||||||
} = useInjectMenu();
|
} = useInjectMenu();
|
||||||
const forceRender = useInjectForceRender();
|
const forceRender = useInjectForceRender();
|
||||||
const placement = computed(() =>
|
const placement = computed(() =>
|
||||||
|
@ -86,6 +87,7 @@ export default defineComponent({
|
||||||
[`${prefixCls}-rtl`]: rtl.value,
|
[`${prefixCls}-rtl`]: rtl.value,
|
||||||
},
|
},
|
||||||
popupClassName,
|
popupClassName,
|
||||||
|
rootClassName.value,
|
||||||
)}
|
)}
|
||||||
stretch={mode === 'horizontal' ? 'minWidth' : null}
|
stretch={mode === 'horizontal' ? 'minWidth' : null}
|
||||||
getPopupContainer={
|
getPopupContainer={
|
||||||
|
|
|
@ -21,6 +21,7 @@ import devWarning from '../../vc-util/devWarning';
|
||||||
import isValid from '../../_util/isValid';
|
import isValid from '../../_util/isValid';
|
||||||
import type { MouseEventHandler } from '../../_util/EventInterface';
|
import type { MouseEventHandler } from '../../_util/EventInterface';
|
||||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||||
|
import type { MenuTheme } from './interface';
|
||||||
|
|
||||||
let indexGuid = 0;
|
let indexGuid = 0;
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ export const subMenuProps = () => ({
|
||||||
internalPopupClose: Boolean,
|
internalPopupClose: Boolean,
|
||||||
eventKey: String,
|
eventKey: String,
|
||||||
expandIcon: Function as PropType<(p?: { isOpen: boolean; [key: string]: any }) => any>,
|
expandIcon: Function as PropType<(p?: { isOpen: boolean; [key: string]: any }) => any>,
|
||||||
|
theme: String as PropType<MenuTheme>,
|
||||||
onMouseenter: Function as PropType<MouseEventHandler>,
|
onMouseenter: Function as PropType<MouseEventHandler>,
|
||||||
onMouseleave: Function as PropType<MouseEventHandler>,
|
onMouseleave: Function as PropType<MouseEventHandler>,
|
||||||
onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>,
|
onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>,
|
||||||
|
@ -193,7 +195,7 @@ export default defineComponent({
|
||||||
const popupClassName = computed(() =>
|
const popupClassName = computed(() =>
|
||||||
classNames(
|
classNames(
|
||||||
prefixCls.value,
|
prefixCls.value,
|
||||||
`${prefixCls.value}-${antdMenuTheme.value}`,
|
`${prefixCls.value}-${props.theme || antdMenuTheme.value}`,
|
||||||
props.popupClassName,
|
props.popupClassName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import type {
|
||||||
|
MenuItemType as VcMenuItemType,
|
||||||
|
MenuDividerType as VcMenuDividerType,
|
||||||
|
SubMenuType as VcSubMenuType,
|
||||||
|
MenuItemGroupType as VcMenuItemGroupType,
|
||||||
|
} from '../interface';
|
||||||
|
import SubMenu from '../SubMenu';
|
||||||
|
import ItemGroup from '../ItemGroup';
|
||||||
|
import MenuDivider from '../Divider';
|
||||||
|
import MenuItem from '../MenuItem';
|
||||||
|
import type { Key } from '../../../_util/type';
|
||||||
|
import { ref, shallowRef, watch } from 'vue';
|
||||||
|
import type { MenuProps } from '../Menu';
|
||||||
|
import type { StoreMenuInfo } from './useMenuContext';
|
||||||
|
|
||||||
|
interface MenuItemType extends VcMenuItemType {
|
||||||
|
danger?: boolean;
|
||||||
|
icon?: any;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubMenuType extends Omit<VcSubMenuType, 'children'> {
|
||||||
|
icon?: any;
|
||||||
|
theme?: 'dark' | 'light';
|
||||||
|
children: ItemType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuItemGroupType extends Omit<VcMenuItemGroupType, 'children'> {
|
||||||
|
children?: MenuItemType[];
|
||||||
|
key?: Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuDividerType extends VcMenuDividerType {
|
||||||
|
dashed?: boolean;
|
||||||
|
key?: Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType | null;
|
||||||
|
|
||||||
|
function convertItemsToNodes(
|
||||||
|
list: ItemType[],
|
||||||
|
store: Map<string, StoreMenuInfo>,
|
||||||
|
parentMenuInfo?: {
|
||||||
|
childrenEventKeys: string[];
|
||||||
|
parentKeys: string[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return (list || [])
|
||||||
|
.map((opt, index) => {
|
||||||
|
if (opt && typeof opt === 'object') {
|
||||||
|
const { label, children, key, type, ...restProps } = opt as any;
|
||||||
|
const mergedKey = key ?? `tmp-${index}`;
|
||||||
|
// 此处 eventKey === key, 移除 children 后可以移除 eventKey
|
||||||
|
const parentKeys = parentMenuInfo ? parentMenuInfo.parentKeys.slice() : [];
|
||||||
|
const childrenEventKeys = [];
|
||||||
|
// if
|
||||||
|
const menuInfo = {
|
||||||
|
eventKey: mergedKey,
|
||||||
|
key: mergedKey,
|
||||||
|
parentEventKeys: ref<string[]>(parentKeys),
|
||||||
|
parentKeys: ref<string[]>(parentKeys),
|
||||||
|
childrenEventKeys: ref<string[]>(childrenEventKeys),
|
||||||
|
isLeaf: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MenuItemGroup & SubMenuItem
|
||||||
|
if (children || type === 'group') {
|
||||||
|
if (type === 'group') {
|
||||||
|
const childrenNodes = convertItemsToNodes(children, store, parentMenuInfo);
|
||||||
|
// Group
|
||||||
|
return (
|
||||||
|
<ItemGroup key={mergedKey} {...restProps} title={label}>
|
||||||
|
{childrenNodes}
|
||||||
|
</ItemGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
store.set(mergedKey, menuInfo);
|
||||||
|
if (parentMenuInfo) {
|
||||||
|
parentMenuInfo.childrenEventKeys.push(mergedKey);
|
||||||
|
}
|
||||||
|
// Sub Menu
|
||||||
|
const childrenNodes = convertItemsToNodes(children, store, {
|
||||||
|
childrenEventKeys,
|
||||||
|
parentKeys: [].concat(parentKeys, mergedKey),
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<SubMenu key={mergedKey} {...restProps} title={label}>
|
||||||
|
{childrenNodes}
|
||||||
|
</SubMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuItem & Divider
|
||||||
|
if (type === 'divider') {
|
||||||
|
return <MenuDivider key={mergedKey} {...restProps} />;
|
||||||
|
}
|
||||||
|
menuInfo.isLeaf = true;
|
||||||
|
store.set(mergedKey, menuInfo);
|
||||||
|
return (
|
||||||
|
<MenuItem key={mergedKey} {...restProps}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(opt => opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Move logic here in v4
|
||||||
|
/**
|
||||||
|
* We simply convert `items` to VueNode for reuse origin component logic. But we need move all the
|
||||||
|
* logic from component into this hooks when in v4
|
||||||
|
*/
|
||||||
|
export default function useItems(props: MenuProps) {
|
||||||
|
const itemsNodes = shallowRef([]);
|
||||||
|
const hasItmes = ref(false);
|
||||||
|
const store = shallowRef<Map<string, StoreMenuInfo>>(new Map());
|
||||||
|
watch(
|
||||||
|
() => props.items,
|
||||||
|
() => {
|
||||||
|
const newStore = new Map<string, StoreMenuInfo>();
|
||||||
|
hasItmes.value = false;
|
||||||
|
if (props.items) {
|
||||||
|
hasItmes.value = true;
|
||||||
|
itemsNodes.value = convertItemsToNodes(props.items as ItemType[], newStore);
|
||||||
|
} else {
|
||||||
|
itemsNodes.value = undefined;
|
||||||
|
}
|
||||||
|
store.value = newStore;
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
return { itemsNodes, store, hasItmes };
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Key } from '../../../_util/type';
|
import type { Key } from '../../../_util/type';
|
||||||
import type { ComputedRef, InjectionKey, PropType, Ref, UnwrapRef } from 'vue';
|
import type { ComputedRef, InjectionKey, PropType, Ref } from 'vue';
|
||||||
import { defineComponent, inject, provide, toRef } from 'vue';
|
import { defineComponent, inject, provide, toRef } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
BuiltinPlacements,
|
BuiltinPlacements,
|
||||||
|
@ -13,15 +13,14 @@ import type { CSSMotionProps } from '../../../_util/transition';
|
||||||
export interface StoreMenuInfo {
|
export interface StoreMenuInfo {
|
||||||
eventKey: string;
|
eventKey: string;
|
||||||
key: Key;
|
key: Key;
|
||||||
parentEventKeys: ComputedRef<string[]>;
|
parentEventKeys: Ref<string[]>;
|
||||||
childrenEventKeys?: Ref<string[]>;
|
childrenEventKeys?: Ref<string[]>;
|
||||||
isLeaf?: boolean;
|
isLeaf?: boolean;
|
||||||
parentKeys: ComputedRef<Key[]>;
|
parentKeys: Ref<Key[]>;
|
||||||
}
|
}
|
||||||
export interface MenuContextProps {
|
export interface MenuContextProps {
|
||||||
isRootMenu: Ref<boolean>;
|
isRootMenu: Ref<boolean>;
|
||||||
|
rootClassName: Ref<string>;
|
||||||
store: Ref<Record<string, UnwrapRef<StoreMenuInfo>>>;
|
|
||||||
registerMenuInfo: (key: string, info: StoreMenuInfo) => void;
|
registerMenuInfo: (key: string, info: StoreMenuInfo) => void;
|
||||||
unRegisterMenuInfo: (key: string) => void;
|
unRegisterMenuInfo: (key: string) => void;
|
||||||
prefixCls: ComputedRef<string>;
|
prefixCls: ComputedRef<string>;
|
||||||
|
|
|
@ -1,6 +1,74 @@
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
import type { Key } from '../../_util/type';
|
import type { Key } from '../../_util/type';
|
||||||
import type { MenuItemProps } from './MenuItem';
|
import type { MenuItemProps } from './MenuItem';
|
||||||
|
|
||||||
|
// ========================= Options =========================
|
||||||
|
interface ItemSharedProps {
|
||||||
|
style?: CSSProperties;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubMenuType extends ItemSharedProps {
|
||||||
|
label?: any;
|
||||||
|
|
||||||
|
children: ItemType[];
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
rootClassName?: string;
|
||||||
|
|
||||||
|
// >>>>> Icon
|
||||||
|
itemIcon?: RenderIconType;
|
||||||
|
expandIcon?: RenderIconType;
|
||||||
|
|
||||||
|
// >>>>> Active
|
||||||
|
onMouseenter?: MenuHoverEventHandler;
|
||||||
|
onMouseleave?: MenuHoverEventHandler;
|
||||||
|
|
||||||
|
// >>>>> Popup
|
||||||
|
popupClassName?: string;
|
||||||
|
popupOffset?: number[];
|
||||||
|
|
||||||
|
// >>>>> Events
|
||||||
|
onClick?: MenuClickEventHandler;
|
||||||
|
onTitleClick?: (info: MenuTitleInfo) => void;
|
||||||
|
onTitleMouseenter?: MenuHoverEventHandler;
|
||||||
|
onTitleMouseleave?: MenuHoverEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuItemType extends ItemSharedProps {
|
||||||
|
label?: any;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
itemIcon?: RenderIconType;
|
||||||
|
|
||||||
|
key: Key;
|
||||||
|
|
||||||
|
// >>>>> Active
|
||||||
|
onMouseenter?: MenuHoverEventHandler;
|
||||||
|
onMouseleave?: MenuHoverEventHandler;
|
||||||
|
|
||||||
|
// >>>>> Events
|
||||||
|
onClick?: MenuClickEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuItemGroupType extends ItemSharedProps {
|
||||||
|
type: 'group';
|
||||||
|
|
||||||
|
label?: any;
|
||||||
|
|
||||||
|
children?: ItemType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuDividerType extends ItemSharedProps {
|
||||||
|
type: 'divider';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ItemType = SubMenuType | MenuItemType | MenuItemGroupType | MenuDividerType | null;
|
||||||
|
|
||||||
export type MenuTheme = 'light' | 'dark';
|
export type MenuTheme = 'light' | 'dark';
|
||||||
|
|
||||||
// ========================== Basic ==========================
|
// ========================== Basic ==========================
|
||||||
|
|
Loading…
Reference in New Issue