AutoComplete.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <template>
  2. <div class="autocomplete-wrapper">
  3. <input
  4. ref="acInput"
  5. v-model="text"
  6. type="text"
  7. autocomplete="off"
  8. :placeholder="placeholder"
  9. :spellcheck="spellcheck"
  10. :autocapitalize="autocapitalize"
  11. :autocorrect="spellcheck"
  12. :class="styles"
  13. @input="updateSuggestions"
  14. @keyup="updateSuggestions"
  15. @click="updateSuggestions"
  16. @keydown="handleKeystroke"
  17. @change="$emit('change', $event)"
  18. />
  19. <ul
  20. v-if="suggestions.length > 0 && suggestionsVisible"
  21. class="suggestions hide-scrollbar"
  22. :style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }"
  23. >
  24. <li
  25. v-for="(suggestion, index) in suggestions"
  26. :key="`suggestion-${index}`"
  27. :class="{ active: currentSuggestionIndex === index }"
  28. @click.prevent="forceSuggestion(suggestion)"
  29. >
  30. {{ suggestion }}
  31. </li>
  32. </ul>
  33. </div>
  34. </template>
  35. <script>
  36. import { defineComponent } from "@nuxtjs/composition-api"
  37. export default defineComponent({
  38. props: {
  39. spellcheck: {
  40. type: Boolean,
  41. default: true,
  42. required: false,
  43. },
  44. autocapitalize: {
  45. type: String,
  46. default: "off",
  47. required: false,
  48. },
  49. placeholder: {
  50. type: String,
  51. default: "",
  52. required: false,
  53. },
  54. source: {
  55. type: Array,
  56. required: true,
  57. },
  58. value: {
  59. type: String,
  60. default: "",
  61. required: false,
  62. },
  63. styles: {
  64. type: String,
  65. default: "",
  66. },
  67. },
  68. data() {
  69. return {
  70. text: this.value,
  71. selectionStart: 0,
  72. suggestionsOffsetLeft: 0,
  73. currentSuggestionIndex: -1,
  74. suggestionsVisible: false,
  75. }
  76. },
  77. computed: {
  78. /**
  79. * Gets the suggestions list to be displayed under the input box.
  80. *
  81. * @returns {default.props.source|{type, required}}
  82. */
  83. suggestions() {
  84. const input = this.text.substring(0, this.selectionStart)
  85. return (
  86. this.source
  87. .filter(
  88. (entry) =>
  89. entry.toLowerCase().startsWith(input.toLowerCase()) &&
  90. input.toLowerCase() !== entry.toLowerCase()
  91. )
  92. // Cut off the part that's already been typed.
  93. .map((entry) => entry.substring(this.selectionStart))
  94. // We only want the top 10 suggestions.
  95. .slice(0, 10)
  96. )
  97. },
  98. },
  99. watch: {
  100. text() {
  101. this.$emit("input", this.text)
  102. },
  103. value(newValue) {
  104. this.text = newValue
  105. },
  106. },
  107. mounted() {
  108. this.updateSuggestions({
  109. target: this.$refs.acInput,
  110. })
  111. },
  112. methods: {
  113. updateSuggestions(event) {
  114. // Hide suggestions if ESC pressed.
  115. if (event.code && event.code === "Escape") {
  116. event.preventDefault()
  117. this.suggestionsVisible = false
  118. this.currentSuggestionIndex = -1
  119. return
  120. }
  121. // As suggestions is a reactive property, this implicitly
  122. // causes suggestions to update.
  123. this.selectionStart = this.$refs.acInput.selectionStart
  124. this.suggestionsOffsetLeft = 12 * this.selectionStart
  125. this.suggestionsVisible = true
  126. },
  127. forceSuggestion(text) {
  128. const input = this.text.substring(0, this.selectionStart)
  129. this.text = input + text
  130. this.selectionStart = this.text.length
  131. this.suggestionsVisible = true
  132. this.currentSuggestionIndex = -1
  133. },
  134. handleKeystroke(event) {
  135. switch (event.code) {
  136. case "Enter":
  137. event.preventDefault()
  138. if (this.currentSuggestionIndex > -1)
  139. this.forceSuggestion(
  140. this.suggestions.find(
  141. (_item, index) => index === this.currentSuggestionIndex
  142. )
  143. )
  144. break
  145. case "ArrowUp":
  146. event.preventDefault()
  147. this.currentSuggestionIndex =
  148. this.currentSuggestionIndex - 1 >= 0
  149. ? this.currentSuggestionIndex - 1
  150. : 0
  151. break
  152. case "ArrowDown":
  153. event.preventDefault()
  154. this.currentSuggestionIndex =
  155. this.currentSuggestionIndex < this.suggestions.length - 1
  156. ? this.currentSuggestionIndex + 1
  157. : this.suggestions.length - 1
  158. break
  159. case "Tab": {
  160. const activeSuggestion =
  161. this.suggestions[
  162. this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0
  163. ]
  164. if (!activeSuggestion) {
  165. return
  166. }
  167. event.preventDefault()
  168. const input = this.text.substring(0, this.selectionStart)
  169. this.text = input + activeSuggestion
  170. break
  171. }
  172. }
  173. },
  174. },
  175. })
  176. </script>
  177. <style scoped lang="scss">
  178. .autocomplete-wrapper {
  179. @apply relative;
  180. @apply contents;
  181. input:focus + ul.suggestions,
  182. ul.suggestions:hover {
  183. @apply block;
  184. }
  185. ul.suggestions {
  186. @apply hidden;
  187. @apply bg-popover;
  188. @apply absolute;
  189. @apply mx-2;
  190. @apply left-0;
  191. @apply z-50;
  192. @apply shadow-lg;
  193. @apply max-h-46;
  194. @apply overflow-y-auto;
  195. top: calc(100% - 4px);
  196. border-radius: 0 0 8px 8px;
  197. li {
  198. @apply w-full;
  199. @apply block;
  200. @apply py-2 px-4;
  201. @apply text-secondary;
  202. &:last-child {
  203. border-radius: 0 0 8px 8px;
  204. }
  205. &:hover,
  206. &.active {
  207. @apply bg-accentDark;
  208. @apply text-accentContrast;
  209. @apply cursor-pointer;
  210. }
  211. }
  212. }
  213. }
  214. </style>