refactor(drawer): use compositionAPI

refactor-drawer
ajuner 2021-09-27 17:09:18 +08:00 committed by tangjinzhou
parent 4fe3c7a919
commit 0c31ada67a
22 changed files with 1338 additions and 407 deletions

View File

@ -22,8 +22,7 @@ Basic drawer.
v-model:visible="visible"
title="Basic Drawer"
placement="right"
:closable="false"
:after-visible-change="afterVisibleChange"
@after-visible-change="afterVisibleChange"
>
<p>Some contents...</p>
<p>Some contents...</p>

View File

@ -0,0 +1,65 @@
<docs>
---
order: 2
title:
zh-CN: 额外操作
en-US: Extra Actions
---
## zh-CN
Ant Design 规范中操作按钮建议放在抽屉的右上角可以使用 extra 属性来实现
## en-US
Extra actions should be placed at corner of drawer in Ant Design, you can using `extra` prop for that.
</docs>
<template>
<a-radio-group v-model:value="placement" style="margin-right: 8px">
<a-radio value="top">top</a-radio>
<a-radio value="right">right</a-radio>
<a-radio value="bottom">bottom</a-radio>
<a-radio value="left">left</a-radio>
</a-radio-group>
<a-button type="primary" @click="showDrawer">Open</a-button>
<a-drawer
:width="500"
title="Basic Drawer"
:placement="placement"
:visible="visible"
@close="onClose"
>
<template #extra>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button>
</template>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</a-drawer>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const placement = ref<string>('left');
const visible = ref<boolean>(false);
const showDrawer = () => {
visible.value = true;
};
const onClose = () => {
visible.value = false;
};
return {
placement,
visible,
showDrawer,
onClose,
};
},
});
</script>

View File

@ -1,6 +1,6 @@
<docs>
---
order: 3
order: 4
title:
zh-CN: 抽屉表单
en-US: Submit form in drawer
@ -26,6 +26,7 @@ Use form in drawer with submit button.
:width="720"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
:footer-style="{ textAlign: 'right' }"
@close="onClose"
>
<a-form :model="form" :rules="rules" layout="vertical">
@ -96,22 +97,10 @@ Use form in drawer with submit button.
</a-col>
</a-row>
</a-form>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button>
</div>
</template>
</a-drawer>
</template>
<script lang="ts">

View File

@ -1,20 +1,24 @@
<template>
<demo-sort>
<basic />
<extra />
<placement />
<render-in-current />
<form-in-drawer />
<user-profile />
<multi-level-drawer />
<size />
</demo-sort>
</template>
<script lang="ts">
import Basic from './basic.vue';
import Extra from './extra.vue';
import Placement from './placement.vue';
import UserProfile from './user-profile.vue';
import MultiLevelDrawer from './multi-level-drawer.vue';
import FormInDrawer from './form-in-drawer.vue';
import RenderInCurrent from './render-in-current.vue';
import Size from './size.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
@ -25,11 +29,13 @@ export default defineComponent({
US,
components: {
Basic,
Extra,
Placement,
UserProfile,
MultiLevelDrawer,
FormInDrawer,
RenderInCurrent,
Size,
},
setup() {
return {};

View File

@ -1,6 +1,24 @@
<docs>
---
order: 5
order: 6
title:
zh-CN: 多层抽屉
en-US: Multi-level drawer
---
## zh-CN
在抽屉内打开新的抽屉用以解决多分支任务的复杂状况
## en-US
Open a new drawer on top of an existing drawer to handle multi branch tasks.
</docs>
<docs>
---
order: 6
title:
zh-CN: 多层抽屉
en-US: Multi-level drawer
@ -22,7 +40,8 @@ Open a new drawer on top of an existing drawer to handle multi branch tasks.
title="Multi-level drawer"
width="520"
:closable="false"
:visible="visible"
v-model:visible="visible"
:footer-style="{ textAlign: 'right' }"
@close="onClose"
>
<a-button type="primary" @click="showChildrenDrawer">Two-level drawer</a-button>
@ -30,27 +49,15 @@ Open a new drawer on top of an existing drawer to handle multi branch tasks.
title="Two-level Drawer"
width="320"
:closable="false"
:visible="childrenDrawer"
@close="onChildrenDrawerClose"
v-model:visible="childrenDrawer"
>
<a-button type="primary" @click="showChildrenDrawer">This is two-level drawer</a-button>
</a-drawer>
<div
:style="{
position: 'absolute',
bottom: 0,
width: '100%',
borderTop: '1px solid #e8e8e8',
padding: '10px 16px',
textAlign: 'right',
left: 0,
background: '#fff',
borderRadius: '0 0 4px 4px',
}"
>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button>
</div>
</template>
</a-drawer>
</template>
<script lang="ts">
@ -71,16 +78,12 @@ export default defineComponent({
const showChildrenDrawer = () => {
childrenDrawer.value = true;
};
const onChildrenDrawerClose = () => {
childrenDrawer.value = false;
};
return {
visible,
childrenDrawer,
showDrawer,
onClose,
showChildrenDrawer,
onChildrenDrawerClose,
};
},
});

View File

@ -1,6 +1,6 @@
<docs>
---
order: 2
order: 3
title:
zh-CN: 渲染在当前 DOM
en-US: Render in current dom
@ -40,7 +40,7 @@ Render in current dom. custom container, check getContainer.
:closable="false"
:visible="visible"
:get-container="false"
:wrap-style="{ position: 'absolute' }"
:style="{ position: 'absolute' }"
@close="onClose"
>
<p>Some contents...</p>

View File

@ -0,0 +1,61 @@
<docs>
---
order: 7
title:
zh-CN: 预设宽度
en-US: Presetted size
---
## zh-CN
抽屉的默认宽度为 `378px`另外还提供一个大号抽屉 `736px`可以用 size 属性来设置
## en-US
The default width (or height) of Drawer is `378px`, and there is a presetted large size `736px`.
</docs>
<template>
<a-button type="primary" @click="showDrawer('default')">Open Default Size (378px)</a-button>
<a-button type="primary" @click="showDrawer('large')">Open Large Size (736px)</a-button>
<a-drawer
title="Basic Drawer"
:size="size"
:placement="placement"
:visible="visible"
@close="onClose"
>
<template #extra>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button>
</template>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</a-drawer>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const visible = ref<boolean>(false);
const size = ref<string>('default');
const showDrawer = (val: string) => {
size.value = val;
visible.value = true;
};
const onClose = () => {
visible.value = false;
};
return {
visible,
size,
showDrawer,
onClose,
};
},
});
</script>

View File

@ -1,6 +1,6 @@
<docs>
---
order: 4
order: 5
title:
zh-CN: 信息预览抽屉
en-US: Preview drawer

View File

@ -15,34 +15,41 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr
- Processing subtasks. When subtasks are too heavy for a Popover and we still want to keep the subtasks in the context of the main task, Drawer comes very handy.
- When the same Form is needed in multiple places.
## API
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not. | boolean | true | |
| destroyOnClose | Whether to unmount child components on closing drawer or not. | boolean | false | |
| getContainer | Return the mounted node for Drawer. | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | |
| mask | Whether to show mask or not. | Boolean | true | |
| maskClosable | Clicking on the mask (area outside the Drawer) to close the Drawer or not. | boolean | true | |
| maskStyle | Style for Drawer's mask element. | object | {} | |
| title | The title for Drawer. | string\|slot | - | |
| visible(v-model) | Whether the Drawer dialog is visible or not. | boolean | false | |
| wrapClassName | The class name of the container of the Drawer dialog. | string | - | |
| wrapStyle | Style of wrapper element which **contains mask** compare to `drawerStyle` | object | - | |
| Props | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | --- |
| autoFocus | Whether Drawer should get focused after open | boolean | true | 3.0.0 |
| bodyStyle | Style of the drawer content part | CSSProperties | - | |
| className(old: wrapClassName) | The class name of the container of the Drawer dialog | string | - | 3.0.0 |
| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not | boolean | true | |
| closeIcon | Custom close icon | VNode \| slot | <CloseOutlined /> | 3.0.0 |
| contentWrapperStyle | Style of the drawer wrapper of content part | CSSProperties | 3.0.0 |
| destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false | |
| drawerStyle | Style of the popup layer element | object | - | |
| headerStyle | Style of the drawer header part | object | - | |
| bodyStyle | Style of the drawer content part | object | - | |
| width | Width of the Drawer dialog. | string\|number | 256 | |
| height | placement is `top` or `bottom`, height of the Drawer dialog. | string\|number | - | |
| zIndex | The `z-index` of the Drawer. | Number | 1000 | |
| placement | The placement of the Drawer. | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | |
| handle | After setting, the drawer is directly mounted on the DOM, and you can control the drawer to open or close through this `handle`. | VNode \| slot | - | |
| afterVisibleChange | Callback after the animation ends when switching drawers. | function(visible) | - | |
| keyboard | Whether support press esc to close | Boolean | true | |
| extra | Extra actions area at corner | VNode \| slot | - | 3.0.0 |
| footer | The footer for Drawer | VNode \| slot | - | 3.0.0 |
| footerStyle | Style of the drawer footer part | CSSProperties | - | 3.0.0 |
| forceRender | Prerender Drawer component forcely | boolean | - | false | 3.0.0 |
| getContainer | Return the mounted node for Drawer | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | |
| headerStyle | Style of the drawer header part | CSSProperties | - | 3.0.0 |
| height | Placement is `top` or `bottom`, height of the Drawer dialog | string \| number | 378 | |
| keyboard | Whether support press esc to close | boolean | true | |
| mask | Whether to show mask or not | Boolean | true | |
| maskClosable | Clicking on the mask (area outside the Drawer) to close the Drawer or not | boolean | true | |
| maskStyle | Style for Drawer's mask element | CSSProperties | {} | |
| placement | The placement of the Drawer | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | |
| push | Nested drawers push behavior | boolean \| {distance: string \| number} | { distance: 180} | 3.0.0 |
| size | presetted size of drawer, default `378px` and large `736px` | `default` \| `large` | `default` | 3.0.0 |
| style(old: wrapStyle) | Style of wrapper element which contains mask compare to drawerStyle | CSSProperties | - | 3.0.0 |
| title | The title for Drawer | string \| slot | - | |
| visible(v-model) | Whether the Drawer dialog is visible or not | boolean | - | |
| width | Width of the Drawer dialog | string \| number | 378 | |
| zIndex | The `z-index` of the Drawer | Number | 1000 | |
## Methods
| Name | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| afterVisibleChange | Callback after the animation ends when switching drawers. | function(visible) | - | |
| close | Specify a callback that will be called when a user clicks mask, close button or Cancel button. | function(e) | - | |

View File

@ -1,189 +1,237 @@
import type { CSSProperties } from 'vue';
import { inject, provide, nextTick, defineComponent } from 'vue';
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import {
inject,
nextTick,
defineComponent,
ref,
onMounted,
provide,
onBeforeMount,
onUpdated,
onUnmounted,
} from 'vue';
import { getPropsSlot } from '../_util/props-util';
import classnames from '../_util/classNames';
import VcDrawer from '../vc-drawer/src';
import VcDrawer from '../vc-drawer';
import PropTypes from '../_util/vue-types';
import BaseMixin from '../_util/BaseMixin';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import { getComponent, getOptionProps } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import { tuple, withInstall } from '../_util/type';
import omit from '../_util/omit';
const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
type placementType = typeof PlacementTypes[number];
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 };
const drawerProps = {
autoFocus: PropTypes.looseBool,
closable: PropTypes.looseBool.def(true),
closeIcon: PropTypes.VNodeChild.def(<CloseOutlined />),
destroyOnClose: PropTypes.looseBool,
forceRender: PropTypes.looseBool,
getContainer: PropTypes.any,
maskClosable: PropTypes.looseBool.def(true),
mask: PropTypes.looseBool.def(true),
maskStyle: PropTypes.object,
style: PropTypes.object,
size: {
type: String as PropType<sizeType>,
},
drawerStyle: PropTypes.object,
headerStyle: PropTypes.object,
bodyStyle: PropTypes.object,
contentWrapperStyle: PropTypes.object,
title: PropTypes.VNodeChild,
visible: PropTypes.looseBool,
className: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
zIndex: PropTypes.number,
prefixCls: PropTypes.string,
push: PropTypes.oneOfType([PropTypes.looseBool, { type: Object as PropType<PushState> }]).def(
defaultPushState,
),
placement: PropTypes.oneOf(PlacementTypes).def('right'),
keyboard: PropTypes.looseBool.def(true),
extra: PropTypes.VNodeChild,
footer: PropTypes.VNodeChild,
footerStyle: PropTypes.object,
level: PropTypes.any.def(null),
levelMove: PropTypes.any,
};
export type DrawerProps = Partial<ExtractPropTypes<typeof drawerProps>>;
const Drawer = defineComponent({
name: 'ADrawer',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
closable: PropTypes.looseBool.def(true),
destroyOnClose: PropTypes.looseBool,
getContainer: PropTypes.any,
maskClosable: PropTypes.looseBool.def(true),
mask: PropTypes.looseBool.def(true),
maskStyle: PropTypes.object,
wrapStyle: PropTypes.object,
bodyStyle: PropTypes.object,
headerStyle: PropTypes.object,
drawerStyle: PropTypes.object,
title: PropTypes.VNodeChild,
visible: PropTypes.looseBool,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256),
zIndex: PropTypes.number,
prefixCls: PropTypes.string,
placement: PropTypes.oneOf(PlacementTypes).def('right'),
level: PropTypes.any.def(null),
wrapClassName: PropTypes.string, // not use class like react, vue will add class to root dom
handle: PropTypes.VNodeChild,
afterVisibleChange: PropTypes.func,
keyboard: PropTypes.looseBool.def(true),
onClose: PropTypes.func,
'onUpdate:visible': PropTypes.func,
},
setup(props) {
props: drawerProps,
emits: ['update:visible', 'close', 'afterVisibleChange'],
setup(props, { emit, slots, attrs }) {
const sPush = ref(false);
const preVisible = ref(props.visible);
const destroyClose = ref(false);
const vcDrawer = ref(null);
const configProvider = inject('configProvider', defaultConfigProvider);
return {
configProvider,
destroyClose: false,
preVisible: props.visible,
parentDrawer: inject('parentDrawer', null),
};
},
data() {
return {
sPush: false,
};
},
beforeCreate() {
provide('parentDrawer', this);
},
mounted() {
// fix: delete drawer in child and re-render, no push started.
// <Drawer>{show && <Drawer />}</Drawer>
const { visible } = this;
if (visible && this.parentDrawer) {
this.parentDrawer.push();
}
},
updated() {
nextTick(() => {
if (this.preVisible !== this.visible && this.parentDrawer) {
if (this.visible) {
this.parentDrawer.push();
} else {
this.parentDrawer.pull();
}
}
this.preVisible = this.visible;
});
},
beforeUnmount() {
// unmount drawer in child, clear push.
if (this.parentDrawer) {
this.parentDrawer.pull();
}
},
methods: {
domFocus() {
if (this.$refs.vcDrawer) {
(this.$refs.vcDrawer as any).domFocus();
}
},
close(e: Event) {
this.$emit('update:visible', false);
this.$emit('close', e);
},
// onMaskClick(e) {
// if (!this.maskClosable) {
// return;
// }
// this.close(e);
// },
push() {
this.setState({
sPush: true,
const parentDrawerOpts = inject('parentDrawerOpts', null);
onBeforeMount(() => {
provide('parentDrawerOpts', {
setPush,
setPull,
});
},
pull() {
this.setState(
{
sPush: false,
},
() => {
this.domFocus();
},
);
},
onDestroyTransitionEnd() {
const isDestroyOnClose = this.getDestroyOnClose();
});
onMounted(() => {
const { visible } = props;
if (visible && parentDrawerOpts) {
parentDrawerOpts.setPush();
}
});
onUnmounted(() => {
if (parentDrawerOpts) {
parentDrawerOpts.setPull();
}
});
onUpdated(() => {
const { visible } = props;
nextTick(() => {
if (preVisible.value !== visible && parentDrawerOpts) {
if (visible) {
parentDrawerOpts.setPush();
} else {
parentDrawerOpts.setPull();
}
}
preVisible.value = visible;
});
});
const domFocus = () => {
vcDrawer.value?.domFocus?.();
};
const close = (e: Event) => {
emit('update:visible', false);
emit('close', e);
};
const afterVisibleChange = (visible: boolean) => {
emit('afterVisibleChange', visible);
};
const setPush = () => {
sPush.value = true;
};
const setPull = () => {
sPush.value = false;
nextTick(() => {
domFocus();
});
};
const onDestroyTransitionEnd = () => {
const isDestroyOnClose = getDestroyOnClose();
if (!isDestroyOnClose) {
return;
}
if (!this.visible) {
this.destroyClose = true;
(this as any).$forceUpdate();
if (!props.visible) {
destroyClose.value = true;
}
},
};
const getDestroyOnClose = () => {
return props.destroyOnClose && !props.visible;
};
const getPushTransform = (placement?: placementType) => {
const { push } = props;
let distance: number | string;
if (typeof push === 'boolean') {
distance = push ? defaultPushState.distance : 0;
} else {
distance = push!.distance;
}
distance = parseFloat(String(distance || 0));
getDestroyOnClose() {
return this.destroyOnClose && !this.visible;
},
// get drawar push width or height
getPushTransform(placement?: placementType) {
if (placement === 'left' || placement === 'right') {
return `translateX(${placement === 'left' ? 180 : -180}px)`;
return `translateX(${placement === 'left' ? distance : -distance}px)`;
}
if (placement === 'top' || placement === 'bottom') {
return `translateY(${placement === 'top' ? 180 : -180}px)`;
return `translateY(${placement === 'top' ? distance : -distance}px)`;
}
},
getRcDrawerStyle() {
const { zIndex, placement, wrapStyle } = this.$props;
const { sPush: push } = this.$data;
};
const getRcDrawerStyle = () => {
const { zIndex, placement, style, mask } = props;
const offsetStyle = mask ? {} : getOffsetStyle();
return {
zIndex,
transform: push ? this.getPushTransform(placement) : undefined,
...wrapStyle,
transform: sPush.value ? getPushTransform(placement) : undefined,
...offsetStyle,
...style,
};
},
renderHeader(prefixCls: string) {
const { closable, headerStyle } = this.$props;
const title = getComponent(this, 'title');
};
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;
}
const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`;
return (
<div class={headerClassName} style={headerStyle}>
{title && <div class={`${prefixCls}-title`}>{title}</div>}
{closable ? this.renderCloseIcon(prefixCls) : null}
<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>
);
},
renderCloseIcon(prefixCls: string) {
const { closable } = this;
};
const renderCloseIcon = (prefixCls: string) => {
const { closable } = props;
const $closeIcon = getPropsSlot(slots, props, 'closeIcon');
return (
closable && (
<button key="closer" onClick={this.close} aria-label="Close" class={`${prefixCls}-close`}>
<CloseOutlined />
<button key="closer" onClick={close} aria-label="Close" class={`${prefixCls}-close`}>
{$closeIcon}
</button>
)
);
},
// render drawer body dom
renderBody(prefixCls: string) {
if (this.destroyClose && !this.visible) {
};
const renderBody = (prefixCls: string) => {
if (destroyClose.value && !props.visible) {
return null;
}
this.destroyClose = false;
const { bodyStyle, drawerStyle } = this.$props;
destroyClose.value = false;
const { bodyStyle, drawerStyle } = props;
const containerStyle: CSSProperties = {};
const isDestroyOnClose = this.getDestroyOnClose();
const isDestroyOnClose = getDestroyOnClose();
if (isDestroyOnClose) {
// Increase the opacity transition, delete children after closing.
containerStyle.opacity = 0;
@ -194,74 +242,93 @@ const Drawer = defineComponent({
<div
class={`${prefixCls}-wrapper-body`}
style={{ ...containerStyle, ...drawerStyle }}
onTransitionend={this.onDestroyTransitionEnd}
onTransitionend={onDestroyTransitionEnd}
>
{this.renderHeader(prefixCls)}
{renderHeader(prefixCls)}
<div key="body" class={`${prefixCls}-body`} style={bodyStyle}>
{this.$slots.default?.()}
{slots.default?.()}
</div>
{renderFooter(prefixCls)}
</div>
);
},
},
render() {
const props: any = getOptionProps(this);
const {
prefixCls: customizePrefixCls,
width,
height,
visible,
placement,
wrapClassName,
mask,
...rest
} = props;
const haveMask = mask ? '' : 'no-mask';
const offsetStyle: CSSProperties = {};
if (placement === 'left' || placement === 'right') {
offsetStyle.width = typeof width === 'number' ? `${width}px` : width;
} else {
offsetStyle.height = typeof height === 'number' ? `${height}px` : height;
}
const handler = getComponent(this, 'handle') || false;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('drawer', customizePrefixCls);
const { class: className } = this.$attrs;
const vcDrawerProps: any = {
...this.$attrs,
...omit(rest, [
'closable',
'destroyOnClose',
'drawerStyle',
'headerStyle',
'bodyStyle',
'title',
'push',
'visible',
'getPopupContainer',
'rootPrefixCls',
'getPrefixCls',
'renderEmpty',
'csp',
'pageHeader',
'autoInsertSpaceInButton',
]),
onClose: this.close,
handler,
...offsetStyle,
prefixCls,
open: visible,
showMask: mask,
placement,
class: classnames({
[className as string]: !!className,
[wrapClassName]: !!wrapClassName,
[haveMask]: !!haveMask,
}),
wrapStyle: this.getRcDrawerStyle(),
ref: 'vcDrawer',
};
return <VcDrawer {...vcDrawerProps}>{this.renderBody(prefixCls)}</VcDrawer>;
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>
);
};
const getOffsetStyle = () => {
// https://github.com/ant-design/ant-design/issues/24287
const { visible, mask, placement, size, width, height } = props;
if (!visible && !mask) {
return {};
}
const offsetStyle: CSSProperties = {};
if (placement === 'left' || placement === 'right') {
const defaultWidth = size === 'large' ? 736 : 378;
offsetStyle.width = typeof width === 'undefined' ? defaultWidth : width;
} else {
const defaultHeight = size === 'large' ? 736 : 378;
offsetStyle.height = typeof height === 'undefined' ? defaultHeight : height;
}
return offsetStyle;
};
return () => {
const {
prefixCls: customizePrefixCls,
width,
height,
visible,
placement,
mask,
className,
...rest
} = props;
const offsetStyle = mask ? getOffsetStyle() : {};
const haveMask = mask ? '' : 'no-mask';
const getPrefixCls = configProvider.getPrefixCls;
const prefixCls = getPrefixCls('drawer', customizePrefixCls);
const vcDrawerProps: any = {
...attrs,
...omit(rest, [
'size',
'closeIcon',
'closable',
'destroyOnClose',
'drawerStyle',
'headerStyle',
'bodyStyle',
'title',
'push',
]),
...offsetStyle,
onClose: close,
afterVisibleChange,
handler: false,
prefixCls,
open: visible,
showMask: mask,
placement,
wrapperClassName: classnames({
[className as string]: className,
[haveMask]: !!haveMask,
}),
style: getRcDrawerStyle(),
ref: vcDrawer,
};
return <VcDrawer {...vcDrawerProps}>{renderBody(prefixCls)}</VcDrawer>;
};
},
});

View File

@ -6,33 +6,50 @@ subtitle: 抽屉
cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
---
屏幕边缘滑出的浮层面板。
## 何时使用
抽屉从父窗体边缘滑入,覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务,操作完成后,可以平滑地回到原任务。
- 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。
- 当需要在当前任务流中插入临时任务,创建或预览附加内容。比如展示协议条款,创建子对象。
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
| autoFocus | 抽屉展开后是否将焦点切换至其 Dom 节点 | boolean | true | 3.0.0 |
| bodyStyle | 可用于设置 Drawer 内容部分的样式 | CSSProperties | - | |
| className(原 wrapClassName) | 对话框外层容器的类名 | string | - | 3.0.0 |
| closable | 是否显示右上角的关闭按钮 | boolean | true | |
| closeIcon | 自定义关闭图标 | VNode \| slot | <CloseOutlined /> | 3.0.0 |
| contentWrapperStyle | 可用于设置 Drawer 包裹内容部分的样式 | CSSProperties | 3.0.0 |
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false | |
| drawerStyle | 用于设置 Drawer 弹出层的样式 | object | - | |
| extra | 抽屉右上角的操作区域 | VNode \| slot | - | 3.0.0 |
| footer | 抽屉的页脚 | VNode \| slot | - | 3.0.0 |
| footerStyle | 抽屉页脚部件的样式 | CSSProperties | - | 3.0.0 |
| forceRender | 预渲染 Drawer 内元素 | boolean | - | false | 3.0.0 |
| getContainer | 指定 Drawer 挂载的 HTML 节点 | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | |
| maskClosable | 点击蒙层是否允许关闭 | boolean | true | |
| headerStyle | 用于设置 Drawer 头部的样式 | CSSProperties | - | 3.0.0 |
| height | 高度, 在 `placement``top``bottom` 时使用 | string \| number | 378 | |
| keyboard | 是否支持键盘 esc 关闭 | boolean | true | |
| mask | 是否展示遮罩 | Boolean | true | |
| maskStyle | 遮罩样式 | object | {} | |
| maskClosable | 点击蒙层是否允许关闭 | boolean | true | |
| maskStyle | 遮罩样式 | CSSProperties | {} | |
| placement | 抽屉的方向 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | |
| push | 用于设置多层 Drawer 的推动行为 | boolean \| {distance: string \| number} | { distance: 180} | 3.0.0 |
| size | 预设抽屉宽度或高度default `378px` 和 large `736px` | `default` \| `large` | `default` | 3.0.0 |
| style(原 wrapStyle) | 可用于设置 Drawer 最外层容器的样式,和 `drawerStyle` 的区别是作用节点包括 `mask` | CSSProperties | - | 3.0.0 |
| title | 标题 | string \| slot | - | |
| visible(v-model) | Drawer 是否可见 | boolean | - | |
| wrapClassName | 对话框外层容器的类名 | string | - | |
| wrapStyle | 可用于设置 Drawer 最外层容器的样式,和 `drawerStyle` 的区别是作用节点包括 `mask` | object | - | |
| drawerStyle | 用于设置 Drawer 弹出层的样式 | object | - | |
| headerStyle | 用于设置 Drawer 头部的样式 | object | - | |
| bodyStyle | 可用于设置 Drawer 内容部分的样式 | object | - | |
| width | 宽度 | string \| number | 256 | |
| height | 高度, 在 `placement``top``bottom` 时使用 | string \| number | 256 | |
| width | 宽度 | string \| number | 378 | |
| zIndex | 设置 Drawer 的 `z-index` | Number | 1000 | |
| placement | 抽屉的方向 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | |
| handle | 设置后抽屉直接挂载到 DOM 上,你可以通过该 handle 控制抽屉打开关闭 | VNode \| slot | - | |
| afterVisibleChange | 切换抽屉时动画结束后的回调 | function(visible) | 无 | |
| keyboard | 是否支持键盘 esc 关闭 | boolean | true | |
## 方法
| 名称 | 描述 | 类型 | 默认值 | 版本 |
| ----- | ------------------------------------ | ----------- | ------ | ---- |
| close | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | 无 | |
| 名称 | 描述 | 类型 | 默认值 | 版本 |
| ------------------ | ------------------------------------ | ----------------- | ------ | ---- |
| afterVisibleChange | 切换抽屉时动画结束后的回调 | function(visible) | 无 | |
| close | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | 无 | |

View File

@ -1,12 +1,11 @@
@import '../../style/themes/index';
@import '../../style/themes/index.less';
// Preserve the typo for compatibility
// https://github.com/ant-design/ant-design/issues/14628
@dawer-prefix-cls: ~'@{ant-prefix}-drawer';
@drawer-prefix-cls: @dawer-prefix-cls;
@drawer-prefix-cls: ~'@{ant-prefix}-drawer';
@picker-prefix-cls: ~'@{ant-prefix}-picker';
.@{drawer-prefix-cls} {
@drawer-header-close-padding: ceil(((@drawer-header-close-size - @font-size-lg) / 2));
position: fixed;
z-index: @zindex-modal;
width: 0%;
@ -20,7 +19,10 @@
&-content-wrapper {
position: absolute;
width: 100%;
height: 100%;
}
.@{drawer-prefix-cls}-content {
width: 100%;
height: 100%;
@ -38,12 +40,17 @@
width: 100%;
transition: transform @animation-duration-slow @ease-base-out;
}
&.@{drawer-prefix-cls}-open.no-mask {
width: 0%;
}
}
&-left {
left: 0;
.@{drawer-prefix-cls} {
&-content-wrapper {
left: 0;
}
}
&.@{drawer-prefix-cls}-open {
.@{drawer-prefix-cls}-content-wrapper {
box-shadow: @shadow-1-right;
@ -84,9 +91,6 @@
height: 100%;
transition: transform @animation-duration-slow @ease-base-out;
}
&.@{drawer-prefix-cls}-open.no-mask {
height: 0%;
}
}
&-top {
@ -118,15 +122,12 @@
}
}
&.@{drawer-prefix-cls}-open {
.@{drawer-prefix-cls} {
&-mask {
height: 100%;
opacity: 1;
transition: none;
animation: antdDrawerFadeIn @animation-duration-slow @ease-base-out;
}
}
&.@{drawer-prefix-cls}-open .@{drawer-prefix-cls}-mask {
height: 100%;
opacity: 1;
transition: none;
animation: antdDrawerFadeIn @animation-duration-slow @ease-base-out;
pointer-events: auto;
}
&-title {
@ -147,19 +148,13 @@
}
&-close {
position: absolute;
top: 0;
right: 0;
z-index: @zindex-popup-close;
display: block;
width: 56px;
height: 56px;
padding: 0;
color: @text-color-secondary;
display: inline-block;
margin-right: 12px;
color: @modal-close-color;
font-weight: 700;
font-size: @font-size-lg;
font-style: normal;
line-height: 56px;
line-height: 1;
text-align: center;
text-transform: none;
text-decoration: none;
@ -179,27 +174,48 @@
&-header {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: @drawer-header-padding;
color: @text-color;
background: @drawer-bg;
border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
&-title {
display: flex;
align-items: center;
justify-content: space-between;
}
&-close-only {
padding-bottom: 0;
border: none;
}
}
&-header-no-title {
color: @text-color;
background: @drawer-bg;
&-wrapper-body {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
width: 100%;
height: 100%;
}
&-body {
flex-grow: 1;
padding: @drawer-body-padding;
overflow: auto;
font-size: @font-size-base;
line-height: @line-height-base;
word-wrap: break-word;
}
&-wrapper-body {
height: 100%;
overflow: auto;
&-footer {
flex-shrink: 0;
padding: @drawer-footer-padding-vertical @drawer-footer-padding-horizontal;
border-top: @border-width-base @border-style-base @border-color-split;
}
&-mask {
@ -212,12 +228,20 @@
opacity: 0;
filter: ~'alpha(opacity=45)';
transition: opacity @animation-duration-slow linear, height 0s ease @animation-duration-slow;
pointer-events: none;
}
&-open {
&-content {
box-shadow: @shadow-2;
}
}
.@{picker-prefix-cls} {
&-clear {
background: @popover-background;
}
}
}
@keyframes antdDrawerFadeIn {

View File

@ -1,3 +1,6 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './drawer';
@import './rtl';
// .popover-customize-bg(@drawer-prefix-cls, @popover-background);

View File

@ -0,0 +1,16 @@
@import '../../style/themes/index';
@drawer-prefix-cls: ~'@{ant-prefix}-drawer';
.@{drawer-prefix-cls} {
&-rtl {
direction: rtl;
}
&-close {
.@{drawer-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 12px;
}
}
}

View File

@ -494,6 +494,9 @@
@modal-footer-bg: transparent;
@modal-footer-border-color-split: @border-color-split;
@modal-mask-bg: fade(@black, 45%);
@modal-close-color: @text-color-secondary;
@modal-footer-padding-vertical: 10px;
@modal-footer-padding-horizontal: 16px;
// Progress
// --
@ -876,9 +879,12 @@
// Drawer
// ---
@drawer-header-padding: 16px 24px;
@drawer-body-padding: 24px;
@drawer-header-padding: @padding-md @padding-lg;
@drawer-body-padding: @padding-lg;
@drawer-bg: @component-background;
@drawer-footer-padding-vertical: @modal-footer-padding-vertical;
@drawer-footer-padding-horizontal: @modal-footer-padding-horizontal;
@drawer-header-close-size: 56px;
// Timeline
// ---

View File

@ -3,25 +3,27 @@
@drawer: drawer;
.@{drawer} {
position: fixed;
top: 0;
z-index: 9999;
> * {
transition: transform @duration @ease-in-out-circ, opacity @duration @ease-in-out-circ,
box-shaow @duration @ease-in-out-circ;
transition: width 0s ease @duration, height 0s ease @duration, transform @duration @ease-in-out-circ;
>* {
transition: transform @duration @ease-in-out-circ, opacity @duration @ease-in-out-circ, box-shadow @duration @ease-in-out-circ;
}
&.@{drawer}-open {
transition: transform @duration @ease-in-out-circ;
}
& &-mask {
background: #000;
opacity: 0;
width: 0;
width: 100%;
height: 0;
position: fixed;
position: absolute;
top: 0;
transition: opacity @duration @ease-in-out-circ, width 0s ease @duration,
left: 0;
transition: opacity @duration @ease-in-out-circ,
height 0s ease @duration;
display: block !important;
}
&-content-wrapper {
position: fixed;
position: absolute;
background: #fff;
}
&-content {
@ -77,41 +79,53 @@
}
&.@{drawer}-open {
width: 100%;
&.no-mask {
width: 0%;
}
}
}
&-left {
top: 0;
left: 0;
.@{drawer} {
&-handle {
right: -40px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
box-shadow: 2px 0 8px rgba(0, 0, 0, .15);
border-radius: 0 4px 4px 0;
}
}
&.@{drawer}-open {
.@{drawer} {
&-content-wrapper {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
box-shadow: 2px 0 8px rgba(0, 0, 0, .15);
}
}
}
}
&-right {
top: 0;
right: 0;
.@{drawer} {
&-content-wrapper {
right: 0;
}
&-handle {
left: -40px;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
box-shadow: -2px 0 8px rgba(0, 0, 0, .15);
border-radius: 4px 0 0 4px;
}
}
&.@{drawer}-open {
& .@{drawer} {
&-content-wrapper {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
box-shadow: -2px 0 8px rgba(0, 0, 0, .15);
}
}
&.no-mask {
// https://github.com/ant-design/ant-design/issues/18607
right: 1px;
transform: translateX(1px);
}
}
}
&-top,
@ -122,60 +136,74 @@
.@{drawer}-content {
width: 100%;
}
.@{drawer}-content {
height: 100%;
}
&.@{drawer}-open {
height: 100%;
&.no-mask {
height: 0%;
}
}
.@{drawer} {
&-handle {
left: 50%;
margin-left: -20px;
}
}
&.@{drawer}-open {
height: 100%;
}
}
&-top {
top: 0;
left: 0;
.@{drawer} {
&-handle {
top: auto;
bottom: -40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
border-radius: 0 0 4px 4px;
}
}
&.@{drawer}-open {
.@{drawer} {
&-wrapper {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
&-content-wrapper {
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
}
}
}
}
&-bottom {
bottom: 0;
left: 0;
.@{drawer} {
&-content-wrapper {
bottom: 0;
}
&-handle {
top: -40px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15);
box-shadow: 0 -2px 8px rgba(0, 0, 0, .15);
border-radius: 4px 4px 0 0;
}
}
&.@{drawer}-open {
.@{drawer} {
&-content-wrapper {
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15);
box-shadow: 0 -2px 8px rgba(0, 0, 0, .15);
}
}
&.no-mask {
// https://github.com/ant-design/ant-design/issues/18607
bottom: 1px;
transform: translateY(1px);
}
}
}
&.@{drawer}-open {
.@{drawer} {
&-mask {
opacity: 0.3;
width: 100%;
opacity: .3;
height: 100%;
animation: fadeIn 0.3s @ease-in-out-circ;
transition: none;
transition: opacity 0.3s @ease-in-out-circ;
}
&-handle {
&-icon {
@ -190,13 +218,4 @@
}
}
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 0.3;
}
}
}

View File

@ -0,0 +1,3 @@
import Drawer from './src/DrawerWrapper';
export default Drawer;

View File

@ -0,0 +1,513 @@
import { defineComponent, reactive, onMounted, onUpdated, onUnmounted, nextTick, watch } from 'vue';
import classnames from '../../_util/classNames';
import getScrollBarSize from '../../_util/getScrollBarSize';
import KeyCode from '../../_util/KeyCode';
import omit from '../../_util/omit';
import supportsPassive from '../../_util/supportsPassive';
import { DrawerChildProps } from './IDrawerPropTypes';
import type { IDrawerChildProps } from './IDrawerPropTypes';
import {
addEventListener,
dataToArray,
getTouchParentScroll,
isNumeric,
removeEventListener,
transformArguments,
transitionEndFun,
transitionStr,
windowIsUndefined,
} from './utils';
const currentDrawer: Record<string, boolean> = {};
const DrawerChild = defineComponent({
inheritAttrs: false,
props: DrawerChildProps,
emits: ['close', 'handleClick', 'change'],
setup(props, { emit, slots, expose }) {
const state = reactive({
levelDom: [],
dom: null,
contentWrapper: null,
contentDom: null,
maskDom: null,
handlerDom: null,
drawerId: null,
timeout: null,
passive: null,
startPos: {
x: null,
y: null,
},
});
onMounted(() => {
nextTick(() => {
if (!windowIsUndefined) {
state.passive = supportsPassive ? { passive: false } : false;
}
const { open, getContainer, showMask, autoFocus } = props;
const container = getContainer?.();
state.drawerId = `drawer_id_${Number(
(Date.now() + Math.random())
.toString()
.replace('.', Math.round(Math.random() * 9).toString()),
).toString(16)}`;
getLevelDom(props);
if (open) {
if (container && container.parentNode === document.body) {
currentDrawer[state.drawerId] = open;
}
// level;
openLevelTransition();
nextTick(() => {
if (autoFocus) {
domFocus();
}
});
if (showMask) {
props.scrollLocker?.lock();
}
}
});
});
onUpdated(() => {
const { open, getContainer, scrollLocker, showMask, autoFocus } = props;
const container = getContainer?.();
if (container && container.parentNode === document.body) {
currentDrawer[state.drawerId] = !!open;
}
openLevelTransition();
if (open) {
if (autoFocus) {
domFocus();
}
if (showMask) {
scrollLocker?.lock();
}
} else {
scrollLocker?.unLock();
}
});
onUnmounted(() => {
const { open, scrollLocker } = props;
delete currentDrawer[state.drawerId];
if (open) {
setLevelTransform(false);
document.body.style.touchAction = '';
}
scrollLocker?.unLock();
});
watch(
() => props.placement,
val => {
if (val) {
// test bug, dom
state.contentDom = null;
if (state.contentWrapper) {
state.contentWrapper.style.transition = `none`;
setTimeout(() => {
state.contentWrapper.style.transition = ``;
});
}
}
},
);
const domFocus = () => {
state.dom?.focus?.();
};
const removeStartHandler = (e: TouchEvent) => {
if (e.touches.length > 1) {
return;
}
state.startPos = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
};
const removeMoveHandler = (e: TouchEvent) => {
if (e.changedTouches.length > 1) {
return;
}
const currentTarget = e.currentTarget as HTMLElement;
const differX = e.changedTouches[0].clientX - state.startPos.x;
const differY = e.changedTouches[0].clientY - state.startPos.y;
if (
(currentTarget === state.maskDom ||
currentTarget === state.handlerDom ||
(currentTarget === state.contentDom &&
getTouchParentScroll(currentTarget, e.target as HTMLElement, differX, differY))) &&
e.cancelable
) {
e.preventDefault();
}
};
const transitionEnd = (e: TransitionEvent) => {
const dom: HTMLElement = e.target as HTMLElement;
removeEventListener(dom, transitionEndFun, transitionEnd);
dom.style.transition = '';
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.keyCode === KeyCode.ESC) {
e.stopPropagation();
onClose(e);
}
};
const onClose = (e: Event) => {
emit('close', e);
};
const onWrapperTransitionEnd = (e: TransitionEvent) => {
const { open, afterVisibleChange } = props;
if (e.target === state.contentWrapper && e.propertyName.match(/transform$/)) {
state.dom.style.transition = '';
if (!open && getCurrentDrawerSome()) {
document.body.style.overflowX = '';
if (state.maskDom) {
state.maskDom.style.left = '';
state.maskDom.style.width = '';
}
}
if (afterVisibleChange) {
afterVisibleChange(!!open);
}
}
};
const openLevelTransition = () => {
const { open, width, height } = props;
const { isHorizontal, placementName } = getHorizontalBoolAndPlacementName();
const contentValue = state.contentDom
? state.contentDom.getBoundingClientRect()[isHorizontal ? 'width' : 'height']
: 0;
const value = (isHorizontal ? width : height) || contentValue;
setLevelAndScrolling(open, placementName, value);
};
const setLevelTransform = (
open?: boolean,
placementName?: string,
value?: string | number,
right?: number,
) => {
const { placement, levelMove, duration, ease, showMask } = props;
// router
state.levelDom.forEach(dom => {
dom.style.transition = `transform ${duration} ${ease}`;
addEventListener(dom, transitionEndFun, transitionEnd);
let levelValue = open ? value : 0;
if (levelMove) {
const $levelMove = transformArguments(levelMove, { target: dom, open });
levelValue = open ? $levelMove[0] : $levelMove[1] || 0;
}
const $value = typeof levelValue === 'number' ? `${levelValue}px` : levelValue;
let placementPos = placement === 'left' || placement === 'top' ? $value : `-${$value}`;
placementPos =
showMask && placement === 'right' && right
? `calc(${placementPos} + ${right}px)`
: placementPos;
dom.style.transform = levelValue ? `${placementName}(${placementPos})` : '';
});
};
const setLevelAndScrolling = (
open?: boolean,
placementName?: string,
value?: string | number,
) => {
if (!windowIsUndefined) {
const right =
document.body.scrollHeight >
(window.innerHeight || document.documentElement.clientHeight) &&
window.innerWidth > document.body.offsetWidth
? getScrollBarSize(true)
: 0;
setLevelTransform(open, placementName, value, right);
toggleScrollingToDrawerAndBody(right);
}
emit('change', open);
};
const toggleScrollingToDrawerAndBody = (right: number) => {
const { getContainer, showMask, open } = props;
const container = getContainer?.();
// body
if (container && container.parentNode === document.body && showMask) {
const eventArray = ['touchstart'];
const domArray = [document.body, state.maskDom, state.handlerDom, state.contentDom];
if (open && document.body.style.overflow !== 'hidden') {
if (right) {
addScrollingEffect(right);
}
document.body.style.touchAction = 'none';
//
domArray.forEach((item, i) => {
if (!item) {
return;
}
addEventListener(
item,
eventArray[i] || 'touchmove',
i ? removeMoveHandler : removeStartHandler,
state.passive,
);
});
} else if (getCurrentDrawerSome()) {
document.body.style.touchAction = '';
if (right) {
remScrollingEffect(right);
}
//
domArray.forEach((item, i) => {
if (!item) {
return;
}
removeEventListener(
item,
eventArray[i] || 'touchmove',
i ? removeMoveHandler : removeStartHandler,
state.passive,
);
});
}
}
};
const addScrollingEffect = (right: number) => {
const { placement, duration, ease } = props;
const widthTransition = `width ${duration} ${ease}`;
const transformTransition = `transform ${duration} ${ease}`;
state.dom.style.transition = 'none';
switch (placement) {
case 'right':
state.dom.style.transform = `translateX(-${right}px)`;
break;
case 'top':
case 'bottom':
state.dom.style.width = `calc(100% - ${right}px)`;
state.dom.style.transform = 'translateZ(0)';
break;
default:
break;
}
clearTimeout(state.timeout);
state.timeout = setTimeout(() => {
if (state.dom) {
state.dom.style.transition = `${transformTransition},${widthTransition}`;
state.dom.style.width = '';
state.dom.style.transform = '';
}
});
};
const remScrollingEffect = (right: number) => {
const { placement, duration, ease } = props;
if (transitionStr) {
document.body.style.overflowX = 'hidden';
}
state.dom.style.transition = 'none';
let heightTransition: string;
let widthTransition = `width ${duration} ${ease}`;
const transformTransition = `transform ${duration} ${ease}`;
switch (placement) {
case 'left': {
state.dom.style.width = '100%';
widthTransition = `width 0s ${ease} ${duration}`;
break;
}
case 'right': {
state.dom.style.transform = `translateX(${right}px)`;
state.dom.style.width = '100%';
widthTransition = `width 0s ${ease} ${duration}`;
if (state.maskDom) {
state.maskDom.style.left = `-${right}px`;
state.maskDom.style.width = `calc(100% + ${right}px)`;
}
break;
}
case 'top':
case 'bottom': {
state.dom.style.width = `calc(100% + ${right}px)`;
state.dom.style.height = '100%';
state.dom.style.transform = 'translateZ(0)';
heightTransition = `height 0s ${ease} ${duration}`;
break;
}
default:
break;
}
clearTimeout(state.timeout);
state.timeout = setTimeout(() => {
if (state.dom) {
state.dom.style.transition = `${transformTransition},${
heightTransition ? `${heightTransition},` : ''
}${widthTransition}`;
state.dom.style.transform = '';
state.dom.style.width = '';
state.dom.style.height = '';
}
});
};
const getCurrentDrawerSome = () => !Object.keys(currentDrawer).some(key => currentDrawer[key]);
const getLevelDom = ({ level, getContainer }: IDrawerChildProps) => {
if (windowIsUndefined) {
return;
}
const container = getContainer?.();
const parent = container ? (container.parentNode as HTMLElement) : null;
state.levelDom = [];
if (level === 'all') {
const children: HTMLElement[] = parent ? Array.prototype.slice.call(parent.children) : [];
children.forEach((child: HTMLElement) => {
if (
child.nodeName !== 'SCRIPT' &&
child.nodeName !== 'STYLE' &&
child.nodeName !== 'LINK' &&
child !== container
) {
state.levelDom.push(child);
}
});
} else if (level) {
dataToArray(level).forEach(key => {
document.querySelectorAll(key).forEach(item => {
state.levelDom.push(item);
});
});
}
};
const getHorizontalBoolAndPlacementName = () => {
const { placement } = props;
const isHorizontal = placement === 'left' || placement === 'right';
const placementName = `translate${isHorizontal ? 'X' : 'Y'}`;
return {
isHorizontal,
placementName,
};
};
const getDerivedStateFromProps = (
props: IDrawerChildProps,
{ prevProps }: { prevProps: IDrawerChildProps },
) => {
const nextState = {
prevProps: props,
};
if (prevProps !== undefined) {
const { placement, level } = props;
if (placement !== prevProps.placement) {
// test bug, dom
state.contentDom = null;
}
if (level !== prevProps.level) {
getLevelDom(props);
}
}
return nextState;
};
expose({ getDerivedStateFromProps });
return () => {
const {
width,
height,
open: $open,
prefixCls,
placement,
level,
levelMove,
ease,
duration,
getContainer,
onChange,
afterVisibleChange,
showMask,
maskClosable,
maskStyle,
keyboard,
getOpenCount,
scrollLocker,
contentWrapperStyle,
style,
...otherProps
} = props;
//
const open = state.dom ? $open : false;
const wrapperClassName = classnames(prefixCls, {
[`${prefixCls}-${placement}`]: true,
[`${prefixCls}-open`]: open,
'no-mask': !showMask,
});
const { placementName } = getHorizontalBoolAndPlacementName();
//
// const defaultValue = !this.contentDom || !level ? '100%' : `${value}px`;
const placementPos = placement === 'left' || placement === 'top' ? '-100%' : '100%';
const transform = open ? '' : `${placementName}(${placementPos})`;
return (
<div
{...omit(otherProps, ['switchScrollingEffect', 'autoFocus'])}
tabindex={-1}
class={wrapperClassName}
style={style}
ref={(c: HTMLElement | null) => {
state.dom = c as HTMLElement;
}}
onKeydown={open && keyboard ? onKeyDown : undefined}
onTransitionend={onWrapperTransitionEnd}
>
{showMask && (
<div
class={`${prefixCls}-mask`}
onClick={maskClosable ? onClose : undefined}
style={maskStyle}
ref={c => {
state.maskDom = c as HTMLElement;
}}
/>
)}
<div
class={`${prefixCls}-content-wrapper`}
style={{
transform,
msTransform: transform,
width: isNumeric(width) ? `${width}px` : width,
height: isNumeric(height) ? `${height}px` : height,
...contentWrapperStyle,
}}
ref={c => {
state.contentWrapper = c as HTMLElement;
}}
>
<div
class={`${prefixCls}-content`}
ref={c => {
state.contentDom = c as HTMLElement;
}}
>
{slots.children?.()}
</div>
</div>
</div>
);
};
},
});
export default DrawerChild;

View File

@ -0,0 +1,118 @@
import Child from './DrawerChild';
import { initDefaultProps } from '../../_util/props-util';
import { Teleport, defineComponent, ref, watch } from 'vue';
import { DrawerProps } from './IDrawerPropTypes';
import type { IDrawerProps } from './IDrawerPropTypes';
const DrawerWrapper = defineComponent({
inheritAttrs: false,
props: initDefaultProps(DrawerProps, {
prefixCls: 'drawer',
placement: 'left',
getContainer: 'body',
level: 'all',
duration: '.3s',
ease: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)',
afterVisibleChange: () => {},
showMask: true,
maskClosable: true,
maskStyle: {},
wrapperClassName: null,
keyboard: true,
forceRender: false,
autoFocus: true,
}),
emits: ['handleClick', 'close'],
setup(props, { emit, expose, slots }) {
const dom = ref<HTMLElement>(null);
const container = ref(props.getContainer || null);
const open = ref<boolean>(props.open);
const $forceRender = ref<boolean>(props.forceRender);
watch(
() => props.open,
val => {
if (!dom.value) {
$forceRender.value = true;
open.value = false;
setTimeout(() => {
open.value = true;
});
} else {
open.value = val;
}
},
);
const getDerivedStateFromProps = (
props: IDrawerProps,
{ prevProps }: { prevProps: IDrawerProps },
) => {
const newState: {
open?: boolean;
prevProps: IDrawerProps;
} = {
prevProps: props,
};
if (typeof prevProps !== 'undefined' && props.open !== prevProps.open) {
newState.open = props.open;
}
return newState;
};
expose({ getDerivedStateFromProps });
const onHandleClick = (e: MouseEvent | KeyboardEvent) => {
emit('handleClick', e);
};
const onClose = (e: MouseEvent | KeyboardEvent) => {
emit('close', e);
};
return () => {
const { afterVisibleChange, getContainer, wrapperClassName, forceRender, ...otherProps } =
props;
let portal = null;
if (!getContainer) {
return (
<div class={wrapperClassName || null} ref={dom}>
<Child
v-slots={{ children: slots.default }}
{...otherProps}
open={open.value}
getContainer={() => dom.value}
onClose={onClose}
onHandleClick={onHandleClick}
></Child>
</div>
);
}
if ($forceRender.value || open.value || dom.value) {
portal = (
<Teleport to={container.value}>
<div class={wrapperClassName || null} ref={dom}>
<Child
v-slots={{ children: slots.default }}
{...props}
open={open.value}
getContainer={() => dom.value}
afterVisibleChange={afterVisibleChange}
onClose={onClose}
onHandleClick={onHandleClick}
/>
</div>
</Teleport>
);
}
return portal;
};
},
});
export default DrawerWrapper;

View File

@ -1,29 +1,33 @@
import PropTypes from '../../_util/vue-types';
import { PropType, ExtractPropTypes } from 'vue';
const IProps = {
width: PropTypes.any,
height: PropTypes.any,
defaultOpen: PropTypes.looseBool,
firstEnter: PropTypes.looseBool,
open: PropTypes.looseBool,
export type IPlacement = 'left' | 'top' | 'right' | 'bottom';
const Props = {
prefixCls: PropTypes.string,
placement: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
style: PropTypes.object,
placement: {
type: String as PropType<IPlacement>,
},
class: PropTypes.string,
level: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
levelMove: PropTypes.oneOfType([PropTypes.number, PropTypes.func, PropTypes.array]),
ease: PropTypes.string,
duration: PropTypes.string,
handler: PropTypes.any,
ease: PropTypes.string,
showMask: PropTypes.looseBool,
maskStyle: PropTypes.object,
className: PropTypes.string,
wrapStyle: PropTypes.object,
maskClosable: PropTypes.looseBool,
maskStyle: PropTypes.object,
afterVisibleChange: PropTypes.func,
keyboard: PropTypes.looseBool,
contentWrapperStyle: PropTypes.object,
autoFocus: PropTypes.looseBool,
open: PropTypes.looseBool,
};
const IDrawerProps = {
...IProps,
const DrawerProps = {
...Props,
wrapperClassName: PropTypes.string,
forceRender: PropTypes.looseBool,
getContainer: PropTypes.oneOfType([
@ -34,11 +38,16 @@ const IDrawerProps = {
]),
};
const IDrawerChildProps = {
...IProps,
type IDrawerProps = Partial<ExtractPropTypes<typeof DrawerProps>>;
const DrawerChildProps = {
...Props,
getContainer: PropTypes.func,
getOpenCount: PropTypes.func,
scrollLocker: PropTypes.any,
switchScrollingEffect: PropTypes.func,
};
export { IDrawerProps, IDrawerChildProps };
type IDrawerChildProps = Partial<ExtractPropTypes<typeof DrawerChildProps>>;
export { DrawerProps, DrawerChildProps, IDrawerProps, IDrawerChildProps };

View File

@ -1,5 +0,0 @@
// base in 1.7.7
// export this package's api
import Drawer from './Drawer';
export default Drawer;

View File

@ -1,47 +1,54 @@
export function dataToArray(vars) {
export function dataToArray(vars: any) {
if (Array.isArray(vars)) {
return vars;
}
return [vars];
}
const transitionEndObject = {
const transitionEndObject: Record<string, string> = {
transition: 'transitionend',
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
};
export const transitionStr = Object.keys(transitionEndObject).filter(key => {
export const transitionStr: string = Object.keys(transitionEndObject).filter(key => {
if (typeof document === 'undefined') {
return false;
}
const html = document.getElementsByTagName('html')[0];
return key in (html ? html.style : {});
})[0];
export const transitionEnd = transitionEndObject[transitionStr];
export const transitionEndFun: string = transitionEndObject[transitionStr];
export function addEventListener(target, eventType, callback, options) {
export function addEventListener(
target: HTMLElement,
eventType: string,
callback: (e: TouchEvent | Event) => void,
options?: any,
) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, options);
} else if (target.attachEvent) {
target.attachEvent(`on${eventType}`, callback);
} else if ((target as any).attachEvent) {
// tslint:disable-line
(target as any).attachEvent(`on${eventType}`, callback); // tslint:disable-line
}
}
export function removeEventListener(target, eventType, callback, options) {
export function removeEventListener(
target: HTMLElement,
eventType: string,
callback: (e: TouchEvent | Event) => void,
options?: any,
) {
if (target.removeEventListener) {
target.removeEventListener(eventType, callback, options);
} else if (target.attachEvent) {
target.detachEvent(`on${eventType}`, callback);
} else if ((target as any).attachEvent) {
// tslint:disable-line
(target as any).detachEvent(`on${eventType}`, callback); // tslint:disable-line
}
}
export function transformArguments(arg, cb) {
let result;
if (typeof arg === 'function') {
result = arg(cb);
} else {
result = arg;
}
export function transformArguments(arg: any, cb: any) {
const result = typeof arg === 'function' ? arg(cb) : arg;
if (Array.isArray(result)) {
if (result.length === 2) {
return result;
@ -51,9 +58,8 @@ export function transformArguments(arg, cb) {
return [result];
}
export const isNumeric = value => {
return !isNaN(parseFloat(value)) && isFinite(value); // eslint-disable-line
};
export const isNumeric = (value: string | number | undefined) =>
!isNaN(parseFloat(value as string)) && isFinite(value as number);
export const windowIsUndefined = !(
typeof window !== 'undefined' &&
@ -61,7 +67,12 @@ export const windowIsUndefined = !(
window.document.createElement
);
export const getTouchParentScroll = (root, currentTarget, differX, differY) => {
export const getTouchParentScroll = (
root: HTMLElement,
currentTarget: HTMLElement | Document | null,
differX: number,
differY: number,
): boolean => {
if (!currentTarget || currentTarget === document || currentTarget instanceof Document) {
return false;
}
@ -92,10 +103,10 @@ export const getTouchParentScroll = (root, currentTarget, differX, differY) => {
(isX &&
(!x ||
(x &&
((currentTarget.scrollLeft >= scrollX && scrollX < 0) ||
(currentTarget.scrollLeft <= 0 && scrollX > 0)))))
((currentTarget.scrollLeft >= scrollX && differX < 0) ||
(currentTarget.scrollLeft <= 0 && differX > 0)))))
) {
return getTouchParentScroll(root, currentTarget.parentNode, differX, differY);
return getTouchParentScroll(root, currentTarget.parentNode as HTMLElement, differX, differY);
}
return false;
};