commit: 4631b1b9f7e7813cab65e0d0705cd1e265f23393
parent c807254d3eadbd57de292360c5200358261cbc0f
Author: Henry Jameson <me@hjkos.com>
Date: Sun, 9 Oct 2022 22:09:50 +0300
suggestor popover
Diffstat:
4 files changed, 127 insertions(+), 94 deletions(-)
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,6 +110,7 @@ const EmojiInput = {
data () {
return {
input: undefined,
+ caretEl: undefined,
highlighted: 0,
caret: 0,
focused: false,
@@ -117,10 +119,12 @@ const EmojiInput = {
temporarilyHideSuggestions: false,
keepOpen: false,
disableClickOutside: false,
- suggestions: []
+ suggestions: [],
+ overlayStyle: {}
}
},
components: {
+ Popover,
EmojiPicker,
UnicodeDomainIndicator
},
@@ -128,7 +132,15 @@ 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 () {
+ console.log(this.focused)
+ console.log(this.suggestions)
return this.focused &&
this.suggestions &&
this.suggestions.length > 0 &&
@@ -191,10 +203,21 @@ const EmojiInput = {
}
},
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
+ 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()
input.addEventListener('blur', this.onBlur)
input.addEventListener('focus', this.onFocus)
@@ -204,6 +227,16 @@ const EmojiInput = {
input.addEventListener('click', this.onClickInput)
input.addEventListener('transitionend', this.onTransition)
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 () {
const { input } = this
@@ -219,22 +252,32 @@ const EmojiInput = {
}
},
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) {
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 || ''
}))
+ this.$refs.suggestorPopover.updateStyles()
},
suggestions: {
handler (newValue) {
@@ -525,29 +568,6 @@ const EmojiInput = {
this.caret = selectionStart
},
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
@@ -6,6 +6,12 @@
: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"
@@ -27,50 +33,52 @@
@sticker-upload-failed="onStickerUploadFailed"
/>
</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 +110,7 @@
color: var(--text, $fallback--text);
}
}
+
.emoji-picker-panel {
position: absolute;
z-index: 20;
@@ -115,31 +124,6 @@
.autocomplete {
&-panel {
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 {
@@ -196,5 +180,25 @@
input, textarea {
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>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
@@ -51,6 +51,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: {},
@@ -63,6 +67,10 @@ const Popover = {
}
},
methods: {
+ setAnchorEl (el) {
+ this.anchorEl = el
+ this.updateStyles()
+ },
containerBoundingClientRect () {
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
return container.getBoundingClientRect()
@@ -75,7 +83,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
@@ -319,6 +327,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
@@ -11,7 +11,7 @@
>
<slot name="trigger" />
</button>
- <teleport to="#popovers">
+ <teleport :disabled="!teleport" to="#popovers">
<transition name="fade">
<div
v-if="!hidden"