Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
27 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 8aa4d81360..3f31387bb2 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -1,540 +1,535 @@
import { useId } from 'vue'
const Popover = {
name: 'Popover',
inheritAttrs: false,
props: {
// Action to trigger popover: either 'hover' or 'click'
trigger: String,
// Accessible label
label: String,
// 'top', 'bottom', 'left', 'right'
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 selector to use as a replacement for the parent container
// for getting boundaries for x an y axis
boundToSelector: String,
// 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,
// Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover
// styles with your custom class.
popoverClass: String,
// If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom.
removePadding: Boolean,
// self-explanatory (i hope)
disabled: Boolean,
// Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
overlayCenters: Boolean,
// What selector (witin popover!) to use for determining center of popover
overlayCentersSelector: String,
// Lets hover popover stay when clicking inside of it
stayOnClick: Boolean,
// Use styled button (to avoid nested buttons)
normalButton: Boolean,
// Use <a> instead of <button>
link: {
type: Boolean,
default: false,
},
// Whether to hide the trigger totally
hideTrigger: {
type: Boolean,
default: false,
},
// Used to set or unset aria-haspopup (attribute not set for tooltips)
tooltip: Boolean,
},
- emits: [
- 'show',
- 'close',
- 'forwardTabContentFocus',
- 'reverseTabContentFocus',
- ],
+ emits: ['show', 'close', 'forwardTabContentFocus', 'reverseTabContentFocus'],
inject: {
// override popover z layer
popoversZLayer: {
default: '',
},
},
setup() {
const uniqueId = useId()
return { uniqueId }
},
data() {
return {
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
// so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
// with popovers refusing to be hidden when user wants to interact with something in below popover
anchorEl: null,
// There's an issue where having teleport enabled by default causes things just...
// not render at all, i.e. main post status form and its emoji inputs
teleport: false,
lockReEntry: false,
hidden: true,
nextFocus: null,
redirectFocus: false,
styles: {},
oldSize: { width: 0, height: 0 },
scrollable: null,
// used to avoid blinking if hovered onto popover
graceTimeout: null,
parentPopover: null,
disableClickOutside: false,
childrenShown: new Set(),
}
},
computed: {
idPrefix() {
return `popover-${this.uniqueId}-`
},
allTriggerAttrs() {
if (process.env.NODE_ENV === 'development') {
if ('aria-hidden' in this.$attrs) {
throw new Error(
'Do not use aria-hidden in attrs. Instead set hideTrigger to true',
)
}
}
const allAttrs = {
...this.$attrs,
}
const {
'aria-hidden': _unused0,
'aria-label': _unused1,
...rest
} = allAttrs
const attrs = { ...rest }
if (this.hideTrigger) {
attrs['aria-hidden'] = true
attrs.tabindex = -1
}
return attrs
},
ariaHasPopup() {
if (this.tooltip || this.disabled) {
return undefined
} else {
return this.$attrs['aria-haspopup'] ?? 'menu'
}
},
triggerClass() {
if (this.link) {
return ''
} else if (this.normalButton) {
return 'button-default btn'
} else {
return 'button-unstyled'
}
},
},
methods: {
setAnchorEl(el) {
this.anchorEl = el
this.updateStyles()
},
containerBoundingClientRect() {
const container = this.boundToSelector
? this.$el.closest(this.boundToSelector)
: this.$el.offsetParent
return container.getBoundingClientRect()
},
updateStyles() {
if (this.hidden) {
this.styles = {}
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 v-slot:trigger.
const anchorEl =
this.anchorEl || this.$refs.trigger?.children[0] || this.$refs.trigger
// SVGs don't have offsetWidth/Height, use fallback
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorScreenBox = anchorEl.getBoundingClientRect()
const anchorStyle = getComputedStyle(anchorEl)
const topPadding = Number.parseFloat(anchorStyle.paddingTop)
const bottomPadding = Number.parseFloat(anchorStyle.paddingBottom)
const rightPadding = Number.parseFloat(anchorStyle.paddingRight)
const leftPadding = Number.parseFloat(anchorStyle.paddingLeft)
// Screen position of the origin point for popover = center of the anchor
const origin = {
x: anchorScreenBox.left + anchorWidth * 0.5,
y: anchorScreenBox.top + anchorHeight * 0.5,
}
const content = this.$refs.content
const overlayCenter = this.overlayCenters
? this.$refs.content.querySelector(this.overlayCentersSelector)
: null
// Minor optimization, don't call a slow reflow call if we don't have to
const parentScreenBox =
(this.boundTo?.x === 'container' || this.boundTo?.y === 'container') &&
this.containerBoundingClientRect()
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?.x === 'container'
? {
min: parentScreenBox.left + (margin.left || 0),
max: parentScreenBox.right - (margin.right || 0),
}
: {
min: 0 + (margin.left || 10),
max: window.innerWidth - (margin.right || 10),
}
const yBounds =
this.boundTo?.y === 'container'
? {
min: parentScreenBox.top + (margin.top || 0),
max: parentScreenBox.bottom - (margin.bottom || 0),
}
: {
min: 0 + (margin.top || 50),
max: window.innerHeight - (margin.bottom || 5),
}
let horizOffset = 0
let vertOffset = 0
if (overlayCenter) {
const box = content.getBoundingClientRect()
const overlayCenterScreenBox = overlayCenter.getBoundingClientRect()
const leftInnerOffset = overlayCenterScreenBox.left - box.left
const topInnerOffset = overlayCenterScreenBox.top - box.top
horizOffset = -leftInnerOffset - overlayCenter.offsetWidth * 0.5
vertOffset = -topInnerOffset - overlayCenter.offsetHeight * 0.5
} else {
horizOffset = content.offsetWidth * -0.5
vertOffset = content.offsetHeight * -0.5
}
const leftBorder = origin.x + horizOffset
const rightBorder = leftBorder + content.offsetWidth
const topBorder = origin.y + vertOffset
const bottomBorder = topBorder + content.offsetHeight
// If overflowing from left, move it so that it doesn't
if (leftBorder < xBounds.min) {
horizOffset += xBounds.min - leftBorder
}
// If overflowing from right, move it so that it doesn't
if (rightBorder > xBounds.max) {
horizOffset -= rightBorder - xBounds.max
}
// If overflowing from top, move it so that it doesn't
if (topBorder < yBounds.min) {
vertOffset += yBounds.min - topBorder
}
// If overflowing from bottom, move it so that it doesn't
if (bottomBorder > yBounds.max) {
vertOffset -= bottomBorder - yBounds.max
}
let translateX = 0
let translateY = 0
if (overlayCenter) {
translateX = origin.x + horizOffset
translateY = origin.y + vertOffset
} else if (this.placement !== 'right' && this.placement !== 'left') {
// 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 no space on bottom,
// regardless of what placement value was. Then check if there's no space on top, and
// force to bottom, again regardless of what placement value was.
const topBoundary =
origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
const bottomBoundary =
origin.y +
anchorHeight * 0.5 -
(this.removePadding ? bottomPadding : 0)
if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
const yOffset = this.offset?.y || 0
translateY = usingTop
? topBoundary - yOffset - content.offsetHeight
: bottomBoundary + yOffset
const xOffset = this.offset?.x || 0
translateX = origin.x + horizOffset + xOffset
} else {
// Default to whatever user wished with placement prop
let usingLeft = this.placement !== 'right'
// Handle special cases, first force to displaying on left if there's no space on right,
// regardless of what placement value was. Then check if there's no space on right, and
// force to left, again regardless of what placement value was.
const leftBoundary =
origin.x - anchorWidth * 0.5 + (this.removePadding ? leftPadding : 0)
const rightBoundary =
origin.x + anchorWidth * 0.5 - (this.removePadding ? rightPadding : 0)
if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true
if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false
const xOffset = this.offset?.x || 0
translateX = usingLeft
? leftBoundary - xOffset - content.offsetWidth
: rightBoundary + xOffset
const yOffset = this.offset?.y || 0
translateY = origin.y + vertOffset + yOffset
}
this.styles = {
left: `${Math.round(translateX)}px`,
top: `${Math.round(translateY)}px`,
}
if (this.popoversZLayer) {
this.styles['--ZI_popover_override'] =
`var(--ZI_${this.popoversZLayer}_popovers)`
}
if (parentScreenBox) {
this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
}
},
showPopover() {
if (this.disabled) return
this.disableClickOutside = true
setTimeout(() => {
this.disableClickOutside = false
}, 0)
const wasHidden = this.hidden
this.hidden = false
this.parentPopover?.onChildPopoverState(this, true)
if (this.trigger === 'click' || this.stayOnClick) {
document.addEventListener('click', this.onClickOutside)
}
this.scrollable.addEventListener('scroll', this.onScroll)
this.scrollable.addEventListener('resize', this.onResize)
// My assumption is that upon showing popover initially has different size
// as its contents are getting populating, so logic uses those incorrect
// sizes as basis
setTimeout(() => {
if (wasHidden) this.$emit('show')
this.updateStyles()
}, 1)
},
hidePopover() {
if (this.disabled) return
if (!this.hidden) this.$emit('close')
this.redirectFocus = false
this.nextFocus = null
this.hidden = true
this.parentPopover?.onChildPopoverState(this, false)
if (this.trigger === 'click') {
document.removeEventListener('click', this.onClickOutside)
}
this.scrollable?.removeEventListener('scroll', this.onScroll)
this.scrollable?.removeEventListener('resize', this.onResize)
},
resizePopover() {
setTimeout(() => {
this.updateStyles()
}, 1)
},
onMouseenter() {
if (this.trigger === 'hover') {
this.lockReEntry = false
clearTimeout(this.graceTimeout)
this.graceTimeout = null
this.showPopover()
}
},
onMouseleave() {
if (this.trigger === 'hover' && this.childrenShown.size === 0) {
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
}
},
onMouseenterContent() {
if (this.trigger === 'hover' && !this.lockReEntry) {
this.lockReEntry = true
clearTimeout(this.graceTimeout)
this.graceTimeout = null
this.showPopover()
}
},
onMouseleaveContent() {
if (this.trigger === 'hover' && this.childrenShown.size === 0) {
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
}
},
// # Focus management.
//
// Since our content is far away in separate #modals or #popovers container
// the native tabindex flow is broken, but we still want user to be able to
// focus popover contents. To do so we do a bit of focus management.
//
// We do a little focus trapping and redirecting. First, we must establish
// our order. Opening a popover (including auto-open by hover/focus) and
// hitting tab should select content's first focusable element. Shift-tab
// away from popover trigger should hide popover and continue on as normal.
// While inside content, shift-tabbing from first focusable element should
// focus trigger again, and tabbing beyond last focusable element should
// focus element next in line relative to trigger.
//
// We use two tricks here:
// 1. we add two bogus ".focus-trap" elements before and after popover
// contents. Those are aria-hidden="1" and empty spans so they don't
// interfere with the UI or accessibility. We use them to detect when
// user tab-navigated beyond first and last focusable elements and act
// accordingly
// 2. when we open up a popover we listen for "Tab" keypress and engage
// "redirectFocus", so that when trigger loses focus (immideately after)
// we a) focus content's first focusable element and b) save reference
// to the next intended focus target.
//
// There is a blind spot in this approach. If user opened a popover and
// navigated in it with a mouse we never get to know the "next intended
// focus target" so focus order ends up being broken.
//
// There's probably a better way to figure out this "next target" but for
// now this will do.
onFocus(e) {
if (this.trigger === 'hover') {
this.showPopover()
}
},
onBlur(e) {
if (this.redirectFocus) {
const firstFocusable = this.$refs.content
?.querySelector('*:not(.focus-trap)')
?.querySelector('a[href], button, [tabindex="0"], input')
if (firstFocusable) {
firstFocusable?.focus()
this.nextFocus = e.relatedTarget
} else {
this.hidePopover()
}
}
this.redirectFocus = false
},
onTab(e) {
this.redirectFocus = true
},
onForwardTabContentFocus() {
this.nextFocus?.focus()
this.hidePopover()
this.$emit('forwardTabContentFocus')
},
onReverseTabContentFocus() {
const triggerer = this.anchorEl ?? this.$refs.trigger
triggerer.focus()
this.$emit('reverseTabContentFocus')
},
onClick(e) {
if (this.trigger === 'click') {
if (e.ctrlKey) return
if (e.shiftKey) return
if (e.metaKey) return
if (e.button > 0) return
e.preventDefault()
if (this.hidden) {
this.showPopover()
} else {
this.hidePopover()
}
}
},
onClickOutside(e) {
if (this.disableClickOutside) return
if (this.hidden) return
if (this.$refs.content?.contains(e.target)) return
if (this.$el.contains(e.target)) return
if (this.childrenShown.size > 0) return
this.hidePopover()
if (this.parentPopover) this.parentPopover.onClickOutside(e)
},
onScroll() {
this.updateStyles()
},
onResize() {
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,
}
}
},
onChildPopoverState(childRef, state) {
if (state) {
this.childrenShown.add(childRef)
} else {
this.childrenShown.delete(childRef)
}
},
},
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
this.onResize()
},
mounted() {
this.teleport = true
let scrollable =
this.$refs.trigger.closest('.column.-scrollable') ||
this.$refs.trigger.closest('.mobile-notifications')
if (!scrollable) scrollable = window
this.scrollable = scrollable
let parent = this.$parent
while (parent && parent.$.type.name !== 'Popover') {
parent = parent.$parent
}
this.parentPopover = parent
},
beforeUnmount() {
this.hidePopover()
},
}
export default Popover
diff --git a/src/components/status_action_buttons/action_button.js b/src/components/status_action_buttons/action_button.js
index d075c947cd..422ff3ad3a 100644
--- a/src/components/status_action_buttons/action_button.js
+++ b/src/components/status_action_buttons/action_button.js
@@ -1,164 +1,167 @@
import Popover from 'src/components/popover/popover.vue'
import StatusBookmarkFolderMenu from 'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBookmark as faBookmarkRegular,
faFaceSmileBeam,
faStar as faStarRegular,
} from '@fortawesome/free-regular-svg-icons'
import {
faBookmark,
faCheck,
faChevronDown,
faChevronRight,
faComments,
faExternalLinkAlt,
faEye,
faEyeSlash,
faHistory,
faList,
faMinus,
faPencil,
faPlus,
faReply,
faRetweet,
faShareAlt,
faStar,
faThumbtack,
faTimes,
faWrench,
} from '@fortawesome/free-solid-svg-icons'
library.add(
faPlus,
faMinus,
faCheck,
faTimes,
faWrench,
faChevronRight,
faChevronDown,
faReply,
faRetweet,
faStar,
faStarRegular,
faFaceSmileBeam,
faBookmark,
faBookmarkRegular,
faEyeSlash,
faEye,
faThumbtack,
faPencil,
faShareAlt,
faComments,
faList,
faExternalLinkAlt,
faHistory,
)
export default {
props: {
button: Object,
status: Object,
extra: Boolean,
funcArg: Object,
getClass: Function,
getComponent: Function,
doAction: Function,
outerClose: Function,
defaultButtonStyle: Boolean,
hideLabel: Boolean,
popoverTrigger: Boolean,
},
components: {
StatusBookmarkFolderMenu,
Popover,
},
data: () => ({
animationState: false,
}),
computed: {
buttonClass() {
return [
this.button.name + '-button',
{
'-with-extra': this.button.name === 'bookmark',
'-extra': this.extra,
'-quick': !this.extra,
},
]
},
userIsMuted() {
return useUsersStore().relationship(this.status.user.id).muting
},
threadIsMuted() {
return this.status.thread_muted
},
hideCustomEmoji() {
return !useInstanceCapabilitiesStore()
.pleromaCustomEmojiReactionsAvailable
},
hidePostStats() {
return useMergedConfigStore().mergedConfig.hidePostStats
},
buttonInnerClass() {
const buttonStyleClass = this.defaultButtonStyle
? 'button-default'
: 'button-unstyled'
return [
this.button.name + '-button',
{
'main-button': this.extra,
[buttonStyleClass]: !this.extra,
'-active': this.button.active?.(this.funcArg),
disabled: this.button.interactive
? !this.button.interactive(this.funcArg)
: false,
},
]
},
remoteInteractionLink() {
return useInstanceStore().getRemoteInteractionLink({
statusId: this.status.id,
})
},
isInteractive() {
return this.functionWrap('interactive', true)(this.funcArg)
},
href() {
if (this.getComponent(this.button) !== 'a') return undefined
- return this.functionWrap('link', remoteInteractionLink)(this.funcArg) ?? remoteInteractionLink
- }
+ return (
+ this.functionWrap('link', this.remoteInteractionLink)(this.funcArg) ??
+ this.remoteInteractionLink
+ )
+ },
},
methods: {
functionWrap(property, fallback) {
const isFunction = typeof this.button[property] === 'function'
return isFunction
? this.button[property]
: () => this.button[property] ?? fallback
},
doActionWrap(
button,
close = () => {
/* no-op */
},
) {
if (!this.isInteractive) return
this.animationState = true
this.getComponent(button) === 'button' && this.doAction(button)
setTimeout(() => {
this.animationState = false
}, 500)
if (!this.button.dropdown) close()
},
},
}
diff --git a/src/components/status_action_buttons/action_button_container.js b/src/components/status_action_buttons/action_button_container.js
index 47edbf1518..f98ff70646 100644
--- a/src/components/status_action_buttons/action_button_container.js
+++ b/src/components/status_action_buttons/action_button_container.js
@@ -1,175 +1,175 @@
import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.vue'
-import ActionButton from './action_button.vue'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import ActionButton from './action_button.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEnvelope,
faEye,
faEyeSlash,
faFolderTree,
faGlobe,
faLock,
faLockOpen,
faUser,
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUser,
faGlobe,
faFolderTree,
faEye,
faEyeSlash,
faLock,
faLockOpen,
faEnvelope,
)
export default {
components: {
ActionButton,
Popover,
MuteConfirm: defineAsyncComponent(
() => import('src/components/confirm_modal/mute_confirm.vue'),
),
UserTimedFilterModal: defineAsyncComponent(
() =>
import(
'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
),
),
EmojiPicker,
},
props: {
button: {
type: Object,
required: true,
},
status: {
type: Object,
required: true,
},
defaultButton: Boolean,
hideLabel: Boolean,
},
emits: ['emojiPickerShown'],
mounted() {
if (this.button.name === 'mute') {
useUsersStore().fetchDomainMutes()
}
},
data() {
return {
randomSeed: genRandomSeed(),
}
},
computed: {
buttonClass() {
return [
this.button.name + '-button',
{
'-with-extra': this.button.name === 'bookmark',
'-extra': this.extra,
'-quick': !this.extra,
},
]
},
user() {
return this.status.user
},
userIsMuted() {
return useUsersStore().relationship(this.user.id).muting
},
conversationIsMuted() {
return this.status.thread_muted
},
domain() {
return this.user.fqn.split('@')[1]
},
domainIsMuted() {
return new Set(useUsersStore().currentUser.domainMutes).has(this.domain)
},
availableScopes() {
return ['private', 'unlisted', 'direct', 'public'].filter((scope) => {
return scope !== this.status.visibility
})
},
},
methods: {
addReaction(event) {
const emoji = event.insertion
const existingReaction = this.status.emoji_reactions.find(
(r) => r.name === emoji,
)
if (existingReaction?.me) {
useStatusesStore().unreactWithEmoji(this.status.id, emoji)
} else {
useStatusesStore().reactWithEmoji(this.status.id, emoji)
}
},
visibilityIcon(visibility) {
switch (visibility) {
case 'private':
return 'lock'
case 'unlisted':
return 'lock-open'
case 'direct':
return 'envelope'
case 'local':
return 'igloo'
default:
return 'globe'
}
},
unmuteUser() {
return useUsersStore().unmuteUser(this.user.id)
},
unmuteConversation() {
return useStatusesStore().unmuteConversation(this.status.id)
},
unmuteDomain() {
return useUsersStore().unmuteDomain(this.domain)
},
toggleUserMute() {
if (this.userIsMuted) {
this.unmuteUser()
} else {
this.$refs.confirmUser.optionallyPrompt()
}
},
setScope(visibility) {
return useAdminSettingsStore().changeStatusScope({
id: this.status.id,
visibility,
})
},
setSensitive(sensitive) {
useAdminSettingsStore().changeStatusScope({
id: this.status.id,
sensitive,
})
},
toggleConversationMute() {
if (this.conversationIsMuted) {
this.unmuteConversation()
} else {
this.$refs.confirmConversation.optionallyPrompt()
}
},
toggleDomainMute() {
if (this.domainIsMuted) {
this.unmuteDomain()
} else {
this.$refs.confirmDomain.optionallyPrompt()
}
},
},
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 30, 7:37 PM (17 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1800472
Default Alt Text
(27 KB)

Event Timeline