From 96f31716f94d0e7691b85ca90e7ea977ca3adb4d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 7 Jun 2019 00:17:49 +0300 Subject: [PATCH] slot-based emoji input/autocomplete component --- src/boot/after_store.js | 12 ++- src/components/emoji-input/emoji-input.js | 99 ++++++++++++------- src/components/emoji-input/emoji-input.vue | 36 ++----- src/components/emoji-input/suggestor.js | 38 +++++++ .../post_status_form/post_status_form.js | 74 +++----------- .../post_status_form/post_status_form.vue | 68 ++++++------- src/components/user_settings/user_settings.js | 16 +++ .../user_settings/user_settings.vue | 24 ++--- 8 files changed, 195 insertions(+), 172 deletions(-) create mode 100644 src/components/emoji-input/suggestor.js diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 603de348..ea5a3f58 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -153,7 +153,11 @@ const getStaticEmoji = async ({ store }) => { if (res.ok) { const values = await res.json() const emoji = Object.keys(values).map((key) => { - return { shortcode: key, image_url: false, 'utf': values[key] } + return { + shortcode: key, + image_url: false, + 'replacement': values[key] + } }) store.dispatch('setInstanceOption', { name: 'emoji', value: emoji }) } else { @@ -174,7 +178,11 @@ const getCustomEmoji = async ({ store }) => { const result = await res.json() const values = Array.isArray(result) ? Object.assign({}, ...result) : result const emoji = Object.keys(values).map((key) => { - return { shortcode: key, image_url: values[key].image_url || values[key] } + return { + shortcode: key, + image_url: values[key].image_url || values[key], + replacement: `:${key}: ` + } }) store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji-input/emoji-input.js index a5bb6eaf..466341c0 100644 --- a/src/components/emoji-input/emoji-input.js +++ b/src/components/emoji-input/emoji-input.js @@ -1,15 +1,17 @@ import Completion from '../../services/completion/completion.js' -import { take, filter, map } from 'lodash' +import { take } from 'lodash' const EmojiInput = { props: [ - 'value', 'placeholder', + 'suggest', + 'value', 'type', 'classname' ], data () { return { + input: undefined, highlighted: 0, caret: 0 } @@ -17,35 +19,46 @@ const EmojiInput = { computed: { suggestions () { const firstchar = this.textAtCaret.charAt(0) - if (firstchar === ':') { - if (this.textAtCaret === ':') { return } - const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1))) - if (matchedEmoji.length <= 0) { - return false - } - return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({ - shortcode: `:${shortcode}:`, - utf: utf || '', - // eslint-disable-next-line camelcase - img: utf ? '' : this.$store.state.instance.server + image_url, - highlighted: index === this.highlighted - })) - } else { + if (this.textAtCaret === firstchar) { return } + const matchedSuggestions = this.suggest(this.textAtCaret) + if (matchedSuggestions.length <= 0) { return false } + return take(matchedSuggestions, 5).map(({shortcode, image_url, replacement}, index) => ({ + shortcode, + replacement, + // eslint-disable-next-line camelcase + img: !image_url ? '' : this.$store.state.instance.server + image_url, + highlighted: index === this.highlighted + })) }, textAtCaret () { return (this.wordAtCaret || {}).word || '' }, wordAtCaret () { - const word = Completion.wordAtPosition(this.value, this.caret - 1) || {} - return word + if (this.value && this.caret) { + const word = Completion.wordAtPosition(this.value, this.caret - 1) || {} + return word + } }, - emoji () { - return this.$store.state.instance.emoji || [] - }, - customEmoji () { - return this.$store.state.instance.customEmoji || [] + }, + mounted () { + const slots = this.$slots.default + if (slots.length === 0) return + const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag)) + if (!input) return + this.input = input + input.elm.addEventListener('keyup', this.setCaret) + input.elm.addEventListener('paste', this.setCaret) + input.elm.addEventListener('focus', this.setCaret) + input.elm.addEventListener('keydown', this.onKeyDown) + }, + unmounted () { + if (this.input) { + this.input.elm.removeEventListener('keyup', this.setCaret) + this.input.elm.removeEventListener('paste', this.setCaret) + this.input.elm.removeEventListener('focus', this.setCaret) + this.input.elm.removeEventListener('keydown', this.onKeyDown) } }, methods: { @@ -54,23 +67,21 @@ const EmojiInput = { this.$emit('input', newValue) this.caret = 0 }, - replaceEmoji (e) { + replaceText () { const len = this.suggestions.length || 0 - if (this.textAtCaret === ':' || e.ctrlKey) { return } + if (this.textAtCaret.length === 1) { return } if (len > 0) { - e.preventDefault() - const emoji = this.suggestions[this.highlighted] - const replacement = emoji.utf || (emoji.shortcode + ' ') + const suggestion = this.suggestions[this.highlighted] + const replacement = suggestion.replacement const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement) this.$emit('input', newValue) this.caret = 0 this.highlighted = 0 } }, - cycleBackward (e) { + cycleBackward () { const len = this.suggestions.length || 0 if (len > 0) { - e.preventDefault() this.highlighted -= 1 if (this.highlighted < 0) { this.highlighted = this.suggestions.length - 1 @@ -79,11 +90,9 @@ const EmojiInput = { this.highlighted = 0 } }, - cycleForward (e) { + cycleForward () { const len = this.suggestions.length || 0 if (len > 0) { - if (e.shiftKey) { return } - e.preventDefault() this.highlighted += 1 if (this.highlighted >= len) { this.highlighted = 0 @@ -92,13 +101,33 @@ const EmojiInput = { this.highlighted = 0 } }, - onKeydown (e) { + onKeyDown (e) { + this.setCaret(e) e.stopPropagation() + + const { ctrlKey, shiftKey, key } = e + if (key === 'Tab') { + if (shiftKey) { + this.cycleBackward() + } else { + this.cycleForward() + } + } + if (key === 'ArrowUp') { + this.cycleBackward() + } else if (key === 'ArrowDown') { + this.cycleForward() + } + if (key === 'Enter') { + if (!ctrlKey) { + this.replaceText() + } + } }, onInput (e) { this.$emit('input', e.target.value) }, - setCaret ({target: {selectionStart}}) { + setCaret ({ target: { selectionStart, value } }) { this.caret = selectionStart } } diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue index 338b77cd..eec33d1a 100644 --- a/src/components/emoji-input/emoji-input.vue +++ b/src/components/emoji-input/emoji-input.vue @@ -1,23 +1,6 @@