From 88c8818debf3b854528277b7202e9912a39f3563 Mon Sep 17 00:00:00 2001
From: eal <eal@waifu.club>
Date: Sat, 3 Feb 2018 16:05:47 +0200
Subject: [PATCH] Add keyboard support for user/emoji picker.

Tab cycles between candidates, shift-tab cycles backwards.
Enter does the action.
---
 .../post_status_form/post_status_form.js      | 51 +++++++++++++++++--
 .../post_status_form/post_status_form.vue     | 20 +++++---
 2 files changed, 59 insertions(+), 12 deletions(-)

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 1f63de25..d4290c85 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -41,6 +41,7 @@ const PostStatusForm = {
       submitDisabled: false,
       error: null,
       posting: false,
+      highlighted: 0,
       newStatus: {
         status: statusText,
         files: []
@@ -57,23 +58,26 @@ const PostStatusForm = {
           return false
         }
         // eslint-disable-next-line camelcase
-        return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}) => ({
+        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
+          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.match(this.textAtCaret.slice(1)))
         if (matchedEmoji.length <= 0) {
           return false
         }
-        return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}) => ({
+        return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
           // eslint-disable-next-line camelcase
           screen_name: `:${shortcode}:`,
           name: '',
           utf: utf || '',
-          img: image_url
+          img: image_url,
+          highlighted: index === this.highlighted
         }))
       } else {
         return false
@@ -103,6 +107,45 @@ const PostStatusForm = {
       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.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
+        const el = this.$el.querySelector('textarea')
+        el.focus()
+        this.caret = 0
+        this.highlighted = 0
+      }
+    },
+    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
+      }
+    },
     setCaret ({target: {selectionStart}}) {
       this.caret = selectionStart
     },
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 8e436428..4a6a574a 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -2,17 +2,21 @@
   <div class="post-status-form">
     <form @submit.prevent="postStatus(newStatus)">
       <div class="form-group base03-border" >
-        <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize" @paste="paste"></textarea>
+        <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" @keydown.down="cycleForward" @keydown.up="cycleBackward" @keydown.shift.tab="cycleBackward" @keydown.tab="cycleForward" @keydown.enter="replaceCandidate" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize" @paste="paste"></textarea>
       </div>
       <div style="position:relative;" v-if="candidates">
         <div class="autocomplete-panel base05-background">
-          <div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))" class="autocomplete base02">
-            <span v-if="candidate.img"><img :src="candidate.img"></img></span>
-            <span v-else>{{candidate.utf}}</span>
-            <span>
-              {{candidate.screen_name}}
-              <small class="base02">{{candidate.name}}</small>
-            </span>
+          <div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
+            <div v-if="candidate.highlighted" class="autocomplete base02">
+              <span v-if="candidate.img"><img :src="candidate.img"></span>
+              <span v-else>{{candidate.utf}}</span>
+              <span>{{candidate.screen_name}}<small class="base02">{{candidate.name}}</small></span>
+            </div>
+            <div v-else class="autocomplete base04">
+              <span v-if="candidate.img"><img :src="candidate.img"></img></span>
+              <span v-else>{{candidate.utf}}</span>
+              <span>{{candidate.screen_name}}<small class="base02">{{candidate.name}}</small></span>
+            </div>
           </div>
         </div>
       </div>