JSFiddle - React, Tailwind, and code Playground
by Max Alex
HTML
<link rel="stylesheet" href="https://unpkg.com/bulma/css/bulma.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<autocomplete-input :options="options" @select="onOptionSelect">
<template slot="item" scope="option">
<article class="media">
<figure class="media-left">
<p class="image is-64x64">
<img :src="option.thumbnail">
</p>
</figure>
<p>
<strong>{{ option.title }}</strong>
<br> {{ option.description }}
</p>
</article>
</template>
</autocomplete-input>
<div>
<label>
{{ this.option }}
</label>
</div>
</div>
<script id="autocomplete-input-template" type="text/x-template">
<div class="autocomplete-input">
<p class="control has-icon has-icon-right">
<input v-model="keyword" class="input is-large" placeholder="Search..." @input="onInput($event.target.value)" @keyup.esc="closeDropdown" @blur="closeDropdown" @keydown.down="moveDown" @keydown.prevent.up="moveUp" @keydown.enter="select">
<i class="fa fa-angle-down"></i>
</p>
<ul v-show="isOpen" class="options-list">
<li :class="{
'highlighted': highlightedPosition === 0
}" @mouseenter="highlightedPosition = 0" @mousedown="select">
Search for "{{ enteredValue }}"
</li>
<li v-for="(option, index) in fOptions" :class="{
'highlighted': index === highlightedPosition - 1
}" @mouseenter="highlightedPosition = index + 1" @mousedown="select">
<slot name="item" :title="option.title" :description="option.description" :thumbnail="option.thumbnail">
</li>
</ul>
</div>
</script>
CSS
html,
body {
height: 100%;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
display: flex;
justify-content: center;
padding-top: 100px;
height: 100%;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
input {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
}
.autocomplete-input {
position: relative;
height: 300px;
}
ul.options-list {
display: flex;
flex-direction: column;
margin-top: -12px;
border: 1px solid #dbdbdb;
border-radius: 0 0 3px 3px;
position: absolute;
width: 100%;
overflow: hidden;
}
ul.options-list li {
width: 100%;
flex-wrap: wrap;
background: white;
margin: 0;
border-bottom: 1px solid #eee;
color: #363636;
padding: 7px;
cursor: pointer;
}
ul.options-list li.highlighted {
background: #f8f8f8
}
JavaScript
Vue.component('autocomplete-input', {
template: '#autocomplete-input-template',
props: {
options: {
type: Array,
required: true
}
},
data() {
return {
isOpen: false,
highlightedPosition: 0,
keyword: '',
enteredValue: ''
}
},
computed: {
fOptions() {
const re = new RegExp(this.enteredValue, 'i')
return this.options.filter(o => o.title.match(re))
}
},
methods: {
onInput(value) {
this.highlightedPosition = 0
this.isOpen = !!value
this.enteredValue = value
},
moveDown() {
if (!this.isOpen) {
return
}
this.highlightedPosition =
(this.highlightedPosition + 1) % (this.fOptions.length + 1)
},
moveUp() {
if (!this.isOpen) {
return
}
this.highlightedPosition = this.highlightedPosition - 1 < 0 ? this.fOptions.length : this.highlightedPosition - 1
},
select() {
const selectedOption = this.highlightedPosition === 0 ? {
title: this.enteredValue
} : this.fOptions[this.highlightedPosition - 1]
this.$emit('select', selectedOption)
this.isOpen = false
this.keyword = this.enteredValue = selectedOption.title
},
closeDropdown() {
this.keyword = this.enteredValue
this.isOpen = false
}
},
watch: {
highlightedPosition() {
this.keyword = this.highlightedPosition === 0 ? this.enteredValue : this.fOptions[this.highlightedPosition - 1].title
}
}
})
new Vue({
el: '#app',
data: {
options: [{
title: 'First Scene',
description: 'lorem ipsum dolor amet.',
thumbnail: 'http://lorempicsum.com/nemo/200/200/1'
}, {
title: 'Second Scene',
description: 'lorem ipsum dolor amet.',
thumbnail:...