import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; import { inject, nextTick, defineComponent, ref, 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 '../_util/hooks/useConfigInject'; import { tuple, withInstall } from '../_util/type'; import omit from '../_util/omit'; import devWarning from '../vc-util/devWarning'; import type { KeyboardEventHandler, MouseEventHandler } from '../_util/EventInterface'; type ILevelMove = number | [number, number]; const PlacementTypes = tuple('top', 'right', 'bottom', 'left'); export type placementType = typeof PlacementTypes[number]; const SizeTypes = tuple('default', 'large'); 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: { type: Object as PropType, default: undefined as CSSProperties }, /** @deprecated Use `style` instead */ wrapStyle: { type: Object as PropType, default: undefined as CSSProperties }, style: { type: Object as PropType, default: undefined as CSSProperties }, class: PropTypes.any, /** @deprecated Use `class` instead */ wrapClassName: String, size: { type: String as PropType, }, drawerStyle: { type: Object as PropType, default: undefined as CSSProperties }, headerStyle: { type: Object as PropType, default: undefined as CSSProperties }, bodyStyle: { type: Object as PropType, default: undefined as CSSProperties }, contentWrapperStyle: { type: Object as PropType, default: undefined as CSSProperties, }, title: PropTypes.any, visible: { 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: { type: Object as PropType, default: undefined as CSSProperties }, 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>, onAfterVisibleChange: Function as PropType<(visible: boolean) => void>, 'onUpdate:visible': Function as PropType<(visible: 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' as placementType, maskClosable: true, mask: true, level: null, keyboard: true, push: defaultPushState, }), slots: ['closeIcon', 'title', 'extra', 'footer', 'handle'], // emits: ['update:visible', 'close', 'afterVisibleChange'], setup(props, { emit, slots, attrs }) { const sPush = ref(false); const destroyClose = ref(false); const vcDrawer = ref(null); const load = ref(false); const visible = ref(false); watch( () => props.visible, propsVisible => { if (propsVisible) { load.value = true; } else { visible.value = false; } }, { immediate: true }, ); watch( [() => props.visible, load], ([propsVisible]) => { if (propsVisible && load.value) { visible.value = true; } }, { immediate: true }, ); const parentDrawerOpts = inject('parentDrawerOpts', null); const { prefixCls, getPopupContainer, direction } = useConfigInject('drawer', props); 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', ); devWarning( props.wrapStyle === undefined, 'Drawer', '`wrapStyle` prop is deprecated, please use `style` instead', ); devWarning( props.wrapClassName === undefined, 'Drawer', '`wrapClassName` prop is deprecated, please use `class` instead', ); const setPush = () => { sPush.value = true; }; const setPull = () => { sPush.value = false; nextTick(() => { domFocus(); }); }; provide('parentDrawerOpts', { setPush, setPull, }); onMounted(() => { if (props.visible && 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('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); }; 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; }); const offsetStyle = computed(() => { // https://github.com/ant-design/ant-design/issues/24287 const { mask, placement, size = 'default', width, height } = props; if (!visible.value && !mask) { return {}; } const val: CSSProperties = {}; if (placement === 'left' || placement === 'right') { const defaultWidth = size === 'large' ? 736 : 378; val.width = typeof width === 'undefined' ? defaultWidth : width; val.width = typeof val.width === 'string' ? val.width : `${val.width}px`; } else { const defaultHeight = size === 'large' ? 736 : 378; val.height = typeof height === 'undefined' ? defaultHeight : height; val.height = typeof val.height === 'string' ? val.height : `${val.height}px`; } return val; }); const drawerStyle = computed(() => { const { zIndex, wrapStyle, mask, style } = props; const val = mask ? {} : offsetStyle.value; return { zIndex, transform: sPush.value ? pushTransform.value : undefined, ...val, ...wrapStyle, ...style, }; }); 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}
); }; return () => { const { width, height, placement, mask, wrapClassName, class: className, forceRender, ...rest } = props; const val = mask ? offsetStyle.value : {}; const haveMask = mask ? '' : 'no-mask'; const vcDrawerProps: any = { ...attrs, ...omit(rest, [ 'size', 'closeIcon', 'closable', 'destroyOnClose', 'drawerStyle', 'headerStyle', 'bodyStyle', 'title', 'push', 'wrapStyle', 'onAfterVisibleChange', 'onClose', 'onUpdate:visible', 'visible', ]), ...val, forceRender, onClose: close, afterVisibleChange, handler: false, prefixCls: prefixCls.value, open: visible.value, showMask: mask, placement, class: classnames({ [className]: className, [wrapClassName]: !!wrapClassName, [haveMask]: !!haveMask, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }), style: drawerStyle.value, ref: vcDrawer, }; return ( props.handle : slots.handle, default: () => renderBody(prefixCls.value), }} > ); }; }, }); export default withInstall(Drawer);