Created
June 3, 2019 17:40
-
-
Save iErik/0127b1265cc79d96f7d3676eeb608488 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<c-input-container | |
v-click-outside="close" | |
:class="containerClasses" | |
:style="{ '--options-length': (computedOptions || []).length }" | |
v-bind="containerAttributes" | |
@click.native="opened = !opened" | |
> | |
<select | |
v-if="mobileNative || nativeCompatible" | |
v-model="selected" | |
:name="name" | |
:class="['native-select', { '-hidden': nativeCompatible }]" | |
> | |
<option | |
v-for="(option, index) in options" | |
:key="index" | |
> | |
{{ getItem(option) }} | |
</option> | |
</select> | |
<div | |
:style="borderStyles" | |
class="border" | |
/> | |
<div | |
:class="selectedClasses" | |
tabindex="0" | |
@keydown="keyboardHandler" | |
> | |
<slot name="icon"> | |
<c-icon v-if="icon" :icon="icon" class="icon" /> | |
</slot> | |
<slot :selected="value"> | |
<c-transition mode="out-in" duration="200"> | |
<span | |
:class="['text', { '-placeholder': !value }]" | |
:key="selected" | |
> | |
{{ selected }} | |
</span> | |
</c-transition> | |
<slot name="select-icon"> | |
<c-icon v-if="selectIcon" icon="chevron-down" class="select-icon" size="11" /> | |
</slot> | |
</slot> | |
</div> | |
<div | |
:style="optionsWrapperStyles" | |
class="options-wrapper" | |
> | |
<transition name="options"> | |
<section | |
v-show="opened && !mobileNative" | |
:class="optionsClasses" | |
ref="optionsList" | |
@scroll="checkPagination" | |
> | |
<slot :options="options" name="options"> | |
<div | |
v-for="(option, index) in computedOptions" | |
:key="index" | |
:class="optionClasses(index)" | |
@click.stop="selected = options[index]" | |
@mouseover="focusedItem = index" | |
> | |
<slot :option="option" name="option"> | |
<p class="text">{{ getItem(option) }}</p> | |
<span :class="['dot', { '-selected': isSelected === index }]" /> | |
</slot> | |
</div> | |
</slot> | |
</section> | |
</transition> | |
</div> | |
</c-input-container> | |
</template> | |
<script> | |
import CInputContainer from '../CInputContainer' | |
import CIcon from '../CIcon' | |
import CTransition from '../CTransition' | |
import clickOutside from '../../directives/clickOutside' | |
// import { Shadowed } from '../../mixins' | |
export default { | |
name: 'CSelect', | |
components: { CInputContainer, CIcon, CTransition }, | |
// mixins: [ Shadowed({ refName: 'optionsList' }) ], | |
directives: { clickOutside }, | |
props: { | |
/** | |
* The options array, it can either be an array of objects, in which | |
* case the trackBy/displayBy props are necessary, or an array of any | |
* other primitive type. | |
*/ | |
options: { | |
type: Array, | |
required: true | |
}, | |
/** | |
* The input name. | |
*/ | |
name: { | |
type: String, | |
default: 'select' | |
}, | |
/** | |
* If `options` is an array of objects, CSelect will use this prop | |
* to compare the currently select item. | |
*/ | |
trackBy: String, | |
/** | |
* If `options` is an array of objects, CSelect will use this prop | |
* to get the label of the option/value to be displayed for the user. | |
*/ | |
displayBy: String, | |
/** | |
* The icon to show field. | |
*/ | |
icon: String, | |
/** | |
* The icon of the dropdown indicator (the little arrow on the left). | |
*/ | |
selectIcon: { | |
type: String, | |
default: 'chevron-down' | |
}, | |
/** | |
* The placeholder of the field. | |
*/ | |
placeholder: { | |
type: String, | |
default: 'Selecione uma opção' | |
}, | |
/** | |
* The value of the field. | |
*/ | |
value: [Object, String], | |
/** | |
* Set style and behavior for using native mobile select. | |
*/ | |
mobileNative: Boolean, | |
/** | |
* Set a hidden native select input for native forms compatibility. | |
*/ | |
nativeCompatible: Boolean, | |
/** | |
* Jumbo style. | |
*/ | |
jumbo: Boolean, | |
/** | |
* Disables the field | |
*/ | |
disabled: Boolean, | |
/** | |
* Paginates the option list. | |
*/ | |
paginated: Boolean, | |
/** | |
* Determines how many items to show at a time, only relevant | |
* when the `paginated` prop is set to `true`. | |
*/ | |
paginationThreshold: { | |
type: [Number, String], | |
default: 10 | |
} | |
}, | |
data () { | |
return { | |
opened: false, | |
focusedItem: -1, | |
paginationPosition: +this.paginationThreshold | |
} | |
}, | |
computed: { | |
selected: { | |
get () { | |
if (this.placeholder && !this.value) return this.placeholder | |
const value = (this.options || []) | |
.find(option => { | |
return this.trackBy && option instanceof Object | |
? option[this.trackBy] === this.value[this.trackBy] | |
: option === this.value | |
}) | |
if (!value) return 'Opção inválida' | |
if (this.value) { | |
if (this.displayBy && value[this.displayBy]) { | |
return value[this.displayBy] | |
? value[this.displayBy] | |
: process.env.NODE_ENV === 'development' ? 'error: displayBy prop does not exist' : '' | |
} else { | |
return this.value | |
} | |
} | |
}, | |
set (item) { | |
this.focusedItem = -1 | |
this.close() | |
this.$emit('input', item) | |
} | |
}, | |
containerClasses () { | |
return ['c-select', { | |
'-opened': this.opened && !this.mobileNative, | |
'-mobile-native': this.mobileNative, | |
'-selected': this.value !== '', | |
'-disabled': this.disabled, | |
'-has-icon': this.icon, | |
'-jumbo': this.jumbo, | |
}] | |
}, | |
containerAttributes () { | |
return { | |
...this.$attrs, | |
...(this.mobileNative || this.nativeCompatible ? {} : { name: this.name }) | |
} | |
}, | |
selectedClasses () { | |
return [ 'selected', { | |
'-slot': this.$slots.default || this.$scopedSlots.default | |
}] | |
}, | |
optionsClasses () { | |
return [ 'options', { | |
'-slot': this.$slots.options || this.$scopedSlots.options | |
}] | |
}, | |
isSelected () { | |
return this.options.findIndex(option => { | |
return this.trackBy | |
? (this.value || {})[this.trackBy] === option[this.trackBy] | |
: this.value === option | |
}) | |
}, | |
borderStyles () { | |
if (!this.jumbo) { | |
const height = this.opened && !this.mobileNative | |
? `${this.computedOptions.length * 40 + 40}px` | |
: '40px' | |
return { height } | |
} | |
}, | |
optionsWrapperStyles () { | |
if (!this.jumbo) { | |
const height = `${(this.computedOptions || []).length * 41 - 1}px` | |
return { height, top: 40 } | |
} | |
}, | |
computedOptions () { | |
return this.paginated | |
? this.options.slice(0, this.paginationPosition) | |
: this.options | |
} | |
}, | |
methods: { | |
getItem (option) { | |
if (this.displayBy) { | |
return option[this.displayBy] | |
? option[this.displayBy] | |
: process.env.NODE_ENV === 'development' ? 'error: displayBy prop does not exist' : '' | |
} | |
return option | |
}, | |
paginate () { | |
if (this.options.length !== this.computedOptions.length) | |
this.paginationPosition += +this.paginationThreshold | |
}, | |
checkPagination () { | |
const el = this.$refs.optionsList | |
const scrollTop = el.scrollTop | |
const scrollHeight = el.scrollHeight | |
const clientHeight = el.clientHeight | |
const scrollHeightThreshold = scrollHeight - 60 | |
if ((scrollTop + clientHeight) >= scrollHeightThreshold) this.paginate() | |
}, | |
close () { | |
this.focusedItem = -1 | |
this.opened = false | |
}, | |
open () { | |
this.opened = true | |
}, | |
optionClasses (index) { | |
return [ 'option', { | |
'-slot': this.$slots.option || this.$scopedSlots.option, | |
'-focused': this.focusedItem === index | |
}] | |
}, | |
keyboardHandler (ev) { | |
if (ev.key === 'Enter') !this.onEnter() && ev.preventDefault() | |
else if (ev.key === 'ArrowUp') !this.onArrowUp() && ev.preventDefault() | |
else if (ev.key === 'ArrowDown') !this.onArrowDown() && ev.preventDefault() | |
else if (ev.key === 'Tab') this.close() | |
}, | |
onEnter () { | |
if (!this.opened) this.open() | |
else if (this.focusedItem > -1) { this.selected = this.options[this.focusedItem] } | |
}, | |
onArrowUp () { | |
if (this.focusedItem > -1) this.focusedItem -= 1 | |
if (this.focusedItem === -1) this.close() | |
}, | |
onArrowDown () { | |
if (!this.opened) this.open() | |
if (this.focusedItem < this.options.length - 1) this.focusedItem += 1 | |
} | |
} | |
} | |
</script> | |
<style lang="scss"> | |
.c-select { | |
position: relative; | |
overflow: visible; | |
z-index: 0; | |
// margin-bottom transition to support | |
// CInputContainer margin-bottom transition | |
transition: z-index .3s, margin-bottom .3s; | |
& > .native-select.-hidden { | |
position: absolute; | |
visibility: hidden; | |
} | |
&.-disabled { | |
user-select: none; | |
pointer-events: none; | |
& > .selected { | |
background-color: map-get($text-color, base-05); | |
color: map-get($text-color, base-80); | |
} | |
} | |
& > .border { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
background: #FFF; | |
border-radius: 5px; | |
border: 1px solid map-get($text-color, base-10); | |
transition: height .3s, box-shadow .3s; | |
z-index: 1; | |
max-height: 240px; | |
} | |
& > .selected { | |
position: relative; | |
border: 1px solid transparent; | |
height: 40px; | |
display: flex; | |
padding-left: 20px; | |
align-items: center; | |
justify-content: flex-start; | |
cursor: pointer; | |
outline: none; | |
z-index: 1; | |
& > .icon { | |
box-sizing: content-box; | |
fill: map-get($text-color, base-30); | |
} | |
& > .text { | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
color: map-get($text-color, base-80); | |
font-size: 14px; | |
font-family: $base-font-family; | |
&.-placeholder { color: map-get($text-color, base-30); } | |
} | |
& > .select-icon { | |
margin-left: auto; | |
margin-right: 17px; | |
fill: map-get($text-color, base-50); | |
transition: transform .3s, fill .3s; | |
} | |
} | |
& > .options-wrapper { | |
overflow: hidden; | |
position: absolute; | |
left: 0; | |
right: 0; | |
border: none; | |
background: transparent; | |
pointer-events: none; | |
z-index: 1; | |
// @include shadowed(70px, #FFF); | |
&, & > .options { max-height: 200px } | |
& > .options { overflow-y: auto; } | |
& > .options > .option { | |
cursor: pointer; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 0 15px; | |
height: 40px; | |
border-bottom: 1px solid map-get($text-color, base-10); | |
&:last-child { border-bottom: none; } | |
&.-focused { background-color: map-get($text-color, base-02); } | |
& > .text { | |
position: relative; | |
color: map-get($text-color, base-80); | |
font-family: $base-font-family; | |
font-size: 14px; | |
} | |
& > .dot { | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
background: set-linear-gradient(horizontal); | |
opacity: 0; | |
transition: opacity 1s; | |
&.-selected { opacity: 1; } | |
} | |
} | |
// transitions | |
.options-enter-active, .options-leave-active { | |
transition: transform .3s, opacity .1s; | |
} | |
.options-enter, .options-leave-to { | |
transform: translateY(-100%); | |
opacity: 0; | |
} | |
} | |
&:focus-within:not(.-opened) > .border { | |
border-color: rgba($primary-color, .35); | |
@include hover(); | |
} | |
&.-validation:not(.-opened) > .border { | |
border-color: rgba($negative-color, .35); | |
@include hover($negative-color); | |
} | |
&.-has-icon > .selected { padding-left: 12px; } | |
&.-has-icon > .selected > .text { padding-left: 10px; } | |
&.-opened { | |
z-index: $z-index-1; | |
& > .border { | |
box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.2); | |
background: linear-gradient(180deg, #FFFFFF 0%, rgba(255,255,255,0.9) 100%); | |
} | |
& > .options-wrapper { pointer-events: all; } | |
& > .selected { | |
border-radius: 5px 5px 0 0; | |
border-bottom: 1px solid transparent; | |
cursor: default; | |
& > .select-icon { | |
fill: $base-text-color; | |
transform: rotateX(180deg); | |
} | |
} | |
} | |
&.-mobile-native { | |
& > .native-select { | |
width: 100%; | |
height: 40px; | |
z-index: $z-index-1; | |
position: absolute; | |
opacity: 0; | |
} | |
& > .border { | |
background-color: rgba(255,255,255,0.1); | |
border-radius: 20px; | |
} | |
& > .selected > .icon { | |
fill: rgba(255, 255, 255, 0.8); | |
filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3)); | |
} | |
& > .selected > .text { | |
color: #FFF; | |
opacity: 0.8; | |
font-family: $title-font-family; | |
font-size: 11px; | |
font-weight: $title-font-weight; | |
text-transform: uppercase; | |
} | |
& > .selected > .select-icon { | |
fill: rgba(255, 255, 255, 0.5); | |
filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3)); | |
} | |
} | |
&.-jumbo { | |
& > .border { | |
height: 50px; | |
max-height: 250px; | |
} | |
&.-opened > .border { height: calc(var(--options-length)*40px + 50px); } | |
& > .selected { | |
height: 50px; | |
padding-left: 15px; | |
border-radius: 5px; | |
& > .select-icon { margin-right: 27px; } | |
& > .text { padding-top: 16px; } | |
& > .text.-placeholder { display: none; } | |
&::before { | |
$background: ( | |
light: rgba(map-get($negative-color-map, light), 0.2), | |
dark: rgba(map-get($negative-color-map, dark), 0.2) | |
); | |
content: ''; | |
display: block; | |
position: absolute; | |
background: set-linear-gradient(315deg, $background); | |
top: 0px; | |
left: 0px; | |
right: 0px; | |
bottom: 0px; | |
opacity: 0; | |
transition: opacity .3s; | |
} | |
} | |
& > .label { | |
z-index: $z-index-2; | |
top: 50%; | |
transform: translateY(-50%); | |
padding-left: 15px; | |
padding-right: 50px; | |
margin-bottom: 0; | |
font-family: $base-font-family; | |
font-size: 14px; | |
color: map-get($text-color, base-30); | |
pointer-events: none; | |
transition: top .3s, font-size .3s; | |
} | |
&.-selected > .label { | |
top: 6px; | |
transform: translateY(0); | |
font-size: 11px; | |
} | |
&.-has-icon > .label { left: 30px; } | |
& > .options-wrapper { | |
top: 50px; | |
height: calc(var(--options-length)*41px + 1px); | |
& > .options > .option { | |
padding-left: 20px; | |
padding-right: 29px; | |
} | |
} | |
&.-validation { | |
&:not(.-opened) > .selected { | |
color: map-get($negative-color-map, light); | |
&::before { opacity: 1; } | |
& > .select-icon { fill: map-get($negative-color-map, light); } | |
} | |
&:not(.-opened) > .border { | |
background: transparent; | |
border-color: rgba($negative-color, .3); | |
box-shadow: none; | |
} | |
&:not(.-opened) > .label { color: map-get($negative-color-map, light); } | |
& > .jumbo-validation { | |
visibility: hidden; | |
position: absolute; | |
} | |
} | |
@include responsive (mobile, desktop) { | |
& > .border { | |
height: 60px; | |
max-height: 260px; | |
} | |
&.-opened > .border { height: calc(var(--options-length)*40px + 60px); } | |
& > .options-wrapper { top: 60px; } | |
& > .selected { | |
height: 60px; | |
padding-left: 20px; | |
} | |
& > .label { | |
padding: 0 20px; | |
} | |
&.-selected > .label { top: 14px; } | |
} | |
} | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment