diff options
| author | lain <lain@soykaf.club> | 2020-06-12 14:17:56 +0000 |
|---|---|---|
| committer | lain <lain@soykaf.club> | 2020-06-12 14:17:56 +0000 |
| commit | fd109fa355ac491b86d1e10fcbfcdd577f1ac4d7 (patch) | |
| tree | c27c2f3b684540b10ef9d4477349b4a8681eb7b2 /src/components/popover | |
| parent | 1946661911c97651ba5356db22a0ddd00ba04864 (diff) | |
| parent | 48365819d14d80d2aeb7174bca05bf76eee2e8e0 (diff) | |
Merge branch 'develop' into 'chore/improve-default-tos'
# Conflicts:
# static/terms-of-service.html
Diffstat (limited to 'src/components/popover')
| -rw-r--r-- | src/components/popover/popover.js | 156 | ||||
| -rw-r--r-- | src/components/popover/popover.vue | 118 |
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> |
