ant-design-vue/components/drawer/index.tsx

411 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 '../config-provider/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<CSSProperties>, default: undefined as CSSProperties },
/** @deprecated Use `style` instead */
wrapStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
style: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
class: PropTypes.any,
/** @deprecated Use `class` instead */
wrapClassName: String,
size: {
type: String as PropType<sizeType>,
},
drawerStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
headerStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
bodyStyle: { type: Object as PropType<CSSProperties>, default: undefined as CSSProperties },
contentWrapperStyle: {
type: Object as PropType<CSSProperties>,
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<PushState> }]),
placement: PropTypes.oneOf(PlacementTypes),
keyboard: { type: Boolean, default: undefined },
extra: PropTypes.any,
footer: PropTypes.any,
footerStyle: { type: Object as PropType<CSSProperties>, 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<MouseEventHandler | KeyboardEventHandler>,
});
export type DrawerProps = Partial<ExtractPropTypes<ReturnType<typeof drawerProps>>>;
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 (
<div
class={classnames(`${prefixCls}-header`, {
[`${prefixCls}-header-close-only`]: closable && !title && !extra,
})}
style={headerStyle}
>
<div class={`${prefixCls}-header-title`}>
{renderCloseIcon(prefixCls)}
{title && <div class={`${prefixCls}-title`}>{title}</div>}
</div>
{extra && <div class={`${prefixCls}-extra`}>{extra}</div>}
</div>
);
};
const renderCloseIcon = (prefixCls: string) => {
const { closable } = props;
const $closeIcon = slots.closeIcon ? slots.closeIcon?.() : props.closeIcon;
return (
closable && (
<button key="closer" onClick={close} aria-label="Close" class={`${prefixCls}-close`}>
{$closeIcon === undefined ? <CloseOutlined></CloseOutlined> : $closeIcon}
</button>
)
);
};
const renderBody = (prefixCls: string) => {
if (destroyClose.value && !props.forceRender && !load.value) {
return null;
}
const { bodyStyle, drawerStyle } = props;
return (
<div class={`${prefixCls}-wrapper-body`} style={drawerStyle}>
{renderHeader(prefixCls)}
<div key="body" class={`${prefixCls}-body`} style={bodyStyle}>
{slots.default?.()}
</div>
{renderFooter(prefixCls)}
</div>
);
};
const renderFooter = (prefixCls: string) => {
const footer = getPropsSlot(slots, props, 'footer');
if (!footer) {
return null;
}
const footerClassName = `${prefixCls}-footer`;
return (
<div class={footerClassName} style={props.footerStyle}>
{footer}
</div>
);
};
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 (
<VcDrawer
{...vcDrawerProps}
getContainer={getContainer.value}
v-slots={{
handler: props.handle ? () => props.handle : slots.handle,
default: () => renderBody(prefixCls.value),
}}
></VcDrawer>
);
};
},
});
export default withInstall(Drawer);