import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; import { inject, nextTick, defineComponent, shallowRef, onMounted, provide, onUnmounted, watch, computed, } from 'vue'; import { getPropsSlot, initDefaultProps } from '../_util/props-util'; import classnames from '../_util/classNames'; import VcDrawer from '../vc-drawer'; import PropTypes from '../_util/vue-types'; import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; import useConfigInject from '../config-provider/hooks/useConfigInject'; import { objectType, withInstall } from '../_util/type'; import type { CustomSlotsType } from '../_util/type'; import omit from '../_util/omit'; import devWarning from '../vc-util/devWarning'; import type { KeyboardEventHandler, MouseEventHandler } from '../_util/EventInterface'; import useStyle from './style'; import { NoCompactStyle } from '../space/Compact'; import isNumeric from '../_util/isNumeric'; import { getTransitionName, getTransitionProps } from '../_util/transition'; type ILevelMove = number | [number, number]; const PlacementTypes = ['top', 'right', 'bottom', 'left'] as const; export type placementType = (typeof PlacementTypes)[number]; const SizeTypes = ['default', 'large'] as const; export type sizeType = (typeof SizeTypes)[number]; export interface PushState { distance: string | number; } const defaultPushState: PushState = { distance: 180 }; type getContainerFunc = () => HTMLElement; export const drawerProps = () => ({ autofocus: { type: Boolean, default: undefined }, closable: { type: Boolean, default: undefined }, closeIcon: PropTypes.any, destroyOnClose: { type: Boolean, default: undefined }, forceRender: { type: Boolean, default: undefined }, getContainer: { type: [String, Function, Boolean, Object] as PropType< string | HTMLElement | getContainerFunc | false >, default: undefined as string | HTMLElement | getContainerFunc | false, }, maskClosable: { type: Boolean, default: undefined }, mask: { type: Boolean, default: undefined }, maskStyle: objectType(), rootClassName: String, rootStyle: objectType(), size: { type: String as PropType, }, drawerStyle: objectType(), headerStyle: objectType(), bodyStyle: objectType(), contentWrapperStyle: { type: Object as PropType, default: undefined as CSSProperties, }, title: PropTypes.any, /** @deprecated Please use `open` instead */ visible: { type: Boolean, default: undefined }, open: { type: Boolean, default: undefined }, width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), zIndex: Number, prefixCls: String, push: PropTypes.oneOfType([PropTypes.looseBool, { type: Object as PropType }]), placement: PropTypes.oneOf(PlacementTypes), keyboard: { type: Boolean, default: undefined }, extra: PropTypes.any, footer: PropTypes.any, footerStyle: objectType(), level: PropTypes.any, levelMove: { type: [Number, Array, Function] as PropType< ILevelMove | ((e: { target: HTMLElement; open: boolean }) => ILevelMove) >, }, handle: PropTypes.any, /** @deprecated Use `@afterVisibleChange` instead */ afterVisibleChange: Function as PropType<(visible: boolean) => void>, /** @deprecated Please use `@afterOpenChange` instead */ onAfterVisibleChange: Function as PropType<(visible: boolean) => void>, onAfterOpenChange: Function as PropType<(open: boolean) => void>, /** @deprecated Please use `onUpdate:open` instead */ 'onUpdate:visible': Function as PropType<(visible: boolean) => void>, 'onUpdate:open': Function as PropType<(open: boolean) => void>, onClose: Function as PropType, }); export type DrawerProps = Partial>>; const Drawer = defineComponent({ compatConfig: { MODE: 3 }, name: 'ADrawer', inheritAttrs: false, props: initDefaultProps(drawerProps(), { closable: true, placement: 'right', maskClosable: true, mask: true, level: null, keyboard: true, push: defaultPushState, }), slots: Object as CustomSlotsType<{ closeIcon: any; title: any; extra: any; footer: any; handle: any; default: any; }>, // emits: ['update:visible', 'close', 'afterVisibleChange'], setup(props, { emit, slots, attrs }) { const sPush = shallowRef(false); const destroyClose = shallowRef(false); const vcDrawer = shallowRef(null); const load = shallowRef(false); const visible = shallowRef(false); const mergedOpen = computed(() => props.open ?? props.visible); watch( mergedOpen, () => { if (mergedOpen.value) { load.value = true; } else { visible.value = false; } }, { immediate: true }, ); watch( [mergedOpen, load], () => { if (mergedOpen.value && load.value) { visible.value = true; } }, { immediate: true }, ); const parentDrawerOpts = inject('parentDrawerOpts', null); const { prefixCls, getPopupContainer, direction } = useConfigInject('drawer', props); const [wrapSSR, hashId] = useStyle(prefixCls); const getContainer = computed(() => // 有可能为 false,所以不能直接判断 props.getContainer === undefined && getPopupContainer?.value ? () => getPopupContainer.value(document.body) : props.getContainer, ); devWarning( !props.afterVisibleChange, 'Drawer', '`afterVisibleChange` prop is deprecated, please use `@afterVisibleChange` event instead', ); // ========================== Warning =========================== if (process.env.NODE_ENV !== 'production') { [ ['visible', 'open'], ['onUpdate:visible', 'onUpdate:open'], ['onAfterVisibleChange', 'onAfterOpenChange'], ].forEach(([deprecatedName, newName]) => { devWarning( !props[deprecatedName], 'Drawer', `\`${deprecatedName}\` is deprecated, please use \`${newName}\` instead.`, ); }); } const setPush = () => { sPush.value = true; }; const setPull = () => { sPush.value = false; nextTick(() => { domFocus(); }); }; provide('parentDrawerOpts', { setPush, setPull, }); onMounted(() => { if (mergedOpen.value && parentDrawerOpts) { parentDrawerOpts.setPush(); } }); onUnmounted(() => { if (parentDrawerOpts) { parentDrawerOpts.setPull(); } }); watch( visible, () => { if (parentDrawerOpts) { if (visible.value) { parentDrawerOpts.setPush(); } else { parentDrawerOpts.setPull(); } } }, { flush: 'post' }, ); const domFocus = () => { vcDrawer.value?.domFocus?.(); }; const close = (e: Event) => { emit('update:visible', false); emit('update:open', false); emit('close', e); }; const afterVisibleChange = (open: boolean) => { if (!open) { if (destroyClose.value === false) { // set true only once destroyClose.value = true; } if (props.destroyOnClose) { load.value = false; } } props.afterVisibleChange?.(open); emit('afterVisibleChange', open); emit('afterOpenChange', open); }; const pushTransform = computed(() => { const { push, placement } = props; let distance: number | string; if (typeof push === 'boolean') { distance = push ? defaultPushState.distance : 0; } else { distance = push!.distance; } distance = parseFloat(String(distance || 0)); if (placement === 'left' || placement === 'right') { return `translateX(${placement === 'left' ? distance : -distance}px)`; } if (placement === 'top' || placement === 'bottom') { return `translateY(${placement === 'top' ? distance : -distance}px)`; } return null; }); // ============================ Size ============================ const mergedWidth = computed(() => props.width ?? (props.size === 'large' ? 736 : 378)); const mergedHeight = computed(() => props.height ?? (props.size === 'large' ? 736 : 378)); const offsetStyle = computed(() => { // https://github.com/ant-design/ant-design/issues/24287 const { mask, placement } = props; if (!visible.value && !mask) { return {}; } const val: CSSProperties = {}; if (placement === 'left' || placement === 'right') { val.width = isNumeric(mergedWidth.value) ? `${mergedWidth.value}px` : mergedWidth.value; } else { val.height = isNumeric(mergedHeight.value) ? `${mergedHeight.value}px` : mergedHeight.value; } return val; }); const wrapperStyle = computed(() => { const { zIndex } = props; const val = offsetStyle.value; return [{ zIndex, transform: sPush.value ? pushTransform.value : undefined }, val]; }); const renderHeader = (prefixCls: string) => { const { closable, headerStyle } = props; const extra = getPropsSlot(slots, props, 'extra'); const title = getPropsSlot(slots, props, 'title'); if (!title && !closable) { return null; } return (
{renderCloseIcon(prefixCls)} {title &&
{title}
}
{extra &&
{extra}
}
); }; const renderCloseIcon = (prefixCls: string) => { const { closable } = props; const $closeIcon = slots.closeIcon ? slots.closeIcon?.() : props.closeIcon; return ( closable && ( ) ); }; const renderBody = (prefixCls: string) => { if (destroyClose.value && !props.forceRender && !load.value) { return null; } const { bodyStyle, drawerStyle } = props; return (
{renderHeader(prefixCls)}
{slots.default?.()}
{renderFooter(prefixCls)}
); }; const renderFooter = (prefixCls: string) => { const footer = getPropsSlot(slots, props, 'footer'); if (!footer) { return null; } const footerClassName = `${prefixCls}-footer`; return (
{footer}
); }; const drawerClassName = computed(() => classnames( { 'no-mask': !props.mask, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }, props.rootClassName, hashId.value, ), ); // =========================== Motion =========================== const maskMotion = computed(() => { return getTransitionProps(getTransitionName(prefixCls.value, 'mask-motion')); }); const panelMotion = (motionPlacement: string) => { return getTransitionProps( getTransitionName(prefixCls.value, `panel-motion-${motionPlacement}`), ); }; return () => { const { width, height, placement, mask, forceRender, ...rest } = props; const vcDrawerProps: any = { ...attrs, ...omit(rest, [ 'size', 'closeIcon', 'closable', 'destroyOnClose', 'drawerStyle', 'headerStyle', 'bodyStyle', 'title', 'push', 'onAfterVisibleChange', 'onClose', 'onUpdate:visible', 'onUpdate:open', 'visible', ]), forceRender, onClose: close, afterVisibleChange, handler: false, prefixCls: prefixCls.value, open: visible.value, showMask: mask, placement, ref: vcDrawer, }; return wrapSSR( props.handle : slots.handle, default: () => renderBody(prefixCls.value), }} > , ); }; }, }); export default withInstall(Drawer);