Add virtual scrolling to emoji picker
Some checks reported errors
continuous-integration/drone/push Build was killed

there is a graphical glitch when swapping from unicode but it works!

Signed-off-by: Sam Therapy <sam@samtherapy.net>
This commit is contained in:
Sam Therapy 2023-01-24 22:10:42 +01:00
parent b731d9e471
commit 9d7f25a91d
Signed by: sam
GPG key ID: 4D8B07C18F31ACBD
6 changed files with 154 additions and 38 deletions

View file

@ -44,6 +44,7 @@
"vue-i18n": "^9.2.2",
"vue-router": "4.0.14",
"vue-template-compiler": "2.6.11",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.0.2"
},
"devDependencies": {

View file

@ -2,6 +2,9 @@ import Cookies from 'js-cookie'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
@ -428,6 +431,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
app.use(vClickOutside)
app.use(VBodyScrollLock)
app.use(DomNodeToComponent)
app.use(VueVirtualScroller)
app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers)

View file

@ -38,7 +38,11 @@ const EmojiPicker = {
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
customEmojiLoadAllConfirmed: false,
groupRefs: {},
emojiRefs: {},
filteredEmojiGroups: [],
width: 0
}
},
components: {
@ -56,16 +60,18 @@ const EmojiPicker = {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
onScroll(startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = this.$refs[ 'emoji-groups' ].$el
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
},
onWheel (e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
},
setGroupRef(name) {
return el => { this.groupRefs[ name ] = el }
},
highlight (key) {
this.setShowStickers(false)
this.activeGroup = key
@ -98,17 +104,46 @@ const EmojiPicker = {
this.loadEmoji()
}
},
scrolledGroup (target) {
scrolledGroup(target, start, end) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) {
this.activeGroup = group.id
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
}
})
})
},
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)
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
@ -154,6 +189,18 @@ const EmojiPicker = {
}
},
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
},
@ -195,6 +242,17 @@ const EmojiPicker = {
}
].concat(emojiPacks)
},
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), [])
},
sortedEmoji () {
const customEmojis = this.$store.state.instance.customEmoji || []
const sortedEmojiGroups = new Map()

View file

@ -8,6 +8,7 @@
>
<span
v-for="group in emojis"
:ref="setGroupRef('group-' + group.id)"
:key="group.id"
class="emoji-tabs-item"
:class="{
@ -51,39 +52,52 @@
@input="$event.target.composing = false"
>
</div>
<div
<DynamicScroller
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
:items="emojisView"
:min-item-size="minItemSize"
:emit-update="true"
@scroll="onScroll"
>
<div
v-for="group in emojisView"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
<template #default="{ item: group, index, active }">
<DynamicScrollerItem
:ref="setGroupRef('group-' + group.id)"
:item="group"
:active="active"
:data-index="index"
:size-dependencies="[group.emojis.length]"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
<div
class="emoji-group"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<h6
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span
v-if="!emoji.imageUrl"
>{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
loading="lazy"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<div class="keep-open">
<Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }}

View file

@ -196,7 +196,7 @@
<div
class="language-selector"
>
>
<Select
id="post-language"
v-model="newStatus.language"

View file

@ -8368,6 +8368,13 @@ __metadata:
languageName: node
linkType: hard
"mitt@npm:^2.1.0":
version: 2.1.0
resolution: "mitt@npm:2.1.0"
checksum: a8ed2f212b41be554c4abde8bf599394d1aa2f2ece30ff505aa367d98d186a03b37a8b0fdd01560e06f475d1b92fb90444f2db5607f30b9e1f60b099ebaa5c77
languageName: node
linkType: hard
"mkdirp@npm:0.5.1":
version: 0.5.1
resolution: "mkdirp@npm:0.5.1"
@ -9293,6 +9300,7 @@ __metadata:
vue-router: "npm:4.0.14"
vue-style-loader: "npm:^4.1.2"
vue-template-compiler: "npm:2.6.11"
vue-virtual-scroller: "npm:^2.0.0-beta.7"
vuex: "npm:4.0.2"
webpack: "npm:^5.75.0"
webpack-dev-middleware: "npm:^5.3.3"
@ -11824,6 +11832,24 @@ __metadata:
languageName: node
linkType: hard
"vue-observe-visibility@npm:^2.0.0-alpha.1":
version: 2.0.0-alpha.1
resolution: "vue-observe-visibility@npm:2.0.0-alpha.1"
peerDependencies:
vue: ^3.0.0
checksum: 793924571c5ac5ea5c6079d76d8c73d3c3d007de3322a9ca8c97fc5cd578ed423cd74c1bcbbadcde7533ae94ea3e356a8c90b35c91567f4cc13e3748f0a31ae3
languageName: node
linkType: hard
"vue-resize@npm:^2.0.0-alpha.1":
version: 2.0.0-alpha.1
resolution: "vue-resize@npm:2.0.0-alpha.1"
peerDependencies:
vue: ^3.0.0
checksum: 4476ae81ddb3c88c549a5e1b0b2216b9a70b941ff5737208dfbe1a8225e9b1a12aaf24dfc903aa0761a02c7929b9bc170ffc993b32b4bdcf0424490c613cf343
languageName: node
linkType: hard
"vue-router@npm:4.0.14":
version: 4.0.14
resolution: "vue-router@npm:4.0.14"
@ -11855,6 +11881,19 @@ __metadata:
languageName: node
linkType: hard
"vue-virtual-scroller@npm:^2.0.0-beta.7":
version: 2.0.0-beta.7
resolution: "vue-virtual-scroller@npm:2.0.0-beta.7"
dependencies:
mitt: "npm:^2.1.0"
vue-observe-visibility: "npm:^2.0.0-alpha.1"
vue-resize: "npm:^2.0.0-alpha.1"
peerDependencies:
vue: ^3.2.0
checksum: e502a18177f3b6c18aa7a52a7153b5c910395fc89a1aaeb61d21f8fa7ee5ffdc9a641fd9072abce1362c214b463031e82e461a42a6ec394550a8151857e0ab16
languageName: node
linkType: hard
"vue@npm:^3.2.31":
version: 3.2.37
resolution: "vue@npm:3.2.37"