From fb0cf6454982a371e68b22e29b0374507425242d Mon Sep 17 00:00:00 2001 From: Xiaofeng An Date: Fri, 8 Feb 2019 10:49:14 -0500 Subject: #255 - make AutoCompleteInput component --- .../autocomplete_input/autocomplete_input.js | 147 +++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/components/autocomplete_input/autocomplete_input.js (limited to 'src/components/autocomplete_input/autocomplete_input.js') diff --git a/src/components/autocomplete_input/autocomplete_input.js b/src/components/autocomplete_input/autocomplete_input.js new file mode 100644 index 00000000..51c1f5d9 --- /dev/null +++ b/src/components/autocomplete_input/autocomplete_input.js @@ -0,0 +1,147 @@ +import Completion from '../../services/completion/completion.js' +import { take, filter, map } from 'lodash' + +const AutoCompleteInput = { + props: [ + 'classObj', + 'value', + 'autoResize', + 'drop', + 'dragoverPrevent', + 'paste', + 'keydownMetaEnter', + 'keyupCtrlEnter' + ], + components: {}, + mounted () { + this.autoResize && this.resize(this.$refs.textarea) + const textLength = this.$refs.textarea.value.length + this.$refs.textarea.setSelectionRange(textLength, textLength) + }, + data () { + return { + caret: 0, + highlighted: 0, + text: this.value + } + }, + computed: { + users () { + return this.$store.state.users.users + }, + emoji () { + return this.$store.state.instance.emoji || [] + }, + customEmoji () { + return this.$store.state.instance.customEmoji || [] + }, + textAtCaret () { + return (this.wordAtCaret || {}).word || '' + }, + wordAtCaret () { + const word = Completion.wordAtPosition(this.text, this.caret - 1) || {} + return word + }, + candidates () { + const firstchar = this.textAtCaret.charAt(0) + if (firstchar === '@') { + const query = this.textAtCaret.slice(1).toUpperCase() + const matchedUsers = filter(this.users, (user) => { + return user.screen_name.toUpperCase().startsWith(query) || + user.name && user.name.toUpperCase().startsWith(query) + }) + if (matchedUsers.length <= 0) { + return false + } + // eslint-disable-next-line camelcase + return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({ + // eslint-disable-next-line camelcase + screen_name: `@${screen_name}`, + name: name, + img: profile_image_url_original, + highlighted: index === this.highlighted + })) + } else 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) => ({ + screen_name: `:${shortcode}:`, + name: '', + utf: utf || '', + // eslint-disable-next-line camelcase + img: utf ? '' : this.$store.state.instance.server + image_url, + highlighted: index === this.highlighted + })) + } else { + return false + } + } + }, + methods: { + setCaret ({target: {selectionStart}}) { + this.caret = selectionStart + }, + cycleBackward (e) { + const len = this.candidates.length || 0 + if (len > 0) { + e.preventDefault() + this.highlighted -= 1 + if (this.highlighted < 0) { + this.highlighted = this.candidates.length - 1 + } + } else { + this.highlighted = 0 + } + }, + cycleForward (e) { + const len = this.candidates.length || 0 + if (len > 0) { + if (e.shiftKey) { return } + e.preventDefault() + this.highlighted += 1 + if (this.highlighted >= len) { + this.highlighted = 0 + } + } else { + this.highlighted = 0 + } + }, + replace (replacement) { + this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement) + const el = this.$el.querySelector('textarea') + el.focus() + this.caret = 0 + }, + replaceCandidate (e) { + const len = this.candidates.length || 0 + if (this.textAtCaret === ':' || e.ctrlKey) { return } + if (len > 0) { + e.preventDefault() + const candidate = this.candidates[this.highlighted] + const replacement = candidate.utf || (candidate.screen_name + ' ') + this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement) + const el = this.$el.querySelector('textarea') + el.focus() + this.caret = 0 + this.highlighted = 0 + } + }, + resize (e) { + const target = e.target || e + if (!(target instanceof window.Element)) { return } + const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) + + Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1)) + // Auto is needed to make textbox shrink when removing lines + target.style.height = 'auto' + target.style.height = `${target.scrollHeight - vertPadding}px` + if (target.value === '') { + target.style.height = null + } + } + } +} + +export default AutoCompleteInput -- cgit v1.2.3-70-g09d2 From 17c70fc0ef380d1faa360aa5b91791fd44d6c419 Mon Sep 17 00:00:00 2001 From: Xiaofeng An Date: Fri, 8 Feb 2019 11:02:28 -0500 Subject: add id & placeholder props to AutoCompleteInput component --- src/components/autocomplete_input/autocomplete_input.js | 3 +++ src/components/autocomplete_input/autocomplete_input.vue | 3 ++- src/components/post_status_form/post_status_form.vue | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src/components/autocomplete_input/autocomplete_input.js') diff --git a/src/components/autocomplete_input/autocomplete_input.js b/src/components/autocomplete_input/autocomplete_input.js index 51c1f5d9..0f1a510c 100644 --- a/src/components/autocomplete_input/autocomplete_input.js +++ b/src/components/autocomplete_input/autocomplete_input.js @@ -3,9 +3,12 @@ import { take, filter, map } from 'lodash' const AutoCompleteInput = { props: [ + 'id', 'classObj', 'value', + 'placeholder', 'autoResize', + 'multiline', 'drop', 'dragoverPrevent', 'paste', diff --git a/src/components/autocomplete_input/autocomplete_input.vue b/src/components/autocomplete_input/autocomplete_input.vue index d3bda597..309f2202 100644 --- a/src/components/autocomplete_input/autocomplete_input.vue +++ b/src/components/autocomplete_input/autocomplete_input.vue @@ -4,9 +4,10 @@ v-if="multiline" ref="textarea" :value="text" + :id="id" @input="text = $event.target.value, $emit('input', $event.target.value), autoResize && resize($event)" @click="setCaret" - @keyup="setCaret" :placeholder="$t('post_status.default')" rows="1" :class="classObj" + @keyup="setCaret" :placeholder="placeholder" rows="1" :class="classObj" @keydown.down="cycleForward" @keydown.up="cycleBackward" @keydown.shift.tab="cycleBackward" diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index ac5f5566..18c2d2b2 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -18,7 +18,9 @@ class="form-cw"> Date: Fri, 8 Feb 2019 11:26:17 -0500 Subject: select either textarea or input --- src/components/autocomplete_input/autocomplete_input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/components/autocomplete_input/autocomplete_input.js') diff --git a/src/components/autocomplete_input/autocomplete_input.js b/src/components/autocomplete_input/autocomplete_input.js index 0f1a510c..2a959fd1 100644 --- a/src/components/autocomplete_input/autocomplete_input.js +++ b/src/components/autocomplete_input/autocomplete_input.js @@ -126,7 +126,7 @@ const AutoCompleteInput = { const candidate = this.candidates[this.highlighted] const replacement = candidate.utf || (candidate.screen_name + ' ') this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement) - const el = this.$el.querySelector('textarea') + const el = this.$el.querySelector('textarea') || this.$el.querySelector('input') el.focus() this.caret = 0 this.highlighted = 0 -- cgit v1.2.3-70-g09d2