Skip to content

Instantly share code, notes, and snippets.

@tonypee
Created February 17, 2017 15:02
Show Gist options
  • Select an option

  • Save tonypee/e1584ff1f8131dd5644e82ab5b60d1c5 to your computer and use it in GitHub Desktop.

Select an option

Save tonypee/e1584ff1f8131dd5644e82ab5b60d1c5 to your computer and use it in GitHub Desktop.

Revisions

  1. tonypee created this gist Feb 17, 2017.
    274 changes: 274 additions & 0 deletions Select.vue
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,274 @@
    <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>