ldx-widgets
Version:
widgets
234 lines (188 loc) • 6.49 kB
text/coffeescript
React = require 'react'
createClass = require 'create-react-class'
PropTypes = require 'prop-types'
TextInput = React.createFactory(require './text_input_2')
{ DOWN_ARROW, UP_ARROW, TAB, ESCAPE, ENTER } = require '../constants/keyboard'
_ = require 'lodash'
{div, ul, li} = React.DOM
###&
@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
&###
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
value: PropTypes.string
noResultsText: PropTypes.string
SelectEl: PropTypes.object
valueField: PropTypes.string
labelField: PropTypes.string
isFilter: PropTypes.bool
getDefaultProps: ->
filter: ''
optionHeight: 20
noResultsText: 'No results found'
isFilter: no
getInitialState: ->
filterValue: ''
focusedOptionIndex: 0
options: @props.options
componentDidMount: ->
{ filter } = @props
# If using a filter, focus that on mount
if filter
@setState
filterValue: filter
if @textInput?
@textInput.focus()
# Add arrow key handlers
window.addEventListener('keydown', @handleKeyDown)
componentWillUnmount: ->
window.removeEventListener('keydown', @handleKeyDown)
componentWillUpdate: (nextProps, nextState) ->
{ filterValue } = nextState
{ options, labelField } = nextProps
if filterValue? and @state.filterValue isnt filterValue
opts = _.filter options, (o) =>
return o[labelField].toLowerCase().search(filterValue.toLowerCase()) > -1
@setState
options: opts
render: ->
{ filterValue, options } = @state
{ placeholder, value, noResultsText, isFilter } = @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
}, [
TextInput {
key: 'input'
ref: (input) => @textInput = input
id: "filter"
value: filterValue
onChange: @handleFilterChange
placeholder: placeholder
} if isFilter
if selectOptions.length
ul {
key: 'options'
className: optionListClass
ref: (optionsList) => @optionsList = optionsList
onScroll: @handleScroll
}, selectOptions
else
div {
key: 'no-results'
className: 'no-results'
}, noResultsText
]
processOption: (opt, index) ->
{ options, focusedOptionIndex } = @state
{ value, optionHeight, labelField, valueField } = @props
optionClass = "option"
if options[focusedOptionIndex] is opt then optionClass += " is-focused"
if value is opt[valueField] then optionClass += " is-selected"
return li {
key: index
onClick: @handleClick.bind(@, opt)
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(SelectEl)
when ESCAPE
e.preventDefault()
return @context.closeOverlay(SelectEl)
when ENTER
e.preventDefault()
currentOption = options[focusedOptionIndex]
if currentOption?
return @props.onChange(currentOption[valueField], @context.closeOverlay)
return @context.closeOverlay(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(adjust) if @optionsList?
adjustScrollPosition: (adjust) ->
{ optionHeight } = @props
{ options } = @state
# Adjust the scroll top
scrollAdjust = adjust * optionHeight
{ scrollTop } = @optionsList
adjustTop = @optionsList.scrollTop + scrollAdjust
maxHeight = optionHeight * options.length
# Don't allow the final value to be above the max or below 0
if adjustTop < 0
adjustTop = 0
else if adjustTop > maxHeight
adjustTop = maxHeight
# Adjust the scrollTop position
@optionsList.scrollTop = adjustTop
handleFilterChange: (e) ->
@setState
filterValue: e
, =>
@props.onChangeFilter?(e)
handleClick: (option, e) ->
{valueField, onChange} = @props
onChange(option[valueField], @context.closeOverlay)
handleOptionsClick: (e) ->
e.stopPropagation()
handleScroll: (e) ->
e.stopPropagation()
module.exports = SelectInputCustomOptions