2020-02-28 16:39:47 +00:00
|
|
|
const Popover = {
|
|
|
|
name: 'Popover',
|
|
|
|
props: {
|
|
|
|
// Action to trigger popover: either 'hover' or 'click'
|
|
|
|
trigger: String,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
// Either 'top' or 'bottom'
|
|
|
|
placement: String,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
// Takes object with properties 'x' and 'y', values of these can be
|
|
|
|
// 'container' for using offsetParent as boundaries for either axis
|
|
|
|
// or 'viewport'
|
|
|
|
boundTo: Object,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-06-16 17:20:36 +03:00
|
|
|
// Takes a selector to use as a replacement for the parent container
|
|
|
|
// for getting boundaries for x an y axis
|
|
|
|
boundToSelector: String,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
// Takes a top/bottom/left/right object, how much space to leave
|
|
|
|
// between boundary and popover element
|
|
|
|
margin: Object,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
// Takes a x/y object and tells how many pixels to offset from
|
|
|
|
// anchor point on either axis
|
|
|
|
offset: Object,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-07-03 12:56:31 +03:00
|
|
|
// Replaces the classes you may want for the popover container.
|
|
|
|
// Use 'popover-default' in addition to get the default popover
|
|
|
|
// styles with your custom class.
|
2020-11-25 18:33:08 +02:00
|
|
|
popoverClass: String,
|
2021-02-25 10:56:16 +02:00
|
|
|
|
2020-11-25 18:33:08 +02:00
|
|
|
// 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
|
2020-02-28 16:39:47 +00:00
|
|
|
},
|
|
|
|
data () {
|
|
|
|
return {
|
|
|
|
hidden: true,
|
|
|
|
styles: { opacity: 0 },
|
|
|
|
oldSize: { width: 0, height: 0 }
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2020-06-16 17:20:36 +03:00
|
|
|
containerBoundingClientRect () {
|
2020-06-16 15:12:44 +00:00
|
|
|
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
|
2020-06-16 17:20:36 +03:00
|
|
|
return container.getBoundingClientRect()
|
|
|
|
},
|
2020-02-28 16:39:47 +00:00
|
|
|
updateStyles () {
|
|
|
|
if (this.hidden) {
|
|
|
|
this.styles = {
|
|
|
|
opacity: 0
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Popover will be anchored around this element, trigger ref is the container, so
|
2021-04-07 22:42:34 +03:00
|
|
|
// its children are what are inside the slot. Expect only one v-slot:trigger.
|
2020-02-28 16:39:47 +00:00
|
|
|
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
2021-03-15 11:02:16 +02:00
|
|
|
// SVGs don't have offsetWidth/Height, use fallback
|
|
|
|
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
|
2022-06-08 03:08:03 +03:00
|
|
|
const anchorScreenBox = anchorEl.getBoundingClientRect()
|
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
// Screen position of the origin point for popover
|
2022-06-08 03:08:03 +03:00
|
|
|
const origin = { x: anchorScreenBox.left, y: anchorScreenBox.top }
|
2020-02-28 16:39:47 +00:00
|
|
|
const content = this.$refs.content
|
2022-06-08 03:08:03 +03:00
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
// Minor optimization, don't call a slow reflow call if we don't have to
|
2022-06-08 03:08:03 +03:00
|
|
|
const parentScreenBox = this.boundTo &&
|
2020-02-28 16:39:47 +00:00
|
|
|
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
2020-06-16 17:20:36 +03:00
|
|
|
this.containerBoundingClientRect()
|
|
|
|
|
2020-02-28 16:39:47 +00:00
|
|
|
const margin = this.margin || {}
|
|
|
|
|
|
|
|
// 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' ? {
|
2022-06-08 03:08:03 +03:00
|
|
|
min: parentScreenBox.left + (margin.left || 0),
|
|
|
|
max: parentScreenBox.right - (margin.right || 0)
|
2020-02-28 16:39:47 +00:00
|
|
|
} : {
|
|
|
|
min: 0 + (margin.left || 10),
|
|
|
|
max: window.innerWidth - (margin.right || 10)
|
|
|
|
}
|
|
|
|
|
|
|
|
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
|
2022-06-08 03:08:03 +03:00
|
|
|
min: parentScreenBox.top + (margin.top || 0),
|
|
|
|
max: parentScreenBox.bottom - (margin.bottom || 0)
|
2020-02-28 16:39:47 +00:00
|
|
|
} : {
|
|
|
|
min: 0 + (margin.top || 50),
|
|
|
|
max: window.innerHeight - (margin.bottom || 5)
|
|
|
|
}
|
|
|
|
|
|
|
|
let horizOffset = 0
|
|
|
|
// If overflowing from left, move it so that it doesn't
|
2022-06-08 03:08:03 +03:00
|
|
|
if ((origin.x) < xBounds.min) {
|
|
|
|
horizOffset += -origin.x + xBounds.min
|
2020-02-28 16:39:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If overflowing from right, move it so that it doesn't
|
2022-06-08 03:08:03 +03:00
|
|
|
if ((origin.x + horizOffset + content.offsetWidth) > xBounds.max) {
|
|
|
|
horizOffset -= (origin.x + horizOffset + content.offsetWidth) - xBounds.max
|
2020-02-28 16:39:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Default to whatever user wished with placement prop
|
|
|
|
let usingTop = this.placement !== 'bottom'
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
const yOffset = (this.offset && this.offset.y) || 0
|
|
|
|
const translateY = usingTop
|
2022-06-08 03:22:50 +03:00
|
|
|
? yOffset - content.offsetHeight
|
|
|
|
: yOffset + anchorHeight
|
2020-02-28 16:39:47 +00:00
|
|
|
|
|
|
|
const xOffset = (this.offset && this.offset.x) || 0
|
2022-06-08 03:08:03 +03:00
|
|
|
const translateX = horizOffset + xOffset
|
2020-02-28 16:39:47 +00:00
|
|
|
|
|
|
|
// Note, separate translateX and translateY avoids blurry text on chromium,
|
|
|
|
// single translate or translate3d resulted in blurry text.
|
|
|
|
this.styles = {
|
|
|
|
opacity: 1,
|
2022-06-08 03:08:03 +03:00
|
|
|
left: `${Math.round(origin.x + translateX)}px`,
|
|
|
|
top: `${Math.round(origin.y + translateY)}px`,
|
2022-05-20 00:56:23 +03:00
|
|
|
position: 'fixed'
|
2020-02-28 16:39:47 +00:00
|
|
|
}
|
2022-06-08 03:18:37 +03:00
|
|
|
|
|
|
|
if (parentScreenBox) {
|
|
|
|
this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
|
|
|
|
}
|
2020-02-28 16:39:47 +00:00
|
|
|
},
|
|
|
|
showPopover () {
|
2021-02-25 17:27:29 +02:00
|
|
|
const wasHidden = this.hidden
|
2020-02-28 16:39:47 +00:00
|
|
|
this.hidden = false
|
2021-02-25 17:27:29 +02:00
|
|
|
this.$nextTick(() => {
|
|
|
|
if (wasHidden) this.$emit('show')
|
|
|
|
this.updateStyles()
|
|
|
|
})
|
2020-02-28 16:39:47 +00:00
|
|
|
},
|
|
|
|
hidePopover () {
|
|
|
|
if (!this.hidden) this.$emit('close')
|
|
|
|
this.hidden = true
|
|
|
|
this.styles = { opacity: 0 }
|
|
|
|
},
|
|
|
|
onMouseenter (e) {
|
|
|
|
if (this.trigger === 'hover') this.showPopover()
|
|
|
|
},
|
|
|
|
onMouseleave (e) {
|
|
|
|
if (this.trigger === 'hover') this.hidePopover()
|
|
|
|
},
|
|
|
|
onClick (e) {
|
|
|
|
if (this.trigger === 'click') {
|
|
|
|
if (this.hidden) {
|
|
|
|
this.showPopover()
|
|
|
|
} else {
|
|
|
|
this.hidePopover()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onClickOutside (e) {
|
|
|
|
if (this.hidden) return
|
|
|
|
if (this.$el.contains(e.target)) return
|
|
|
|
this.hidePopover()
|
2022-06-08 03:22:15 +03:00
|
|
|
},
|
|
|
|
onScroll () {
|
|
|
|
this.hidePopover()
|
2020-02-28 16:39:47 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
updated () {
|
|
|
|
// Monitor changes to content size, update styles only when content sizes have changed,
|
|
|
|
// that should be the only time we need to move the popover box if we don't care about scroll
|
|
|
|
// or resize
|
|
|
|
const content = this.$refs.content
|
|
|
|
if (!content) return
|
|
|
|
if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
|
|
|
|
this.updateStyles()
|
|
|
|
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
|
|
|
}
|
|
|
|
},
|
|
|
|
created () {
|
|
|
|
document.addEventListener('click', this.onClickOutside)
|
2022-06-08 03:22:15 +03:00
|
|
|
window.addEventListener('scroll', this.onScroll)
|
2020-02-28 16:39:47 +00:00
|
|
|
},
|
2021-04-25 13:44:50 +03:00
|
|
|
unmounted () {
|
2020-02-28 16:39:47 +00:00
|
|
|
document.removeEventListener('click', this.onClickOutside)
|
2022-06-08 03:22:15 +03:00
|
|
|
window.removeEventListener('scroll', this.onScroll)
|
2020-02-28 16:39:47 +00:00
|
|
|
this.hidePopover()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Popover
|