ant-design-vue/components/menu/src/Menu.tsx

282 lines
8.2 KiB
Vue

import { Key } from '../../_util/type';
import {
computed,
defineComponent,
ExtractPropTypes,
ref,
PropType,
inject,
watchEffect,
watch,
reactive,
onMounted,
} from 'vue';
import shallowEqual from '../../_util/shallowequal';
import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext';
import useConfigInject from '../../_util/hooks/useConfigInject';
import {
MenuTheme,
MenuMode,
BuiltinPlacements,
TriggerSubMenuAction,
MenuInfo,
SelectInfo,
} from './interface';
import devWarning from 'ant-design-vue/es/vc-util/devWarning';
import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition';
export const menuProps = {
prefixCls: String,
disabled: Boolean,
inlineCollapsed: Boolean,
overflowDisabled: Boolean,
openKeys: Array,
selectedKeys: Array,
selectable: Boolean,
multiple: Boolean,
motion: Object as PropType<CSSMotionProps>,
theme: { type: String as PropType<MenuTheme>, default: 'light' },
mode: { type: String as PropType<MenuMode>, default: 'vertical' },
inlineIndent: { type: Number, default: 24 },
subMenuOpenDelay: { type: Number, default: 0.1 },
subMenuCloseDelay: { type: Number, default: 0.1 },
builtinPlacements: { type: Object as PropType<BuiltinPlacements> },
triggerSubMenuAction: { type: String as PropType<TriggerSubMenuAction>, default: 'hover' },
getPopupContainer: Function as PropType<(node: HTMLElement) => HTMLElement>,
};
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
export default defineComponent({
name: 'AMenu',
props: menuProps,
emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys'],
setup(props, { slots, emit }) {
const { prefixCls, direction } = useConfigInject('menu', props);
const store = reactive<Record<string, StoreMenuInfo>>({});
const siderCollapsed = inject(
'layoutSiderCollapsed',
computed(() => undefined),
);
const inlineCollapsed = computed(() => {
const { inlineCollapsed } = props;
if (siderCollapsed.value !== undefined) {
return siderCollapsed.value;
}
return inlineCollapsed;
});
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});
watchEffect(() => {
devWarning(
!('inlineCollapsed' in props && props.mode !== 'inline'),
'Menu',
'`inlineCollapsed` should only be used when `mode` is inline.',
);
devWarning(
!(siderCollapsed.value !== undefined && 'inlineCollapsed' in props),
'Menu',
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
);
});
const activeKeys = ref([]);
const mergedSelectedKeys = ref([]);
watch(
() => props.selectedKeys,
(selectedKeys = mergedSelectedKeys.value) => {
mergedSelectedKeys.value = selectedKeys;
},
{ immediate: true },
);
// >>>>> Trigger select
const triggerSelection = (info: MenuInfo) => {
if (!props.selectable) {
return;
}
// Insert or Remove
const { key: targetKey } = info;
const exist = mergedSelectedKeys.value.includes(targetKey);
let newSelectedKeys: Key[];
if (exist) {
newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey);
} else if (props.multiple) {
newSelectedKeys = [...mergedSelectedKeys.value, targetKey];
} else {
newSelectedKeys = [targetKey];
}
mergedSelectedKeys.value = newSelectedKeys;
// Trigger event
const selectInfo: SelectInfo = {
...info,
selectedKeys: newSelectedKeys,
};
if (exist) {
emit('deselect', selectInfo);
} else {
emit('select', selectInfo);
}
};
const mergedOpenKeys = ref([]);
watch(
() => props.openKeys,
(openKeys = mergedOpenKeys.value) => {
mergedOpenKeys.value = openKeys;
},
{ immediate: true },
);
const changeActiveKeys = (keys: Key[]) => {
activeKeys.value = keys;
};
const disabled = computed(() => !!props.disabled);
const isRtl = computed(() => direction.value === 'rtl');
const mergedMode = ref<MenuMode>('vertical');
const mergedInlineCollapsed = ref(false);
const isInlineMode = computed(() => mergedMode.value === 'inline');
// >>>>> Cache & Reset open keys when inlineCollapsed changed
const inlineCacheOpenKeys = ref([]);
// Cache
watchEffect(() => {
if (isInlineMode.value) {
inlineCacheOpenKeys.value = mergedOpenKeys.value;
}
});
const mountRef = ref(false);
// Restore
watch(isInlineMode, () => {
if (!mountRef.value) {
mountRef.value = true;
return;
}
if (isInlineMode.value) {
mergedOpenKeys.value = inlineCacheOpenKeys.value;
} else {
const empty = [];
mergedOpenKeys.value = empty;
// Trigger open event in case its in control
emit('update:openKeys', empty);
emit('openChange', empty);
}
});
watchEffect(() => {
if (props.mode === 'inline' && inlineCollapsed.value) {
mergedMode.value = 'vertical';
mergedInlineCollapsed.value = inlineCollapsed.value;
}
mergedMode.value = props.mode;
mergedInlineCollapsed.value = false;
});
const className = computed(() => {
return {
[`${prefixCls.value}`]: true,
[`${prefixCls.value}-root`]: true,
[`${prefixCls.value}-${mergedMode.value}`]: true,
[`${prefixCls.value}-inline-collapsed`]: mergedInlineCollapsed.value,
[`${prefixCls.value}-rtl`]: isRtl.value,
[`${prefixCls.value}-${props.theme}`]: true,
};
});
const defaultMotions = {
horizontal: { name: `ant-slide-up` },
inline: collapseMotion,
other: { name: `ant-zoom-big` },
};
useProvideFirstLevel(true);
const getChildrenKeys = (eventKeys: string[]): Key[] => {
const keys = [];
eventKeys.forEach(eventKey => {
const { key, childrenEventKeys } = store[eventKey] as any;
keys.push(key, ...getChildrenKeys(childrenEventKeys.value));
});
return keys;
};
const onInternalOpenChange = (eventKey: Key, open: boolean) => {
const { key, childrenEventKeys } = store[eventKey] as any;
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
if (open) {
newOpenKeys.push(key);
} else if (mergedMode.value !== 'inline') {
// We need find all related popup to close
const subPathKeys = getChildrenKeys(childrenEventKeys.value);
newOpenKeys = newOpenKeys.filter(k => !subPathKeys.includes(k));
}
if (!shallowEqual(mergedOpenKeys, newOpenKeys)) {
mergedOpenKeys.value = newOpenKeys;
emit('update:openKeys', newOpenKeys);
emit('openChange', newOpenKeys);
}
};
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
store[key] = info as any;
};
const unRegisterMenuInfo = (key: string) => {
delete store[key];
};
useProvideMenu({
store,
prefixCls,
activeKeys,
openKeys: mergedOpenKeys,
selectedKeys: mergedSelectedKeys,
changeActiveKeys,
disabled,
rtl: isRtl,
mode: mergedMode,
inlineIndent: computed(() => props.inlineIndent),
subMenuCloseDelay: computed(() => props.subMenuCloseDelay),
subMenuOpenDelay: computed(() => props.subMenuOpenDelay),
builtinPlacements: computed(() => props.builtinPlacements),
triggerSubMenuAction: computed(() => props.triggerSubMenuAction),
getPopupContainer: computed(() => props.getPopupContainer),
inlineCollapsed: mergedInlineCollapsed,
antdMenuTheme: computed(() => props.theme),
siderCollapsed,
defaultMotions: computed(() => (isMounted.value ? defaultMotions : null)),
motion: computed(() => (isMounted.value ? props.motion : null)),
overflowDisabled: computed(() => props.overflowDisabled),
onOpenChange: onInternalOpenChange,
onItemClick: triggerSelection,
registerMenuInfo,
unRegisterMenuInfo,
});
return () => {
return <ul class={className.value}>{slots.default?.()}</ul>;
};
},
});