AutoCompleteInput.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <template>
  2. <div class="formcontrol">
  3. <label class="formcontrol-label" for="" v-if="optional || label"
  4. >{{ label }}
  5. <div class="formcontrol-optional" v-if="optional">Optional</div></label
  6. >
  7. <div
  8. class="select-multiple"
  9. ref="container"
  10. :class="{ 'select-multiple--expanded': showAutocomplete }"
  11. @click="openDropDown"
  12. >
  13. <div class="select-multiple-value">
  14. <input
  15. ref="input"
  16. class="select-multiple-input"
  17. :placeholder="placeholder"
  18. :size="Math.max(value.length, placeholder.length, 10)"
  19. v-model="value"
  20. @keyup="onKeyup"
  21. />
  22. </div>
  23. <div class="dropdown-options" tabindex="0">
  24. <div
  25. v-for="(v, i) in displayedAutocomplete"
  26. :class="{ 'select--active': i == activeIndex }"
  27. @click="toggle(v, $event)"
  28. :key="v.id"
  29. >
  30. <span v-html="highlight(v.name)"></span>
  31. </div>
  32. <div v-if="filtredAutocomplete.length > displayedAutocomplete.length">...</div>
  33. </div>
  34. </div>
  35. </div>
  36. </template>
  37. <script lang="ts">
  38. import AutocompleteOptions from "@/models/AutocompleteOptions";
  39. import { defineComponent, PropType } from "vue";
  40. import toast from "@/utils/Toast";
  41. export default defineComponent({
  42. props: {
  43. id: String,
  44. label: String,
  45. modelValue: {
  46. type: String,
  47. },
  48. autocompleteList: {
  49. type: Array as PropType<Array<AutocompleteOptions>>,
  50. default: () => [],
  51. },
  52. placeholder: { type: String, default: () => "" },
  53. optional: Boolean,
  54. limit: {
  55. type: Number,
  56. default: () => Infinity,
  57. },
  58. strictAutocomplete: {
  59. type: Boolean,
  60. default: () => false,
  61. },
  62. },
  63. data: function () {
  64. return {
  65. value: "",
  66. activeIndex: -1,
  67. expanded: false,
  68. };
  69. },
  70. watch: {
  71. modelValue(v) {
  72. this.value = this.autocompleteList.find((o) => o.id == v)?.name || "";
  73. },
  74. value(value) {
  75. if (!this.strictAutocomplete) this.$emit("update:modelValue", value);
  76. },
  77. },
  78. computed: {
  79. filtredAutocomplete(): Array<AutocompleteOptions> {
  80. const lower = this.value.toLowerCase();
  81. return this.autocompleteList.filter((o) => o.name.toLowerCase().includes(lower));
  82. },
  83. displayedAutocomplete(): Array<AutocompleteOptions> {
  84. return this.filtredAutocomplete.slice(0, this.limit);
  85. },
  86. showAutocomplete(): boolean {
  87. return this.expanded && this.filtredAutocomplete.length > 0;
  88. },
  89. input(): HTMLInputElement {
  90. return this.$refs["input"] as HTMLInputElement;
  91. },
  92. },
  93. methods: {
  94. openDropDown: function (e: MouseEvent) {
  95. this.expanded = true;
  96. this.input.focus();
  97. window.addEventListener("click", this.closeDropDown);
  98. e.stopPropagation();
  99. },
  100. closeDropDown: function (e: MouseEvent) {
  101. if (this.$refs["container"]) {
  102. if (!(this.$refs["container"] as HTMLElement).contains(e.target as Node)) {
  103. this.expanded = false;
  104. window.removeEventListener("click", this.closeDropDown);
  105. e.stopPropagation();
  106. if (this.value == "") this.$emit("update:modelValue", this.value);
  107. }
  108. }
  109. },
  110. toggle(v: AutocompleteOptions, e: MouseEvent) {
  111. e.stopPropagation();
  112. this.expanded = false;
  113. this.value = v.name;
  114. this.$emit("update:modelValue", v.id);
  115. },
  116. onKeyup: function (e: KeyboardEvent): void {
  117. if (e.key === "Enter") {
  118. if (this.value === "") {
  119. this.$emit("update:modelValue", "");
  120. this.expanded = false;
  121. } else if (this.activeIndex > -1 && this.activeIndex < this.displayedAutocomplete.length) {
  122. const v = this.displayedAutocomplete[this.activeIndex];
  123. this.value = v.name;
  124. this.$emit("update:modelValue", v.id);
  125. this.expanded = false;
  126. } else if (this.strictAutocomplete) {
  127. toast({
  128. html: "Vous ne pouvez ajouter que des valeurs prédéfinies",
  129. classes: "red",
  130. });
  131. }
  132. }
  133. if (e.key === "ArrowUp") {
  134. this.activeIndex--;
  135. }
  136. if (e.key === "ArrowDown") {
  137. this.activeIndex++;
  138. }
  139. if (this.activeIndex < -1) {
  140. this.activeIndex = this.displayedAutocomplete.length - 1;
  141. }
  142. if (this.activeIndex >= this.displayedAutocomplete.length) {
  143. this.activeIndex = 0;
  144. }
  145. },
  146. highlight: function (txt: string) {
  147. if (this.value) {
  148. const reg = new RegExp(this.value, "ig");
  149. const highlightedText = txt.match(reg);
  150. if (highlightedText) {
  151. const plaintext = txt.split(reg);
  152. return (
  153. highlightedText.map((t, i) => plaintext[i] + `<b class="highlight">${t}</b>`).join("") +
  154. plaintext.pop()
  155. );
  156. }
  157. }
  158. return txt;
  159. },
  160. },
  161. });
  162. </script>
  163. <style src="../assets/css/multiple-select.css"></style>