@ -31,13 +31,24 @@ const Popover = {
// If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom.
removePadding : Boolean
removePadding : Boolean ,
// self-explanatory (i hope)
disabled : Boolean ,
// Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
overlayCenters : Boolean ,
// What selector (witin popover!) to use for determining center of popover
overlayCentersSelector : String
} ,
data ( ) {
return {
hidden : true ,
styles : { opacity : 0 } ,
oldSize : { width : 0 , height : 0 }
styles : { } ,
oldSize : { width : 0 , height : 0 } ,
// used to avoid blinking if hovered onto popover
graceTimeout : null
}
} ,
methods : {
@ -47,9 +58,7 @@ const Popover = {
} ,
updateStyles ( ) {
if ( this . hidden ) {
this . styles = {
opacity : 0
}
this . styles = { }
return
}
@ -57,14 +66,26 @@ const Popover = {
// its children are what are inside the slot. Expect only one v-slot:trigger.
const anchorEl = ( this . $refs . trigger && this . $refs . trigger . children [ 0 ] ) || this . $el
// SVGs don't have offsetWidth/Height, use fallback
const anchorWidth = anchorEl . offsetWidth || anchorEl . clientWidth
const anchorHeight = anchorEl . offsetHeight || anchorEl . clientHeight
const screenBox = anchorEl . getBoundingClientRect ( )
// Screen position of the origin point for popover
const origin = { x : screenBox . left + screenBox . width * 0.5 , y : screenBox . top }
const anchorWidth = anchorEl . offsetWidth || anchorEl . clientWidth
const anchorScreenBox = anchorEl . getBoundingClientRect ( )
const anchorStyle = getComputedStyle ( anchorEl )
const topPadding = parseFloat ( anchorStyle . paddingTop )
const bottomPadding = parseFloat ( anchorStyle . paddingBottom )
// Screen position of the origin point for popover = center of the anchor
const origin = {
x : anchorScreenBox . left + anchorWidth * 0.5 ,
y : anchorScreenBox . top + anchorHeight * 0.5
}
const content = this . $refs . content
const overlayCenter = this . overlayCentersSelector
? this . $refs . content . querySelector ( this . overlayCentersSelector )
: null
// Minor optimization, don't call a slow reflow call if we don't have to
const parentBounds = this . boundTo &&
const parentScreenBox = this . boundTo &&
( this . boundTo . x === 'container' || this . boundTo . y === 'container' ) &&
this . containerBoundingClientRect ( )
@ -73,64 +94,85 @@ const Popover = {
// What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default margin values to dodge the navbar
const xBounds = this . boundTo && this . boundTo . x === 'container' ? {
min : parentBounds . left + ( margin . left || 0 ) ,
max : parentBounds . right - ( margin . right || 0 )
min : parentScreenBox . left + ( margin . left || 0 ) ,
max : parentScreenBox . right - ( margin . right || 0 )
} : {
min : 0 + ( margin . left || 10 ) ,
max : window . innerWidth - ( margin . right || 10 )
}
const yBounds = this . boundTo && this . boundTo . y === 'container' ? {
min : parentBounds . top + ( margin . top || 0 ) ,
max : parentBounds . bottom - ( margin . bottom || 0 )
min : parentScreenBox . top + ( margin . top || 0 ) ,
max : parentScreenBox . bottom - ( margin . bottom || 0 )
} : {
min : 0 + ( margin . top || 50 ) ,
max : window . innerHeight - ( margin . bottom || 5 )
}
let horizOffset = 0
let vertOffset = 0
if ( overlayCenter ) {
const box = content . getBoundingClientRect ( )
const overlayCenterScreenBox = overlayCenter . getBoundingClientRect ( )
const leftInnerOffset = overlayCenterScreenBox . left - box . left
const topInnerOffset = overlayCenterScreenBox . top - box . top
horizOffset = - leftInnerOffset - overlayCenter . offsetWidth * 0.5
vertOffset = - topInnerOffset - overlayCenter . offsetWidth * 0.5
} else {
horizOffset = content . offsetWidth * - 0.5
vertOffset = content . offsetWidth * - 0.5
}
const leftBorder = origin . x + horizOffset
const rightBorder = origin . x - horizOffset
// If overflowing from left, move it so that it doesn't
if ( ( origin . x - content . offsetWidth * 0.5 ) < xBounds . min ) {
horizOffset += - ( origin . x - content . offsetWidth * 0.5 ) + xBounds . min
if ( leftBorder < xBounds . min ) {
horizOffset += xBounds . min - leftBorder
}
// If overflowing from right, move it so that it doesn't
if ( ( origin . x + horizOffset + content . offsetWidth * 0.5 ) > xBounds . max ) {
horizOffset -= ( origin . x + horizOffset + content . offsetWidth * 0.5 ) - xBounds . max
if ( rightBorder > xBounds . max ) {
horizOffset -= rightBorder - xBounds . max
}
// Default to whatever user wished with placement prop
let usingTop = this . placement !== 'bottom'
let translateX = 0
let translateY = 0
// Handle special cases, first force to displaying on top if there's not space on bottom,
// regardless of what placement value was. Then check if there's not space on top, and
// force to bottom, again regardless of what placement value was.
if ( origin . y + content . offsetHeight > yBounds . max ) usingTop = true
if ( origin . y - content . offsetHeight < yBounds . min ) usingTop = false
if ( overlayCenter ) {
translateX = origin . x + horizOffset
translateY = origin . y + vertOffset
} else {
// Default to whatever user wished with placement prop
let usingTop = this . placement !== 'bottom'
let vPadding = 0
if ( this . removePadding && usingTop ) {
const anchorStyle = getComputedStyle ( anchorEl )
vPadding = parseFloat ( anchorStyle . paddingTop ) + parseFloat ( anchorStyle . paddingBottom )
}
// Handle special cases, first force to displaying on top if there's not space on bottom,
// regardless of what placement value was. Then check if there's not space on top, and
// force to bottom, again regardless of what placement value was.
const topBoundary = origin . y - anchorHeight * 0.5 + ( this . removePadding ? topPadding : 0 )
const bottomBoundary = origin . y + anchorHeight * 0.5 - ( this . removePadding ? bottomPadding : 0 )
if ( bottomBoundary + content . offsetHeight > yBounds . max ) usingTop = true
if ( topBoundary - content . offsetHeight < yBounds . min ) usingTop = false
const yOffset = ( this . offset && this . offset . y ) || 0
const translateY = usingTop
? - anchorHeight + vPadding - yOffset - content . offsetHeight
: yOffset
const yOffset = ( this . offset && this . offset . y ) || 0
translateY = usingTop
? topBoundary - yOffset - content . offsetHeight
: bottomBoundary + yOffset
const xOffset = ( this . offset && this . offset . x ) || 0
const translateX = anchorWidth * 0.5 - content . offsetWidth * 0.5 + horizOffset + xOffset
const xOffset = ( this . offset && this . offset . x ) || 0
translateX = origin . x + horizOffset + xOffset
}
// Note, separate translateX and translateY avoids blurry text on chromium,
// single translate or translate3d resulted in blurry text.
this . styles = {
opacity : 1 ,
transform : ` translateX( ${ Math . round ( translateX ) } px) translateY( ${ Math . round ( translateY ) } px) `
left : ` ${ Math . round ( translateX ) } px ` ,
top : ` ${ Math . round ( translateY ) } px `
}
if ( parentScreenBox ) {
this . styles . maxWidth = ` ${ Math . round ( parentScreenBox . width ) } px `
}
} ,
showPopover ( ) {
if ( this . disabled ) return
const wasHidden = this . hidden
this . hidden = false
this . $nextTick ( ( ) => {
@ -141,13 +183,30 @@ const Popover = {
hidePopover ( ) {
if ( ! this . hidden ) this . $emit ( 'close' )
this . hidden = true
this . styles = { opacity : 0 }
} ,
onMouseenter ( e ) {
if ( this . trigger === 'hover' ) this . showPopover ( )
if ( this . trigger === 'hover' ) {
clearTimeout ( this . graceTimeout )
this . graceTimeout = null
this . showPopover ( )
}
} ,
onMouseleave ( e ) {
if ( this . trigger === 'hover' ) this . hidePopover ( )
if ( this . trigger === 'hover' ) {
this . graceTimeout = setTimeout ( ( ) => this . hidePopover ( ) , 1 )
}
} ,
onMouseenterContent ( e ) {
if ( this . trigger === 'hover' ) {
clearTimeout ( this . graceTimeout )
this . graceTimeout = null
this . showPopover ( )
}
} ,
onMouseleaveContent ( e ) {
if ( this . trigger === 'hover' ) {
this . graceTimeout = setTimeout ( ( ) => this . hidePopover ( ) , 1 )
}
} ,
onClick ( e ) {
if ( this . trigger === 'click' ) {
@ -160,8 +219,12 @@ const Popover = {
} ,
onClickOutside ( e ) {
if ( this . hidden ) return
if ( this . $refs . content . contains ( e . target ) ) return
if ( this . $el . contains ( e . target ) ) return
this . hidePopover ( )
} ,
onScroll ( e ) {
this . updateStyles ( )
}
} ,
updated ( ) {
@ -175,11 +238,17 @@ const Popover = {
this . oldSize = { width : content . offsetWidth , height : content . offsetHeight }
}
} ,
created ( ) {
mounted ( ) {
let scrollable = this . $refs . trigger . closest ( '.column.-scrollable' )
if ( ! scrollable ) scrollable = window
document . addEventListener ( 'click' , this . onClickOutside )
scrollable . addEventListener ( 'scroll' , this . onScroll )
} ,
unmounted ( ) {
beforeUnmount ( ) {
let scrollable = this . $refs . trigger . closest ( '.column.-scrollable' )
if ( ! scrollable ) scrollable = window
document . removeEventListener ( 'click' , this . onClickOutside )
scrollable . removeEventListener ( 'scroll' , this . onScroll )
this . hidePopover ( )
}
}