A simple virtual scroller is now used for the emoji grid. This avoids loading all emoji images at once, saving network bandwidth and reducing load on the server, while also putting less work on the browser's DOM and layout engine. Co-authored-by: yan <yan@omg.lol> Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/275 Co-authored-by: yanchan09 <yan@omg.lol> Co-committed-by: yanchan09 <yan@omg.lol>
133 lines
3.3 KiB
JavaScript
133 lines
3.3 KiB
JavaScript
const EMOJI_SIZE = 32 + 8
|
|
const GROUP_TITLE_HEIGHT = 24
|
|
const BUFFER_SIZE = 3 * EMOJI_SIZE
|
|
|
|
const EmojiGrid = {
|
|
props: {
|
|
groups: {
|
|
required: true,
|
|
type: Array
|
|
}
|
|
},
|
|
data () {
|
|
return {
|
|
containerWidth: 0,
|
|
containerHeight: 0,
|
|
scrollPos: 0,
|
|
resizeObserver: null
|
|
}
|
|
},
|
|
mounted () {
|
|
const rect = this.$refs.container.getBoundingClientRect()
|
|
this.containerWidth = rect.width
|
|
this.containerHeight = rect.height
|
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
this.containerWidth = entry.contentRect.width
|
|
this.containerHeight = entry.contentRect.height
|
|
}
|
|
})
|
|
this.resizeObserver.observe(this.$refs.container)
|
|
},
|
|
beforeUnmount () {
|
|
this.resizeObserver.disconnect()
|
|
this.resizeObserver = null
|
|
},
|
|
watch: {
|
|
groups () {
|
|
// Scroll to top when grid content changes
|
|
if (this.$refs.container) {
|
|
this.$refs.container.scrollTo(0, 0)
|
|
}
|
|
},
|
|
activeGroup (group) {
|
|
this.$emit('activeGroup', group)
|
|
}
|
|
},
|
|
methods: {
|
|
onScroll () {
|
|
this.scrollPos = this.$refs.container.scrollTop
|
|
},
|
|
onEmoji (emoji) {
|
|
this.$emit('emoji', emoji)
|
|
},
|
|
scrollToItem (itemId) {
|
|
const container = this.$refs.container
|
|
if (!container) return
|
|
|
|
for (const item of this.itemList) {
|
|
if (item.id === itemId) {
|
|
container.scrollTo(0, item.position.y)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
// Total height of scroller content
|
|
gridHeight () {
|
|
if (this.itemList.length === 0) return 0
|
|
const lastItem = this.itemList[this.itemList.length - 1]
|
|
return (
|
|
lastItem.position.y +
|
|
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
|
|
)
|
|
},
|
|
activeGroup () {
|
|
const items = this.itemList
|
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
const item = items[i]
|
|
if ('title' in item && item.position.y <= this.scrollPos) {
|
|
return item.id
|
|
}
|
|
}
|
|
return null
|
|
},
|
|
itemList () {
|
|
const items = []
|
|
let x = 0
|
|
let y = 0
|
|
for (const group of this.groups) {
|
|
items.push({ position: { x, y }, id: group.id, title: group.text })
|
|
if (group.text.length) {
|
|
y += GROUP_TITLE_HEIGHT
|
|
}
|
|
for (const emoji of group.emojis) {
|
|
items.push({
|
|
position: { x, y },
|
|
id: `${group.id}-${emoji.displayText}`,
|
|
emoji
|
|
})
|
|
x += EMOJI_SIZE
|
|
if (x + EMOJI_SIZE > this.containerWidth) {
|
|
y += EMOJI_SIZE
|
|
x = 0
|
|
}
|
|
}
|
|
if (x > 0) {
|
|
y += EMOJI_SIZE
|
|
x = 0
|
|
}
|
|
}
|
|
return items
|
|
},
|
|
visibleItems () {
|
|
const startPos = this.scrollPos - BUFFER_SIZE
|
|
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
|
|
return this.itemList.filter((i) => {
|
|
return i.position.y >= startPos && i.position.y < endPos
|
|
})
|
|
},
|
|
scrolledClass () {
|
|
if (this.scrollPos <= 5) {
|
|
return 'scrolled-top'
|
|
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {
|
|
return 'scrolled-bottom'
|
|
} else {
|
|
return 'scrolled-middle'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default EmojiGrid
|