Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85829880
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/changelog.d/scroll-emoji-selector-safari.fix b/changelog.d/scroll-emoji-selector-safari.fix
new file mode 100644
index 0000000000..3f5dda7d80
--- /dev/null
+++ b/changelog.d/scroll-emoji-selector-safari.fix
@@ -0,0 +1 @@
+Fix scrolling emoji selector in modal in safari ios
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 349b043df5..30c01aa528 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,357 +1,361 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue'
import StillImage from '../still-image/still-image.vue'
import { ensureFinalFallback } from '../../i18n/languages.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
faStickyNote,
faSmileBeam,
faSmile,
faUser,
faPaw,
faIceCream,
faBus,
faBasketballBall,
faLightbulb,
faCode,
faFlag
} from '@fortawesome/free-solid-svg-icons'
import { debounce, trim, chunk } from 'lodash'
library.add(
faBoxOpen,
faStickyNote,
faSmileBeam,
faSmile,
faUser,
faPaw,
faIceCream,
faBus,
faBasketballBall,
faLightbulb,
faCode,
faFlag
)
const UNICODE_EMOJI_GROUP_ICON = {
'smileys-and-emotion': 'smile',
'people-and-body': 'user',
'animals-and-nature': 'paw',
'food-and-drink': 'ice-cream',
'travel-and-places': 'bus',
activities: 'basketball-ball',
objects: 'lightbulb',
symbols: 'code',
flags: 'flag'
}
const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
const res = [emoji.displayText, nameLocalizer(emoji)]
if (emoji.annotations) {
languages.forEach(lang => {
const keywords = emoji.annotations[lang]?.keywords || []
const name = emoji.annotations[lang]?.name
res.push(...(keywords.concat([name]).filter(k => k)))
})
}
return res
}
const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
if (keyword === '') return list
const keywordLowercase = keyword.toLowerCase()
const orderedEmojiList = []
for (const emoji of list) {
const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
.map(k => k.toLowerCase().indexOf(keywordLowercase))
.filter(k => k > -1)
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
if (indexOfKeyword > -1) {
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
orderedEmojiList[indexOfKeyword] = []
}
orderedEmojiList[indexOfKeyword].push(emoji)
}
}
return orderedEmojiList.flat()
}
const getOffset = (elem) => {
const style = elem.style.transform
const res = /translateY\((\d+)px\)/.exec(style)
if (!res) { return 0 }
return res[1]
}
const toHeaderId = id => {
return id.replace(/^row-\d+-/, '')
}
const EmojiPicker = {
props: {
enableStickerPicker: {
required: false,
type: Boolean,
default: false
},
hideCustomEmoji: {
required: false,
type: Boolean,
default: false
}
},
+ inject: ['popoversZLayer'],
data () {
return {
keyword: '',
activeGroup: 'custom',
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiTimeout: null,
// Lazy-load only after the first time `showing` becomes true.
contentLoaded: false,
groupRefs: {},
emojiRefs: {},
filteredEmojiGroups: [],
width: 0
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox,
StillImage,
Popover
},
methods: {
showPicker () {
this.$refs.popover.showPopover()
this.onShowing()
},
hidePicker () {
this.$refs.popover.hidePopover()
},
setAnchorEl (el) {
this.$refs.popover.setAnchorEl(el)
},
setGroupRef (name) {
return el => { this.groupRefs[name] = el }
},
onPopoverShown () {
this.$emit('show')
},
onPopoverClosed () {
this.$emit('close')
},
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
},
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
if (!this.keepOpen) {
this.$refs.popover.hidePopover()
}
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = this.$refs['emoji-groups'].$el
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
},
scrolledGroup (target, start, end) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojiItems.slice(start, end + 1).forEach(group => {
const headerId = toHeaderId(group.id)
const ref = this.groupRefs['group-' + group.id]
if (!ref) { return }
const elem = ref.$el.parentElement
if (!elem) { return }
if (elem && getOffset(elem) <= top) {
this.activeGroup = headerId
}
})
this.scrollHeader()
})
},
scrollHeader () {
// Scroll the active tab's header into view
const headerRef = this.groupRefs['group-header-' + this.activeGroup]
const left = headerRef.offsetLeft
const right = left + headerRef.offsetWidth
const headerCont = this.$refs.header
const currentScroll = headerCont.scrollLeft
const currentScrollRight = currentScroll + headerCont.clientWidth
const setScroll = s => { headerCont.scrollLeft = s }
const margin = 7 // .emoji-tabs-item: padding
if (left - margin < currentScroll) {
setScroll(left - margin)
} else if (right + margin > currentScrollRight) {
setScroll(right + margin - headerCont.clientWidth)
}
},
highlight (groupId) {
this.setShowStickers(false)
const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
this.$refs['emoji-groups'].scrollToItem(indexInList)
},
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
}
},
toggleStickers () {
this.showingStickers = !this.showingStickers
},
setShowStickers (value) {
this.showingStickers = value
},
filterByKeyword (list, keyword) {
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
},
onShowing () {
const oldContentLoaded = this.contentLoaded
this.recalculateItemPerRow()
this.$nextTick(() => {
this.$refs.search.focus()
})
this.contentLoaded = true
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
if (!oldContentLoaded) {
this.$nextTick(() => {
if (this.defaultGroup) {
this.highlight(this.defaultGroup)
}
})
}
},
getFilteredEmojiGroups () {
return this.allEmojiGroups
.map(group => ({
...group,
emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
}))
.filter(group => group.emojis.length > 0)
},
recalculateItemPerRow () {
this.$nextTick(() => {
if (!this.$refs['emoji-groups']) {
return
}
this.width = this.$refs['emoji-groups'].$el.clientWidth
})
}
},
watch: {
keyword () {
this.onScroll()
this.debouncedHandleKeywordChange()
},
allCustomGroups () {
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
}
},
computed: {
minItemSize () {
return this.emojiHeight
},
emojiHeight () {
return 32 + 4
},
emojiWidth () {
return 32 + 4
},
itemPerRow () {
return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6
},
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
},
stickersAvailable () {
if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0
}
return 0
},
allCustomGroups () {
if (this.hideCustomEmoji) {
return {}
}
const emojis = this.$store.getters.groupedCustomEmojis
if (emojis.unpacked) {
emojis.unpacked.text = this.$t('emoji.unpacked')
}
return emojis
},
defaultGroup () {
return Object.keys(this.allCustomGroups)[0]
},
unicodeEmojiGroups () {
return this.$store.getters.standardEmojiGroupList.map(group => ({
id: `standard-${group.id}`,
text: this.$t(`emoji.unicode_groups.${group.id}`),
icon: UNICODE_EMOJI_GROUP_ICON[group.id],
emojis: group.emojis
}))
},
allEmojiGroups () {
return Object.entries(this.allCustomGroups)
.map(([_, v]) => v)
.concat(this.unicodeEmojiGroups)
},
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0
},
debouncedHandleKeywordChange () {
return debounce(() => {
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
}, 500)
},
emojiItems () {
return this.filteredEmojiGroups.map(group =>
chunk(group.emojis, this.itemPerRow)
.map((items, index) => ({
...group,
id: index === 0 ? group.id : `row-${index}-${group.id}`,
emojis: items,
isFirstRow: index === 0
})))
.reduce((a, c) => a.concat(c), [])
},
languages () {
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
},
maybeLocalizedEmojiName () {
return emoji => {
if (!emoji.annotations) {
return emoji.displayText
}
if (emoji.displayTextI18n) {
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
}
for (const lang of this.languages) {
if (emoji.annotations[lang]?.name) {
return emoji.annotations[lang].name
}
}
return emoji.displayText
}
+ },
+ isInModal () {
+ return this.popoversZLayer === 'modals'
}
}
}
export default EmojiPicker
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 6972164b02..3e77d52322 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,150 +1,152 @@
<template>
<Popover
ref="popover"
trigger="click"
popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true }"
@show="onPopoverShown"
@close="onPopoverClosed"
>
<template #content>
<div class="heading">
<span
ref="header"
class="emoji-tabs"
+ v-body-scroll-lock="isInModal"
>
<span
v-for="group in filteredEmojiGroups"
:ref="setGroupRef('group-header-' + group.id)"
:key="group.id"
class="emoji-tabs-item"
:class="{
active: activeGroupView === group.id
}"
:title="group.text"
@click.prevent="highlight(group.id)"
>
<span
v-if="group.image"
class="emoji-picker-header-image"
>
<still-image
:alt="group.text"
:src="group.image"
/>
</span>
<FAIcon
v-else
:icon="group.icon"
fixed-width
/>
</span>
</span>
<span
v-if="stickerPickerEnabled"
class="additional-tabs"
>
<span
class="stickers-tab-icon additional-tabs-item"
:class="{active: showingStickers}"
:title="$t('emoji.stickers')"
@click.prevent="toggleStickers"
>
<FAIcon
icon="sticky-note"
fixed-width
/>
</span>
</span>
</div>
<div
v-if="contentLoaded"
class="content"
>
<div
class="emoji-content"
:class="{hidden: showingStickers}"
>
<div class="emoji-search">
<input
ref="search"
v-model="keyword"
type="text"
class="form-control"
:placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false"
>
</div>
<DynamicScroller
ref="emoji-groups"
class="emoji-groups"
+ v-body-scroll-lock="isInModal"
:class="groupsScrolledClass"
:min-item-size="minItemSize"
:items="emojiItems"
:emit-update="true"
@update="onScroll"
@visible="recalculateItemPerRow"
@resize="recalculateItemPerRow"
>
<template #default="{ item: group, index, active }">
<DynamicScrollerItem
:ref="setGroupRef('group-' + group.id)"
:item="group"
:active="active"
:data-index="index"
:size-dependencies="[group.emojis.length]"
>
<div
class="emoji-group"
>
<h6
v-if="group.isFirstRow"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="maybeLocalizedEmojiName(emoji)"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span
v-if="!emoji.imageUrl"
class="emoji-picker-emoji -unicode"
>{{ emoji.replacement }}</span>
<still-image
v-else
class="emoji-picker-emoji -custom"
loading="lazy"
:src="emoji.imageUrl"
:data-emoji-name="group.id + emoji.displayText"
/>
</span>
</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<div class="keep-open">
<Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }}
</Checkbox>
</div>
</div>
<div
v-if="showingStickers"
class="stickers-content"
>
<sticker-picker
@uploaded="onStickerUploaded"
@upload-failed="onStickerUploadFailed"
/>
</div>
</div>
</template>
</Popover>
</template>
<script src="./emoji_picker.js"></script>
<style lang="scss" src="./emoji_picker.scss"></style>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 1:05 AM (19 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802036
Default Alt Text
(15 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline