SelectChipInput.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <template>
  2. <div class="select-multiple" :class="{ 'select-multiple--expanded': expanded }">
  3. <div class="select-multiple-value" @click="openDropDown">
  4. <span v-if="checkedItems.length === 0"></span>
  5. <div v-for="item of displayedItems" class="ds-chip" :key="item.id">
  6. <span class="ds-chip-label">{{ item.text }}</span>
  7. <button aria-label="Clear" class="ds-chip-remove-btn" @click="toggle(item.id, $event)" />
  8. </div>
  9. <div v-if="checkedItems.length > maxItemDisplayed" class="ds-chip">
  10. <div class="ds-chip-label">+{{ checkedItems.length - maxItemDisplayed }}</div>
  11. </div>
  12. <input
  13. class="select-multiple-input"
  14. ref="input"
  15. :placeholder="placeholder"
  16. v-model="inputValue"
  17. :size="Math.max(inputValue.length, placeholder.length)"
  18. @keyup="keyUp"
  19. />
  20. </div>
  21. <div class="dropdown-options">
  22. <div
  23. v-for="(item, index) in filteredItems"
  24. :class="{
  25. 'select--checked': item.isChecked,
  26. 'select--active': index == activeIndex,
  27. }"
  28. :key="item.id"
  29. @click="toggle(item.id, $event)"
  30. v-html="highlight(item.text)"
  31. ></div>
  32. </div>
  33. </div>
  34. </template>
  35. <script lang="ts">
  36. import { defineComponent, PropType } from "vue";
  37. interface selectItem {
  38. id: number;
  39. isChecked: boolean;
  40. value: any;
  41. text: string;
  42. }
  43. interface selectOption {
  44. value: any;
  45. text: string;
  46. }
  47. export default defineComponent({
  48. name: "selectChip",
  49. props: {
  50. // Values selected by the component
  51. valueModel: {
  52. type: Array as PropType<Array<string>>,
  53. default: () => [],
  54. },
  55. /*
  56. * Potential values that could be selected by the user
  57. * type Array<String> | Array<Option>
  58. * Option {
  59. * value:Any,
  60. * text:String ,
  61. * }
  62. */
  63. options: {
  64. type: Array as PropType<Array<string | selectOption>>,
  65. default: () => [],
  66. },
  67. placeholder: {
  68. type: String,
  69. default: "Choose among the list",
  70. },
  71. // Number of selected item displayed before displaying an ellipsis appear
  72. maxItemDisplayed: {
  73. type: Number,
  74. default: Infinity,
  75. },
  76. },
  77. data() {
  78. return {
  79. items: [] as Array<selectItem>,
  80. expanded: false,
  81. inputValue: "",
  82. activeIndex: -1,
  83. };
  84. },
  85. watch: {
  86. valueModel: function () {
  87. this.updateItems();
  88. },
  89. },
  90. emit: ["update:valueModel"],
  91. methods: {
  92. toggle(idx: number, e: MouseEvent): void {
  93. this.items[idx].isChecked = !this.items[idx].isChecked;
  94. this.$emit(
  95. "update:valueModel",
  96. this.items.filter((o) => o.isChecked).map((o) => o.value)
  97. );
  98. this.input.focus();
  99. e.stopPropagation();
  100. },
  101. updateItems(): void {
  102. this.items = this.options.map((o: selectOption | string, idx: number) => {
  103. let itemValue: any, text: string;
  104. if (typeof o == "object") {
  105. itemValue = "value" in o ? o.value : o;
  106. text = "text" in o ? o.text : itemValue;
  107. } else {
  108. itemValue = o;
  109. text = o;
  110. }
  111. return {
  112. id: idx,
  113. isChecked: this.valueModel.includes(itemValue),
  114. value: itemValue,
  115. text: text,
  116. };
  117. });
  118. },
  119. openDropDown: function (e: MouseEvent) {
  120. this.expanded = true;
  121. this.input.focus();
  122. window.addEventListener("click", this.closeDropDown);
  123. e.stopPropagation();
  124. },
  125. closeDropDown: function (e: MouseEvent) {
  126. if (!this.$el.contains(e.target)) {
  127. this.expanded = false;
  128. window.removeEventListener("click", this.closeDropDown);
  129. e.stopPropagation();
  130. }
  131. },
  132. highlight(txt: string): string {
  133. if (this.inputValue) {
  134. let reg = new RegExp(this.inputValue, "ig");
  135. let boldTxt = txt.match(reg);
  136. if (boldTxt !== null) {
  137. const plain = txt.split(reg);
  138. let output = "";
  139. for (let i = 0; i < boldTxt.length; i++) {
  140. output += plain[i] + "<b>" + boldTxt[i] + "</b>";
  141. }
  142. return output + plain.pop();
  143. }
  144. }
  145. return txt;
  146. },
  147. keyUp(e: KeyboardEvent) {
  148. if (e.key == "Enter") {
  149. if (this.selectedItem) this.selectedItem.isChecked = !this.selectedItem.isChecked;
  150. }
  151. if (e.key == "ArrowUp") {
  152. this.activeIndex--;
  153. }
  154. if (e.key == "ArrowDown") {
  155. this.activeIndex++;
  156. }
  157. this.boundActiveIndex();
  158. /*
  159. if(e.key=="Backspace" && this.inputValue==""){
  160. // Remove the last entry
  161. const lastItem = this.checkedItems[this.checkedItems.length-1]
  162. if (lastItem){
  163. lastItem.isChecked = false
  164. }
  165. }
  166. */
  167. },
  168. boundActiveIndex() {
  169. if (this.activeIndex < 0) {
  170. this.activeIndex = this.filteredItems.length;
  171. }
  172. if (this.activeIndex > this.filteredItems.length) {
  173. this.activeIndex = 0;
  174. }
  175. },
  176. },
  177. computed: {
  178. checkedItems(): Array<selectItem> {
  179. return this.items.filter((i) => i.isChecked);
  180. },
  181. displayedItems(): Array<selectItem> {
  182. return this.checkedItems.filter((i, idx) => idx < this.maxItemDisplayed);
  183. },
  184. input(): HTMLInputElement {
  185. return this.$refs["input"] as HTMLInputElement;
  186. },
  187. filteredItems(): Array<selectItem> {
  188. if (this.inputValue) {
  189. const low = this.inputValue.toLowerCase();
  190. const output = this.items.filter((o) => o.text.toLowerCase().includes(low));
  191. return output;
  192. }
  193. return this.items;
  194. },
  195. selectedItem(): selectItem | undefined {
  196. return this.filteredItems[this.activeIndex];
  197. },
  198. },
  199. beforeMount: function () {
  200. this.updateItems();
  201. },
  202. });
  203. </script>
  204. <style src="../assets/css/multiple-select.css"></style>