diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index ffc0ffac..b365043c 100644 --- 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 index 43581dbf..8a4bc244 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -6,6 +6,12 @@ :class="{ 'with-picker': !hideEmojiButton }" > + +
+ {{ preText }} + x + {{ postText }} +
-
-
+ +
@@ -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; + } + } } diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index dd332c35..60c42f0f 100644 --- 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 index 623af8d2..217eba7c 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -11,7 +11,7 @@ > - +