ldx-widgets
Version:
widgets
255 lines (200 loc) • 7.45 kB
text/coffeescript
React = require 'react'
createClass = require 'create-react-class'
PropTypes = require 'prop-types'
SearchInput = React.createFactory(require './search_input')
Spinner = React.createFactory(require './spinner')
{ DOWN_ARROW, UP_ARROW, TAB, ESCAPE, ENTER } = require '../constants/keyboard'
_filter = require 'lodash/filter'
{div, ul, li} = require 'react-dom-factories'
###&
@general
Filter select options component. This component lives on the overlay layer, and requires integrated context methods closeOverlay and openOverlay within the application.
@props.filter - [String] - Optional
Initialize the component overlay with a filter value. This will start filtering option labels based on this value.
@props.placeholder - [String] - Optional
Placeholder value for the filter input
@props.onChange - [Function] - Required
Function that is fired when a selection change is made
@props.options - [Array] - Optional
Array of options to render in the select
@props.optionHeight - [Number] - Optional
The fixed height of each menu option
@props.value - [String|Number] - Optional
The value of the currently selected option object
@props.noResultsText - [String] - Optional
Text displayed in the menu when no results match the filter input value
@props.SelectEl - [Function] - Optional
Reference to the select menu component that opens this overlay. If provided, focus will be directed back to the input when closing the overlay
@props.onChangeFilter - [Function] - Optional
Function fired when the filter input changes
@props.searchWidth - [Number] - Optional
Width of the search input. Default is 250
&###
SelectInputCustomOptions = createClass
displayName: 'SelectInputCustomOptions'
contextTypes:
closeOverlay: PropTypes.func
propTypes:
filter: PropTypes.string
placeholder: PropTypes.string
onChange: PropTypes.func.isRequired
options: PropTypes.array
optionHeight: PropTypes.number
OPTION_PADDING: PropTypes.number
value: PropTypes.string
noResultsText: PropTypes.string
SelectEl: PropTypes.object
valueField: PropTypes.string
labelField: PropTypes.string
isFilter: PropTypes.bool
searchWidth: PropTypes.number
getDefaultProps: ->
filter: ''
optionHeight: 20
isFilter: no
searchWidth: 250
getInitialState: ->
filterValue: ''
focusedOptionIndex: -1
options: @props.options.slice(0)
componentDidMount: ->
{ filter, SelectEl } = @props
# If using a filter, focus that on mount
if filter
@handleFilterChange filter
else
SelectEl?.refs.input?.blur()
if @textInput?
@textInput.focus()
# Add arrow key handlers
window.addEventListener('keydown', @handleKeyDown)
componentWillUnmount: ->
window.removeEventListener('keydown', @handleKeyDown)
render: ->
passedOptions = @props.options
{ filterValue, options } = @state
{ placeholder, value, noResultsText, isFilter, searchWidth, labelField } = @props
selectOptions = []
optionListClass = if isFilter then 'options-list' else 'options-list no-filter'
# Render options elements
options.forEach (o, i) =>
selectOptions.push(@processOption(o, i))
div {
className: "select-options"
onClick: @handleOptionsClick
}, [
SearchInput {
key: 'input'
ref: (input) => @textInput = input
id: "filter"
term: filterValue
handleChange: @handleFilterChange
placeholder: placeholder
wrapClass: 'custom-filter-select'
width: searchWidth
} if isFilter
if passedOptions.length is 0 # Assume loading state when the options lenght is 0
Spinner {
key: 'spinner'
length: 7
radius: 7
lines: 12
width: 2
}
else if selectOptions.length is 0 # No mathes to the filter term
div {
key: 'no-results'
className: 'no-results'
}, if noResultsText? then noResultsText else t 'No matches found'
else
ul {
key: 'options'
className: optionListClass
ref: (optionsList) => @optionsList = optionsList
onScroll: @handleScroll
}, selectOptions
]
processOption: (opt, index) ->
{ options, focusedOptionIndex } = @state
{ value, optionHeight, labelField, valueField } = @props
optionClass = "option"
if index is focusedOptionIndex then optionClass += " is-focused"
if value is opt[valueField] then optionClass += " is-selected"
return li {
key: index
onClick: @handleClick.bind(@, opt)
onMouseOver: @handleMouseOver.bind(@, index)
onMouseOut: @handleMouseOut.bind(@, index)
className: optionClass
title: opt[labelField]
style:
height: optionHeight
lineHeight: "#{optionHeight}px"
}, opt[labelField]
handleKeyDown: (e) ->
{ options, focusedOptionIndex, isFilter } = @state
{ SelectEl, labelField, valueField } = @props
adjust = 0
switch e.keyCode
when UP_ARROW
e.preventDefault()
adjust = -1
break
when DOWN_ARROW
e.preventDefault()
adjust = 1
break
when TAB
e.preventDefault()
return @context.closeOverlay({ refocusEl: SelectEl })
when ESCAPE
e.preventDefault()
return @context.closeOverlay({ refocusEl: SelectEl })
when ENTER
e.preventDefault()
currentOption = options[focusedOptionIndex]
if currentOption?
return @props.onChange(currentOption[valueField], @context.closeOverlay)
return @context.closeOverlay({ refocusEl: SelectEl })
else
e.stopPropagation()
break
newIndex = focusedOptionIndex + adjust
if newIndex < 0
newIndex = 0
else if newIndex >= options.length - 1
newIndex = options.length - 1
@setState
focusedOptionIndex: newIndex
, =>
@textInput.focus() if isFilter
@adjustScrollPosition(newIndex) if @optionsList?
adjustScrollPosition: (newIndex) ->
{ optionHeight, OPTION_PADDING } = @props
fullHeight = optionHeight + OPTION_PADDING
{ scrollTop, clientHeight } = @optionsList
newIndexTop = fullHeight * newIndex
newIndexBottom = newIndexTop + fullHeight
isOffBottom = newIndexBottom > scrollTop + clientHeight
isOffTop = newIndexTop < scrollTop
adjust = 0
if isOffBottom then adjust = newIndexBottom - (scrollTop + clientHeight)
else if isOffTop then adjust = 0 - (scrollTop - newIndexTop)
# Adjust the scrollTop position
@optionsList.scrollTop = scrollTop + adjust if adjust isnt 0
handleFilterChange: (filterValue) ->
{options, labelField} = @props
options = (opt for opt in options when opt[labelField].toLowerCase().search(filterValue.toLowerCase()) > -1)
@setState {filterValue, options}
, =>
@props.onChangeFilter?(filterValue)
handleClick: (option, e) ->
{valueField, onChange} = @props
onChange(option[valueField], @context.closeOverlay)
handleMouseOver: (focusedOptionIndex, e) -> @setState {focusedOptionIndex}
handleMouseOut: (focusedOptionIndex, e) -> @setState {focusedOptionIndex: -1}
handleOptionsClick: (e) ->
e.stopPropagation()
handleScroll: (e) ->
e.stopPropagation()
module.exports = SelectInputCustomOptions