UNPKG

ldx-widgets

Version:

widgets

234 lines (188 loc) 6.49 kB
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