suggestor popover

This commit is contained in:
Henry Jameson 2022-10-09 22:09:50 +03:00
parent c807254d3e
commit 4631b1b9f7
4 changed files with 127 additions and 94 deletions

View File

@ -1,5 +1,6 @@
import Completion from '../../services/completion/completion.js' import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue' 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 UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash' import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -109,6 +110,7 @@ const EmojiInput = {
data () { data () {
return { return {
input: undefined, input: undefined,
caretEl: undefined,
highlighted: 0, highlighted: 0,
caret: 0, caret: 0,
focused: false, focused: false,
@ -117,10 +119,12 @@ const EmojiInput = {
temporarilyHideSuggestions: false, temporarilyHideSuggestions: false,
keepOpen: false, keepOpen: false,
disableClickOutside: false, disableClickOutside: false,
suggestions: [] suggestions: [],
overlayStyle: {}
} }
}, },
components: { components: {
Popover,
EmojiPicker, EmojiPicker,
UnicodeDomainIndicator UnicodeDomainIndicator
}, },
@ -128,7 +132,15 @@ const EmojiInput = {
padEmoji () { padEmoji () {
return this.$store.getters.mergedConfig.padEmoji return this.$store.getters.mergedConfig.padEmoji
}, },
preText () {
return this.modelValue.slice(0, this.caret)
},
postText () {
return this.modelValue.slice(this.caret)
},
showSuggestions () { showSuggestions () {
console.log(this.focused)
console.log(this.suggestions)
return this.focused && return this.focused &&
this.suggestions && this.suggestions &&
this.suggestions.length > 0 && this.suggestions.length > 0 &&
@ -191,10 +203,21 @@ const EmojiInput = {
} }
}, },
mounted () { mounted () {
const { root } = this.$refs const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
if (!input) return if (!input) return
this.input = input this.input = input
this.caretEl = hiddenOverlayCaret
suggestorPopover.setAnchorEl(this.caretEl)
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() this.resize()
input.addEventListener('blur', this.onBlur) input.addEventListener('blur', this.onBlur)
input.addEventListener('focus', this.onFocus) input.addEventListener('focus', this.onFocus)
@ -204,6 +227,16 @@ const EmojiInput = {
input.addEventListener('click', this.onClickInput) input.addEventListener('click', this.onClickInput)
input.addEventListener('transitionend', this.onTransition) input.addEventListener('transitionend', this.onTransition)
input.addEventListener('input', this.onInput) input.addEventListener('input', this.onInput)
input.addEventListener('scroll', (e) => {
console.log({
top: this.input.scrollTop,
left: this.input.scrollLeft
})
this.$refs.hiddenOverlay.scrollTo({
top: this.input.scrollTop,
left: this.input.scrollLeft
})
})
}, },
unmounted () { unmounted () {
const { input } = this const { input } = this
@ -219,22 +252,32 @@ const EmojiInput = {
} }
}, },
watch: { watch: {
showSuggestions: function (newValue) { showSuggestions: function (newValue, oldValue) {
this.$emit('shown', newValue) this.$emit('shown', newValue)
if (newValue) {
this.$refs.suggestorPopover.showPopover()
} else {
this.$refs.suggestorPopover.hidePopover()
}
}, },
textAtCaret: async function (newWord) { textAtCaret: async function (newWord) {
const firstchar = newWord.charAt(0) const firstchar = newWord.charAt(0)
this.suggestions = [] if (newWord === firstchar) {
if (newWord === firstchar) return this.suggestions = []
return
}
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords) const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
// Async: cancel if textAtCaret has changed during wait // Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
if (matchedSuggestions.length <= 0) return this.suggestions = []
return
}
this.suggestions = take(matchedSuggestions, 5) this.suggestions = take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }) => ({ .map(({ imageUrl, ...rest }) => ({
...rest, ...rest,
img: imageUrl || '' img: imageUrl || ''
})) }))
this.$refs.suggestorPopover.updateStyles()
}, },
suggestions: { suggestions: {
handler (newValue) { handler (newValue) {
@ -525,29 +568,6 @@ const EmojiInput = {
this.caret = selectionStart this.caret = selectionStart
}, },
resize () { 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
} }
} }
} }

View File

@ -6,6 +6,12 @@
:class="{ 'with-picker': !hideEmojiButton }" :class="{ 'with-picker': !hideEmojiButton }"
> >
<slot /> <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"> <template v-if="enableEmojiPicker">
<button <button
v-if="!hideEmojiButton" v-if="!hideEmojiButton"
@ -27,50 +33,52 @@
@sticker-upload-failed="onStickerUploadFailed" @sticker-upload-failed="onStickerUploadFailed"
/> />
</template> </template>
<div <Popover
ref="panel"
class="autocomplete-panel" class="autocomplete-panel"
:class="{ hide: !showSuggestions }" placement="bottom"
ref="suggestorPopover"
> >
<div <template #content>
ref="panel-body"
class="autocomplete-panel-body"
>
<div <div
v-for="(suggestion, index) in suggestions" ref="panel-body"
:key="index" class="autocomplete-panel-body"
class="autocomplete-item"
:class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)"
> >
<span class="image"> <div
<img v-for="(suggestion, index) in suggestions"
v-if="suggestion.img" :key="index"
:src="suggestion.img" class="autocomplete-item"
> :class="{ highlighted: index === highlighted }"
<span v-else>{{ suggestion.replacement }}</span> @click.stop.prevent="onClick($event, suggestion)"
</span> >
<div class="label"> <span class="image">
<span <img
v-if="suggestion.user" v-if="suggestion.img"
class="displayText" :src="suggestion.img"
> >
{{ suggestion.displayText }}<UnicodeDomainIndicator <span v-else>{{ suggestion.replacement }}</span>
:user="suggestion.user"
:at="false"
/>
</span> </span>
<span <div class="label">
v-if="!suggestion.user" <span
class="displayText" v-if="suggestion.user"
> class="displayText"
{{ maybeLocalizedEmojiName(suggestion) }} >
</span> {{ suggestion.displayText }}<UnicodeDomainIndicator
<span class="detailText">{{ suggestion.detailText }}</span> :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>
</div> </template>
</div> </Popover>
</div> </div>
</template> </template>
@ -102,6 +110,7 @@
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }
} }
.emoji-picker-panel { .emoji-picker-panel {
position: absolute; position: absolute;
z-index: 20; z-index: 20;
@ -115,31 +124,6 @@
.autocomplete { .autocomplete {
&-panel { &-panel {
position: absolute; position: absolute;
z-index: 20;
margin-top: 2px;
&.hide {
display: none
}
&-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);
}
} }
&-item { &-item {
@ -196,5 +180,25 @@
input, textarea { input, textarea {
flex: 1 0 auto; flex: 1 0 auto;
} }
.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;
}
}
} }
</style> </style>

View File

@ -51,6 +51,10 @@ const Popover = {
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content // 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 // 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 // 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, lockReEntry: false,
hidden: true, hidden: true,
styles: {}, styles: {},
@ -63,6 +67,10 @@ const Popover = {
} }
}, },
methods: { methods: {
setAnchorEl (el) {
this.anchorEl = el
this.updateStyles()
},
containerBoundingClientRect () { containerBoundingClientRect () {
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
return container.getBoundingClientRect() return container.getBoundingClientRect()
@ -75,7 +83,7 @@ const Popover = {
// Popover will be anchored around this element, trigger ref is the container, so // 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. // 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 // SVGs don't have offsetWidth/Height, use fallback
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
@ -319,6 +327,7 @@ const Popover = {
} }
}, },
mounted () { mounted () {
this.teleport = true
let scrollable = this.$refs.trigger.closest('.column.-scrollable') || let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
this.$refs.trigger.closest('.mobile-notifications') this.$refs.trigger.closest('.mobile-notifications')
if (!scrollable) scrollable = window if (!scrollable) scrollable = window

View File

@ -11,7 +11,7 @@
> >
<slot name="trigger" /> <slot name="trigger" />
</button> </button>
<teleport to="#popovers"> <teleport :disabled="!teleport" to="#popovers">
<transition name="fade"> <transition name="fade">
<div <div
v-if="!hidden" v-if="!hidden"