forked from AkkomaGang/akkoma-fe
develop #2
7 changed files with 261 additions and 198 deletions
133
src/components/emoji_grid/emoji_grid.js
Normal file
133
src/components/emoji_grid/emoji_grid.js
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
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
|
60
src/components/emoji_grid/emoji_grid.scss
Normal file
60
src/components/emoji_grid/emoji_grid.scss
Normal file
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/components/emoji_grid/emoji_grid.vue
Normal file
48
src/components/emoji_grid/emoji_grid.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="container"
|
||||||
|
class="emoji-grid"
|
||||||
|
:class="scrolledClass"
|
||||||
|
@scroll.passive="onScroll"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
height: `${gridHeight}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-for="item in visibleItems">
|
||||||
|
<h6
|
||||||
|
v-if="'title' in item && item.title.length"
|
||||||
|
:key="'title-' + item.id"
|
||||||
|
class="emoji-group-title"
|
||||||
|
:style="{
|
||||||
|
top: item.position.y + 'px',
|
||||||
|
left: item.position.x + 'px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</h6>
|
||||||
|
<span
|
||||||
|
v-else-if="'emoji' in item"
|
||||||
|
:key="'emoji-' + item.id"
|
||||||
|
class="emoji-item"
|
||||||
|
:title="item.emoji.displayText"
|
||||||
|
:style="{
|
||||||
|
top: item.position.y + 'px',
|
||||||
|
left: item.position.x + 'px'
|
||||||
|
}"
|
||||||
|
@click.stop.prevent="onEmoji(item.emoji)"
|
||||||
|
>
|
||||||
|
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="item.emoji.imageUrl"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./emoji_grid.js"></script>
|
||||||
|
<style lang="scss" src="./emoji_grid.scss"></style>
|
|
@ -205,7 +205,6 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.focusPickerInput()
|
this.focusPickerInput()
|
||||||
|
@ -223,7 +222,6 @@ const EmojiInput = {
|
||||||
this.showPicker = !this.showPicker
|
this.showPicker = !this.showPicker
|
||||||
if (this.showPicker) {
|
if (this.showPicker) {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(this.focusPickerInput)
|
this.$nextTick(this.focusPickerInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
|
@ -14,13 +15,6 @@ library.add(
|
||||||
faSmileBeam
|
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 = {
|
const EmojiPicker = {
|
||||||
props: {
|
props: {
|
||||||
enableStickerPicker: {
|
enableStickerPicker: {
|
||||||
|
@ -39,16 +33,13 @@ const EmojiPicker = {
|
||||||
keyword: '',
|
keyword: '',
|
||||||
activeGroup: 'standard',
|
activeGroup: 'standard',
|
||||||
showingStickers: false,
|
showingStickers: false,
|
||||||
groupsScrolledClass: 'scrolled-top',
|
keepOpen: false
|
||||||
keepOpen: false,
|
|
||||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
|
||||||
customEmojiTimeout: null,
|
|
||||||
customEmojiLoadAllConfirmed: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
EmojiGrid
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
|
@ -61,12 +52,6 @@ const EmojiPicker = {
|
||||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
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) {
|
onWheel (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||||
|
@ -74,68 +59,12 @@ const EmojiPicker = {
|
||||||
highlight (key) {
|
highlight (key) {
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
},
|
if (this.keyword.length) {
|
||||||
updateScrolledClass (target) {
|
this.$refs.emojiGrid.scrollToItem(key)
|
||||||
if (target.scrollTop <= 5) {
|
|
||||||
this.groupsScrolledClass = 'scrolled-top'
|
|
||||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
|
||||||
this.groupsScrolledClass = 'scrolled-bottom'
|
|
||||||
} else {
|
|
||||||
this.groupsScrolledClass = 'scrolled-middle'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerLoadMore (target) {
|
onActiveGroup (group) {
|
||||||
const ref = this.$refs['group-end-custom']
|
this.activeGroup = group
|
||||||
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
|
|
||||||
},
|
},
|
||||||
toggleStickers () {
|
toggleStickers () {
|
||||||
this.showingStickers = !this.showingStickers
|
this.showingStickers = !this.showingStickers
|
||||||
|
@ -151,13 +80,6 @@ const EmojiPicker = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
keyword () {
|
|
||||||
this.customEmojiLoadAllConfirmed = false
|
|
||||||
this.onScroll()
|
|
||||||
this.startEmojiLoad(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
activeGroupView () {
|
activeGroupView () {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
return this.showingStickers ? '' : this.activeGroup
|
||||||
|
@ -173,9 +95,6 @@ const EmojiPicker = {
|
||||||
this.$store.state.instance.customEmoji || []
|
this.$store.state.instance.customEmoji || []
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
customEmojiBuffer () {
|
|
||||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
|
||||||
},
|
|
||||||
emojis () {
|
emojis () {
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
const standardEmojis = this.$store.state.instance.emoji || []
|
||||||
const customEmojis = this.sortedEmoji
|
const customEmojis = this.sortedEmoji
|
||||||
|
|
|
@ -85,10 +85,6 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-groups {
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-tabs {
|
.additional-tabs {
|
||||||
border-left: 1px solid;
|
border-left: 1px solid;
|
||||||
border-left-color: $fallback--icon;
|
border-left-color: $fallback--icon;
|
||||||
|
@ -167,76 +163,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji-search {
|
||||||
&-search {
|
padding: 5px;
|
||||||
padding: 5px;
|
flex: 0 0 auto;
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="emoji-picker panel panel-default panel-body">
|
<div class="emoji-picker panel panel-default panel-body">
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<span
|
<span
|
||||||
|
ref="emoji-tabs"
|
||||||
class="emoji-tabs"
|
class="emoji-tabs"
|
||||||
@wheel="onWheel"
|
@wheel="onWheel"
|
||||||
ref="emoji-tabs"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="group in emojis"
|
v-for="group in emojis"
|
||||||
|
@ -51,39 +51,12 @@
|
||||||
@input="$event.target.composing = false"
|
@input="$event.target.composing = false"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<EmojiGrid
|
||||||
ref="emoji-groups"
|
ref="emojiGrid"
|
||||||
class="emoji-groups"
|
:groups="emojisView"
|
||||||
:class="groupsScrolledClass"
|
@emoji="onEmoji"
|
||||||
@scroll="onScroll"
|
@active-group="onActiveGroup"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
v-for="group in emojisView"
|
|
||||||
:key="group.id"
|
|
||||||
class="emoji-group"
|
|
||||||
>
|
|
||||||
<h6
|
|
||||||
:ref="'group-' + group.id"
|
|
||||||
class="emoji-group-title"
|
|
||||||
>
|
|
||||||
{{ group.text }}
|
|
||||||
</h6>
|
|
||||||
<span
|
|
||||||
v-for="emoji in group.emojis"
|
|
||||||
:key="group.id + emoji.displayText"
|
|
||||||
:title="emoji.displayText"
|
|
||||||
class="emoji-item"
|
|
||||||
@click.stop.prevent="onEmoji(emoji)"
|
|
||||||
>
|
|
||||||
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
:src="emoji.imageUrl"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span :ref="'group-end-' + group.id" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="showKeepOpen"
|
v-if="showKeepOpen"
|
||||||
class="keep-open"
|
class="keep-open"
|
||||||
|
|
Loading…
Reference in a new issue