suggestor popover
This commit is contained in:
parent
c807254d3e
commit
4631b1b9f7
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue