console
console.clear()
const data = [
'Apple',
'Artichoke',
'Asparagus',
'Banana',
'Beets',
'Bell pepper',
'Broccoli',
'Brussels sprout',
'Cabbage',
'Carrot',
'Cauliflower',
'Celery',
'Chard',
'Chicory',
'Corn',
'Cucumber',
'Daikon',
'Date',
'Edamame',
'Eggplant',
'Elderberry',
'Fennel',
'Fig',
'Garlic',
'Grape',
'Honeydew melon',
'Iceberg lettuce',
'Jerusalem artichoke',
'Kale',
'Kiwi',
'Leek',
'Lemon',
'Mango',
'Mangosteen',
'Melon',
'Mushroom',
'Nectarine',
'Okra',
'Olive',
'Onion',
'Orange',
'Parship',
'Pea',
'Pear',
'Pineapple',
'Potato',
'Pumpkin',
'Quince',
'Radish',
'Rhubarb',
'Shallot',
'Spinach',
'Squash',
'Strawberry',
'Sweet potato',
'Tomato',
'Turnip',
'Ugli fruit',
'Victoria plum',
'Watercress',
'Watermelon',
'Yam',
'Zucchini'
]
class Autocomplete {
constructor({
rootNode,
inputNode,
resultsNode,
searchFn,
shouldAutoSelect = false,
onShow = () => {},
onHide = () => {}
} = {}) {
this.rootNode = rootNode
this.inputNode = inputNode
this.resultsNode = resultsNode
this.searchFn = searchFn
this.shouldAutoSelect = shouldAutoSelect
this.onShow = onShow
this.onHide = onHide
this.activeIndex = -1
this.resultsCount = 0
this.showResults = false
this.hasInlineAutocomplete = this.inputNode.getAttribute('aria-autocomplete') === 'both'
document.body.addEventListener('click', this.handleDocumentClick)
this.inputNode.addEventListener('keyup', this.handleKeyup)
this.inputNode.addEventListener('keydown', this.handleKeydown)
this.inputNode.addEventListener('focus', this.handleFocus)
this.resultsNode.addEventListener('click', this.handleResultClick)
}
handleDocumentClick = event => {
if (event.target === this.inputNode || this.rootNode.contains(event.target) || event.target.nodeName === 'LI') {
return
}
this.hideResults()
}
handleKeyup = event => {
const { key } = event
switch (key) {
case 'ArrowUp':
case 'ArrowDown':
case 'Escape':
case 'Enter':
event.preventDefault()
return
default:
this.updateResults()
}
if (this.hasInlineAutocomplete) {
switch(key) {
case 'Backspace':
return
default:
this.autocompleteItem()
}
}
}
handleKeydown = event => {
const { key } = event
let activeIndex = this.activeIndex
if (key === 'Escape') {
this.hideResults()
this.inputNode.value = ''
return
}
if (this.resultsCount < 1) {
if (this.hasInlineAutocomplete && (key === 'ArrowDown' || key === 'ArrowUp')) {
this.updateResults()
} else {
return
}
}
const prevActive = this.getItemAt(activeIndex)
let activeItem
switch(key) {
case 'ArrowUp':
if (activeIndex <= 0) {
activeIndex = this.resultsCount - 1
} else {
activeIndex -= 1
}
break
case 'ArrowDown':
if (activeIndex === -1 || activeIndex >= this.resultsCount - 1) {
activeIndex = 0
} else {
activeIndex += 1
}
break
case 'Enter':
activeItem = this.getItemAt(activeIndex)
this.selectItem(activeItem)
return
case 'Tab':
this.checkSelection()
this.hideResults()
return
default:
return
}
event.preventDefault()
activeItem = this.getItemAt(activeIndex)
this.activeIndex = activeIndex
if (prevActive) {
prevActive.classList.remove('selected')
prevActive.setAttribute('aria-selected', 'false')
}
if (activeItem) {
this.inputNode.setAttribute('aria-activedescendant', `autocomplete-result-${activeIndex}`)
activeItem.classList.add('selected')
activeItem.setAttribute('aria-selected', 'true')
if (this.hasInlineAutocomplete) {
this.inputNode.value = activeItem.innerText
}
} else {
this.inputNode.setAttribute('aria-activedescendant', '')
}
}
handleFocus = event => {
this.updateResults()
}
handleResultClick = event => {
if (event.target && event.target.nodeName === 'LI') {
this.selectItem(event.target)
}
}
getItemAt = index => {
return this.resultsNode.querySelector(`#autocomplete-result-${index}`)
}
selectItem = node => {
if (node) {
var list = this.inputNode.value.split(",")
list.splice(0,0,node.innerText)
this.inputNode.value = list.join(",")
this.inputNode.focus()
}
}
checkSelection = () => {
if (this.activeIndex < 0) {
return
}
const activeItem = this.getItemAt(this.activeIndex)
this.selectItem(activeItem)
}
autocompleteItem = event => {
const autocompletedItem = this.resultsNode.querySelector('.selected')
const input = this.inputNode.value
if (!autocompletedItem || !input) {
return
}
const autocomplete = autocompletedItem.innerText
if (input !== autocomplete) {
this.inputNode.value = autocomplete
this.inputNode.setSelectionRange(input.length, autocomplete.length)
}
}
updateResults = () => {
const input = this.inputNode.value
const results = this.searchFn(input)
this.hideResults()
if (results.length === 0) {
return
}
this.resultsNode.innerHTML = results.map((result, index) => {
const isSelected = this.shouldAutoSelect && index === 0
if (isSelected) {
this.activeIndex = 0
}
return `
<div class="autocomplete-result search-list" role='option'>
<div class="search-res">
<div class="check-input">
<input type="checkbox" checked="checked"></div>
<div class="light-text">高某某</div>
<li
id='autocomplete-result-${index}'
class='content-right'
${isSelected ? "aria-selected='true'" : ''}
>
${result}
</li>
<div class="tip">!</div>
</div>
<li class="content-bottom">
${result}
</li>
</div>
`
}).join('')
this.resultsNode.classList.remove('hidden')
this.rootNode.setAttribute('aria-expanded', true)
this.resultsCount = results.length
this.shown = true
this.onShow()
}
hideResults = () => {
this.shown = false
this.activeIndex = -1
this.resultsNode.innerHTML = ''
this.resultsNode.classList.add('hidden')
this.rootNode.setAttribute('aria-expanded', 'false')
this.resultsCount = 0
this.inputNode.setAttribute('aria-activedescendant', '')
this.onHide()
}
}
const search = input => {
if (input.length < 1) {
return []
}
var list = input.split(",")
input = list[list.length - 1]
console.log(input)
if(!input) return [];
return data.filter(item => item.toLowerCase().startsWith(input.toLowerCase()))
}
const autocomplete = new Autocomplete({
rootNode: document.querySelector('.autocomplete'),
inputNode: document.querySelector('.autocomplete-input'),
resultsNode: document.querySelector('.autocomplete-results'),
searchFn: search,
shouldAutoSelect: false
})
document.querySelector('form').addEventListener('submit', (event) => {
event.preventDefault()
const result = document.querySelector('.search-result')
const input = document.querySelector('.autocomplete-input')
result.innerHTML = 'Searched for: ' + input.value
})
<header>
<h1>Autocomplete</h1>
<p>Demonstration of a fully accessible autocomplete/search component in vanilla JavaScript. Based on the <a href='https://www.w3.org/TR/wai-aria-practices-1.1/#combobox' target='_blank'>WAI-ARIA authoring practices 1.1</a>.</p>
</header>
<main>
<div class='container'>
<form class='autocomplete-container'>
<div
class='autocomplete'
role='combobox'
aria-expanded='false'
aria-owns='autocomplete-results'
aria-haspopup='listbox'
>
<input
class='autocomplete-input'
placeholder='Search for a fruit or vegetable'
aria-label='Search for a fruit or vegetable'
aria-autocomplete='both'
aria-controls='autocomplete-results'
>
<button type='submit' class='autocomplete-submit' aria-label='Search'>
<svg aria-hidden='true' viewBox='0 0 24 24'>
<path d='M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z' />
</svg>
</button>
</div>
<ul
id='autocomplete-results'
class='autocomplete-results hidden'
role='listbox'
aria-label='Search for a fruit or vegetable'
>
</ul>
</form>
<p class='search-result'></p>
</div>
</main>
main {
margin: 64px 0;
padding: 0 16px;
}
.container {
margin: 0 auto;
width: 100%;
max-width: 600px;
}
.autocomplete-container {
position: relative;
}
.autocomplete {
display: flex;
}
input,
button {
font-family: inherit;
}
.autocomplete-input {
border: 1px solid rgba(0, 0, 0, 0.54);
width: 100%;
padding: 8px;
font-size: 16px;
line-height: 1.5;
flex: 1;
}
.autocomplete-submit {
border: 1px solid #1c5b72;
padding: 8px 16px;
background: #1c5b72;
display: flex;
align-items: center;
justify-content: center;
}
.autocomplete-submit svg {
width: 24px;
height: 24px;
fill: #fff;
}
.autocomplete-results {
position: absolute;
margin-top: -1px;
border: 1px solid rgba(0, 0, 0, 0.54);
padding: 4px 0;
width: 100%;
z-index: 1;
background: #fff;
margin: 0;
padding: 0;
list-style: none;
transition: none;
}
.autocomplete-result {
cursor: default;
padding: 4px 8px;
}
.autocomplete-result:hover {
background: rgba(0, 0, 0, 0.12);
}
.autocomplete-result.selected {
background: rgba(0, 0, 0, 0.12);
}
.search-result {
margin-top: 64px;
text-align: center;
}
.hidden {
display: none;
}
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400");
* {
box-sizing: border-box;
position: relative;
transition: all .3s ease
}
html {
font-size: 16px
}
body {
font-family: Open Sans, Verdana, sans-serif;
color: rgba(0, 0, 0, .87);
font-weight: 400;
line-height: 1.45
}
body,
header {
background: #fafafa
}
header {
padding: 40px;
min-height: 200px;
text-align: center;
color: rgba(0, 0, 0, .87)
}
header > * {
max-width: 800px;
margin-left: auto;
margin-right: auto
}
header>:last-child {
margin-bottom: 0
}
h1 {
margin-bottom: 0.5em;
font-weight: inherit;
line-height: 1.2;
color: #1c5b72;
font-size: 2.618em
}
p {
margin-bottom: 1.3em;
line-height: 1.618
}
@media (min-width:800px) {
h1 {
font-size: 4.236em;
font-weight: 300
}
p {
font-size: 1.3em
}
}
a {
color: #e03616;
text-decoration: none
}
.search-res {
display: flex;
align-items: center;
justify-content: space-between;
text-align: left;
}
.tip {
color: red;
font-size: 20px;
}
.light-text {
width: 100px;
font-weight: bold;
}
.check-input {
width: 50px;
}
.search-list {
display: flex;
flex-direction: column;
}
.content-bottom {
margin-left: 50px;
}
.content-right {
flex: 1;
}