<template> <div id="searchable-select" ref="main"> <div id="ss-modal" @click.self="hideModal" v-if="modalShow"> <div id="ss-selector"> <div class="p-2"> <input type="text" class="form-control search" v-model="q" :placeholder="xtitle"> </div> <div class="p-2"> <ul id="vue-search-list" class="list-group list-group-flush"> <template v-for="(item,i) in items"> <li tabindex="-1" v-if="finder(item[titleField])" @click="selecting(item[valueField])" :class="`list-group-item ${val.indexOf(item[valueField]) !== -1?'selected':''} ${focsed == i?'focused':''}`"> <template v-if="xlang == null"> {{ item[titleField] }} </template> <template v-else> {{ item[titleField]?.[xlang] ?? item[titleField] }} </template> </li> </template> </ul> </div> </div> </div> <div class="input-group mb-3"> <div class="input-group-prepend" id="vue-search-btn"> <button class="input-group-text" id="basic-addon1" type="button" @click="showModal"> <i class="ri-check-line"></i> </button> </div> <div class="form-control" id="vue-lst" @click.self="showModal"> <template v-for="item in items"> <span class="tag-select" v-if=" val.indexOf(item[valueField]) !== -1"> <template v-if="xlang == null"> {{ item[titleField] }} </template> <template v-else> {{ item[titleField]?.[xlang] ?? item[titleField] }} </template> <i class="ri-close-line" @click="rem(item[valueField])"></i> </span> </template> </div> </div> </div> <input type="hidden" :name="xname" :value="JSON.stringify(val)"> </template> <script> export default { name: "searchable-select", components: {}, data: () => { return { modalShow: false, // modal handle q: '', // search query val: [], focsed: -1, } }, emits: ['update:modelValue'], props: { xlang: { default: null }, modelValue: { default: 'nop', }, items: { required: true, default: [], type: Array, }, valueField: { default: 'id', type: String, }, titleField: { default: 'title', type: String, }, xname: { default: "", type: String, }, xtitle: { default: "Please select", type: String, }, xvalue: { default: [], type: Array, }, xid: { default: "", type: String, }, customClass: { default: "", type: String, }, err: { default: false, type: Boolean, }, onSelect: { default: function () { }, type: Function, }, }, mounted() { if (this.modelValue != 'nop') { this.val = this.modelValue; } else { this.val = this.xvalue; } }, computed: { getClass: function () { if (this.err == true || (typeof this.err == 'String' && this.err.trim() == '1')) { return 'form-control is-invalid ' + this.customClass; } return 'form-control ' + this.customClass; }, }, methods: { finder(term = '') { //(q != '' && item[titleField].indexOf(q) != -1) || (q == '') if (this.q == '' || term == '') { return true; } if (typeof term == 'string' && term.toLocaleLowerCase().indexOf(this.q.toLocaleLowerCase()) != -1) { return true } else if (typeof term == 'object') { try { for (const t in term) { if (term[t].toLowerCase().indexOf(this.q.toLocaleLowerCase()) != -1) { return true; } } } catch (e) { console.log(e.message); } } else { return true; } return false; }, rem(i) { this.val.splice(this.val.indexOf(i), 1); this.onSelect(this.val, i); }, selecting(i) { if (this.val.indexOf(i) == -1) { this.val.push(i); } else { this.val.splice(this.val.indexOf(i), 1); } this.onSelect(this.val, i); }, select() { this.onSelect(this.val); }, hideModal: function () { this.modalShow = false; document.removeEventListener('keydown', this.keyHandle); }, showModal() { this.modalShow = true; setTimeout(() => { if (this.$refs.main.querySelector('.search') != null) { this.$refs.main.querySelector('.search').focus(); } }, 100); // this.$refs.main.querySelector('.search').focus(); document.addEventListener('keydown', this.keyHandle); }, keyHandle(e) { if (e.key == 'Escape') { this.hideModal(); } try { if (this.$refs.main.querySelector('.search') == document.activeElement) { if (e.code == 'Tab') { e.preventDefault(); this.$refs.main.querySelector('.search').blur(); this.focsed = 0; } return; } } catch { } e.preventDefault(); if (e.key == 'ArrowDown') { this.focsed++; if (this.focsed > this.items.length - 1) { this.focsed = this.items.length - 1; } } else if (e.key == 'ArrowUp') { this.focsed--; if (this.focsed < -1) { this.focsed = -1; } } else if (e.key == ' ' || e.key == 'Enter') { this.selecting(this.items[this.focsed][this.valueField]); } return false; // console.log(e.key); } }, watch: { val(newValue) { if (this.modelValue != 'nop') { this.$emit('update:modelValue', newValue); } } } } </script> <style scoped> #searchable-select { } #vue-search-btn { cursor: pointer; user-select: none; } #vue-search-btn:hover .input-group-text { background: deepskyblue; } #ss-modal { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 999; background: #00000033; backdrop-filter: blur(4px); user-select: none; } #ss-selector { height: 60vh; border-radius: 7px; min-width: 350px; width: 400px; max-width: 90%; margin: 20vh auto; background: #282D47; box-shadow: 0 0 4px gray; padding: 5px; } #vue-search-list { height: calc(60vh - 90px); overflow-x: auto; } #vue-search-list .list-group-item:hover, #vue-search-list .list-group-item.focused { background: #6610F2; } #vue-search-list .list-group-item.selected { background: darkred; color: white;; } #vue-search-list .list-group-item.selected:hover, #vue-search-list .list-group-item.selected.focused { background: #6610F2 !important; } #vue-lst { user-select: none; white-space: nowrap; overflow: hidden; } .tag-select { display: inline-block; padding: 0 4px 0 20px; margin-right: 5px; background: #282c34dd; color: white; position: relative; border-radius: 3px; } .tag-select i { font-size: 20px; position: absolute; left: 0; top: -5px; } .tag-select i:hover { color: red; } </style>