import Vue from 'vue' import PropTypes from '../_util/vue-types' import contains from '../_util/Dom/contains' import { hasProp, getComponentFromProp, getEvents, filterEmpty } 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 } 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 }, } }, parent: self, el: div, render () { const { popupEvents, ...otherProps } = this.popupProps const p = { props: otherProps, on: popupEvents, ref: 'popup', // style: popupStyle, } return ( {getComponentFromProp(self, '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] this.childOriginEvents = getEvents(child) const newChildProps = { props: {}, on: {}, 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 }, }