626 lines
20 KiB
JavaScript
626 lines
20 KiB
JavaScript
import classnames from 'classnames'
|
||
import Vue from 'vue'
|
||
import ref from 'vue-ref'
|
||
import BaseMixin from '../../_util/BaseMixin'
|
||
import { initDefaultProps, getEvents, getClass } from '../../_util/props-util'
|
||
import { cloneElement } from '../../_util/vnode'
|
||
import ContainerRender from '../../_util/ContainerRender'
|
||
import getScrollBarSize from '../../_util/getScrollBarSize'
|
||
import drawerProps from './drawerProps'
|
||
import {
|
||
dataToArray,
|
||
transitionEnd,
|
||
transitionStr,
|
||
addEventListener,
|
||
removeEventListener,
|
||
transformArguments,
|
||
isNumeric,
|
||
} from './utils'
|
||
|
||
function noop () {}
|
||
|
||
const currentDrawer = {}
|
||
const windowIsUndefined = !(
|
||
typeof window !== 'undefined' &&
|
||
window.document &&
|
||
window.document.createElement
|
||
)
|
||
|
||
Vue.use(ref, { name: 'ant-ref' })
|
||
const Drawer = {
|
||
mixins: [BaseMixin],
|
||
props: initDefaultProps(drawerProps, {
|
||
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: '',
|
||
}),
|
||
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,
|
||
}
|
||
},
|
||
mounted () {
|
||
this.$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 () {
|
||
this.$nextTick(() => {
|
||
// dom 没渲染时,重走一遍。
|
||
if (!this.sFirstEnter && this.container) {
|
||
this.$forceUpdate()
|
||
this.sFirstEnter = true
|
||
}
|
||
})
|
||
},
|
||
beforeDestroy () {
|
||
delete currentDrawer[this.drawerId]
|
||
delete this.isOpenChange
|
||
if (this.container) {
|
||
if (this.sOpen) {
|
||
this.setLevelDomTransform(false, true)
|
||
}
|
||
document.body.style.overflow = ''
|
||
// 拦不住。。直接删除;
|
||
if (this.getSelfContainer) {
|
||
this.container.parentNode.removeChild(this.container)
|
||
}
|
||
}
|
||
this.sFirstEnter = false
|
||
clearTimeout(this.timeout)
|
||
// 需要 didmount 后也会渲染,直接 unmount 将不会渲染,加上判断.
|
||
if (this.renderComponent) {
|
||
this.renderComponent({
|
||
afterClose: this.removeContainer,
|
||
onClose () { },
|
||
visible: false,
|
||
})
|
||
}
|
||
},
|
||
methods: {
|
||
onMaskTouchEnd (e) {
|
||
this.$emit('maskClick', 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) {
|
||
this.dom.style.transition = ''
|
||
if (!this.sOpen && this.getCurrentDrawerSome()) {
|
||
document.body.style.overflowX = ''
|
||
if (this.maskDom) {
|
||
this.maskDom.style.left = ''
|
||
this.maskDom.style.width = ''
|
||
}
|
||
}
|
||
}
|
||
},
|
||
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 { change } = this.$listeners
|
||
if (change && this.isOpenChange && this.sFirstEnter) {
|
||
change(open)
|
||
this.isOpenChange = false
|
||
}
|
||
},
|
||
getChildToRender (open) {
|
||
const {
|
||
prefixCls,
|
||
placement,
|
||
handler,
|
||
showMask,
|
||
maskStyle,
|
||
width,
|
||
height,
|
||
} = this.$props
|
||
const children = this.$slots.default
|
||
const wrapperClassname = classnames(prefixCls, {
|
||
[`${prefixCls}-${placement}`]: true,
|
||
[`${prefixCls}-open`]: open,
|
||
...getClass(this),
|
||
})
|
||
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 = (
|
||
<div class='drawer-handle'>
|
||
<i class='drawer-handle-icon' />
|
||
</div>
|
||
)
|
||
const { handler: handlerSlot } = this.$slots
|
||
const handlerSlotVnode = handlerSlot || handlerDefalut
|
||
const { click: handleIconClick } = getEvents(handlerSlotVnode)
|
||
handlerChildren = cloneElement(handlerSlotVnode, {
|
||
on: {
|
||
click: (e) => {
|
||
handleIconClick && handleIconClick()
|
||
this.onIconTouchEnd(e)
|
||
},
|
||
},
|
||
directives: [{
|
||
name: 'ant-ref',
|
||
value: (c) => {
|
||
this.handlerdom = c
|
||
},
|
||
}],
|
||
})
|
||
}
|
||
const domContProps = {
|
||
class: wrapperClassname,
|
||
directives: [{
|
||
name: 'ant-ref',
|
||
value: (c) => {
|
||
this.dom = c
|
||
},
|
||
}],
|
||
on: {
|
||
transitionend: this.onWrapperTransitionEnd,
|
||
},
|
||
}
|
||
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 (
|
||
<div
|
||
{...domContProps}
|
||
>
|
||
{showMask && (
|
||
<div
|
||
class={`${prefixCls}-mask`}
|
||
onClick={this.onMaskTouchEnd}
|
||
style={maskStyle}
|
||
{...{ directives: directivesMaskDom }}
|
||
/>
|
||
)}
|
||
<div
|
||
class={`${prefixCls}-content-wrapper`}
|
||
style={{
|
||
transform,
|
||
msTransform: transform,
|
||
width: isNumeric(width) ? `${width}px` : width,
|
||
height: isNumeric(height) ? `${height}px` : height,
|
||
}}
|
||
{...{ directives: directivesContentWrapper }}
|
||
>
|
||
<div
|
||
class={`${prefixCls}-content`}
|
||
{...{ directives: directivesContentDom }}
|
||
onTouchstart={open ? this.removeStartHandler : noop} // 跑用例用
|
||
onTouchmove={open ? this.removeMoveHandler : noop} // 跑用例用
|
||
>
|
||
{children}
|
||
</div>
|
||
{handlerChildren}
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
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
|
||
/**
|
||
* <div style="height: 300px">
|
||
* <div style="height: 900px"></div>
|
||
* </div>
|
||
* 在没设定 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 } = this.$props
|
||
const open = this.getOpen()
|
||
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 (
|
||
<div
|
||
class={wrapperClassName}
|
||
{...{ directives }}
|
||
>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
if (!this.container || (!open && !this.sFirstEnter)) {
|
||
return null
|
||
}
|
||
return (
|
||
<ContainerRender
|
||
parent={this}
|
||
visible
|
||
autoMount
|
||
autoDestroy={false}
|
||
getComponent={() => children}
|
||
getContainer={this.getSelfContainer}
|
||
children={({ renderComponent, removeContainer }) => {
|
||
this.renderComponent = renderComponent
|
||
this.removeContainer = removeContainer
|
||
return null
|
||
}}
|
||
>
|
||
</ContainerRender>
|
||
)
|
||
},
|
||
}
|
||
|
||
export default Drawer
|