UNPKG

ldx-widgets

Version:

widgets

270 lines (217 loc) 7.86 kB
React = require 'react' SearchInput = React.createFactory(require './search_input') TextInput = React.createFactory(require './text_input') {UP_ARROW, DOWN_ARROW, ENTER, PAGE_UP, PAGE_DOWN} = require('../constants/keyboard') {div, ul} = React.DOM ### Input Type Ahead Props @results - REQUIRED - Array Array of items that are printed in the results list @value - OPTIONAL - String | Number Value of input field @searchInterval - OPTIONAL - Number Number in milliseconds before firing onSearch, after user finishes typing @onChange - REQUIRED - Function Function that is fired when the value of the input changes @onSearch - REQUIRED - Function Function that is fired when the timer triggers after the set searchInterval @className - OPTIONAL - String CSS class applied to form wrapper @placeholder - OPTIONAL - String | Number Default placeholder value of text input @resultConfig - REQUIRED - Object @component: React component that's rendered for each result item @height: Height for each result item - should be evenly divisible into maxContainerHeight @onResultSelect: Function that is fired when a list item is selected - should be used to alter state and pass new values to 'value' prop @maxContainerHeight - OPTIONAL - Number | String The maximum height of the typeahead results container. The resultConfig.height property should be evenly divisible into maxContainerHeight ### InputTypeAhead = React.createClass displayName: 'InputTypeAhead' propTypes: resultConfig: React.PropTypes.shape { component: React.PropTypes.func.isRequired height: React.PropTypes.number.isRequired onResultSelect: React.PropTypes.func.isRequired } onSearch: React.PropTypes.func.isRequired onChange: React.PropTypes.func.isRequired className: React.PropTypes.string results: React.PropTypes.array.isRequired minLength: React.PropTypes.number value: React.PropTypes.oneOfType [ React.PropTypes.number React.PropTypes.string ] maxContainerHeight: React.PropTypes.oneOfType [ React.PropTypes.number React.PropTypes.string ] placeholder: React.PropTypes.oneOfType [ React.PropTypes.number React.PropTypes.string ] searchInterval: React.PropTypes.number getInitialState: -> loading: false selectedResultIndex: 0 value: @props.value or '' showResults: false componentWillReceiveProps: (nextProps) -> showResults = nextProps.results.length > 0 and nextProps.value.length > 0 selectedResultIndex = if showResults and nextProps.value isnt @props.value then 0 else @state.selectedResultIndex @setState showResults: showResults selectedResultIndex: selectedResultIndex value: nextProps.value getDefaultProps: -> className: '' results: [] minLength: 1 searchInterval: 300 maxContainerHeight: 175 handleClick: (e) -> e.stopPropagation() componentWillMount: -> document.addEventListener('keydown', @handleKeyDown) window.addEventListener('hashchange', @close) document.addEventListener('click', @close) componentWillUnmount: -> document.removeEventListener('keydown', @handleKeyDown) window.removeEventListener('hashchange', @close) document.removeEventListener('click', @close) close: (e) -> if @isMounted() e?.stopPropagation() @setState showResults: false , => @props.onClose?() calculateMaxVisibleResults: -> {resultConfig, results, maxContainerHeight} = @props # Figure out how many results can fit in the container resultsHeight = resultConfig.height * results.length containerHeight = if resultsHeight > maxContainerHeight then maxContainerHeight else resultsHeight return containerHeight / resultConfig.height onResultSelect: (result) -> @props.resultConfig.onResultSelect(result) @close() handleKeyDown: (e) -> {results} = @props {selectedResultIndex, showResults, loading} = @state numResults = @calculateMaxVisibleResults() #! temporary loading = false # Only make these keystrokes work when the menu is open if loading or not showResults then return switch e.keyCode when DOWN_ARROW e.preventDefault() e.stopPropagation() @traverseResults(1) when UP_ARROW e.preventDefault() e.stopPropagation() @traverseResults(-1) when PAGE_DOWN e.preventDefault() e.stopPropagation() @traverseResults(numResults) when PAGE_UP e.preventDefault() e.stopPropagation() @traverseResults(-(numResults)) when ENTER e.preventDefault() e.stopPropagation() result = results[selectedResultIndex] @onResultSelect(result) handleChange: -> @props.onChange() @executeSearch(@refs.input.getValue()) executeSearch: (simpleTerm) -> {onSearch, minLength, searchInterval} = @props {length} = simpleTerm clearInterval(@searchTimer) if @searchTimer? if length is 0 then @setState { value: '' } else if length < minLength then return else unless @state.loading then @setState { loading: true } @searchTimer = setTimeout => onSearch?(simpleTerm) , searchInterval traverseResults: (change) -> unless @refs.results? then return {results, resultConfig} = @props {selectedResultIndex} = @state newResult = selectedResultIndex + change resultsTop = @refs.results.getBoundingClientRect().top {scrollTop} = @refs.results # Adjust the change to make sure it will work if results.length and newResult >= results.length then newResult = results.length - 1 if newResult <= 0 then newResult = 0 itemTop = @refs.resultsList.children[newResult].getBoundingClientRect().top if itemTop + resultConfig.height > resultsTop + resultConfig.height or itemTop < resultsTop @refs.results.scrollTop = newResult * resultConfig.height @setState selectedResultIndex: newResult getValue: -> @refs.input.getValue() render: -> {results, className, headerComponent, placeholder, showNoResults, resultConfig} = @props {selectedResultIndex, loading, value, showResults} = @state resultItems = [] noResults = not results.length and showNoResults #! temporary loading = false for result, index in results resultItems.push resultConfig.component { key: index selectedResultIndex: selectedResultIndex index: index height: resultConfig.height maxVisibleRows: @calculateMaxVisibleResults() result: result onResultSelect: resultConfig.onResultSelect } div { className: "input-type-ahead #{className}" onClick: @handleClick }, [ TextInput { key: 'input' ref: 'input' autoComplete: false value: value placeholder: placeholder loading: loading onChange: @handleChange className: if resultItems.length and not loading then 'no-radius' } headerComponent { key: 'header' } if resultItems.length and not loading and headerComponent? div { key: 'blah' className: 'blah' } div { key: 'results' ref: 'results' className: 'type-ahead-results' style: height: @calculateMaxVisibleResults() * resultConfig.height + 2 }, [ if resultItems.length ul { key: 'results-list' ref: 'resultsList' }, resultItems else if noResults div { className: 'no-results' key: 'no-results' }, 'No Results' ] if not loading and showResults ] module.exports = InputTypeAhead