import classnames from 'classnames'; import { cloneVNode, withDirectives, Teleport, nextTick } from 'vue'; import antRef from '../../_util/ant-ref'; import BaseMixin from '../../_util/BaseMixin'; import { initDefaultProps, getSlot } from '../../_util/props-util'; import getScrollBarSize from '../../_util/getScrollBarSize'; import { IDrawerProps } from './IDrawerPropTypes'; import KeyCode from '../../_util/KeyCode'; import { dataToArray, transitionEnd, transitionStr, addEventListener, removeEventListener, transformArguments, isNumeric, } from './utils'; function noop() {} const currentDrawer = {}; const windowIsUndefined = !( typeof window !== 'undefined' && window.document && window.document.createElement ); const Drawer = { name: 'Drawer', mixins: [BaseMixin], inheritAttrs: false, directives: { 'ant-ref': antRef }, props: initDefaultProps(IDrawerProps, { prefixCls: 'drawer', placement: 'left', getContainer: 'body', level: 'all', duration: '.3s', ease: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)', firstEnter: false, // 记录首次进入. showMask: true, handler: true, maskStyle: {}, wrapperClassName: '', className: '', }), data() { this.levelDom = []; this.contentDom = null; this.maskDom = null; this.handlerdom = null; this.mousePos = null; this.sFirstEnter = this.firstEnter; this.timeout = null; this.children = null; this.drawerId = Number( (Date.now() + Math.random()).toString().replace('.', Math.round(Math.random() * 9)), ).toString(16); const open = this.open !== undefined ? this.open : !!this.defaultOpen; currentDrawer[this.drawerId] = open; this.orignalOpen = this.open; this.preProps = { ...this.$props }; return { sOpen: open, isOpenChange: undefined, passive: undefined, container: undefined, }; }, mounted() { nextTick(() => { if (!windowIsUndefined) { let passiveSupported = false; window.addEventListener( 'test', null, Object.defineProperty({}, 'passive', { get: () => { passiveSupported = true; return null; }, }), ); this.passive = passiveSupported ? { passive: false } : false; } const open = this.getOpen(); if (this.handler || open || this.sFirstEnter) { this.getDefault(this.$props); if (open) { this.isOpenChange = true; } this.$forceUpdate(); } }); }, watch: { open(val) { if (val !== undefined && val !== this.preProps.open) { this.isOpenChange = true; // 没渲染 dom 时,获取默认数据; if (!this.container) { this.getDefault(this.$props); } this.setState({ sOpen: open, }); } this.preProps.open = val; }, placement(val) { if (val !== this.preProps.placement) { // test 的 bug, 有动画过场,删除 dom this.contentDom = null; } this.preProps.placement = val; }, level(val) { if (this.preProps.level !== val) { this.getParentAndLevelDom(this.$props); } this.preProps.level = val; }, }, updated() { nextTick(() => { // dom 没渲染时,重走一遍。 if (!this.sFirstEnter && this.container) { this.$forceUpdate(); this.sFirstEnter = true; } }); }, beforeUnmount() { delete currentDrawer[this.drawerId]; delete this.isOpenChange; if (this.container) { if (this.sOpen) { this.setLevelDomTransform(false, true); } document.body.style.overflow = ''; } this.sFirstEnter = false; clearTimeout(this.timeout); }, methods: { onKeyDown(e) { if (e.keyCode === KeyCode.ESC) { e.stopPropagation(); this.$emit('close', e); } }, onMaskTouchEnd(e) { this.$emit('close', e); this.onTouchEnd(e, true); }, onIconTouchEnd(e) { this.$emit('handleClick', e); this.onTouchEnd(e); }, onTouchEnd(e, close) { if (this.open !== undefined) { return; } const open = close || this.sOpen; this.isOpenChange = true; this.setState({ sOpen: !open, }); }, onWrapperTransitionEnd(e) { if (e.target === this.contentWrapper && e.propertyName.match(/transform$/)) { const open = this.getOpen(); this.dom.style.transition = ''; if (!open && this.getCurrentDrawerSome()) { document.body.style.overflowX = ''; if (this.maskDom) { this.maskDom.style.left = ''; this.maskDom.style.width = ''; } } if (this.afterVisibleChange) { this.afterVisibleChange(!!open); } } }, getDefault(props) { this.getParentAndLevelDom(props); if (props.getContainer || props.parent) { this.container = this.defaultGetContainer(); } }, getCurrentDrawerSome() { return !Object.keys(currentDrawer).some(key => currentDrawer[key]); }, getSelfContainer() { return this.container; }, getParentAndLevelDom(props) { if (windowIsUndefined) { return; } const { level, getContainer } = props; this.levelDom = []; if (getContainer) { if (typeof getContainer === 'string') { const dom = document.querySelectorAll(getContainer)[0]; this.parent = dom; } if (typeof getContainer === 'function') { this.parent = getContainer(); } if (typeof getContainer === 'object' && getContainer instanceof window.HTMLElement) { this.parent = getContainer; } } if (!getContainer && this.container) { this.parent = this.container.parentNode; } if (level === 'all') { const children = Array.prototype.slice.call(this.parent.children); children.forEach(child => { if ( child.nodeName !== 'SCRIPT' && child.nodeName !== 'STYLE' && child.nodeName !== 'LINK' && child !== this.container ) { this.levelDom.push(child); } }); } else if (level) { dataToArray(level).forEach(key => { document.querySelectorAll(key).forEach(item => { this.levelDom.push(item); }); }); } }, setLevelDomTransform(open, openTransition, placementName, value) { const { placement, levelMove, duration, ease, getContainer } = this.$props; if (!windowIsUndefined) { this.levelDom.forEach(dom => { if (this.isOpenChange || openTransition) { /* eslint no-param-reassign: "error" */ dom.style.transition = `transform ${duration} ${ease}`; addEventListener(dom, transitionEnd, this.trnasitionEnd); 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; const placementPos = placement === 'left' || placement === 'top' ? $value : `-${$value}`; dom.style.transform = levelValue ? `${placementName}(${placementPos})` : ''; dom.style.msTransform = levelValue ? `${placementName}(${placementPos})` : ''; } }); // 处理 body 滚动 if (getContainer === 'body') { const eventArray = ['touchstart']; const domArray = [document.body, this.maskDom, this.handlerdom, this.contentDom]; const right = document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && window.innerWidth > document.body.offsetWidth ? getScrollBarSize(1) : 0; let widthTransition = `width ${duration} ${ease}`; const trannsformTransition = `transform ${duration} ${ease}`; if (open && document.body.style.overflow !== 'hidden') { document.body.style.overflow = 'hidden'; if (right) { document.body.style.position = 'relative'; document.body.style.width = `calc(100% - ${right}px)`; this.dom.style.transition = 'none'; switch (placement) { case 'right': this.dom.style.transform = `translateX(-${right}px)`; this.dom.style.msTransform = `translateX(-${right}px)`; break; case 'top': case 'bottom': this.dom.style.width = `calc(100% - ${right}px)`; this.dom.style.transform = 'translateZ(0)'; break; default: break; } clearTimeout(this.timeout); this.timeout = setTimeout(() => { this.dom.style.transition = `${trannsformTransition},${widthTransition}`; this.dom.style.width = ''; this.dom.style.transform = ''; this.dom.style.msTransform = ''; }); } // 手机禁滚 domArray.forEach((item, i) => { if (!item) { return; } addEventListener( item, eventArray[i] || 'touchmove', i ? this.removeMoveHandler : this.removeStartHandler, this.passive, ); }); } else if (this.getCurrentDrawerSome()) { document.body.style.overflow = ''; if ((this.isOpenChange || openTransition) && right) { document.body.style.position = ''; document.body.style.width = ''; if (transitionStr) { document.body.style.overflowX = 'hidden'; } this.dom.style.transition = 'none'; let heightTransition; switch (placement) { case 'right': { this.dom.style.transform = `translateX(${right}px)`; this.dom.style.msTransform = `translateX(${right}px)`; this.dom.style.width = '100%'; widthTransition = `width 0s ${ease} ${duration}`; if (this.maskDom) { this.maskDom.style.left = `-${right}px`; this.maskDom.style.width = `calc(100% + ${right}px)`; } break; } case 'top': case 'bottom': { this.dom.style.width = `calc(100% + ${right}px)`; this.dom.style.height = '100%'; this.dom.style.transform = 'translateZ(0)'; heightTransition = `height 0s ${ease} ${duration}`; break; } default: break; } clearTimeout(this.timeout); this.timeout = setTimeout(() => { this.dom.style.transition = `${trannsformTransition},${ heightTransition ? `${heightTransition},` : '' }${widthTransition}`; this.dom.style.transform = ''; this.dom.style.msTransform = ''; this.dom.style.width = ''; this.dom.style.height = ''; }); } domArray.forEach((item, i) => { if (!item) { return; } removeEventListener( item, eventArray[i] || 'touchmove', i ? this.removeMoveHandler : this.removeStartHandler, this.passive, ); }); } } } const { onChange } = this.$attrs; if (onChange && this.isOpenChange && this.sFirstEnter) { onChange(open); this.isOpenChange = false; } }, getChildToRender(open) { const { className, prefixCls, placement, handler, showMask, maskStyle, width, height, wrapStyle, keyboard, maskClosable, } = this.$props; const { class: cls, style } = this.$attrs; const children = getSlot(this); const wrapperClassname = classnames(prefixCls, { [`${prefixCls}-${placement}`]: true, [`${prefixCls}-open`]: open, [className]: !!className, 'no-mask': !showMask, [cls]: true, }); const isOpenChange = this.isOpenChange; const isHorizontal = placement === 'left' || placement === 'right'; const placementName = `translate${isHorizontal ? 'X' : 'Y'}`; // 百分比与像素动画不同步,第一次打用后全用像素动画。 // const defaultValue = !this.contentDom || !level ? '100%' : `${value}px`; const placementPos = placement === 'left' || placement === 'top' ? '-100%' : '100%'; const transform = open ? '' : `${placementName}(${placementPos})`; if (isOpenChange === undefined || isOpenChange) { const contentValue = this.contentDom ? this.contentDom.getBoundingClientRect()[isHorizontal ? 'width' : 'height'] : 0; const value = (isHorizontal ? width : height) || contentValue; this.setLevelDomTransform(open, false, placementName, value); } let handlerChildren; if (handler !== false) { const handlerDefalut = (
{}}>
); const { handler: handlerSlot } = this; const handlerSlotVnode = handlerSlot || handlerDefalut; const handleIconClick = handlerSlotVnode.props && handlerSlotVnode.props.onClick; handlerChildren = withDirectives( cloneVNode(handlerSlotVnode, { onClick: e => { handleIconClick && handleIconClick(); this.onIconTouchEnd(e); }, }), [ [ antRef, //directive c => { this.handlerdom = c; }, // value ], ], ); } const domContProps = { class: wrapperClassname, // directives: [ // { // name: 'ant-ref', // value: c => { // this.dom = c; // }, // }, // ], onTransitionend: this.onWrapperTransitionEnd, onKeydown: open && keyboard ? this.onKeyDown : noop, style: { ...wrapStyle, ...style }, }; // const directivesMaskDom = [ // { // name: 'ant-ref', // value: c => { // this.maskDom = c; // }, // }, // ]; // const directivesContentWrapper = [ // { // name: 'ant-ref', // value: c => { // this.contentWrapper = c; // }, // }, // ]; // const directivesContentDom = [ // { // name: 'ant-ref', // value: c => { // this.contentDom = c; // }, // }, // ]; return (
{ this.dom = c; }} {...domContProps} tabIndex={-1} > {showMask && (
{ this.maskDom = c; }} // {...{ directives: directivesMaskDom }} /> )}
{ this.contentWrapper = c; }} // {...{ directives: directivesContentWrapper }} >
{ this.contentDom = c; }} // {...{ directives: directivesContentDom }} onTouchstart={open ? this.removeStartHandler : noop} // 跑用例用 onTouchmove={open ? this.removeMoveHandler : noop} // 跑用例用 > {children}
{handlerChildren}
); }, getOpen() { return this.open !== undefined ? this.open : this.sOpen; }, getTouchParentScroll(root, currentTarget, differX, differY) { if (!currentTarget || currentTarget === document) { return false; } // root 为 drawer-content 设定了 overflow, 判断为 root 的 parent 时结束滚动; if (currentTarget === root.parentNode) { return true; } const isY = Math.max(Math.abs(differX), Math.abs(differY)) === Math.abs(differY); const isX = Math.max(Math.abs(differX), Math.abs(differY)) === Math.abs(differX); const scrollY = currentTarget.scrollHeight - currentTarget.clientHeight; const scrollX = currentTarget.scrollWidth - currentTarget.clientWidth; /** *
*
*
* 在没设定 overflow: auto 或 scroll 时,currentTarget 里获取不到 scrollTop 或 scrollLeft, * 预先用 scrollTo 来滚动,如果取出的值跟滚动前取出不同,则 currnetTarget 被设定了 overflow; 否则就是上面这种。 */ const t = currentTarget.scrollTop; const l = currentTarget.scrollLeft; if (currentTarget.scrollTo) { currentTarget.scrollTo(currentTarget.scrollLeft + 1, currentTarget.scrollTop + 1); } const currentT = currentTarget.scrollTop; const currentL = currentTarget.scrollLeft; if (currentTarget.scrollTo) { currentTarget.scrollTo(currentTarget.scrollLeft - 1, currentTarget.scrollTop - 1); } if ( (isY && (!scrollY || !(currentT - t) || (scrollY && ((currentTarget.scrollTop >= scrollY && differY < 0) || (currentTarget.scrollTop <= 0 && differY > 0))))) || (isX && (!scrollX || !(currentL - l) || (scrollX && ((currentTarget.scrollLeft >= scrollX && differX < 0) || (currentTarget.scrollLeft <= 0 && differX > 0))))) ) { return this.getTouchParentScroll(root, currentTarget.parentNode, differX, differY); } return false; }, removeStartHandler(e) { if (e.touches.length > 1) { return; } this.startPos = { x: e.touches[0].clientX, y: e.touches[0].clientY, }; }, removeMoveHandler(e) { if (e.changedTouches.length > 1) { return; } const currentTarget = e.currentTarget; const differX = e.changedTouches[0].clientX - this.startPos.x; const differY = e.changedTouches[0].clientY - this.startPos.y; if ( currentTarget === this.maskDom || currentTarget === this.handlerdom || (currentTarget === this.contentDom && this.getTouchParentScroll(currentTarget, e.target, differX, differY)) ) { e.preventDefault(); } }, trnasitionEnd(e) { removeEventListener(e.target, transitionEnd, this.trnasitionEnd); e.target.style.transition = ''; }, defaultGetContainer() { if (windowIsUndefined) { return null; } const container = document.createElement('div'); this.parent.appendChild(container); if (this.wrapperClassName) { container.className = this.wrapperClassName; } return container; }, }, render() { const { getContainer, wrapperClassName, handler, forceRender } = this.$props; const open = this.getOpen(); let portal = null; currentDrawer[this.drawerId] = open ? this.container : open; const children = this.getChildToRender(this.sFirstEnter ? open : false); if (!getContainer) { // const directives = [ // { // name: 'ant-ref', // value: c => { // this.container = c; // }, // }, // ]; return (
{ this.container = c; }} > {children}
); } if (!this.container || (!open && !this.sFirstEnter)) { return null; } // 如果有 handler 为内置强制渲染; const $forceRender = !!handler || forceRender; if ($forceRender || open || this.dom) { portal = {children}; } return portal; }, }; export default Drawer;