Skip to content

Instantly share code, notes, and snippets.

@tonypee
Created February 17, 2017 15:02
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 tonypee/e1584ff1f8131dd5644e82ab5b60d1c5 to your computer and use it in GitHub Desktop.
Save tonypee/e1584ff1f8131dd5644e82ab5b60d1c5 to your computer and use it in GitHub Desktop.
<style scoped lang="less">
@import "../../styles/variables.less";
.select-container {
position: relative;
width: 210px;
&.invalid {
button {
border: 1px solid @color_error!important;
}
}
button {
width: 100%;
text-align: left;
padding: 0 16px;
}
button:active, button:hover, button:focus {
border-color: #ccc;
}
&.active {
button {
border-radius: 5px 5px 0 0;
}
.select-menu {
border-top: none;
border-radius: 0 0 5px 5px;
}
}
&.up {
&.active button {
border-radius: 0 0 5px 5px;
}
.select-menu {
bottom: 37px;
border-radius: 5px 5px 0 0;
box-shadow: 0px -2px 3px rgba(0, 0, 0, .175);
}
}
// force scroll bars
::-webkit-scrollbar {
-webkit-appearance: none;
max-width: 8px;
max-height: 8px;
}
::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(0,0,0,.35);
-webkit-box-shadow: 0 0 1px rgba(255,255,255,.35);
}
ul.select-menu {
z-index: 999;
list-style-type: none;
margin: 0;
padding: 0;
box-shadow: 0px 2px 3px rgba(0, 0, 0, .175);
position: absolute;
border: 1px solid #ccc;
border-radius: 5px;
background: white;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
max-height: 300px;
width: 100%;
a.item {
padding: 7px 16px;
display: block;
&:hover {
cursor: pointer;
background-color: #CFF6F8!important;
}
&.selected {
background-color: #E7FAFB;
}
}
li {
margin: 0;
}
}
.caret {
position: relative;
top: 5px;
}
.caret:before {
display: inline-block;
float: right;
margin-top: 16px;
margin-right: -4px;
color: #999;
border-style: solid;
border-width: 5px 5px 0 5px;
border-color: #999999 transparent transparent transparent;
content: "";
}
.search {
width: 100%;
padding: 8px;
border-width: 0 0 1px 0;
}
}
</style>
<template>
<div :class="['select-container', show && 'active', !this.isValid && 'invalid', up && 'up']"
v-click-outside="onClickOutside">
<slot name="before"></slot>
<slot name="button">
<button type="button" @click="toggle" @keyup.esc="show = false" :disabled="disabled">
<span class="caret"></span>
<span class="label" v-if="mode === 'dropdown' || options.length">
<span v-if="!multiple">{{ getLabel(value) || placeholder }}</span>
<span v-if="multiple" >
<span v-if="value && value.length">
<span v-for="(val, index) of value">
{{ getLabel(val) }}{{ index < (value.length -1) ? ',' : '' }}
</span>
{{ value.length == 0 ? placeholder : '' }}
</span>
<span v-else>
{{ placeholder }}
</span>
</span>
</span>
<span class="label" v-else>
{{ emptyMessage }}
</span>
</button>
</slot>
<slot name="select-menu">
<ul class="select-menu" v-show="show && options.length" @click="onClickMenu">
<span v-if="!multiple && showSearch">
<input v-model="search" ref="search" class="search" placeholder="Search Items..." />
</span>
<a :class="['item', isSelected(option.value) && 'selected']"
v-for="option of filteredObjects"
@click="select(option.value)">{{ option.label }}</a>
</ul>
</slot>
</div>
</template>
<script>
import { Component } from 'vue-property-decorator'
import ClickOutside from '../../core/directives/ClickOutside.js'
import { isObject } from '../../core/utility'
import validatejs from 'validate.js'
import validation from '../../core/mixins/validation'
@Component({
props: {
name: { type: String },
value: {},
constraint: { type: Object, default: () => null },
options: { type: Array, default: () => [] },
multiple: { type: Boolean, default: false },
placeholder: { type: String, default: '- Select -' },
label: { type: String, default: null },
valueFrom: { type: String, default: 'value' },
labelFrom: { type: String, default: 'label' },
emptyMessage: { type: String, default: 'No Data' },
mode: { type: String, default: 'select' },
showSearch: { type: Boolean, default: false },
up: { type: Boolean, default: false },
},
directives: { ClickOutside },
mixins: [ validation ]
})
export default class select {
componentType = 'input'
show = false
disabled = false
search = ''
get filteredObjects() {
return this.optionObjects.filter(o => {
return !this.search.length || ~o.label.toUpperCase().indexOf(this.search.toUpperCase())
})
}
get optionObjects() {
return this.options.map(option => {
return isObject(option) ? {
label: option[this.labelFrom],
value: option[this.valueFrom],
} : {
label: option,
value: option,
}
})
}
get labelMap() {
return this.optionObjects.reduce((o, v) => {
o[String(v.value)] = v.label
return o
}, {})
}
getLabel(value) {
if (this.mode === 'dropdown') {
return this.placeholder
} else {
return this.labelMap[String(value)]
}
}
select(value) {
if (this.multiple) {
const existing = this.value.concat()
if (!~existing.indexOf(value)) {
existing.push(value)
} else {
existing.splice(existing.indexOf(value), 1)
}
this.$emit('input', existing)
} else {
this.$emit('input', value)
this.$emit('select', value)
}
}
toggle() {
this.show = !this.show
this.search = ''
setTimeout(() => {
if (this.show) {
this.$refs.search && this.$refs.search.focus()
}
})
}
onClickMenu(e) {
if (e.target === this.$refs.search) {
return
}
this.hide()
}
onClickOutside() {
this.hide()
}
hide() {
this.show = false
}
validate() {
const constraint = this.constraint ||
(this.form && this.form.constraints && this.form.constraints[this.name])
if (!constraint || this.multiple) {
return
}
this.errors = validatejs.single(this.value, constraint)
this.isValid = !this.errors
this.$emit('error', this.errors)
return this.errors
}
isSelected(option) {
if (this.multiple) {
return ~this.value.indexOf(option)
} else {
return this.value === option
}
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment