diff --git a/src/components/emoji_grid/emoji_grid.js b/src/components/emoji_grid/emoji_grid.js
new file mode 100644
index 00000000..236051ea
--- /dev/null
+++ b/src/components/emoji_grid/emoji_grid.js
@@ -0,0 +1,125 @@
+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, 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
diff --git a/src/components/emoji_grid/emoji_grid.scss b/src/components/emoji_grid/emoji_grid.scss
new file mode 100644
index 00000000..5d5b153f
--- /dev/null
+++ b/src/components/emoji_grid/emoji_grid.scss
@@ -0,0 +1,60 @@
+.emoji {
+ &-grid {
+ flex: 1 1 1px;
+ position: relative;
+ overflow: auto;
+ user-select: none;
+ mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
+ linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
+ linear-gradient(to top, white, white);
+ transition: mask-size 150ms;
+ mask-size: 100% 20px, 100% 20px, auto;
+ // Autoprefixed seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ &.scrolled {
+ &-top {
+ mask-size: 100% 20px, 100% 0, auto;
+ }
+ &-bottom {
+ mask-size: 100% 0, 100% 20px, auto;
+ }
+ }
+ margin-left: 5px;
+ min-height: 200px;
+ }
+
+ &-group-title {
+ position: absolute;
+ font-size: 0.85em;
+ width: 100%;
+ margin: 0;
+ height: 24px;
+ display: flex;
+ align-items: end;
+
+ &.disabled {
+ display: none;
+ }
+ }
+
+ &-item {
+ position: absolute;
+ width: 32px;
+ height: 32px;
+ box-sizing: border-box;
+ display: flex;
+ font-size: 32px;
+ align-items: center;
+ justify-content: center;
+ margin: 4px;
+
+ cursor: pointer;
+
+ img {
+ object-fit: contain;
+ max-width: 100%;
+ max-height: 100%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/emoji_grid/emoji_grid.vue b/src/components/emoji_grid/emoji_grid.vue
new file mode 100644
index 00000000..94732319
--- /dev/null
+++ b/src/components/emoji_grid/emoji_grid.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+ {{ item.title }}
+
+
+ {{ item.emoji.replacement }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 902ec384..eb70244e 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -202,7 +202,6 @@ const EmojiInput = {
},
triggerShowPicker () {
this.showPicker = true
- this.$refs.picker.startEmojiLoad()
this.$nextTick(() => {
this.scrollIntoView()
this.focusPickerInput()
@@ -220,7 +219,6 @@ const EmojiInput = {
this.showPicker = !this.showPicker
if (this.showPicker) {
this.scrollIntoView()
- this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput)
}
},
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 6617a937..a102dbf4 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
+import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
@@ -14,13 +15,6 @@ library.add(
faSmileBeam
)
-// At widest, approximately 20 emoji are visible in a row,
-// loading 3 rows, could be overkill for narrow picker
-const LOAD_EMOJI_BY = 60
-
-// When to start loading new batch emoji, in pixels
-const LOAD_EMOJI_MARGIN = 64
-
const EmojiPicker = {
props: {
enableStickerPicker: {
@@ -34,16 +28,13 @@ const EmojiPicker = {
keyword: '',
activeGroup: 'standard',
showingStickers: false,
- groupsScrolledClass: 'scrolled-top',
- keepOpen: false,
- customEmojiBufferSlice: LOAD_EMOJI_BY,
- customEmojiTimeout: null,
- customEmojiLoadAllConfirmed: false
+ keepOpen: false
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
- Checkbox
+ Checkbox,
+ EmojiGrid
},
methods: {
onStickerUploaded (e) {
@@ -56,12 +47,6 @@ 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)
- },
onWheel (e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
@@ -69,68 +54,12 @@ const EmojiPicker = {
highlight (key) {
this.setShowStickers(false)
this.activeGroup = key
- },
- 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'
+ if (this.keyword.length) {
+ this.$refs.emojiGrid.scrollToItem(key)
}
},
- triggerLoadMore (target) {
- const ref = this.$refs['group-end-custom']
- if (!ref) return
- const bottom = ref.offsetTop + ref.offsetHeight
-
- const scrollerBottom = target.scrollTop + target.clientHeight
- const scrollerTop = target.scrollTop
- const scrollerMax = target.scrollHeight
-
- // Loads more emoji when they come into view
- const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
- // Always load when at the very top in case there's no scroll space yet
- const atTop = scrollerTop < 5
- // Don't load when looking at unicode category or at the very bottom
- const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
- if (!bottomAboveViewport && (approachingBottom || atTop)) {
- this.loadEmoji()
- }
- },
- scrolledGroup (target) {
- 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
- }
- })
- })
- },
- loadEmoji () {
- const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
-
- if (allLoaded) {
- return
- }
-
- this.customEmojiBufferSlice += LOAD_EMOJI_BY
- },
- startEmojiLoad (forceUpdate = false) {
- if (!forceUpdate) {
- this.keyword = ''
- }
- this.$nextTick(() => {
- this.$refs['emoji-groups'].scrollTop = 0
- })
- const bufferSize = this.customEmojiBuffer.length
- const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
- if (bufferPrefilledAll && !forceUpdate) {
- return
- }
- this.customEmojiBufferSlice = LOAD_EMOJI_BY
+ onActiveGroup (group) {
+ this.activeGroup = group
},
toggleStickers () {
this.showingStickers = !this.showingStickers
@@ -146,13 +75,6 @@ const EmojiPicker = {
})
}
},
- watch: {
- keyword () {
- this.customEmojiLoadAllConfirmed = false
- this.onScroll()
- this.startEmojiLoad(true)
- }
- },
computed: {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
@@ -168,9 +90,6 @@ const EmojiPicker = {
this.$store.state.instance.customEmoji || []
)
},
- customEmojiBuffer () {
- return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
- },
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index a37b2a40..6f2c1c22 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -69,10 +69,6 @@
flex-grow: 1;
}
- .emoji-groups {
- min-height: 200px;
- }
-
.additional-tabs {
border-left: 1px solid;
border-left-color: $fallback--icon;
@@ -151,76 +147,12 @@
}
}
- .emoji {
- &-search {
- padding: 5px;
- flex: 0 0 auto;
+ .emoji-search {
+ padding: 5px;
+ flex: 0 0 auto;
- input {
- width: 100%;
- }
+ input {
+ width: 100%;
}
-
- &-groups {
- flex: 1 1 1px;
- position: relative;
- overflow: auto;
- user-select: none;
- mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
- linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
- linear-gradient(to top, white, white);
- transition: mask-size 150ms;
- mask-size: 100% 20px, 100% 20px, auto;
- // Autoprefixed seem to ignore this one, and also syntax is different
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- &.scrolled {
- &-top {
- mask-size: 100% 20px, 100% 0, auto;
- }
- &-bottom {
- mask-size: 100% 0, 100% 20px, auto;
- }
- }
- }
-
- &-group {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- padding-left: 5px;
- justify-content: left;
-
- &-title {
- font-size: 0.85em;
- width: 100%;
- margin: 0;
-
- &.disabled {
- display: none;
- }
- }
- }
-
- &-item {
- width: 32px;
- height: 32px;
- box-sizing: border-box;
- display: flex;
- font-size: 32px;
- align-items: center;
- justify-content: center;
- margin: 4px;
-
- cursor: pointer;
-
- img {
- object-fit: contain;
- max-width: 100%;
- max-height: 100%;
- }
- }
-
}
-
}
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 00ffb9d2..fe2e39b2 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -2,9 +2,9 @@
+
-
-
- {{ group.text }}
-
-
- {{ emoji.replacement }}
-
-
-
-
-
-
{{ $t('emoji.keep_open') }}