aboutsummaryrefslogtreecommitdiff
path: root/src/components/popover
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/popover')
-rw-r--r--src/components/popover/popover.js156
-rw-r--r--src/components/popover/popover.vue118
2 files changed, 274 insertions, 0 deletions
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
new file mode 100644
index 00000000..5881d266
--- /dev/null
+++ b/src/components/popover/popover.js
@@ -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
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
new file mode 100644
index 00000000..a271cb1b
--- /dev/null
+++ b/src/components/popover/popover.vue
@@ -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>