Commit 900f0555 authored by Shpuld Shpludson's avatar Shpuld Shpludson

Merge branch 'fix/popover-performance' into feat/virtual-with-popover

parents abf81216 0723c075
Pipeline #22953 passed with stages
in 8 minutes and 57 seconds
......@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Registration fixed
- Deactivation of remote accounts from frontend
- Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying
- Improved performance of anything that uses popovers (most notably statuses)
## [1.1.7 and earlier] - 2019-12-14
### Added
......
import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
const AccountActions = {
props: [
......@@ -8,7 +9,8 @@ const AccountActions = {
return { }
},
components: {
ProgressButton
ProgressButton,
Popover
},
methods: {
showRepeats () {
......
<template>
<div class="account-actions">
<v-popover
<Popover
trigger="click"
class="account-tools-popover"
:container="false"
placement="bottom-end"
:offset="5"
placement="bottom"
>
<div slot="popover">
<div
slot="content"
class="account-tools-popover"
>
<div class="dropdown-menu">
<template v-if="user.following">
<button
......@@ -51,10 +51,13 @@
</button>
</div>
</div>
<div class="btn btn-default ellipsis-button">
<div
slot="trigger"
class="btn btn-default ellipsis-button"
>
<i class="icon-ellipsis trigger-button" />
</div>
</v-popover>
</Popover>
</div>
</template>
......@@ -62,11 +65,13 @@
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.account-actions {
margin: 0 .8em;
}
.account-tools-popover {
}
.account-actions button.dropdown-item {
margin-left: 0;
}
......
import UserAvatar from '../user_avatar/user_avatar.vue'
import Popover from '../popover/popover.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12
const EmojiReactions = {
name: 'EmojiReactions',
components: {
UserAvatar
UserAvatar,
Popover
},
props: ['status'],
data: () => ({
showAll: false,
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
showAll: false
}),
computed: {
tooManyReactions () {
......
<template>
<div class="emoji-reactions">
<v-popover
<Popover
v-for="(reaction) in emojiReactions"
:key="reaction.name"
:popper-options="popperOptions"
trigger="hover"
placement="top"
:offset="{ y: 5 }"
>
<div
slot="popover"
slot="content"
class="reacted-users"
>
<div v-if="accountsForEmoji[reaction.name].length">
......@@ -34,6 +33,7 @@
</div>
</div>
<button
slot="trigger"
class="emoji-reaction btn btn-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
@click="emojiOnClick(reaction.name, $event)"
......@@ -42,7 +42,7 @@
<span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span>
</button>
</v-popover>
</Popover>
<a
v-if="tooManyReactions"
@click="toggleShowAll"
......@@ -78,6 +78,7 @@
display: flex;
flex-direction: column;
margin-left: 0.5em;
min-width: 5em;
img {
width: 1em;
......
import Popover from '../popover/popover.vue'
const ExtraButtons = {
props: [ 'status' ],
components: { Popover },
methods: {
deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm'))
......
<template>
<v-popover
<Popover
v-if="canDelete || canMute || canPin"
trigger="click"
placement="top"
class="extra-button-popover"
>
<div slot="popover">
<div slot="content">
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
......@@ -47,17 +47,19 @@
</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>
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.icon-ellipsis {
cursor: pointer;
......
import DialogModal from '../dialog_modal/dialog_modal.vue'
import Popover from '../popover/popover.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
......@@ -14,7 +15,6 @@ const ModerationTools = {
],
data () {
return {
showDropDown: false,
tags: {
FORCE_NSFW,
STRIP_MEDIA,
......@@ -28,7 +28,8 @@ const ModerationTools = {
}
},
components: {
DialogModal
DialogModal,
Popover
},
computed: {
tagsSet () {
......
<template>
<div>
<v-popover
<Popover
trigger="click"
class="moderation-tools-popover"
placement="bottom-end"
@show="showDropDown = true"
@hide="showDropDown = false"
placement="bottom"
:offset="{ y: 5 }"
>
<div slot="popover">
<div slot="content">
<div class="dropdown-menu">
<span v-if="user.is_local">
<button
......@@ -122,12 +121,12 @@
</div>
</div>
<button
slot="trigger"
class="btn btn-default btn-block"
:class="{ pressed: showDropDown }"
>
{{ $t('user_card.admin_menu.moderation') }}
</button>
</v-popover>
</Popover>
<portal to="modal">
<DialogModal
v-if="showDeleteUserDialog"
......@@ -160,7 +159,6 @@
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.menu-checkbox {
float: right;
......
const Popover = {
name: 'Popover',
props: [
'trigger',
'placement',
'boundTo',
'padding',
'offset',
'popoverClass'
],
data () {
return {
hidden: true,
styles: { opacity: 0 },
oldSize: { width: 0, height: 0 }
}
},
computed: {
display () {
return !this.hidden
}
},
methods: {
updateStyles () {
if (this.hidden) return { opacity: 0 }
// Popover will be anchored around this element
const anchorEl = this.$refs.trigger || 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 padding = this.padding || {}
// What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default padding values to dodge the navbar
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
min: parentBounds.left + (padding.left || 0),
max: parentBounds.right - (padding.right || 0)
} : {
min: 0 + (padding.left || 10),
max: window.innerWidth - (padding.right || 10)
}
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
min: parentBounds.top + (padding.top || 0),
max: parentBounds.bottom - (padding.bottom || 0)
} : {
min: 0 + (padding.top || 50),
max: window.innerHeight - (padding.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 + yOffset
const xOffset = (this.offset && this.offset.x) || 0
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
this.styles = {
opacity: 1,
transform: `translate(${Math.floor(translateX)}px, ${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
<template>
<div
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
<div
ref="trigger"
@click="onClick"
>
<slot name="trigger" />
</div>
<div
v-if="display"
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';
.tooltip.popover {
.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;
}
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(--bg, $fallback--bg);
}
.dropdown-menu {
......@@ -103,6 +52,7 @@
list-style: none;
max-width: 100vw;
z-index: 10;
white-space: nowrap;
.dropdown-divider {
height: 0;
......@@ -121,7 +71,7 @@
clear: both;
font-weight: 400;
text-align: inherit;
white-space: normal;
white-space: nowrap;
border: none;
border-radius: 0px;
background-color: transparent;
......@@ -145,3 +95,4 @@
}
}
}
</style>
import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex'
const ReactButton = {
props: ['status', 'loggedIn'],
data () {
return {
showTooltip: false,
filterWord: '',
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
filterWord: ''
}
},
components: {
Popover
},
methods: {
openReactionSelect () {
this.showTooltip = true
this.filterWord = ''
},
closeReactionSelect () {
this.showTooltip = false
},
addReaction (event, emoji) {
addReaction (event, emoji, close) {
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
if (existingReaction && existingReaction.me) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
} else {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
}
this.closeReactionSelect()
close()
}
},
computed: {
......
<template>
<v-popover
:popper-options="popperOptions"
:open="showTooltip"
trigger="manual"
<Popover
trigger="click"
placement="top"
class="react-button-popover"
@hide="closeReactionSelect"
>
<div slot="popover">
<div
slot="content"
slot-scope="{close}"
>
<div class="reaction-picker-filter">
<input
v-model="filterWord"
......@@ -19,7 +19,7 @@
v-for="emoji in commonEmojis"
:key="emoji"
class="emoji-button"
@click="addReaction($event, emoji)"
@click="addReaction($event, emoji, close)"
>
{{ emoji }}
</span>
......@@ -28,7 +28,7 @@
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
@click="addReaction($event, emoji.replacement)"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
</span>
......@@ -37,14 +37,14 @@
</div>
<div
v-if="loggedIn"
@click.prevent="openReactionSelect"
slot="trigger"
>
<i
class="icon-smile button-icon add-reaction-button"
:title="$t('tool_tip.add_reaction')"
/>
</div>
</v-popover>
</Popover>
</template>
<script src="./react_button.js" ></script>
......
......@@ -177,6 +177,8 @@
<StatusPopover
v-if="!isPreview"
:status-id="status.in_reply_to_status_id"
class="reply-to-popover"
style="min-width: 0"
>
<a
class="reply-to"
......@@ -564,11 +566,10 @@ $status-margin: 0.75em;
align-items: stretch;
> .reply-to-and-accountname > a {
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
word-break: break-all;
}
}
......@@ -577,7 +578,6 @@ $status-margin: 0.75em;
display: flex;
height: 18px;
margin-right: 0.5em;
overflow: hidden;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
......@@ -588,6 +588,10 @@ $status-margin: 0.75em;
display: flex;
}
.reply-to-popover {
min-width: 0;
}
.reply-to {
display: flex;
}
......@@ -595,6 +599,7 @@ $status-margin: 0.75em;
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.4em 0 0.2em;
color: $fallback--faint;
color: var(--faint, $fallback--faint);
......
......@@ -5,22 +5,14 @@ const StatusPopover = {