import m from 'mithril' ; import classnames from 'classnames' ; import PopperJS , { Boundary } from 'popper.js' ; import { Classes , IAttrs , Style , safeCall , getClosest , elementIsOrContains } from '../../_shared' ; import { AbstractComponent } from '../abstract-component' ; import { IOverlayableAttrs , Overlay } from '../overlay' ; import { PopoverInteraction , PopoverPosition } from './popoverTypes' ; export interface IPopoverAttrs extends IOverlayableAttrs , IAttrs { default boundariesEl ? : Boundary | Element ; /** Close the popover on inner content click */ closeOnContentClick ? : boolean ; /** Inner content */ content : m . Children ; /** Initial open when in uncontrolled mode */ defaultIsOpen ? : boolean ; default hasArrow ? : boolean ; default hoverCloseDelay ? : number ; default hoverOpenDelay ? : number ; default interactionType ? : PopoverInteraction ; /**
     * Toggles visibility;
     * Specifying this attr will place the Popover in controlled mode
     * and will invoke the `onInteraction` callback for each open/close state change
     */ isOpen ? : boolean ; /**
     * Options to pass to the PopperJS instance;
     * see <a href="https://popper.js.org/popper-documentation.html#modifiers">HERE</a> for more details
     */ modifiers ? : PopperJS . Modifiers ; default position ? : PopoverPosition ; /** Callback invoked in controlled mode when a popover action will modify the open state */ onInteraction ? : ( nextOpenState : boolean , e : Event ) => void ; /**
     * Toggles visibilty when trigger is keyboard focused;
     * Only works when interactionType is hover or hover-trigger
     */ openOnTriggerFocus ? : boolean ; /** Overlay HTML container class */ overlayClass ? : string ; /** Overlay HTML container styles */ overlayStyle ? : Style ; /** Trigger element */ trigger : m . Vnode < any , any > ; } export interface IPopoverTriggerAttrs extends IAttrs { onclick ? ( e ) : void ; onmouseenter ? ( e ) : void ; onmouseleave ? ( e ) : void ; onfocus ? ( e ) : void ; onblur ? ( e ) : void ; [ htmlAttrs ] : any ; } export class Popover extends AbstractComponent < IPopoverAttrs > { private isOpen : boolean ; private popper ? : PopperJS & { options ? : PopperJS . PopperOptions ; } ; private trigger : m . VnodeDOM < IPopoverTriggerAttrs > ; public getDefaultAttrs ( ) { return { boundariesEl : 'window' , restoreFocus : false , hasBackdrop : false , hoverCloseDelay : 100 , hoverOpenDelay : 0 , interactionType : 'click' , position : 'bottom' , hasArrow : true } as IPopoverAttrs ; } public oninit ( vnode ) { super . oninit ( vnode ) ; const { isOpen , defaultIsOpen } = this . attrs ; this . isOpen = isOpen != null ? isOpen : defaultIsOpen != null ? defaultIsOpen : false ; } public onbeforeupdate ( vnode , old ) { super . onbeforeupdate ( vnode , old ) ; const isOpen = vnode . attrs . isOpen ; const wasOpen = old . attrs . isOpen ; if ( isOpen && ! wasOpen ) { this . isOpen = true ; } else if ( ! isOpen && wasOpen ) { this . isOpen = false ; } } public onupdate ( ) { if ( this . popper ) { this . popper . options . placement = this . attrs . position as PopperJS . Placement ; this . popper . scheduleUpdate ( ) ; } } public onremove ( ) { this . destroyPopper ( ) ; } public view ( ) { const { class : className , style , content , hasArrow , trigger , interactionType , inline , backdropClass , overlayClass , overlayStyle } = this . attrs ; this . trigger = trigger as m . VnodeDOM ; this . setTriggerAttrs ( ) ; const innerContent = m ( '' , { class : classnames ( Classes . POPOVER , className ) , onclick : this . handlePopoverClick , onmouseenter : this . handleTriggerMouseEnter , onmouseleave : this . handleTriggerMouseLeave , style } , [ hasArrow && m ( `.${ Classes . POPOVER_ARROW }` ) , m ( `.${ Classes . POPOVER_CONTENT }` , content ) ] ) ; return m . fragment ( { } , [ this . trigger , m ( Overlay , { restoreFocus : this . isClickInteraction ( ) , ... this . attrs as IOverlayableAttrs , backdropClass : classnames ( Classes . POPOVER_BACKDROP , backdropClass ) , class : overlayClass , closeOnOutsideClick : interactionType !== 'click-trigger' , content : innerContent , inline , isOpen : this . isOpen , onClose : this . handleOverlayClose , onOpened : this . handleOpened , onClosed : this . handleClosed , style : overlayStyle } ) ] ) ; } private handleOpened = ( contentEl ) => { if ( ! this . popper && contentEl ) { const popoverEl = contentEl . querySelector ( `.${ Classes . POPOVER }` ) ! ; this . createPopper ( popoverEl as HTMLElement ) ; safeCall ( this . attrs . onOpened , contentEl ) ; } } ; private handleClosed = ( ) => { this . destroyPopper ( ) ; safeCall ( this . attrs . onClosed ) ; } ; private handleOverlayClose = ( e ) => { const target = e . target as HTMLElement ; const isTriggerClick = elementIsOrContains ( this . trigger . dom as HTMLElement , target ) ; if ( ! isTriggerClick || e instanceof KeyboardEvent ) { this . isControlled ? this . handleInteraction ( e ) : this . isOpen = false ; } } ; private createPopper ( el ) { const { position , hasArrow , boundariesEl , modifiers } = this . attrs ; const options = { placement : position , modifiers : { arrow : { enabled : hasArrow , element : `.${ Classes . POPOVER_ARROW }` } , offset : { enabled : hasArrow , fn : ( data ) => this . getContentOffset ( data , el ) } , preventOverflow : { enabled : true , boundariesElement : boundariesEl , padding : 0 } , ... modifiers } } as PopperJS . PopperOptions ; this . popper = new PopperJS ( this . trigger . dom , el , options ) ; } private destroyPopper ( ) { if ( this . popper ) { this . popper . destroy ( ) ; this . popper = undefined ; } } private setTriggerAttrs ( ) { const isControlled = this . isControlled ; if ( ! this . trigger . attrs ) { this . trigger . attrs = { } ; } const triggerAttrs = this . trigger . attrs ; if ( this . isOpen ) { triggerAttrs . class = classnames ( triggerAttrs . className || triggerAttrs . class , Classes . ACTIVE , Classes . POPOVER_TRIGGER_ACTIVE ) ; } else triggerAttrs . class = triggerAttrs . className || triggerAttrs . class || '' ; const triggerEvents = { onmouseenter : triggerAttrs . onmouseenter , onmouseleave : triggerAttrs . onmouseleave , onfocus : triggerAttrs . onfocus , onblur : triggerAttrs . onblur , onclick : triggerAttrs . onclick } ; if ( this . isClickInteraction ( ) ) { triggerAttrs . onclick = ( e ) => { isControlled ? this . handleInteraction ( e ) : this . handleTriggerClick ( ) ; safeCall ( triggerEvents . onclick ) ; } ; } else { triggerAttrs . onmouseenter = ( e ) => { isControlled ? this . handleInteraction ( e ) : this . handleTriggerMouseEnter ( e ) ; safeCall ( triggerEvents . onmouseenter ) ; } ; triggerAttrs . onmouseleave = ( e ) => { isControlled ? this . handleInteraction ( e ) : this . handleTriggerMouseLeave ( e ) ; safeCall ( triggerEvents . onmouseleave ) ; } ; triggerAttrs . onfocus = ( e ) => { isControlled ? this . handleInteraction ( e ) : this . handleTriggerFocus ( e ) ; safeCall ( triggerEvents . onfocus ) ; } ; triggerAttrs . onblur = ( e ) => { isControlled ? this . handleInteraction ( e ) : this . handleTriggerBlur ( e ) ; safeCall ( triggerEvents . onblur ) ; } ; } } private handleInteraction ( e ) { safeCall ( this . attrs . onInteraction , ! this . isOpen , e ) ; } private handlePopoverClick = ( e ) => { const target = e . target as HTMLElement ; const hasDimiss = getClosest ( target , `.${ Classes . POPOVER_DISSMISS }` ) != null ; if ( this . attrs . closeOnContentClick || hasDimiss ) { this . isControlled ? this . handleInteraction ( e ) : this . isOpen = false ; } else ( e as any ) . redraw = false ; } ; private handleTriggerClick ( ) { this . isOpen = ! this . isOpen ; } private handleTriggerFocus ( e ) { if ( this . attrs . openOnTriggerFocus ) { this . handleTriggerMouseEnter ( e as any ) ; } else ( e as any ) . redraw = false ; } private handleTriggerBlur ( e ) { if ( this . attrs . openOnTriggerFocus ) { this . handleTriggerMouseLeave ( e as any ) ; } else ( e as any ) . redraw = false ; } private handleTriggerMouseEnter = ( e ) => { const { hoverOpenDelay , interactionType } = this . attrs ; if ( interactionType !== 'hover-trigger' ) { this . clearTimeouts ( ) ; } if ( ! this . isOpen && this . isHoverInteraction ( ) ) { if ( hoverOpenDelay ! > 0 ) { this . setTimeout ( ( ) => { this . isOpen = true ; m . redraw ( ) ; } , hoverOpenDelay ) ; } else { this . isOpen = true ; m . redraw ( ) ; } } ( e as any ) . redraw = false ; } ; private handleTriggerMouseLeave = ( e ) => { const { hoverCloseDelay } = this . attrs ; this . clearTimeouts ( ) ; if ( this . isOpen && this . isHoverInteraction ( ) ) { if ( hoverCloseDelay ! > 0 ) { this . setTimeout ( ( ) => { this . isOpen = false ; m . redraw ( ) ; } , hoverCloseDelay ) ; } else { this . isOpen = false ; m . redraw ( ) ; } } ( e as any ) . redraw = false ; } ; private isHoverInteraction ( ) { const interactionType = this . attrs . interactionType ; return interactionType === 'hover' || interactionType === 'hover-trigger' ; } private isClickInteraction ( ) { const interactionType = this . attrs . interactionType ; return interactionType === 'click' || interactionType === 'click-trigger' ; } private get isControlled ( ) { return this . attrs . isOpen != null ; } private getContentOffset = ( data , containerEl ) => { if ( ! this . attrs . hasArrow ) { return data ; } const placement = data . placement ; const isHorizontal = placement . includes ( 'left' ) || placement . includes ( 'right' ) ; const position = isHorizontal ? 'left' : 'top' ; const arrowSize = ( containerEl . children [ 0 ] as HTMLElement ) . clientHeight + 1 ; const offset = placement . includes ( 'top' ) || placement . includes ( 'left' ) ? - arrowSize : arrowSize ; data . offsets . popper [ position ] += offset ; return data ; } ; }