UNPKG

ldx-widgets

Version:

widgets

255 lines (200 loc) 7.45 kB
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