parent
d19c64314f
commit
31225f5d14
@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
const Popover = {
|
||||||
|
name: 'Popover',
|
||||||
|
props: {
|
||||||
|
// Action to trigger popover: either 'hover' or 'click'
|
||||||
|
trigger: String,
|
||||||
|
// Either 'top' or 'bottom'
|
||||||
|
placement: String,
|
||||||
|
// 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,
|
||||||
|
// Takes a top/bottom/left/right object, how much space to leave
|
||||||
|
// between boundary and popover element
|
||||||
|
margin: Object,
|
||||||
|
// Takes a x/y object and tells how many pixels to offset from
|
||||||
|
// anchor point on either axis
|
||||||
|
offset: Object,
|
||||||
|
// Additional styles you may want for the popover container
|
||||||
|
popoverClass: String
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
hidden: true,
|
||||||
|
styles: { opacity: 0 },
|
||||||
|
oldSize: { width: 0, height: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateStyles () {
|
||||||
|
if (this.hidden) {
|
||||||
|
this.styles = {
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popover will be anchored around this element, trigger ref is the container, so
|
||||||
|
// its children are what are inside the slot. Expect only one slot="trigger".
|
||||||
|
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
||||||
|
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 content = this.$refs.content
|
||||||
|
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||||
|
const parentBounds = this.boundTo &&
|
||||||
|
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||||
|
this.$el.offsetParent.getBoundingClientRect()
|
||||||
|
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' ? {
|
||||||
|
min: parentBounds.left + (margin.left || 0),
|
||||||
|
max: parentBounds.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: 0 + (margin.top || 50),
|
||||||
|
max: window.innerHeight - (margin.bottom || 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
let horizOffset = 0
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
|
||||||
|
: yOffset
|
||||||
|
|
||||||
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
|
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + 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.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showPopover () {
|
||||||
|
if (this.hidden) this.$emit('show')
|
||||||
|
this.hidden = false
|
||||||
|
this.$nextTick(this.updateStyles)
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
document.removeEventListener('click', this.onClickOutside)
|
||||||
|
this.hidePopover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Popover
|
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
@mouseenter="onMouseenter"
|
||||||
|
@mouseleave="onMouseleave"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="trigger"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!hidden"
|
||||||
|
ref="content"
|
||||||
|
:style="styles"
|
||||||
|
class="popover"
|
||||||
|
:class="popoverClass"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="content"
|
||||||
|
class="popover-inner"
|
||||||
|
:close="hidePopover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./popover.js" />
|
||||||
|
|
||||||
|
<style lang=scss>
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
z-index: 8;
|
||||||
|
position: absolute;
|
||||||
|
min-width: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||||
|
box-shadow: var(--panelShadow);
|
||||||
|
border-radius: $fallback--btnRadius;
|
||||||
|
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||||
|
|
||||||
|
background-color: $fallback--bg;
|
||||||
|
background-color: var(--popover, $fallback--bg);
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--popoverText, $fallback--text);
|
||||||
|
--faint: var(--popoverFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||||
|
--postLink: var(--popoverPostLink, $fallback--link);
|
||||||
|
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||||
|
--icon: var(--popoverIcon, $fallback--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
padding: .5rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
list-style: none;
|
||||||
|
max-width: 100vw;
|
||||||
|
z-index: 10;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 0;
|
||||||
|
margin: .5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 1px solid $fallback--border;
|
||||||
|
border-top: 1px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
line-height: 21px;
|
||||||
|
margin-right: 5px;
|
||||||
|
overflow: auto;
|
||||||
|
display: block;
|
||||||
|
padding: .25rem 1.0rem .25rem 1.5rem;
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
--btnText: var(--popoverText, $fallback--text);
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
color: var(--menuPopoverIcon, $fallback--icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active, &:hover {
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--selectedMenuPopoverText, $fallback--link);
|
||||||
|
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||||
|
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||||
|
i {
|
||||||
|
color: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,164 +0,0 @@
|
|||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.tooltip.popover {
|
|
||||||
z-index: 8;
|
|
||||||
|
|
||||||
.popover-inner {
|
|
||||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
|
||||||
box-shadow: var(--panelShadow);
|
|
||||||
border-radius: $fallback--btnRadius;
|
|
||||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
|
||||||
background-color: $fallback--bg;
|
|
||||||
background-color: var(--popover, $fallback--bg);
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--popoverText, $fallback--text);
|
|
||||||
--faint: var(--popoverFaintText, $fallback--faint);
|
|
||||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
|
||||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
|
||||||
--postLink: var(--popoverPostLink, $fallback--link);
|
|
||||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
|
||||||
--icon: var(--popoverIcon, $fallback--icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
position: absolute;
|
|
||||||
margin: 5px;
|
|
||||||
border-color: $fallback--bg;
|
|
||||||
border-color: var(--bg, $fallback--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="top"] {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 5px 5px 0 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
bottom: -4px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="bottom"] {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 0 5px 5px 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
top: -4px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="right"] {
|
|
||||||
margin-left: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 5px 5px 5px 0;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
left: -4px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="left"] {
|
|
||||||
margin-right: 5px;
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-width: 5px 0 5px 5px;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
right: -4px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden='true'] {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .15s, visibility .15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden='false'] {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity .15s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
display: block;
|
|
||||||
padding: .5rem 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
list-style: none;
|
|
||||||
max-width: 100vw;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.dropdown-divider {
|
|
||||||
height: 0;
|
|
||||||
margin: .5rem 0;
|
|
||||||
overflow: hidden;
|
|
||||||
border-top: 1px solid $fallback--border;
|
|
||||||
border-top: 1px solid var(--border, $fallback--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
line-height: 21px;
|
|
||||||
margin-right: 5px;
|
|
||||||
overflow: auto;
|
|
||||||
display: block;
|
|
||||||
padding: .25rem 1.0rem .25rem 1.5rem;
|
|
||||||
clear: both;
|
|
||||||
font-weight: 400;
|
|
||||||
text-align: inherit;
|
|
||||||
white-space: normal;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0px;
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
--btnText: var(--popoverText, $fallback--text);
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active, &:hover {
|
|
||||||
background-color: $fallback--lightBg;
|
|
||||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--selectedMenuPopoverText, $fallback--link);
|
|
||||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
|
||||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
|
||||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
|
||||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
|
||||||
i {
|
|
||||||
color: var(--selectedMenuPopoverIcon, $fallback--icon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue