Skip to content

Instantly share code, notes, and snippets.

@nonsocode
Last active August 2, 2018 21:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nonsocode/bcf769f5efbecbee42f66f7fb46327b9 to your computer and use it in GitHub Desktop.
Save nonsocode/bcf769f5efbecbee42f66f7fb46327b9 to your computer and use it in GitHub Desktop.
A simple multi select Component for Vuejs
<template>
<div class="n-select" :class="{'is-small' : small, opened: list}" ref="nSelect">
<div class="over-con">
<div class="search-container" ref="searchContainer" @click.stop="openAndSearch">
<template v-if="multiple">
<div class="selected-item" v-for="val in selected" :key="trackBy ? val[trackBy] : val" >
<span class="text">{{label ? val[label] : val}}</span>
<button class="close" @click="removeItem(val)">&times;</button>
</div>
</template>
<div v-else class="" style="padding: 2px">{{label && selected ? selected[label] : selected}}</div>
<input type="text" :style="{width}" class="input search" v-model="search" @keydown.down="semiSelectNext" @keydown.up="semiSelectPrevious" @keyup.esc="exit" @keydown.delete="mayPop" @keydown.enter="selectSemiSelected" ref="input" @focus="showList">
</div>
<div class="options-container" v-show="list">
<div class="field is-grouped" v-show="selected && selected.length">
<div class="control">
<button class="button is-small is-dark" @click="clearAll">Clear{{selected && selected.length > 1 ? ' All': ''}}</button>
</div>
</div>
<ul class="options-list" ref="listContainer">
<template v-if="filteredOptions.length">
<li class="option-item selectable" :ref="'item' + option[refKey(index)]" @mouseenter="semiSelect(option)" @click="toggle(option)" :key="trackBy ? option[trackBy] : option" v-for="(option, index) in filteredOptions" :class="{selected : isSelected(option),'semi-selected' : semi == option }">
{{label ? option[label] : option}}
</li>
</template>
<li v-else class="option-item unselectable">No options available</li>
</ul>
</div>
</div>
</div>
</template>
<script>
const required = true;
export default{
props:{
label:{type:String, default:''},
options: {type: Array, required},
closeOnSelect: {type: Boolean, default:true},
clearSearchOnSelect: {type: Boolean, default: true},
trackBy:{type: String, default:'id'},
value: {required},
small: {default: false, type: Boolean},
multiple: {type: Boolean, default: false}
},
computed:{
filteredOptions(){
return this.search ? this.options.filter(item => {
const search = new RegExp(`.*?${this.search}.*?`, 'i')
return item[this.label].match(search)
}) :this.options
},
width(){
return ((this.search.length + 1) * 10 + "px");
}
},
data(){
return {
list:false,
selected : [],
search: '',
semi: undefined,
}
},
methods:{
isSelected(option){
if (this.multiple)
return this.selected && this.selected.some(val => val == option)
else this.selected == this.option
},
refKey(index){
return this.trackBy ? this.trackBy : this.label ? this.label : index
},
semiSelectNext(){
if(this.filteredOptions.length){
if(!this.semi){
this.semi = this.filteredOptions[0]
}else{
const semiIndex = this.filteredOptions.indexOf(this.semi)
this.semi = this.filteredOptions[ semiIndex == this.filteredOptions.length-1 ? 0 : semiIndex + 1]
}
this.mayScroll()
}
},
semiSelectPrevious(){
if(this.filteredOptions.length){
if(!this.semi){
this.semi = this.filteredOptions[this.filteredOptions.length -1]
}else{
const semiIndex = this.filteredOptions.indexOf(this.semi)
this.semi = this.filteredOptions[ semiIndex == 0 ? this.filteredOptions.length - 1 : semiIndex - 1]
}
this.mayScroll()
}
},
mayScroll(){
const refName = 'item' + this.semi[this.refKey(this.filteredOptions.indexOf(this.semi))]
const ref = this.$refs[refName][0]
const shouldScroll = this.shouldScroll(ref)
if(shouldScroll > 0 ) this.$refs.listContainer.scrollTop = ref.offsetTop - this.$refs.listContainer.offsetHeight + 150
if(shouldScroll < 0 ) this.$refs.listContainer.scrollTop = ref.offsetTop - 150
},
shouldScroll(el){
const container =this.$refs.listContainer
const viewHeight = container.offsetHeight, top = container.scrollTop
if ((el.offsetTop + el.offsetHeight) > (viewHeight + top)) return 1;
if (el.offsetTop < top ) return -1
return 0;
},
semiSelect(item){
this.semi = item
},
exit(){
this.hideList()
this.$refs.input.blur()
},
hideList(){this.list = false},
showList(){this.list = true},
clearSemi(){this.semi = undefined},
selectSemiSelected(){
if(this.semi){
this.toggle(this.semi)
} else{
this.selectNearestUnselected()
}
},
selectNearestUnselected(){
const unselected = this.filteredOptions.find(i => !this.selected.find(j => j == i))
if (unselected) {
this.selectItem(unselected)
this.search = ''
}
},
clickOutside(e){
let el = this.$refs.nSelect
if(!el.contains(e.target) && el !== e.target){
this.clearSearch()
this.hideList()
}
},
emit(){
this.$emit('input', this.selected)
},
selectAll(){
this.selected = [...this.options]
this.emit()
},
clearAll(){
this.selected = this.multiple ? [] : undefined
this.emit()
},
alreadySelected(item){
return this.multiple ? this.selected.some(i => i == item) : this.selected == item
},
removeItem(item){
this.multiple ?
this.selected = this.selected.filter(i => i!=item) :
this.selected = undefined
this.emit()
this.afterToggle()
},
toggle(item){
this.alreadySelected(item) ? this.removeItem(item) : this.selectItem(item)
this.afterToggle()
},
afterToggle(){
if (this.closeOnSelect){this.hideList();}
else{ this.$refs.input.focus() }
if (this.clearSearchOnSelect) this.clearSearch()
},
selectItem(item){
if (this.multiple)
this.selected.push(item);
else this.selected = item;
this.emit()
},
openAndSearch(e){
if(e.target == this.$refs.searchContainer){
this.showList()
this.$refs.input.focus()
}
},
clearSearch(){
this.search = ''
},
mayPop(e){
if(!this.search && (this.multiple ? this.selected.length : this.selected)){
this.multiple ? this.selected.pop() : this.selected = undefined
this.emit()
}
},
},
watch:{
value(val){
this.selected = val
}
},
beforeMount(){
document.addEventListener('mousedown',this.clickOutside)
},
beforeDestroy(){
document.removeEventListener('mousedown',this.clickOutside)
}
}
</script>
<style lang="scss">
.n-select {
position: relative;
&.opened .search-container:after{
opacity: 0.2;
transform: rotateZ(180deg);
}
.fade{
&-enter-active, &-leave-active{
transition: opacity 10.1s ease-in-out;
-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-o-backface-visibility: hidden;
backface-visibility: hidden;
transform:translateZ(0) scale(1.0, 1.0);
}
&-enter-to, &-leave{opacity:1}
&-leave-to, &-enter{opacity:0}
}
&.is-small{
.search-container{
min-height: 20px;
max-width:300px;
&:after{
font-size: 0.8em;
}
input.input{
font-size: 0.8em;
}
.selected-item{
font-size: 0.8em;
}
}
}
.options-container{
position: absolute;
width: 100%;
background: white;
}
.search-container {
min-height: 30px;
min-width:150px;
border-radius: 3px;
/*box-shadow: inset 0px 2px 4px rgba(10,10,10,0.07);*/
flex-wrap:wrap;
padding: 2px;
padding-right: 10px;
display: flex;
background: white;
border: 1px solid #ccc;
input.input {
height: 17px;
box-shadow: none;
background: none;
font-size: 1.1em;
padding: 0;
width: auto;
border-radius: 0;
border: none;
display: inline-flex;
}
.selected-item {
border-radius: 3px;
background-color: #eeeeee;
border:1px solid #ccc;
margin-top: 1px;
padding: 0 0 0 3px;
margin-right: 3px;
font-weight: bold;
align-items: center;
span {
display: inherit;
line-height: 1;
}
min-height: 17px;
font-size: 1.1em;
display: inherit;
button.close {
padding: 3px;
outline: none;
background: none;
border: none;
cursor: pointer;
display: inline-block;
/*height:100%;*/
/*padding:0;*/
}
}
&:after{
position: absolute;
top:calc(50% - 0.7em);
right: 7px;
opacity:1;
color:rgba(0,0,0,0.5);
content: '\f078';
transition: all 0.2s ease-in-out;
font-family: 'FontAwesome';
}
}
.options-list {
z-index: 1000;
position: absolute;
top:100%;
display: block;
width: 100%;
max-height: 250px;
overflow-y: auto;
box-shadow: 0px 05px 25px rgba(0,0,0,0.15);
border: 1px solid #ddd;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 0 0 5px 5px;
background: white;
.option-item {
padding: 5px 10px;
display: block;
/*border-bottom: 1px solid #eee;*/
&:last-child {
border-bottom: none;
}
&.semi-selected{
background: #eee;
}
&.selected {
background: #007bff;
color: #fff;
}
&.selectable{
cursor: pointer;
}
&.unselectable{
cursor: not-allowed;
}
}
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment