ant-design-vue/components/trigger/index.vue

555 lines
17 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.

<script>
import Vue from 'vue'
import PropTypes from '../_util/vue-types'
import contains from '../_util/Dom/contains'
import { hasProp, getComponentFromProp } from '../_util/props-util'
import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout'
import addEventListener from '../_util/Dom/addEventListener'
import warning from '../_util/warning'
import Popup from './Popup'
import { getAlignFromPlacement, getPopupClassNameFromAlign, noop } from './utils'
import BaseMixin from '../_util/BaseMixin'
import { cloneElement, filterEmpty, getEvents } from '../_util/vnode'
function returnEmptyString () {
return ''
}
function returnDocument () {
return window.document
}
const ALL_HANDLERS = ['click', 'mousedown', 'touchstart', 'mouseenter',
'mouseleave', 'focus', 'blur', 'contextmenu']
export default {
name: 'Trigger',
props: {
action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).def([]),
showAction: PropTypes.any.def([]),
hideAction: PropTypes.any.def([]),
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
// onPopupVisibleChange: PropTypes.func.def(noop),
afterPopupVisibleChange: PropTypes.func.def(noop),
popup: PropTypes.any,
popupStyle: PropTypes.object.def({}),
prefixCls: PropTypes.string.def('rc-trigger-popup'),
popupClassName: PropTypes.string.def(''),
popupPlacement: PropTypes.string,
builtinPlacements: PropTypes.object,
popupTransitionName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
popupAnimation: PropTypes.any,
mouseEnterDelay: PropTypes.number.def(0),
mouseLeaveDelay: PropTypes.number.def(0.1),
zIndex: PropTypes.number,
focusDelay: PropTypes.number.def(0),
blurDelay: PropTypes.number.def(0.15),
getPopupContainer: PropTypes.func,
getDocument: PropTypes.func.def(returnDocument),
forceRender: PropTypes.bool,
destroyPopupOnHide: PropTypes.bool.def(false),
mask: PropTypes.bool.def(false),
maskClosable: PropTypes.bool.def(true),
// onPopupAlign: PropTypes.func.def(noop),
popupAlign: PropTypes.object.def({}),
popupVisible: PropTypes.bool,
defaultPopupVisible: PropTypes.bool.def(false),
maskTransitionName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
maskAnimation: PropTypes.string,
},
mixins: [BaseMixin],
data () {
const props = this.$props
let popupVisible
if (hasProp(this, 'popupVisible')) {
popupVisible = !!props.popupVisible
} else {
popupVisible = !!props.defaultPopupVisible
}
return {
sPopupVisible: popupVisible,
}
},
beforeCreate () {
ALL_HANDLERS.forEach((h) => {
this[`fire${h}`] = (e) => {
this.fireEvents(h, e)
}
})
},
mounted () {
this.$nextTick(() => {
this.updatedCal()
})
},
watch: {
popupVisible (val) {
if (val !== undefined) {
this.sPopupVisible = val
}
},
sPopupVisible (val) {
this.$nextTick(() => {
this.afterPopupVisibleChange(val)
})
},
},
updated () {
this.$nextTick(() => {
this.updatedCal()
})
},
beforeDestroy () {
this.clearDelayTimer()
this.clearOutsideHandler()
if (this._component) {
this._component.$destroy()
this._component = null
this.popupContainer.remove()
}
},
methods: {
updatedCal () {
const props = this.$props
const state = this.$data
// We must listen to `mousedown` or `touchstart`, edge case:
// https://github.com/ant-design/ant-design/issues/5804
// https://github.com/react-component/calendar/issues/250
// https://github.com/react-component/trigger/issues/50
if (state.sPopupVisible) {
let currentDocument
if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextmenuToShow())) {
currentDocument = props.getDocument()
this.clickOutsideHandler = addEventListener(currentDocument,
'mousedown', this.onDocumentClick)
}
// always hide on mobile
if (!this.touchOutsideHandler) {
currentDocument = currentDocument || props.getDocument()
this.touchOutsideHandler = addEventListener(currentDocument,
'touchstart', this.onDocumentClick)
}
// close popup when trigger type contains 'onContextmenu' and document is scrolling.
if (!this.contextmenuOutsideHandler1 && this.isContextmenuToShow()) {
currentDocument = currentDocument || props.getDocument()
this.contextmenuOutsideHandler1 = addEventListener(currentDocument,
'scroll', this.onContextmenuClose)
}
// close popup when trigger type contains 'onContextmenu' and window is blur.
if (!this.contextmenuOutsideHandler2 && this.isContextmenuToShow()) {
this.contextmenuOutsideHandler2 = addEventListener(window,
'blur', this.onContextmenuClose)
}
} else {
this.clearOutsideHandler()
}
},
onMouseenter (e) {
this.fireEvents('mouseenter', e)
this.delaySetPopupVisible(true, this.$props.mouseEnterDelay)
},
onMouseleave (e) {
this.fireEvents('mouseleave', e)
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay)
},
onPopupMouseenter () {
this.clearDelayTimer()
},
onPopupMouseleave (e) {
if (e.relatedTarget && !e.relatedTarget.setTimeout &&
this._component &&
this._component.$refs.popup &&
this._component.$refs.popup.getPopupDomNode &&
contains(this._component.$refs.popup.getPopupDomNode(), e.relatedTarget)) {
return
}
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay)
},
onFocus (e) {
this.fireEvents('focus', e)
// incase focusin and focusout
this.clearDelayTimer()
if (this.isFocusToShow()) {
this.focusTime = Date.now()
this.delaySetPopupVisible(true, this.$props.focusDelay)
}
},
onMousedown (e) {
this.fireEvents('mousedown', e)
this.preClickTime = Date.now()
},
onTouchstart (e) {
this.fireEvents('touchstart', e)
this.preTouchTime = Date.now()
},
onBlur (e) {
this.fireEvents('blur', e)
this.clearDelayTimer()
if (this.isBlurToHide()) {
this.delaySetPopupVisible(false, this.$props.blurDelay)
}
},
onContextmenu (e) {
e.preventDefault()
this.fireEvents('contextmenu', e)
this.setPopupVisible(true)
},
onContextmenuClose () {
if (this.isContextmenuToShow()) {
this.close()
}
},
onClick (event) {
this.fireEvents('click', event)
// focus will trigger click
if (this.focusTime) {
let preTime
if (this.preClickTime && this.preTouchTime) {
preTime = Math.min(this.preClickTime, this.preTouchTime)
} else if (this.preClickTime) {
preTime = this.preClickTime
} else if (this.preTouchTime) {
preTime = this.preTouchTime
}
if (Math.abs(preTime - this.focusTime) < 20) {
return
}
this.focusTime = 0
}
this.preClickTime = 0
this.preTouchTime = 0
event.preventDefault && event.preventDefault()
if (event.domEvent) {
event.domEvent.preventDefault()
}
const nextVisible = !this.$data.sPopupVisible
if (this.isClickToHide() && !nextVisible || nextVisible && this.isClickToShow()) {
this.setPopupVisible(!this.$data.sPopupVisible)
}
},
onDocumentClick (event) {
if (this.$props.mask && !this.$props.maskClosable) {
return
}
const target = event.target
const root = this.$el
const popupNode = this.getPopupDomNode()
if (!contains(root, target) && !contains(popupNode, target)) {
this.close()
}
},
getPopupDomNode () {
if (this._component && this._component.$refs.popup && this._component.$refs.popup.getPopupDomNode) {
return this._component.$refs.popup.getPopupDomNode()
}
return null
},
getRootDomNode () {
return this.$el
// return this.$el.children[0] || this.$el
},
handleGetPopupClassFromAlign (align) {
const className = []
const props = this.$props
const { popupPlacement, builtinPlacements, prefixCls } = props
if (popupPlacement && builtinPlacements) {
className.push(getPopupClassNameFromAlign(builtinPlacements, prefixCls, align))
}
if (props.getPopupClassNameFromAlign) {
className.push(props.getPopupClassNameFromAlign(align))
}
return className.join(' ')
},
getPopupAlign () {
const props = this.$props
const { popupPlacement, popupAlign, builtinPlacements } = props
if (popupPlacement && builtinPlacements) {
return getAlignFromPlacement(builtinPlacements, popupPlacement, popupAlign)
}
return popupAlign
},
renderComponent () {
const self = this
const mouseProps = {}
if (this.isMouseEnterToShow()) {
mouseProps.mouseenter = self.onPopupMouseenter
}
if (this.isMouseLeaveToHide()) {
mouseProps.mouseleave = self.onPopupMouseleave
}
const { prefixCls, destroyPopupOnHide, sPopupVisible,
popupStyle, popupClassName, action,
popupAnimation, handleGetPopupClassFromAlign, getRootDomNode,
mask, zIndex, popupTransitionName, getPopupAlign,
maskAnimation, maskTransitionName, getContainer } = self
const popupProps = {
prefixCls,
destroyPopupOnHide,
visible: sPopupVisible,
action,
align: getPopupAlign(),
animation: popupAnimation,
getClassNameFromAlign: handleGetPopupClassFromAlign,
getRootDomNode,
mask,
zIndex,
transitionName: popupTransitionName,
maskAnimation,
maskTransitionName,
getContainer,
popupClassName,
popupStyle,
popupEvents: {
align: self.$listeners.popupAlign || noop,
...mouseProps,
},
}
if (!this._component) {
const div = document.createElement('div')
this.getContainer().appendChild(div)
this._component = new Vue({
data () {
return {
popupProps: { ...popupProps },
}
},
el: div,
render () {
const { popupStyle, popupEvents, ...otherProps } = this.popupProps
const p = {
props: otherProps,
on: popupEvents,
ref: 'popup',
style: popupStyle,
}
return (
<Popup
{...p}
>
{getComponentFromProp(self, 'popup')}
</Popup>
)
},
})
} else {
this._component.popupProps = popupProps
}
},
getContainer () {
const { $props: props } = this
const popupContainer = document.createElement('div')
// Make sure default popup container will never cause scrollbar appearing
// https://github.com/react-component/trigger/issues/41
popupContainer.style.position = 'absolute'
popupContainer.style.top = '0'
popupContainer.style.left = '0'
popupContainer.style.width = '100%'
const mountNode = props.getPopupContainer
? props.getPopupContainer(this.$el) : props.getDocument().body
mountNode.appendChild(popupContainer)
this.popupContainer = popupContainer
return popupContainer
},
setPopupVisible (sPopupVisible) {
this.clearDelayTimer()
if (this.$data.sPopupVisible !== sPopupVisible) {
if (!hasProp(this, 'popupVisible')) {
this.setState({
sPopupVisible,
})
}
this.$listeners.popupVisibleChange && this.$listeners.popupVisibleChange(sPopupVisible)
}
},
delaySetPopupVisible (visible, delayS) {
const delay = delayS * 1000
this.clearDelayTimer()
if (delay) {
this.delayTimer = requestAnimationTimeout(() => {
this.setPopupVisible(visible)
this.clearDelayTimer()
}, delay)
} else {
this.setPopupVisible(visible)
}
},
clearDelayTimer () {
if (this.delayTimer) {
cancelAnimationTimeout(this.delayTimer)
this.delayTimer = null
}
},
clearOutsideHandler () {
if (this.clickOutsideHandler) {
this.clickOutsideHandler.remove()
this.clickOutsideHandler = null
}
if (this.contextmenuOutsideHandler1) {
this.contextmenuOutsideHandler1.remove()
this.contextmenuOutsideHandler1 = null
}
if (this.contextmenuOutsideHandler2) {
this.contextmenuOutsideHandler2.remove()
this.contextmenuOutsideHandler2 = null
}
if (this.touchOutsideHandler) {
this.touchOutsideHandler.remove()
this.touchOutsideHandler = null
}
},
createTwoChains (event) {
let fn = () => {
}
const events = this.$listeners
if (this.childOriginEvents[event] && events[event]) {
return this[`fire${event}`]
}
fn = this.childOriginEvents[event] || events[event] || fn
return fn
},
isClickToShow () {
const { action, showAction } = this.$props
return action.indexOf('click') !== -1 || showAction.indexOf('click') !== -1
},
isContextmenuToShow () {
const { action, showAction } = this.$props
return action.indexOf('contextmenu') !== -1 || showAction.indexOf('contextmenu') !== -1
},
isClickToHide () {
const { action, hideAction } = this.$props
return action.indexOf('click') !== -1 || hideAction.indexOf('click') !== -1
},
isMouseEnterToShow () {
const { action, showAction } = this.$props
return action.indexOf('hover') !== -1 || showAction.indexOf('mouseenter') !== -1
},
isMouseLeaveToHide () {
const { action, hideAction } = this.$props
return action.indexOf('hover') !== -1 || hideAction.indexOf('mouseleave') !== -1
},
isFocusToShow () {
const { action, showAction } = this.$props
return action.indexOf('focus') !== -1 || showAction.indexOf('focus') !== -1
},
isBlurToHide () {
const { action, hideAction } = this.$props
return action.indexOf('focus') !== -1 || hideAction.indexOf('blur') !== -1
},
forcePopupAlign () {
if (this.$data.sPopupVisible && this._component && this._component.$refs.popup && this._component.$refs.popup.$refs.alignInstance) {
this._component.$refs.popup.$refs.alignInstance.forceAlign()
}
},
fireEvents (type, e) {
if (this.childOriginEvents[type]) {
this.childOriginEvents[type](e)
}
this.__emit(type, e)
},
close () {
this.setPopupVisible(false)
},
},
render (h) {
const children = filterEmpty(this.$slots.default)
if (children.length > 1) {
warning(false, 'Trigger $slots.default.length > 1, just support only one default', true)
}
const child = children[0]
const events = getEvents(child)
// 黑科技vue暂未发现保留原事件的方法使用_ANT_TRIGGER_EVENT_HACK来判断事件是否更新
if (!events._ANT_TRIGGER_EVENT_HACK) {
this.childOriginEvents = events
}
const newChildProps = {
props: {},
on: { _ANT_TRIGGER_EVENT_HACK: () => {} },
key: 'trigger',
}
if (this.isContextmenuToShow()) {
newChildProps.on.contextmenu = this.onContextmenu
} else {
newChildProps.on.contextmenu = this.createTwoChains('contextmenu')
}
if (this.isClickToHide() || this.isClickToShow()) {
newChildProps.on.click = this.onClick
newChildProps.on.mousedown = this.onMousedown
newChildProps.on.touchstart = this.onTouchstart
} else {
newChildProps.on.click = this.createTwoChains('click')
newChildProps.on.mousedown = this.createTwoChains('mousedown')
newChildProps.on.touchstart = this.createTwoChains('onTouchstart')
}
if (this.isMouseEnterToShow()) {
newChildProps.on.mouseenter = this.onMouseenter
} else {
newChildProps.on.mouseenter = this.createTwoChains('mouseenter')
}
if (this.isMouseLeaveToHide()) {
newChildProps.on.mouseleave = this.onMouseleave
} else {
newChildProps.on.mouseleave = this.createTwoChains('mouseleave')
}
if (this.isFocusToShow() || this.isBlurToHide()) {
newChildProps.on.focus = this.onFocus
newChildProps.on.blur = this.onBlur
} else {
newChildProps.on.focus = this.createTwoChains('focus')
newChildProps.on.blur = this.createTwoChains('blur')
}
const { sPopupVisible, forceRender } = this
if (sPopupVisible || forceRender || this._component) {
this.renderComponent(h)
}
const trigger = cloneElement(child, newChildProps)
return trigger
},
}
</script>