From 61fa33739d6b5fb5cf11cd5d4272ca09b7181ed0 Mon Sep 17 00:00:00 2001 From: Sam Therapy Date: Tue, 24 Jan 2023 22:32:52 +0100 Subject: [PATCH] Add virtual scrolling to emoji picker Signed-off-by: Sam Therapy --- package.json | 1 + src/boot/after_store.js | 9 +- src/components/emoji_picker/emoji_picker.js | 80 ++++++++++++++--- src/components/emoji_picker/emoji_picker.vue | 66 ++++++++------ yarn.lock | 92 +++++++++++++++----- 5 files changed, 187 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 2b23dea0..db318a1d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "vue-i18n": "^7.3.2", "vue-router": "^3.0.1", "vue-template-compiler": "^2.6.11", + "vue-virtual-scroller": "^1.1.2", "vuelidate": "^0.7.4", "vuex": "^3.0.1" }, diff --git a/src/boot/after_store.js b/src/boot/after_store.js index cc0c7c5e..861f576e 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -2,6 +2,7 @@ import Vue from 'vue' import VueRouter from 'vue-router' import routes from './routes' import App from '../App.vue' +import VueVirtualScroller from 'vue-virtual-scroller' import { windowWidth } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' @@ -377,14 +378,18 @@ const afterStoreSetup = async ({ store, i18n }) => { } }) - /* eslint-disable no-new */ - return new Vue({ + let vue = new Vue({ router, store, i18n, el: '#app', render: h => h(App) }) + + Vue.use(VueVirtualScroller) + + /* eslint-disable no-new */ + return vue } export default afterStoreSetup diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 6617a937..940b1a28 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -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() diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index 00ffb9d2..6a93dca6 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -8,6 +8,7 @@ > -
-
-
+ - {{ group.text }} -
- - {{ emoji.replacement }} - - - -
-
+
+ {{ group.text }} +
+ + {{ emoji.replacement }} + + + + + + +
{{ $t('emoji.keep_open') }} diff --git a/yarn.lock b/yarn.lock index 20ca5650..813157bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -337,7 +337,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.18.10, @babel/parser@npm:^7.18.4, @babel/parser@npm:^7.20.5": +"@babel/parser@npm:^7.18.10, @babel/parser@npm:^7.20.5": version: 7.20.5 resolution: "@babel/parser@npm:7.20.5" bin: @@ -346,6 +346,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.18.4": + version: 7.20.13 + resolution: "@babel/parser@npm:7.20.13" + bin: + parser: ./bin/babel-parser.js + checksum: 01991ec7420dbdbf323067addd205690e7e599b1ed2c5fd0b6c2a0703b2dde4819828a5dce76e60efc07340470d8471b4122daa711536c44fa2ae0b9bd8a5732 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -1244,7 +1253,16 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4": +"@babel/runtime@npm:^7.7.6": + version: 7.20.13 + resolution: "@babel/runtime@npm:7.20.13" + dependencies: + regenerator-runtime: "npm:^0.13.11" + checksum: 0e09b4915318248aeeccdf6dc6a8ccdeedbe4b7187fa4159eaa569c4a407c36f3b8689296296b25246671987efe2253dd6e3bab63a40deedcdbd49b8845204e6 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.8.4": version: 7.20.6 resolution: "@babel/runtime@npm:7.20.6" dependencies: @@ -1322,19 +1340,12 @@ __metadata: languageName: node linkType: hard -"@fortawesome/fontawesome-common-types@npm:^0.3.0": - version: 0.3.0 - resolution: "@fortawesome/fontawesome-common-types@npm:0.3.0" - checksum: b59fe576d0b45c41e5266ef6cf2ad6eb46f30eb0cd8de87fb4909a40219650ac94b333c2396ccd1674c8e6d4b53700d9a8bc443dea59f320563e29de6e433109 - languageName: node - linkType: hard - "@fortawesome/fontawesome-svg-core@npm:^1.2.32": - version: 1.3.0 - resolution: "@fortawesome/fontawesome-svg-core@npm:1.3.0" + version: 1.2.36 + resolution: "@fortawesome/fontawesome-svg-core@npm:1.2.36" dependencies: - "@fortawesome/fontawesome-common-types": "npm:^0.3.0" - checksum: 5530e1ede3fd4f9249e2995e33a4a90de3bb7842650853b41c9c788b9f4e40872f97d9ea7d1b085e7356c400691fe24a5dd2944e03c1b22c1118699be9837fa4 + "@fortawesome/fontawesome-common-types": "npm:^0.2.36" + checksum: 2ae3d765cb10321ce3ddc205895c0436d140f9201eddc7ea4e445c14077d8844ffbd7579bb868d45abb13b44f3158a8ebeb14e94b955fb64853ef6db6ecfadb9 languageName: node linkType: hard @@ -1357,12 +1368,12 @@ __metadata: linkType: hard "@fortawesome/vue-fontawesome@npm:^2.0.0": - version: 2.0.9 - resolution: "@fortawesome/vue-fontawesome@npm:2.0.9" + version: 2.0.10 + resolution: "@fortawesome/vue-fontawesome@npm:2.0.10" peerDependencies: "@fortawesome/fontawesome-svg-core": ~1 || ~6 vue: ~2 - checksum: 2690b2d993fb0925855491d11fda515e58e13aeb76ae04e13c63a96126c53a6452c1995421e07a735ee0fef19f09edeb9f0b2cc8e563a3e3f3437294fcfd5bcc + checksum: e2acd05d769393cf787cee8d7b234372311d519185364add60362df7e4dab831fc2127ccaa1b632c41c65b027f0dbd028ee703588f0ac0b6b0af046a977e3c9f languageName: node linkType: hard @@ -10747,6 +10758,7 @@ __metadata: vue-router: "npm:^3.0.1" vue-style-loader: "npm:^4.0.0" vue-template-compiler: "npm:^2.6.11" + vue-virtual-scroller: "npm:^1.1.2" vuelidate: "npm:^0.7.4" vuex: "npm:^3.0.1" webpack: "npm:^4.44.0" @@ -11291,13 +11303,13 @@ __metadata: linkType: hard "postcss@npm:^8.4.14": - version: 8.4.19 - resolution: "postcss@npm:8.4.19" + version: 8.4.21 + resolution: "postcss@npm:8.4.21" dependencies: nanoid: "npm:^3.3.4" picocolors: "npm:^1.0.0" source-map-js: "npm:^1.0.2" - checksum: 583897de1f1b39bed59fecfd2697e34195d6f2f85710572a8f060a14898102e13b0a74a96fd5490b2f8bdc6ed51ae43169a5a24f37684606f7c8272221b5d111 + checksum: 4fb944abed714e5aa88c76b6eae19b293e84ced7ea4162fe0da6ab5215ded572330df93ce3bb5073ee18a9ad0122a6a65e3000897dc3a828e96281d8b3e1f91e languageName: node linkType: hard @@ -11504,9 +11516,9 @@ __metadata: linkType: hard "punycode.js@npm:^2.1.0": - version: 2.1.0 - resolution: "punycode.js@npm:2.1.0" - checksum: 52a45b30cc2ed64ef379f7d989889edbb1dda2f3fa6b47d3cda50d46216a462f37dd2640398549ebde03211f1587937670e532f10debaef5b14bfd66faf9a1d0 + version: 2.3.0 + resolution: "punycode.js@npm:2.3.0" + checksum: cee463755965bbaccc822208288da86fb1e5728aa17b25b5da98f099178ff1774c242eec65a0e03ecf00690cae3d340274c190fcf8e422c481146e0036813c41 languageName: node linkType: hard @@ -12424,6 +12436,13 @@ __metadata: languageName: node linkType: hard +"scrollparent@npm:^2.0.1": + version: 2.0.1 + resolution: "scrollparent@npm:2.0.1" + checksum: b8be0b508e4a1755dd3452fb7925e587c1e34d8e7b9de731c7447400caba80820ffcab3e774dcbee35438486933e2791f3cb7b50fd4f8c02ca9ff280c3f50227 + languageName: node + linkType: hard + "selenium-server@npm:2.53.1": version: 2.53.1 resolution: "selenium-server@npm:2.53.1" @@ -14368,6 +14387,22 @@ __metadata: languageName: node linkType: hard +"vue-observe-visibility@npm:^0.4.4": + version: 0.4.6 + resolution: "vue-observe-visibility@npm:0.4.6" + checksum: f50bc3816684b27af511b1d5f68959b6d4c822cd4d569e46918131460a51d34be55bd25269b630171557edd19d613c5fca730ee8ad123af281c43be4cd39f3cf + languageName: node + linkType: hard + +"vue-resize@npm:^0.4.5": + version: 0.4.5 + resolution: "vue-resize@npm:0.4.5" + peerDependencies: + vue: ^2.3.0 + checksum: 53bd89f8b2c1f0a7a3a41835bc52c9a2783029738c708502faf262305336a13b971d8fbf06ea94f12fb38a7b08852f90f4a8e07e0c9e15924017df2fde680fc0 + languageName: node + linkType: hard + "vue-router@npm:^3.0.1": version: 3.6.5 resolution: "vue-router@npm:3.6.5" @@ -14402,6 +14437,19 @@ __metadata: languageName: node linkType: hard +"vue-virtual-scroller@npm:^1.1.2": + version: 1.1.2 + resolution: "vue-virtual-scroller@npm:1.1.2" + dependencies: + scrollparent: "npm:^2.0.1" + vue-observe-visibility: "npm:^0.4.4" + vue-resize: "npm:^0.4.5" + peerDependencies: + vue: ^2.6.11 + checksum: 2b9a5c871e078adbf0f5717fa22085c56d8f52020fd56d4a4071de89e355f2eb8c5b2d2a27d9c70ca143e0bccb37452b85a8488096dc45ca0075aad06051715f + languageName: node + linkType: hard + "vue@npm:^2.7.14": version: 2.7.14 resolution: "vue@npm:2.7.14"