Selaa lähdekoodia

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

Remove lozad, use virtual scrolling

See merge request pleroma/pleroma-fe!1717
HJ 2 vuotta sitten
vanhempi
sitoutus
a7387332ed

+ 1 - 1
package.json

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

+ 3 - 0
src/boot/after_store.js

@@ -1,6 +1,8 @@
 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'
 
@@ -397,6 +399,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
 
   app.use(vClickOutside)
   app.use(VBodyScrollLock)
+  app.use(VueVirtualScroller)
 
   app.component('FAIcon', FontAwesomeIcon)
   app.component('FALayers', FontAwesomeLayers)

+ 60 - 57
src/components/emoji_picker/emoji_picker.js

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

+ 1 - 0
src/components/emoji_picker/emoji_picker.scss

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

+ 44 - 33
src/components/emoji_picker/emoji_picker.vue

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

+ 2 - 1
src/components/still-image/still-image.js

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

+ 1 - 0
src/components/still-image/still-image.vue

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

+ 24 - 5
yarn.lock

@@ -6103,11 +6103,6 @@ lower-case@^2.0.2:
   dependencies:
     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:
   version "5.1.1"
   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"
   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:
   version "0.5.1"
   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"
     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:
   version "4.1.6"
   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"
     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:
   version "3.2.45"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8"