Cookbook Home ~ alanwsmith.com ~ links ~ podcast ~ twitter

Search Input Example

TL;DR


Goals

(NOTE: this is my scratch pad. There's not specific order and there are duplicates, etc...)

In The Future

Notes

HTML

<div id="awsselect--top-wrapper">
  <input
    id="awsselect--filter"
    type="text"
    autocomplete="off"
    spellcheck="false"
    name="awsselect--this-is-the-input"
  />
  <br />
  <div id="awsselect--options-wrapper"></div>
</div>

<h4>Goals</h4>
<p>
  (NOTE: this is my scratch pad. There's not specific order and there are
  duplicates, etc...)
</p>
<ul>
  <li>[.] - Show a placeholder when there is no initial input</li>
  <li>[.] - Show a placeholder with most recent selected value</li>
  <li>
    [.] - Don't show the selection list if either it or the text field are not
    selected
  </li>
  <li>[.] - Clicking into the field clears the placeholder</li>
  <li>
    [.] - Clicking into the field opens the selection list with unfiltered
    options
  </li>
  <li>[.] - Don't show the drop down until the cursor is in the input field</li>
  <li>[.] - Typing in the input field filters the items in the menu</li>
  <li>
    [.] - Hitting enter in the text field selets the top value from the
    selection list
  </li>
  <li>
    [.] - The first item in the list is selected as you type to identify what
    will be used if you hit enter
  </li>
  <li>[.] - Clicking outside the input resets it to the place holder</li>
  <li>[.] - Clicking an item in the list sets the value and closes the list</li>
  <li>
    [.] - Hitting enter when there is not valid selection item doesn't do
    anything
  </li>
  <li>
    [.] - Pressing the down arrow in the input field takes you to the first list
    item if there's no filter text
  </li>
  <li>
    [.] - Pressing the down arrow in the input field takes you to the second
    list item if there's a filter (thinking folks would hit enter if they wanted
    the first one
  </li>
  <li>[.] - Pressing escape in the input field takes you out and clear it.</li>
  <li>
    [.] - Clicking an item in the menu selects it, closes the menu, and returns
    the text to the placeholder
  </li>
  <li>
    [.] - Don't select the first item in the list if there is nothing in the
    text field
  </li>
  <li>[.] - Hitting enter while on the selection list triggers update</li>
  <li>[.] - Menu list doesn't shift layout</li>
  <li>
    [.] - On arrow down, if there's nothing in the input field, select the first
    option
  </li>
  <li>
    [.] - On arrow down, if there is something in the input field, select the
    second option
  </li>
  <li>
    [.] - Keep the size of the selection list the same so it doesn't change when
    filtering
  </li>
  <li>[.] - If you only go down one option make sure up arrow works</li>
  <li>
    [.] - If youre in the menu and you click into the text field it needs to
    close/kill the select menu
  </li>
  <li>
    [.] - Reset selection in menu when opening the form so you don't hit the
    arrow and end up way down the list from the last interaction
  </li>
  <li>
    [.] - Fix issue with safari where it would keep track of how far down the
    last selection (even if you reset it) was which was undesireable. (Solution
    was to remove the element from the DOM and create a new one)
  </li>
  <li>
    [.] - Hitting enter on the text field when it's empty doesn't change place
    holder or selected value and the filter stays open.
  </li>
  <li>
    [.] - have the uparrow move back to the text if it's hit while on the first
    element in the list
  </li>
  <li>[.] - Put a message in when there are no matches.</li>
  <li>[ ] - Provide for multiple instances on a page</li>
</ul>

<h4>In The Future</h4>
<ul>
  <li>
    [ ] - When a selection is made, move the tab index so a press of tab goes
    back to the same menu
  </li>
  <li>
    [ ] - Investigate if populating the selection list in HTML offers any
    accessability improvements
  </li>
  <li>[ ] - Put items you've selected before up at the top</li>
  <li>
    [ ] - Add sections to the drop down list to show the current top filterd
    item and previous selections
  </li>
  <li>[ ] - Set the size of the selection box spacer dynamically</li>
  <li>
    [ ] - Truncate the text of items that would extend past whatever spaceing is
    set
  </li>
  <li>
    [ ] - Maybe setup to keep primary sort order during inital browse but swtich
    to alpha when text is in the search field
  </li>
  <li>
    [ ] - Fix bug where if there's only one option and you hit the down arrow it
    selects the diabled spacer line
  </li>
  <li>[ ] - Investigate a more advanced search algorythim</li>
</ul>

<h4>Notes</h4>
<ul>
  <li>
    This is not current the search input type. I'm playing around with trying to
    get my custom text selection working for the fonts site
  </li>
</ul>

CSS

#awsselect--options-wrapper {
  position: absolute;
}

#awsselect--top-wrapper {
  display: inline;
  position: relative;
}

JavaScript

const fontsByPopularity = [
    { key: 'roboto', value: 'Roboto' },
    { key: 'opensans', value: 'OpenSans' },
    { key: 'montserrat', value: 'Montserrat' },
    { key: 'lato', value: 'Lato' },
    { key: 'poppins', value: 'Poppins' },
    { key: 'sourcesanspro', value: 'Source Sans Pro' },
    { key: 'robotocondensed', value: 'Roboto Condensed' },
    { key: 'oswald', value: 'Oswald' },
    { key: 'robotomono', value: 'Roboto Mono' },
    { key: 'raleway', value: 'Raleway' },
    { key: 'inter', value: 'Inter' },
    { key: 'notosans', value: 'Noto Sans' },
    { key: 'ubuntu', value: 'Ubuntu' },
    { key: 'mukta', value: 'Mukta' },
    { key: 'robotoslab', value: 'Roboto Slab' },
    { key: 'nunito', value: 'Nunito' },
    { key: 'playfairdisplay', value: 'Playfair Display' },
    { key: 'ptsans', value: 'PT Sans' },
    { key: 'nunitosans', value: 'Nunito Sans' },
    { key: 'merriweather', value: 'Merriweather' },
    { key: 'rubik', value: 'Rubik' },
    { key: 'notosanskr', value: 'Noto Sans KR' },
    { key: 'worksans', value: 'Work Sans' },
    { key: 'lora', value: 'Lora' },
    { key: 'firasans', value: 'Fira Sans' },
]

const state = {
    filter: '',
    placeholder: 'Pick a font',
    filterEl: null,
    optionsEl: null,
    options: [],
    selection: null,
    upArrowCheck: null,
}

const deactivateSelector = () => {
    if (state.optionsEl) {
        state.optionsEl.blur()
        state.optionsEl.remove()
    }
    setPlaceholder()
    state.filterEl.value = ''
    state.filterEl.blur()
}

const handleFilterFocus = () => {
    if (state.optionsEl) {
        state.optionsEl.blur()
        state.optionsEl.remove()
    }
    state.optionsEl = document.createElement('select')
    state.optionsEl.size = 5
    state.optionsEl.id = 'awsselect--options'
    state.wrapperEl.appendChild(state.optionsEl)
    state.optionsEl.addEventListener('keyup', handleOptionsKeyup)
    state.filterEl.placeholder = ''
    setOptions()
}

const handleFilterKeydown = (event) => {
    const pressedKey = event.key.toLowerCase()
    console.log(pressedKey)
    if (pressedKey === 'tab') {
        event.preventDefault()
        state.filterEl.blur()
        state.optionsEl.querySelector('option').setAttribute('selected', true)
        state.optionsEl.focus()
    }
}

const handleFilterKeyup = (event) => {
    const pressedKey = event.key.toLowerCase()
    console.log(pressedKey)
    if (pressedKey === 'enter') {
        if (state.filterEl.value !== '') {
            pickSelection()
        }
    } else if (pressedKey === 'escape') {
        deactivateSelector()
    } else if (pressedKey === 'arrowdown') {
        state.optionsEl.focus()
        if (state.filterEl.value === '') {
            // TODO: Use class selectors for this so you can
            // multiple on the same page.
            state.optionsEl.querySelector('option').selected = 'selected'
        } else {
            state.optionsEl.querySelectorAll('option')[1].selected = 'selected'
        }
        state.upArrowCheck = state.optionsEl.value
    } else {
        setOptions()
    }
}

const handleOptionsKeyup = (event) => {
    const pressedKey = event.key.toLowerCase()
    if (pressedKey === 'enter') {
        pickSelection(event.target.value)
    } else if (pressedKey === 'escape') {
        deactivateSelector()
    } else if (pressedKey === 'arrowup') {
        console.log(state.optionsEl.value)
        if (state.upArrowCheck === state.optionsEl.value) {
            state.filterEl.focus()
        }
    }
    state.upArrowCheck = state.optionsEl.value
}

const handlePageClick = (event) => {
    if (!event.target.id) {
        deactivateSelector()
    } else {
        const idParts = event.target.id.split('--')
        if (idParts[0] !== 'awsselect') {
            deactivateSelector()
        } else {
            if (idParts[1] === 'selection') {
                const theValue = event.target.value
                pickSelection(theValue)
            }
        }
    }
}

const pickSelection = (key = null) => {
    console.log(`pickSelection: ${key}`)
    if (state.options.length > 0) {
        if (key === null) {
            state.selection = state.options[0]
        } else {
            for (
                let fontIndex = 0;
                fontIndex < fontsByPopularity.length;
                fontIndex += 1
            ) {
                if (fontsByPopularity[fontIndex].key === key) {
                    state.selection = fontsByPopularity[fontIndex]
                    break
                }
            }
        }
        state.placeholder = state.selection.value
        console.log(state.placeholder)
        deactivateSelector()
    }
}

const removeOptions = () => {
    if (state.optionsEl) {
        while (state.optionsEl.firstChild) {
            state.optionsEl.removeChild(state.optionsEl.firstChild)
        }
    }
}

const setOptions = () => {
    state.options = []
    state.filter = state.filterEl.value
    fontsByPopularity.forEach((font) => {
        if (state.filter) {
            if (font.value.toLowerCase().includes(state.filter.toLowerCase())) {
                state.options.push(font)
            }
        } else {
            state.options.push(font)
        }
    })
    updateOptions()
}

const setPlaceholder = (newValue = null) => {
    if (newValue) {
        state.placeholder = newValue
    }
    state.filterEl.placeholder = state.placeholder
}

const updateOptions = () => {
    removeOptions()
    if (state.optionsEl) {
        state.options.forEach((font, fontIndex) => {
            const newOption = document.createElement('option')
            newOption.value = font.key
            newOption.innerHTML = font.value
            newOption.id = `awsselect--selection--${font.key}`
            if (fontIndex === 0 && state.filterEl.value !== '') {
                newOption.selected = 'selected'
            }
            state.optionsEl.appendChild(newOption)
        })

        if (state.options.length > 0) {
            const spacingOption = document.createElement('option')
            spacingOption.innerHTML = '-----------------------------'
            spacingOption.disabled = 'disabled'
            state.optionsEl.appendChild(spacingOption)
        } else {
            const errorOption = document.createElement('option')
            errorOption.innerHTML = '  No Matches'
            errorOption.disabled = 'disabled'
            state.optionsEl.appendChild(errorOption)
            const spacingOption = document.createElement('option')
            spacingOption.innerHTML = '-----------------------------'
            spacingOption.disabled = 'disabled'
            state.optionsEl.appendChild(spacingOption)
        }
    }
}

const kickoff = () => {
    console.log('kickoff')
    state.filterEl = document.getElementById('awsselect--filter')
    state.filterEl.addEventListener('focus', handleFilterFocus)
    state.filterEl.addEventListener('keyup', handleFilterKeyup)
    state.filterEl.addEventListener('keydown', handleFilterKeydown)
    state.wrapperEl = document.getElementById('awsselect--options-wrapper')
    setPlaceholder()
    updateOptions()
    document.addEventListener('mousedown', handlePageClick)
}

document.addEventListener('DOMContentLoaded', kickoff)

References