import type { PropType , CSSProperties , ComponentPublicInstance , Ref , } from 'vue' ; import { defineComponent , ref , reactive , computed , nextTick , watch , inject , provide , Teleport , Transition , onUpdated , onMounted , onBeforeUnmount , } from 'vue' ; import { getPrefixCls } from '../_utils/global-config' ; import type { AnimationDuration , ClassName , EmitType } from '../_utils/types' ; import type { TriggerEvent , TriggerPosition } from '../_utils/constant' ; import { TRIGGER_EVENTS } from '../_utils/constant' ; import { getArrowStyle , getPopupStyle , PopupTranslate , getElementScrollRect , getScrollElements , } from './utils' ; import ResizeObserver from '../_components/resize-observer' ; import { getElement , off , on } from '../_utils/dom' ; import { isEmptyChildren , isComponentInstance , mergeFirstChild , } from '../_utils/vue-utils' ; import usePickSlots from '../_hooks/use-pick-slots' ; import { triggerInjectionKey } from './context' ; import { throttleByRaf } from '../_utils/throttle-by-raf' ; import usePopupManager from '../_hooks/use-popup-manager' ; import { useResizeObserver } from '../_hooks/use-resize-observer' ; export default defineComponent ( { name : 'Trigger' , inheritAttrs : false , props : { zh en vModel popupVisible : { type : Boolean , default : undefined } , zh en defaultPopupVisible : { type : Boolean , default : false } , zh en values trigger : { type : [ String , Array ] as PropType < TriggerEvent | TriggerEvent [ ] > , default : 'hover' , validator : ( value ) => { const values = [ ] . concat ( value ) ; for ( const value of values ) { if ( ! TRIGGER_EVENTS . includes ( value ) ) { return false ; } } return true ; } } , zh en values position : { type : String as PropType < TriggerPosition > , default : 'bottom' } , zh en disabled : { type : Boolean , default : false } , zh en popupOffset : { type : Number , default : 0 } , zh en popupTranslate : { type : [ Array , Object ] as PropType < PopupTranslate > } , zh en showArrow : { type : Boolean , default : false } , zh en alignPoint : { type : Boolean , default : false } , zh en popupHoverStay : { type : Boolean , default : true } , zh en blurToClose : { type : Boolean , default : true } , zh en clickToClose : { type : Boolean , default : true } , zh en clickOutsideToClose : { type : Boolean , default : true } , zh en unmountOnClose : { type : Boolean , default : true } , zh en contentClass : { type : [ String , Array , Object ] as PropType < ClassName > } , zh en contentStyle : { type : Object as PropType < CSSProperties > } , zh en arrowClass : { type : [ String , Array , Object ] as PropType < ClassName > } , zh en arrowStyle : { type : Object as PropType < CSSProperties > } , zh en popupStyle : { type : Object as PropType < CSSProperties > } , zh en animationName : { type : String , default : 'fade-in' } , zh en duration : { type : [ Number , Object ] as PropType < AnimationDuration > } , zh en mouseEnterDelay : { type : Number , default : 100 } , zh en mouseLeaveDelay : { type : Number , default : 100 } , zh en focusDelay : { type : Number , default : 0 } , zh en autoFitPopupWidth : { type : Boolean , default : false } , zh en autoFitPopupMinWidth : { type : Boolean , default : false } , zh en autoFixPosition : { type : Boolean , default : true } , zh en popupContainer : { type : [ String , Object ] as PropType < string | HTMLElement > } , zh us updateAtScroll : { type : Boolean , default : false } , autoFitTransformOrigin : { type : Boolean , default : false } , hideEmpty : { type : Boolean , default : false } , openedCls : { type : String } , zh en autoFitPosition : { type : Boolean , default : true } , zh en renderToBody : { type : Boolean , default : true } , zh en preventFocus : { type : Boolean , default : false } , onPopupVisibleChange : { type : [ Function , Array ] as PropType < EmitType < ( popupVisible : boolean ) => void > > } } , emits : [ 'update:popupVisible' , 'popupVisibleChange' , ] , zh en slot setup ( props , { emit , slots , attrs } ) { const prefixCls = getPrefixCls ( 'trigger' ) ; const triggerMethods = computed ( ( ) => ( [ ] as Array < TriggerEvent > ) . concat ( props . trigger ) ) ; const childrenRefs = new Set < Ref < HTMLElement > > ( ) ; const triggerCtx = inject ( triggerInjectionKey , undefined ) ; const triggerRef = ref < HTMLElement | ComponentPublicInstance > ( ) ; const triggerEle = computed < HTMLElement > ( ( ) => isComponentInstance ( triggerRef . value ) ? triggerRef . value . $el : triggerRef . value ) ; const popupRef = ref < HTMLElement > ( ) ; const popupVisible = ref ( props . defaultPopupVisible ) ; const popupPosition = ref ( props . position ) ; const popupStyle = ref < CSSProperties > ( { } ) ; const arrowStyle = ref < CSSProperties > ( { } ) ; const containerEle = ref < HTMLElement > ( ) ; const arrowRef = ref < HTMLElement > ( ) ; const mousePosition = ref ( { top : 0 , left : 0 } ) ; const computedVisible = computed ( ( ) => props . popupVisible ?? popupVisible . value ) ; const { zIndex } = usePopupManager ( { visible : computedVisible } ) ; let delayTimer = 0 ; let outsideListener = false ; const cleanDelayTimer = ( ) => { if ( delayTimer ) { window . clearTimeout ( delayTimer ) ; delayTimer = 0 ; } } ; const updateMousePosition = ( e ) => { if ( props . alignPoint ) { const { pageX , pageY } = e ; mousePosition . value = { top : pageY , left : pageX } ; } } ; const updatePopupStyle = ( ) => { if ( ! triggerEle . value || ! popupRef . value || ! containerEle . value ) { return ; } const containerRect = containerEle . value . getBoundingClientRect ( ) ; const triggerRect = props . alignPoint ? { top : mousePosition . value . top , bottom : mousePosition . value . top , left : mousePosition . value . left , right : mousePosition . value . left , scrollTop : mousePosition . value . top , scrollBottom : mousePosition . value . top , scrollLeft : mousePosition . value . left , scrollRight : mousePosition . value . left , width : 0 , height : 0 } : getElementScrollRect ( triggerEle . value , containerRect ) ; const popupRect = getElementScrollRect ( popupRef . value , containerRect ) ; const { style , position } = getPopupStyle ( props . position , containerRect , triggerRect , popupRect , { offset : props . popupOffset , translate : props . popupTranslate , customStyle : props . popupStyle , autoFitPosition : props . autoFitPosition , autoFitTransformOrigin : props . autoFitTransformOrigin } ) ; if ( props . autoFitPopupMinWidth ) { style . minWidth = `${ triggerRect . width }px` ; } else if ( props . autoFitPopupWidth ) { style . width = `${ triggerRect . width }px` ; } if ( popupPosition . value !== position ) { popupPosition . value = position ; } popupStyle . value = style ; if ( props . showArrow ) { arrowStyle . value = getArrowStyle ( position , triggerRect , popupRect , { customStyle : props . arrowStyle } ) ; } } ; const changeVisible = ( visible , delay ? ) => { if ( visible === computedVisible . value && delayTimer === 0 ) { return ; } const update = ( ) => { popupVisible . value = visible ; emit ( 'update:popupVisible' , visible ) ; emit ( 'popupVisibleChange' , visible ) ; if ( visible ) { nextTick ( ( ) => { updatePopupStyle ( ) ; } ) ; } } ; if ( delay ) { cleanDelayTimer ( ) ; if ( visible !== computedVisible . value ) { delayTimer = window . setTimeout ( update , delay ) ; } } else { update ( ) ; } } ; const handleClick = ( e ) => { if ( props . disabled || ! triggerMethods . value . includes ( 'click' ) || ( computedVisible . value && ! props . clickToClose ) ) { return ; } updateMousePosition ( e ) ; changeVisible ( ! computedVisible . value ) ; } ; const handleMouseEnter = ( e ) => { if ( props . disabled || ! triggerMethods . value . includes ( 'hover' ) ) { return ; } updateMousePosition ( e ) ; changeVisible ( true , props . mouseEnterDelay ) ; } ; const handleMouseEnterWithContext = ( e ) => { triggerCtx ?. onMouseenter ( e ) ; handleMouseEnter ( e ) ; } ; const handleMouseLeave = ( e ) => { if ( props . disabled || ! triggerMethods . value . includes ( 'hover' ) ) { return ; } changeVisible ( false , props . mouseLeaveDelay ) ; } ; const handleMouseLeaveWithContext = ( e ) => { triggerCtx ?. onMouseleave ( e ) ; handleMouseLeave ( e ) ; } ; const handleFocusin = ( e ) => { if ( props . disabled || ! triggerMethods . value . includes ( 'focus' ) ) { return ; } changeVisible ( true , props . focusDelay ) ; } ; const handleFocusout = ( e ) => { if ( props . disabled || ! triggerMethods . value . includes ( 'focus' ) ) { return ; } if ( ! props . blurToClose ) { return ; } changeVisible ( false ) ; } ; const handleContextmenu = ( e ) => { e . preventDefault ( ) ; if ( props . disabled || ! triggerMethods . value . includes ( 'contextMenu' ) || ( computedVisible . value && ! props . clickToClose ) ) { return ; } updateMousePosition ( e ) ; changeVisible ( ! computedVisible . value ) ; } ; const addChildRef = ( ref ) => { childrenRefs . add ( ref ) ; triggerCtx ?. addChildRef ( ref ) ; } ; const removeChildRef = ( ref ) => { childrenRefs . delete ( ref ) ; triggerCtx ?. removeChildRef ( ref ) ; } ; provide ( triggerInjectionKey , reactive ( { onMouseenter : handleMouseEnterWithContext , onMouseleave : handleMouseLeaveWithContext , addChildRef , removeChildRef } ) ) ; const removeOutsideListener = ( ) => { off ( document . documentElement , 'mousedown' , handleOutsideClick ) ; outsideListener = false ; } ; const contentSlot = usePickSlots ( slots , 'content' ) ; const hidePopup = computed ( ( ) => { return props . hideEmpty && isEmptyChildren ( contentSlot . value ?. ( ) ) ; } ) ; const handleOutsideClick = ( e ) => { if ( triggerEle . value ?. contains ( e . target as HTMLElement ) || popupRef . value ?. contains ( e . target as HTMLElement ) ) { return ; } for ( const item of childrenRefs ) { if ( item . value ?. contains ( e . target as HTMLElement ) ) { return ; } } removeOutsideListener ( ) ; changeVisible ( false ) ; } ; const handleScroll = throttleByRaf ( ( ) => { if ( computedVisible . value ) { updatePopupStyle ( ) ; } } ) ; const handleResize = ( ) => { if ( computedVisible . value ) { updatePopupStyle ( ) ; } } ; const handlePopupMouseDown = ( e ) => { if ( props . preventFocus ) { e . preventDefault ( ) ; } } ; triggerCtx ?. addChildRef ( popupRef ) ; const triggerCls = computed ( ( ) => { return computedVisible . value ? props . openedCls : undefined ; } ) ; let scrollElements ; watch ( computedVisible , ( value ) => { if ( props . clickOutsideToClose ) { if ( outsideListener && ! value ) { removeOutsideListener ( ) ; return ; } if ( ! outsideListener ) { on ( document . documentElement , 'mousedown' , handleOutsideClick ) ; outsideListener = true ; } } if ( props . updateAtScroll ) { if ( value ) { scrollElements = getScrollElements ( triggerEle . value ) ; for ( const item of scrollElements ) { item . addEventListener ( 'scroll' , handleScroll ) ; } } else if ( scrollElements ) { for ( const item of scrollElements ) { item . removeEventListener ( 'scroll' , handleScroll ) ; } } } } ) ; watch ( ( ) => [ props . autoFitPopupWidth , props . autoFitPopupMinWidth ] , ( ) => { if ( computedVisible . value ) { updatePopupStyle ( ) ; } } ) ; const { createResizeObserver , destroyResizeObserver } = useResizeObserver ( { elementRef : containerEle , onResize : handleResize } ) ; onMounted ( ( ) => { if ( props . popupContainer ) { containerEle . value = getElement ( props . popupContainer ) ; } else { containerEle . value = document . documentElement ; } createResizeObserver ( ) ; if ( computedVisible . value ) { updatePopupStyle ( ) ; } } ) ; onUpdated ( ( ) => { if ( computedVisible . value ) { updatePopupStyle ( ) ; } } ) ; onBeforeUnmount ( ( ) => { triggerCtx ?. removeChildRef ( popupRef ) ; destroyResizeObserver ( ) ; } ) ; return ( ) => { const children = slots . default ?. ( ) ?? [ ] ; mergeFirstChild ( children , { ref : triggerRef , class : triggerCls . value , onClick : handleClick , onMouseenter : handleMouseEnter , onMouseleave : handleMouseLeave , onFocusin : handleFocusin , onFocusout : handleFocusout , onContextmenu : handleContextmenu } ) ; return ( < > { props . autoFixPosition ? ( < ResizeObserver onResize = { handleResize } > { children } < /ResizeObserver>) : (children)} < Teleport to = { props . popupContainer ?? 'body' } disabled = { ! props . renderToBody } > < Transition name = { props . animationName } duration = { props . duration } > { ( ! props . unmountOnClose || computedVisible . value ) && ! hidePopup . value && ( < ResizeObserver onResize = { handleResize } > < div v - show = { computedVisible . value } ref = { popupRef } class = { [ `${ prefixCls }-popup` , `${ prefixCls }-position-${ popupPosition . value }` , ] } style = { { ... popupStyle . value , zIndex : zIndex . value } } trigger - placement = { popupPosition . value } onMouseenter = { handleMouseEnterWithContext } onMouseleave = { handleMouseLeaveWithContext } onMousedown = { handlePopupMouseDown } { ... attrs } > < div class = { [ `${ prefixCls }-content` , props . contentClass ] } style = { props . contentStyle } > { slots . content ?. ( ) } < /div> { props . showArrow && ( < div ref = { arrowRef } class = { [ `${ prefixCls }-arrow` , props . arrowClass ] } style = { arrowStyle . value } / > ) } < /div> < /ResizeObserver>)} < /Transition> < /Teleport> < />); } ; } } ) ;