rewrite popover because v-tooltip is slow as heck

This commit is contained in:
Shpuld Shpuldson 2020-02-12 19:22:40 +02:00
parent 8fcb9c42aa
commit 5262676e0e
13 changed files with 417 additions and 168 deletions

View file

@ -12,6 +12,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
import MobileNav from './components/mobile_nav/mobile_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import PopoverTarget from './components/popover/popover_target.vue'
import { windowWidth } from './services/window_utils/window_utils'
export default {
@ -30,7 +31,8 @@ export default {
MobilePostStatusButton,
MobileNav,
UserReportingModal,
PostStatusModal
PostStatusModal,
PopoverTarget
},
data: () => ({
mobileActivePanel: 'timeline',

View file

@ -123,6 +123,7 @@
<UserReportingModal />
<PostStatusModal />
<portal-target name="modal" />
<PopoverTarget />
</div>
</template>

View file

@ -1,5 +1,8 @@
import Popover from '../popover/popover.vue'
const ExtraButtons = {
props: [ 'status' ],
components: { Popover },
methods: {
deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm'))

View file

@ -1,11 +1,11 @@
<template>
<v-popover
<Popover
v-if="canDelete || canMute || canPin"
trigger="click"
placement="top"
placement="bottom"
class="extra-button-popover"
>
<div slot="popover">
<div slot="content">
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
@ -47,10 +47,10 @@
</button>
</div>
</div>
<div class="button-icon">
<div slot="trigger" class="button-icon">
<i class="icon-ellipsis" />
</div>
</v-popover>
</Popover>
</template>
<script src="./extra_buttons.js" ></script>
@ -59,6 +59,10 @@
@import '../../_variables.scss';
@import '../popper/popper.scss';
.dropdown-menu {
flex-shrink: 0;
}
.icon-ellipsis {
cursor: pointer;

View file

@ -0,0 +1,113 @@
const Popover = {
name: 'Popover',
props: [
'trigger',
'placement',
'show'
],
data () {
return {
hidden: true,
styles: { opacity: 0 }
}
},
computed: {
display () {
return !this.hidden
}
},
methods: {
/*
registerPopover (e) {
if (!this.targetId) {
this.$store.dispatch('registerPopover', e.target).then(id => this.targetId = id)
}
},
unregisterPopover () {
if (this.targetId) {
this.$store.dispatch('unregisterPopover', this.targetId)
this.targetId = null
}
},
*/
showPopover () {
this.hidden = false
this.$nextTick(function () {
if (this.hidden) return { opacity: 0 }
const anchorEl = this.$refs.trigger || this.$el
console.log(anchorEl)
const screenBox = anchorEl.getBoundingClientRect()
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top}
const content = this.$refs.content
let horizOffset = 0
if ((origin.x - content.offsetWidth * 0.5) < 25) {
horizOffset += -(origin.x - content.offsetWidth * 0.5) + 25
}
console.log((origin.x + content.offsetWidth * 0.5), (window.innerWidth - 25))
if ((origin.x + content.offsetWidth * 0.5) > window.innerWidth - 25) {
horizOffset -= (origin.x + content.offsetWidth * 0.5) - (window.innerWidth - 25)
}
// 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 > (window.innerHeight - 25)) usingTop = true
if (origin.y - content.offsetHeight < 50) usingTop = false
const vertAlign = usingTop ?
{
bottom: `${anchorEl.offsetHeight}px`
} :
{
top: `${anchorEl.offsetHeight}px`
}
this.styles = {
opacity: '100%',
left: `${(anchorEl.offsetLeft + anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset}px`,
...vertAlign
}
})
},
hidePopover () {
this.hidden = true
this.styles = { opacity: 0 }
},
onMouseenter (e) {
console.log(this.trigger)
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
console.log(e.target)
this.hidePopover()
}
},
created () {
document.addEventListener("click", this.onClickOutside)
},
destroyed () {
document.removeEventListener("click", this.onClickOutside)
this.hidePopover()
}
}
export default Popover

View file

@ -0,0 +1,195 @@
<template>
<!-- This is for the weird portal shit
<div
@mouseenter="registerPopover"
@mouseleave="unregisterPopover"
>
<slot name="trigger"></slot>
<portal
v-if="targetId"
:to="targetId"
>
<slot name="content"></slot>
</portal>
</div>
-->
<div
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
<div @click="onClick" ref="trigger">
<slot name="trigger"></slot>
</div>
<div
v-if="display"
:style="styles"
class="popover"
>
<div
ref="content"
class="popover-inner"
>
<!-- onSuccess is to mimic basic functionality of v-popover -->
<slot
name="content"
@onSuccess="hidePopover"
></slot>
</div>
</div>
</div>
</template>
<script src="./popover.js" />
<style lang=scss>
@import '../../_variables.scss';
.popover {
z-index: 8;
position: absolute;
min-width: 0;
.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(--bg, $fallback--bg);
}
.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;
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%;
&-icon {
padding-left: 0.5rem;
i {
margin-right: 0.25rem;
}
}
&:hover {
// TODO: improve the look on breeze themes
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
box-shadow: none;
}
}
}
</style>

View file

@ -0,0 +1,23 @@
const PopoverTarget = {
name: 'PopoverTarget',
computed: {
ids () {
const popovers = this.$store.state.popover.popovers
return Object.keys(popovers)
},
styles () {
return this.ids.reduce((acc, id) => {
const el = this.$store.state.popover.popovers[id]
const box = el.getBoundingClientRect()
acc[id] = {
top: `${box.y}px`,
left: `${box.x}px`
}
console.log(acc)
return acc
}, {})
}
}
}
export default PopoverTarget

View file

@ -0,0 +1,25 @@
<template>
<div class="popover-targets">
<portal-target
v-for="id in ids"
:key="id"
:name="id"
:style="styles[id]"
class="popover-target"
/>
</div>
</template>
<script src="./popover_target.js" />
<style lang="scss">
.popover-targets {
top: 0;
left: 0;
position: fixed;
}
.popover-target {
position: fixed;
}
</style>

View file

@ -1,147 +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(--bg, $fallback--bg);
}
.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%;
&-icon {
padding-left: 0.5rem;
i {
margin-right: 0.25rem;
}
}
&:hover {
// TODO: improve the look on breeze themes
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
box-shadow: none;
}
}
}

View file

@ -20,7 +20,8 @@ const StatusPopover = {
}
},
components: {
Status: () => import('../status/status.vue')
Status: () => import('../status/status.vue'),
Popover: () => import('../popover/popover.vue')
},
methods: {
enter () {

View file

@ -1,11 +1,9 @@
<template>
<v-popover
popover-class="status-popover"
placement="top-start"
:popper-options="popperOptions"
@show="enter()"
>
<template slot="popover">
<Popover trigger="hover">
<template slot="trigger">
<slot />
</template>
<div slot="content" class="status-popover">
<Status
v-if="status"
:is-preview="true"
@ -18,10 +16,8 @@
>
<i class="icon-spin4 animate-spin" />
</div>
</template>
<slot />
</v-popover>
</div>
</Popover>
</template>
<script src="./status_popover.js" ></script>
@ -29,11 +25,11 @@
<style lang="scss">
@import '../../_variables.scss';
.tooltip.popover.status-popover {
.status-popover {
font-size: 1rem;
min-width: 15em;
max-width: 95%;
margin-left: 0.5em;
margin: 0.5em 0;
.popover-inner {
border-color: $fallback--border;

View file

@ -19,6 +19,7 @@ import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js'
import popoverModule from './modules/popover.js'
import VueI18n from 'vue-i18n'
@ -89,7 +90,8 @@ const persistedStateOptions = {
oauthTokens: oauthTokensModule,
reports: reportsModule,
polls: pollsModule,
postStatus: postStatusModule
postStatus: postStatusModule,
popover: popoverModule
},
plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now.

31
src/modules/popover.js Normal file
View file

@ -0,0 +1,31 @@
import { omit } from 'lodash'
import { set } from 'vue'
const popover = {
state: {
popovers: {}
},
mutations: {
registerPopover (state, { id, el }) {
set(state.popovers, id, el)
},
unregisterPopover (state, { id }) {
state.popovers = omit(state.popovers, id)
}
},
actions: {
registerPopover (store, el) {
// Generate unique id, it will be used to link portal and portal-target
// popover-target will make portal targets for each registered popover
// el will be used by popover target to put popovers in their places.
let id = Math.floor(Math.random() * 1000000).toString()
store.commit('registerPopover', { id, el })
return id
},
unregisterPopover (store, id) {
store.commit('unregisterPopover', { id })
}
}
}
export default popover