commit: 72a5eaf40af20275a052a25b7aa911d3ef4bdcd7
parent 896cbf89f6431c0198eb2fb50fc7cef9cce05b3c
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Mon, 21 Nov 2022 19:36:15 +0000
Merge branch 'emoji-popovers' into 'develop'
use Popover for Emoji picker + suggestor
See merge request pleroma/pleroma-fe!1648
Diffstat:
10 files changed, 331 insertions(+), 312 deletions(-)
diff --git a/index.html b/index.html
@@ -10,5 +10,6 @@
<noscript>To use Pleroma, please enable JavaScript.</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
+ <div id="popovers" />
</body>
</html>
diff --git a/src/App.vue b/src/App.vue
@@ -73,7 +73,6 @@
<UpdateNotification />
<div id="modal" />
<GlobalNoticeList />
- <div id="popovers" />
</div>
</template>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
@@ -1,5 +1,6 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import Popover from 'src/components/popover/popover.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@@ -109,18 +110,20 @@ const EmojiInput = {
data () {
return {
input: undefined,
+ caretEl: undefined,
highlighted: 0,
caret: 0,
focused: false,
blurTimeout: null,
- showPicker: false,
temporarilyHideSuggestions: false,
- keepOpen: false,
disableClickOutside: false,
- suggestions: []
+ suggestions: [],
+ overlayStyle: {},
+ pickerShown: false
}
},
components: {
+ Popover,
EmojiPicker,
UnicodeDomainIndicator
},
@@ -128,15 +131,21 @@ const EmojiInput = {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
+ preText () {
+ return this.modelValue.slice(0, this.caret)
+ },
+ postText () {
+ return this.modelValue.slice(this.caret)
+ },
showSuggestions () {
return this.focused &&
this.suggestions &&
this.suggestions.length > 0 &&
- !this.showPicker &&
+ !this.pickerShown &&
!this.temporarilyHideSuggestions
},
textAtCaret () {
- return (this.wordAtCaret || {}).word || ''
+ return this.wordAtCaret?.word
},
wordAtCaret () {
if (this.modelValue && this.caret) {
@@ -188,13 +197,35 @@ const EmojiInput = {
return emoji.displayText
}
+ },
+ onInputScroll () {
+ this.$refs.hiddenOverlay.scrollTo({
+ top: this.input.scrollTop,
+ left: this.input.scrollLeft
+ })
}
},
mounted () {
- const { root } = this.$refs
+ const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
if (!input) return
this.input = input
+ this.caretEl = hiddenOverlayCaret
+ if (suggestorPopover.setAnchorEl) {
+ suggestorPopover.setAnchorEl(this.caretEl) // unit test compat
+ this.$refs.picker.setAnchorEl(this.caretEl)
+ } else {
+ console.warn('setAnchorEl not found, are we in a unit test?')
+ }
+ const style = getComputedStyle(this.input)
+ this.overlayStyle.padding = style.padding
+ this.overlayStyle.border = style.border
+ this.overlayStyle.margin = style.margin
+ this.overlayStyle.lineHeight = style.lineHeight
+ this.overlayStyle.fontFamily = style.fontFamily
+ this.overlayStyle.fontSize = style.fontSize
+ this.overlayStyle.wordWrap = style.wordWrap
+ this.overlayStyle.whiteSpace = style.whiteSpace
this.resize()
input.addEventListener('blur', this.onBlur)
input.addEventListener('focus', this.onFocus)
@@ -204,6 +235,7 @@ const EmojiInput = {
input.addEventListener('click', this.onClickInput)
input.addEventListener('transitionend', this.onTransition)
input.addEventListener('input', this.onInput)
+ input.addEventListener('scroll', this.onInputScroll)
},
unmounted () {
const { input } = this
@@ -216,45 +248,43 @@ const EmojiInput = {
input.removeEventListener('click', this.onClickInput)
input.removeEventListener('transitionend', this.onTransition)
input.removeEventListener('input', this.onInput)
+ input.removeEventListener('scroll', this.onInputScroll)
}
},
watch: {
- showSuggestions: function (newValue) {
+ showSuggestions: function (newValue, oldValue) {
this.$emit('shown', newValue)
+ if (newValue) {
+ this.$refs.suggestorPopover.showPopover()
+ } else {
+ this.$refs.suggestorPopover.hidePopover()
+ }
},
textAtCaret: async function (newWord) {
+ if (newWord === undefined) return
const firstchar = newWord.charAt(0)
- this.suggestions = []
- if (newWord === firstchar) return
+ if (newWord === firstchar) {
+ this.suggestions = []
+ return
+ }
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
// Async: cancel if textAtCaret has changed during wait
- if (this.textAtCaret !== newWord) return
- if (matchedSuggestions.length <= 0) return
+ if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
+ this.suggestions = []
+ return
+ }
this.suggestions = take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }) => ({
...rest,
img: imageUrl || ''
}))
- },
- suggestions: {
- handler (newValue) {
- this.$nextTick(this.resize)
- },
- deep: true
}
},
methods: {
- focusPickerInput () {
- const pickerEl = this.$refs.picker.$el
- if (!pickerEl) return
- const pickerInput = pickerEl.querySelector('input')
- if (pickerInput) pickerInput.focus()
- },
triggerShowPicker () {
- this.showPicker = true
this.$nextTick(() => {
+ this.$refs.picker.showPicker()
this.scrollIntoView()
- this.focusPickerInput()
})
// This temporarily disables "click outside" handler
// since external trigger also means click originates
@@ -266,11 +296,12 @@ const EmojiInput = {
},
togglePicker () {
this.input.focus()
- this.showPicker = !this.showPicker
- if (this.showPicker) {
+ if (!this.pickerShown) {
this.scrollIntoView()
+ this.$refs.picker.showPicker()
this.$refs.picker.startEmojiLoad()
- this.$nextTick(this.focusPickerInput)
+ } else {
+ this.$refs.picker.hidePicker()
}
},
replace (replacement) {
@@ -307,7 +338,6 @@ const EmojiInput = {
spaceAfter,
after
].join('')
- this.keepOpen = keepOpen
this.$emit('update:modelValue', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) {
@@ -407,8 +437,11 @@ const EmojiInput = {
}
})
},
- onTransition (e) {
- this.resize()
+ onPickerShown () {
+ this.pickerShown = true
+ },
+ onPickerClosed () {
+ this.pickerShown = false
},
onBlur (e) {
// Clicking on any suggestion removes focus from autocomplete,
@@ -416,7 +449,6 @@ const EmojiInput = {
this.blurTimeout = setTimeout(() => {
this.focused = false
this.setCaret(e)
- this.resize()
}, 200)
},
onClick (e, suggestion) {
@@ -428,18 +460,13 @@ const EmojiInput = {
this.blurTimeout = null
}
- if (!this.keepOpen) {
- this.showPicker = false
- }
this.focused = true
this.setCaret(e)
- this.resize()
this.temporarilyHideSuggestions = false
},
onKeyUp (e) {
const { key } = e
this.setCaret(e)
- this.resize()
// Setting hider in keyUp to prevent suggestions from blinking
// when moving away from suggested spot
@@ -451,7 +478,6 @@ const EmojiInput = {
},
onPaste (e) {
this.setCaret(e)
- this.resize()
},
onKeyDown (e) {
const { ctrlKey, shiftKey, key } = e
@@ -496,58 +522,24 @@ const EmojiInput = {
this.input.focus()
}
}
-
- this.showPicker = false
- this.resize()
},
onInput (e) {
- this.showPicker = false
this.setCaret(e)
- this.resize()
this.$emit('update:modelValue', e.target.value)
},
- onClickInput (e) {
- this.showPicker = false
- },
- onClickOutside (e) {
- if (this.disableClickOutside) return
- this.showPicker = false
- },
onStickerUploaded (e) {
- this.showPicker = false
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
- this.showPicker = false
this.$emit('sticker-upload-Failed', e)
},
setCaret ({ target: { selectionStart } }) {
this.caret = selectionStart
+ this.$nextTick(() => {
+ this.$refs.suggestorPopover.updateStyles()
+ })
},
resize () {
- const panel = this.$refs.panel
- if (!panel) return
- const picker = this.$refs.picker.$el
- const panelBody = this.$refs['panel-body']
- const { offsetHeight, offsetTop } = this.input
- const offsetBottom = offsetTop + offsetHeight
-
- this.setPlacement(panelBody, panel, offsetBottom)
- this.setPlacement(picker, picker, offsetBottom)
- },
- setPlacement (container, target, offsetBottom) {
- if (!container || !target) return
-
- target.style.top = offsetBottom + 'px'
- target.style.bottom = 'auto'
-
- if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
- target.style.top = 'auto'
- target.style.bottom = this.input.offsetHeight + 'px'
- }
- },
- overflowsBottom (el) {
- return el.getBoundingClientRect().bottom > window.innerHeight
}
}
}
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
@@ -1,11 +1,16 @@
<template>
<div
ref="root"
- v-click-outside="onClickOutside"
class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
>
<slot />
+ <!-- TODO: make the 'x' disappear if at the end maybe? -->
+ <div class="hidden-overlay" :style="overlayStyle" ref="hiddenOverlay">
+ <span>{{ preText }}</span>
+ <span class="caret" ref="hiddenOverlayCaret">x</span>
+ <span>{{ postText }}</span>
+ </div>
<template v-if="enableEmojiPicker">
<button
v-if="!hideEmojiButton"
@@ -18,59 +23,61 @@
<EmojiPicker
v-if="enableEmojiPicker"
ref="picker"
- :class="{ hide: !showPicker }"
- :showing="showPicker"
:enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel"
@emoji="insert"
@sticker-uploaded="onStickerUploaded"
@sticker-upload-failed="onStickerUploadFailed"
+ @show="onPickerShown"
+ @close="onPickerClosed"
/>
</template>
- <div
- ref="panel"
+ <Popover
class="autocomplete-panel"
- :class="{ hide: !showSuggestions }"
+ placement="bottom"
+ ref="suggestorPopover"
>
- <div
- ref="panel-body"
- class="autocomplete-panel-body"
- >
+ <template #content>
<div
- v-for="(suggestion, index) in suggestions"
- :key="index"
- class="autocomplete-item"
- :class="{ highlighted: index === highlighted }"
- @click.stop.prevent="onClick($event, suggestion)"
+ ref="panel-body"
+ class="autocomplete-panel-body"
>
- <span class="image">
- <img
- v-if="suggestion.img"
- :src="suggestion.img"
- >
- <span v-else>{{ suggestion.replacement }}</span>
- </span>
- <div class="label">
- <span
- v-if="suggestion.user"
- class="displayText"
- >
- {{ suggestion.displayText }}<UnicodeDomainIndicator
- :user="suggestion.user"
- :at="false"
- />
+ <div
+ v-for="(suggestion, index) in suggestions"
+ :key="index"
+ class="autocomplete-item"
+ :class="{ highlighted: index === highlighted }"
+ @click.stop.prevent="onClick($event, suggestion)"
+ >
+ <span class="image">
+ <img
+ v-if="suggestion.img"
+ :src="suggestion.img"
+ >
+ <span v-else>{{ suggestion.replacement }}</span>
</span>
- <span
- v-if="!suggestion.user"
- class="displayText"
- >
- {{ maybeLocalizedEmojiName(suggestion) }}
- </span>
- <span class="detailText">{{ suggestion.detailText }}</span>
+ <div class="label">
+ <span
+ v-if="suggestion.user"
+ class="displayText"
+ >
+ {{ suggestion.displayText }}<UnicodeDomainIndicator
+ :user="suggestion.user"
+ :at="false"
+ />
+ </span>
+ <span
+ v-if="!suggestion.user"
+ class="displayText"
+ >
+ {{ maybeLocalizedEmojiName(suggestion) }}
+ </span>
+ <span class="detailText">{{ suggestion.detailText }}</span>
+ </div>
</div>
</div>
- </div>
- </div>
+ </template>
+ </Popover>
</div>
</template>
@@ -102,6 +109,7 @@
color: var(--text, $fallback--text);
}
}
+
.emoji-picker-panel {
position: absolute;
z-index: 20;
@@ -112,89 +120,83 @@
}
}
- .autocomplete {
- &-panel {
- position: absolute;
- z-index: 20;
- margin-top: 2px;
-
- &.hide {
- display: none
- }
+ input, textarea {
+ flex: 1 0 auto;
+ }
- &-body {
- margin: 0 0.5em 0 0.5em;
- border-radius: $fallback--tooltipRadius;
- border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
- box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
- box-shadow: var(--popupShadow);
- min-width: 75%;
- background-color: $fallback--bg;
- background-color: var(--popover, $fallback--bg);
- color: $fallback--link;
- color: var(--popoverText, $fallback--link);
- --faint: var(--popoverFaintText, $fallback--faint);
- --faintLink: var(--popoverFaintLink, $fallback--faint);
- --lightText: var(--popoverLightText, $fallback--lightText);
- --postLink: var(--popoverPostLink, $fallback--link);
- --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
- --icon: var(--popoverIcon, $fallback--icon);
- }
+ .hidden-overlay {
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ overflow: hidden;
+ /* DEBUG STUFF */
+ color: red;
+ /* set opacity to non-zero to see the overlay */
+
+ .caret {
+ width: 0;
+ margin-right: calc(-1ch - 1px);
+ border: 1px solid red;
}
+ }
+}
+.autocomplete {
+ &-panel {
+ position: absolute;
+ }
- &-item {
- display: flex;
- cursor: pointer;
- padding: 0.2em 0.4em;
- border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+ &-item {
+ display: flex;
+ cursor: pointer;
+ padding: 0.2em 0.4em;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+ height: 32px;
+
+ .image {
+ width: 32px;
height: 32px;
+ line-height: 32px;
+ text-align: center;
+ font-size: 32px;
+
+ margin-right: 4px;
- .image {
+ img {
width: 32px;
height: 32px;
- line-height: 32px;
- text-align: center;
- font-size: 32px;
-
- margin-right: 4px;
-
- img {
- width: 32px;
- height: 32px;
- object-fit: contain;
- }
+ object-fit: contain;
}
+ }
- .label {
- display: flex;
- flex-direction: column;
- justify-content: center;
- margin: 0 0.1em 0 0.2em;
-
- .displayText {
- line-height: 1.5;
- }
+ .label {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0.1em 0 0.2em;
- .detailText {
- font-size: 9px;
- line-height: 9px;
- }
+ .displayText {
+ line-height: 1.5;
}
- &.highlighted {
- background-color: $fallback--fg;
- background-color: var(--selectedMenuPopover, $fallback--fg);
- color: var(--selectedMenuPopoverText, $fallback--text);
- --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
- --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+ .detailText {
+ font-size: 9px;
+ line-height: 9px;
}
}
- }
- input, textarea {
- flex: 1 0 auto;
+ &.highlighted {
+ background-color: $fallback--fg;
+ background-color: var(--selectedMenuPopover, $fallback--fg);
+ color: var(--selectedMenuPopoverText, $fallback--text);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+ }
}
}
</style>
diff --git 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 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'
@@ -87,10 +88,6 @@ const EmojiPicker = {
required: false,
type: Boolean,
default: false
- },
- showing: {
- required: true,
- type: Boolean
}
},
data () {
@@ -111,15 +108,32 @@ const EmojiPicker = {
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox,
- StillImage
+ StillImage,
+ Popover
},
methods: {
+ showPicker () {
+ this.$refs.popover.showPopover()
+ this.onShowing()
+ },
+ hidePicker () {
+ this.$refs.popover.hidePopover()
+ },
+ setAnchorEl (el) {
+ this.$refs.popover.setAnchorEl(el)
+ },
setGroupRef (name) {
return el => { this.groupRefs[name] = el }
},
setEmojiRef (name) {
return el => { this.emojiRefs[name] = el }
},
+ onPopoverShown () {
+ this.$emit('show')
+ },
+ onPopoverClosed () {
+ this.$emit('close')
+ },
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
@@ -128,6 +142,9 @@ const EmojiPicker = {
},
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
+ if (!this.keepOpen) {
+ this.$refs.popover.hidePopover()
+ }
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
@@ -223,6 +240,9 @@ const EmojiPicker = {
},
onShowing () {
const oldContentLoaded = this.contentLoaded
+ this.$nextTick(() => {
+ this.$refs.search.focus()
+ })
this.contentLoaded = true
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
@@ -251,16 +271,6 @@ const EmojiPicker = {
allCustomGroups () {
this.waitForDomAndInitializeLazyLoad()
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
- },
- showing (val) {
- if (val) {
- this.onShowing()
- }
- }
- },
- mounted () {
- if (this.showing) {
- this.onShowing()
}
},
destroyed () {
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
@@ -6,14 +6,10 @@ $emoji-picker-header-picture-height: 32px;
$emoji-picker-emoji-size: 32px;
.emoji-picker {
+ width: 25em;
+ max-width: 100vw;
display: flex;
flex-direction: column;
- position: absolute;
- right: 0;
- left: 0;
- margin: 0 !important;
- // TODO: actually use popover in emoji picker
- z-index: var(--ZI_popovers);
background-color: $fallback--bg;
background-color: var(--popover, $fallback--bg);
color: $fallback--link;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
@@ -1,129 +1,136 @@
<template>
- <div
- class="emoji-picker panel panel-default panel-body"
+ <Popover
+ trigger="click"
+ popover-class="emoji-picker popover-default"
+ ref="popover"
+ @show="onPopoverShown"
+ @close="onPopoverClosed"
>
- <div class="heading">
- <span
- ref="header"
- class="emoji-tabs"
- >
+ <template #content>
+ <div class="heading">
<span
- v-for="group in filteredEmojiGroups"
- :ref="setGroupRef('group-header-' + group.id)"
- :key="group.id"
- class="emoji-tabs-item"
- :class="{
- active: activeGroupView === group.id
- }"
- :title="group.text"
- @click.prevent="highlight(group.id)"
+ ref="header"
+ class="emoji-tabs"
>
<span
- v-if="group.image"
- class="emoji-picker-header-image"
+ v-for="group in filteredEmojiGroups"
+ :ref="setGroupRef('group-header-' + group.id)"
+ :key="group.id"
+ class="emoji-tabs-item"
+ :class="{
+ active: activeGroupView === group.id
+ }"
+ :title="group.text"
+ @click.prevent="highlight(group.id)"
>
- <still-image
- :alt="group.text"
- :src="group.image"
+ <span
+ v-if="group.image"
+ class="emoji-picker-header-image"
+ >
+ <still-image
+ :alt="group.text"
+ :src="group.image"
+ />
+ </span>
+ <FAIcon
+ v-else
+ :icon="group.icon"
+ fixed-width
/>
</span>
- <FAIcon
- v-else
- :icon="group.icon"
- fixed-width
- />
</span>
- </span>
- <span
- v-if="stickerPickerEnabled"
- class="additional-tabs"
- >
<span
- class="stickers-tab-icon additional-tabs-item"
- :class="{active: showingStickers}"
- :title="$t('emoji.stickers')"
- @click.prevent="toggleStickers"
+ v-if="stickerPickerEnabled"
+ class="additional-tabs"
>
- <FAIcon
- icon="sticky-note"
- fixed-width
- />
+ <span
+ class="stickers-tab-icon additional-tabs-item"
+ :class="{active: showingStickers}"
+ :title="$t('emoji.stickers')"
+ @click.prevent="toggleStickers"
+ >
+ <FAIcon
+ icon="sticky-note"
+ fixed-width
+ />
+ </span>
</span>
- </span>
- </div>
- <div
- v-if="contentLoaded"
- class="content"
- >
+ </div>
<div
- class="emoji-content"
- :class="{hidden: showingStickers}"
+ v-if="contentLoaded"
+ class="content"
>
- <div class="emoji-search">
- <input
- v-model="keyword"
- type="text"
- class="form-control"
- :placeholder="$t('emoji.search_emoji')"
- @input="$event.target.composing = false"
- >
- </div>
<div
- ref="emoji-groups"
- class="emoji-groups"
- :class="groupsScrolledClass"
- @scroll="onScroll"
+ class="emoji-content"
+ :class="{hidden: showingStickers}"
>
+ <div class="emoji-search">
+ <input
+ v-model="keyword"
+ type="text"
+ class="form-control"
+ :placeholder="$t('emoji.search_emoji')"
+ @input="$event.target.composing = false"
+ ref="search"
+ >
+ </div>
<div
- v-for="group in filteredEmojiGroups"
- :key="group.id"
- class="emoji-group"
+ ref="emoji-groups"
+ class="emoji-groups"
+ :class="groupsScrolledClass"
+ @scroll="onScroll"
>
- <h6
- :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)"
+ <div
+ v-for="group in filteredEmojiGroups"
+ :key="group.id"
+ class="emoji-group"
>
+ <h6
+ :ref="setGroupRef('group-' + group.id)"
+ class="emoji-group-title"
+ >
+ {{ group.text }}
+ </h6>
<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)" />
+ 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
+ :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="keep-open">
+ <Checkbox v-model="keepOpen">
+ {{ $t('emoji.keep_open') }}
+ </Checkbox>
</div>
</div>
- <div class="keep-open">
- <Checkbox v-model="keepOpen">
- {{ $t('emoji.keep_open') }}
- </Checkbox>
+ <div
+ v-if="showingStickers"
+ class="stickers-content"
+ >
+ <sticker-picker
+ @uploaded="onStickerUploaded"
+ @upload-failed="onStickerUploadFailed"
+ />
</div>
</div>
- <div
- v-if="showingStickers"
- class="stickers-content"
- >
- <sticker-picker
- @uploaded="onStickerUploaded"
- @upload-failed="onStickerUploadFailed"
- />
- </div>
- </div>
- </div>
+ </template>
+ </Popover>
</template>
<script src="./emoji_picker.js"></script>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
@@ -56,6 +56,10 @@ const Popover = {
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
// so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
// with popovers refusing to be hidden when user wants to interact with something in below popover
+ anchorEl: null,
+ // There's an issue where having teleport enabled by default causes things just...
+ // not render at all, i.e. main post status form and its emoji inputs
+ teleport: false,
lockReEntry: false,
hidden: true,
styles: {},
@@ -64,10 +68,15 @@ const Popover = {
// used to avoid blinking if hovered onto popover
graceTimeout: null,
parentPopover: null,
+ disableClickOutside: false,
childrenShown: new Set()
}
},
methods: {
+ setAnchorEl (el) {
+ this.anchorEl = el
+ this.updateStyles()
+ },
containerBoundingClientRect () {
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
return container.getBoundingClientRect()
@@ -80,7 +89,7 @@ const Popover = {
// Popover will be anchored around this element, trigger ref is the container, so
// its children are what are inside the slot. Expect only one v-slot:trigger.
- const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
+ const anchorEl = this.anchorEl || (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
@@ -231,6 +240,10 @@ const Popover = {
},
showPopover () {
if (this.disabled) return
+ this.disableClickOutside = true
+ setTimeout(() => {
+ this.disableClickOutside = false
+ }, 0)
const wasHidden = this.hidden
this.hidden = false
this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
@@ -291,6 +304,7 @@ const Popover = {
}
},
onClickOutside (e) {
+ if (this.disableClickOutside) return
if (this.hidden) return
if (this.$refs.content && this.$refs.content.contains(e.target)) return
if (this.$el.contains(e.target)) return
@@ -324,6 +338,7 @@ const Popover = {
}
},
mounted () {
+ this.teleport = true
let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
this.$refs.trigger.closest('.mobile-notifications')
if (!scrollable) scrollable = window
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
@@ -12,7 +12,7 @@
>
<slot name="trigger" />
</button>
- <teleport to="#popovers">
+ <teleport :disabled="!teleport" to="#popovers">
<transition name="fade">
<div
v-if="!hidden"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
@@ -501,7 +501,6 @@ const PostStatusForm = {
if (target.value === '') {
target.style.height = null
this.$emit('resize')
- this.$refs['emoji-input'].resize()
return
}
@@ -588,8 +587,6 @@ const PostStatusForm = {
} else {
scrollerRef.scrollTop = targetScroll
}
-
- this.$refs['emoji-input'].resize()
},
showEmojiPicker () {
this.$refs.textarea.focus()