logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
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:

Mindex.html1+
Msrc/App.vue1-
Msrc/components/emoji_input/emoji_input.js142+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/emoji_input/emoji_input.vue218++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/components/emoji_picker/emoji_picker.js40+++++++++++++++++++++++++---------------
Msrc/components/emoji_picker/emoji_picker.scss8++------
Msrc/components/emoji_picker/emoji_picker.vue211+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/components/popover/popover.js17++++++++++++++++-
Msrc/components/popover/popover.vue2+-
Msrc/components/post_status_form/post_status_form.js3---
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()