diff --git a/src/components/autocomplete_input/autocomplete_input.js b/src/components/autocomplete_input/autocomplete_input.js
new file mode 100644
index 00000000..2a959fd1
--- /dev/null
+++ b/src/components/autocomplete_input/autocomplete_input.js
@@ -0,0 +1,150 @@
+import Completion from '../../services/completion/completion.js'
+import { take, filter, map } from 'lodash'
+
+const AutoCompleteInput = {
+ props: [
+ 'id',
+ 'classObj',
+ 'value',
+ 'placeholder',
+ 'autoResize',
+ 'multiline',
+ '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') || this.$el.querySelector('input')
+ 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
diff --git a/src/components/autocomplete_input/autocomplete_input.vue b/src/components/autocomplete_input/autocomplete_input.vue
new file mode 100644
index 00000000..56233535
--- /dev/null
+++ b/src/components/autocomplete_input/autocomplete_input.vue
@@ -0,0 +1,104 @@
+
+
{{$t('settings.name')}}
- +{{$t('settings.bio')}}
- +