Merge branch 'from/develop/tusooa/emoji-picker-lazy' into 'develop'

Remove lozad, use virtual scrolling

See merge request pleroma/pleroma-fe!1717
This commit is contained in:
HJ 2023-01-03 18:57:26 +00:00
commit a7387332ed
8 changed files with 136 additions and 97 deletions

View File

@ -34,7 +34,6 @@
"escape-html": "1.0.3", "escape-html": "1.0.3",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"localforage": "1.10.0", "localforage": "1.10.0",
"lozad": "1.16.0",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"phoenix": "1.6.2", "phoenix": "1.6.2",
"punycode.js": "2.1.0", "punycode.js": "2.1.0",
@ -46,6 +45,7 @@
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.6", "vue-router": "4.1.6",
"vue-template-compiler": "2.7.14", "vue-template-compiler": "2.7.14",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0" "vuex": "4.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,8 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' 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 { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
@ -397,6 +399,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
app.use(vClickOutside) app.use(vClickOutside)
app.use(VBodyScrollLock) app.use(VBodyScrollLock)
app.use(VueVirtualScroller)
app.component('FAIcon', FontAwesomeIcon) app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers) app.component('FALayers', FontAwesomeLayers)

View File

@ -3,7 +3,6 @@ import Checkbox from '../checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import { ensureFinalFallback } from '../../i18n/languages.js' import { ensureFinalFallback } from '../../i18n/languages.js'
import lozad from 'lozad'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBoxOpen, faBoxOpen,
@ -19,7 +18,7 @@ import {
faCode, faCode,
faFlag faFlag
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { debounce, trim } from 'lodash' import { debounce, trim, chunk } from 'lodash'
library.add( library.add(
faBoxOpen, faBoxOpen,
@ -82,6 +81,17 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
return orderedEmojiList.flat() 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 = { const EmojiPicker = {
props: { props: {
enableStickerPicker: { enableStickerPicker: {
@ -102,7 +112,8 @@ const EmojiPicker = {
contentLoaded: false, contentLoaded: false,
groupRefs: {}, groupRefs: {},
emojiRefs: {}, emojiRefs: {},
filteredEmojiGroups: [] filteredEmojiGroups: [],
width: 0
} }
}, },
components: { components: {
@ -125,9 +136,6 @@ const EmojiPicker = {
setGroupRef (name) { setGroupRef (name) {
return el => { this.groupRefs[name] = el } return el => { this.groupRefs[name] = el }
}, },
setEmojiRef (name) {
return el => { this.emojiRefs[name] = el }
},
onPopoverShown () { onPopoverShown () {
this.$emit('show') this.$emit('show')
}, },
@ -147,18 +155,21 @@ const EmojiPicker = {
} }
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
}, },
onScroll (e) { onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = (e && e.target) || this.$refs['emoji-groups'] const target = this.$refs['emoji-groups'].$el
this.updateScrolledClass(target) this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
this.scrolledGroup(target)
}, },
scrolledGroup (target) { scrolledGroup (target, start, end) {
const top = target.scrollTop + 5 const top = target.scrollTop + 5
this.$nextTick(() => { this.$nextTick(() => {
this.allEmojiGroups.forEach(group => { this.emojiItems.slice(start, end + 1).forEach(group => {
const headerId = toHeaderId(group.id)
const ref = this.groupRefs['group-' + group.id] const ref = this.groupRefs['group-' + group.id]
if (ref && ref.offsetTop <= top) { if (!ref) { return }
this.activeGroup = group.id const elem = ref.$el.parentElement
if (!elem) { return }
if (elem && getOffset(elem) <= top) {
this.activeGroup = headerId
} }
}) })
this.scrollHeader() this.scrollHeader()
@ -181,14 +192,10 @@ const EmojiPicker = {
setScroll(right + margin - headerCont.clientWidth) setScroll(right + margin - headerCont.clientWidth)
} }
}, },
highlight (key) { highlight (groupId) {
const ref = this.groupRefs['group-' + key]
const top = ref.offsetTop
this.setShowStickers(false) this.setShowStickers(false)
this.activeGroup = key const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
this.$nextTick(() => { this.$refs['emoji-groups'].scrollToItem(indexInList)
this.$refs['emoji-groups'].scrollTop = top + 1
})
}, },
updateScrolledClass (target) { updateScrolledClass (target) {
if (target.scrollTop <= 5) { if (target.scrollTop <= 5) {
@ -208,43 +215,13 @@ const EmojiPicker = {
filterByKeyword (list, keyword) { filterByKeyword (list, keyword) {
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName) return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
}, },
initializeLazyLoad () {
this.destroyLazyLoad()
this.$nextTick(() => {
this.$lozad = lozad('.still-image.emoji-picker-emoji', {
load: el => {
const name = el.getAttribute('data-emoji-name')
const vn = this.emojiRefs[name]
if (!vn) {
return
}
vn.loadLazy()
}
})
this.$lozad.observe()
})
},
waitForDomAndInitializeLazyLoad () {
this.$nextTick(() => this.initializeLazyLoad())
},
destroyLazyLoad () {
if (this.$lozad) {
if (this.$lozad.observer) {
this.$lozad.observer.disconnect()
}
if (this.$lozad.mutationObserver) {
this.$lozad.mutationObserver.disconnect()
}
}
},
onShowing () { onShowing () {
const oldContentLoaded = this.contentLoaded const oldContentLoaded = this.contentLoaded
this.recalculateItemPerRow()
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.search.focus() this.$refs.search.focus()
}) })
this.contentLoaded = true this.contentLoaded = true
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
if (!oldContentLoaded) { if (!oldContentLoaded) {
this.$nextTick(() => { this.$nextTick(() => {
@ -261,6 +238,14 @@ const EmojiPicker = {
emojis: this.filterByKeyword(group.emojis, trim(this.keyword)) emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
})) }))
.filter(group => group.emojis.length > 0) .filter(group => group.emojis.length > 0)
},
recalculateItemPerRow () {
this.$nextTick(() => {
if (!this.$refs['emoji-groups']) {
return
}
this.width = this.$refs['emoji-groups'].$el.offsetWidth
})
} }
}, },
watch: { watch: {
@ -269,14 +254,22 @@ const EmojiPicker = {
this.debouncedHandleKeywordChange() this.debouncedHandleKeywordChange()
}, },
allCustomGroups () { allCustomGroups () {
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
} }
}, },
destroyed () {
this.destroyLazyLoad()
},
computed: { 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 () { activeGroupView () {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup
}, },
@ -314,10 +307,20 @@ const EmojiPicker = {
}, },
debouncedHandleKeywordChange () { debouncedHandleKeywordChange () {
return debounce(() => { return debounce(() => {
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups() this.filteredEmojiGroups = this.getFilteredEmojiGroups()
}, 500) }, 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 () { languages () {
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
}, },

View File

@ -74,6 +74,7 @@ $emoji-picker-emoji-size: 32px;
} }
.emoji-groups { .emoji-groups {
height: 100%;
min-height: 200px; min-height: 200px;
} }

View File

@ -74,45 +74,56 @@
@input="$event.target.composing = false" @input="$event.target.composing = false"
> >
</div> </div>
<div <DynamicScroller
ref="emoji-groups" ref="emoji-groups"
class="emoji-groups" class="emoji-groups"
:class="groupsScrolledClass" :class="groupsScrolledClass"
@scroll="onScroll" :min-item-size="minItemSize"
:items="emojiItems"
:emit-update="true"
@update="onScroll"
@visible="recalculateItemPerRow"
> >
<div <template #default="{ item: group, index, active }">
v-for="group in filteredEmojiGroups" <DynamicScrollerItem
:key="group.id"
class="emoji-group"
>
<h6
:ref="setGroupRef('group-' + group.id)" :ref="setGroupRef('group-' + group.id)"
class="emoji-group-title" :item="group"
:active="active"
:data-index="index"
:size-dependencies="[group.emojis.length]"
> >
{{ group.text }} <div
</h6> class="emoji-group"
<span >
v-for="emoji in group.emojis" <h6
:key="group.id + emoji.displayText" v-if="group.isFirstRow"
:title="maybeLocalizedEmojiName(emoji)" class="emoji-group-title"
class="emoji-item" >
@click.stop.prevent="onEmoji(emoji)" {{ group.text }}
> </h6>
<span <span
v-if="!emoji.imageUrl" v-for="emoji in group.emojis"
class="emoji-picker-emoji -unicode" :key="group.id + emoji.displayText"
>{{ emoji.replacement }}</span> :title="maybeLocalizedEmojiName(emoji)"
<still-image class="emoji-item"
v-else @click.stop.prevent="onEmoji(emoji)"
:ref="setEmojiRef(group.id + emoji.displayText)" >
class="emoji-picker-emoji -custom" <span
:data-src="emoji.imageUrl" v-if="!emoji.imageUrl"
:data-emoji-name="group.id + emoji.displayText" class="emoji-picker-emoji -unicode"
/> >{{ emoji.replacement }}</span>
</span> <still-image
<span :ref="setGroupRef('group-end-' + group.id)" /> v-else
</div> class="emoji-picker-emoji -custom"
</div> loading="lazy"
:src="emoji.imageUrl"
:data-emoji-name="group.id + emoji.displayText"
/>
</span>
</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<div class="keep-open"> <div class="keep-open">
<Checkbox v-model="keepOpen"> <Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }} {{ $t('emoji.keep_open') }}

View File

@ -8,7 +8,8 @@ const StillImage = {
'alt', 'alt',
'height', 'height',
'width', 'width',
'dataSrc' 'dataSrc',
'loading'
], ],
data () { data () {
return { return {

View File

@ -17,6 +17,7 @@
:data-src="dataSrc" :data-src="dataSrc"
:src="realSrc" :src="realSrc"
:referrerpolicy="referrerpolicy" :referrerpolicy="referrerpolicy"
:loading="loading"
@load="onLoad" @load="onLoad"
@error="onError" @error="onError"
> >

View File

@ -6103,11 +6103,6 @@ lower-case@^2.0.2:
dependencies: dependencies:
tslib "^2.0.3" tslib "^2.0.3"
lozad@1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/lozad/-/lozad-1.16.0.tgz#86ce732c64c69926ccdebb81c8c90bb3735948b4"
integrity sha512-JBr9WjvEFeKoyim3svo/gsQPTkgG/mOHJmDctZ/+U9H3ymUuvEkqpn8bdQMFsvTMcyRJrdJkLv0bXqGm0sP72w==
lru-cache@^5.1.1: lru-cache@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@ -6375,6 +6370,11 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mitt@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==
mkdirp@^0.5.1: mkdirp@^0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@ -8810,6 +8810,16 @@ vue-loader@17.0.1:
hash-sum "^2.0.0" hash-sum "^2.0.0"
loader-utils "^2.0.0" loader-utils "^2.0.0"
vue-observe-visibility@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz#1e4eda7b12562161d58984b7e0dea676d83bdb13"
integrity sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==
vue-resize@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
vue-router@4.1.6: vue-router@4.1.6:
version "4.1.6" version "4.1.6"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1"
@ -8833,6 +8843,15 @@ vue-template-compiler@2.7.14:
de-indent "^1.0.2" de-indent "^1.0.2"
he "^1.2.0" he "^1.2.0"
vue-virtual-scroller@^2.0.0-beta.7:
version "2.0.0-beta.7"
resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.7.tgz#4ea8158638c84b2033b001a8b26c5fcb6896b271"
integrity sha512-OrouVj1i2939jaLjVfu8f5fsDlbzhAb4bOsYZYrAkpcVLylAmMoGtIL7eT3hJrdTiaKbwQpRdnv7DKf9Fn+tHg==
dependencies:
mitt "^2.1.0"
vue-observe-visibility "^2.0.0-alpha.1"
vue-resize "^2.0.0-alpha.1"
vue@3.2.45: vue@3.2.45:
version "3.2.45" version "3.2.45"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8"