UNPKG

ldx-widgets

Version:

widgets

305 lines (248 loc) 9.3 kB
React = require 'react' PropTypes = require 'prop-types' createClass = require 'create-react-class' TextInput = React.createFactory(require './text_input_2') {UP_ARROW, DOWN_ARROW, ENTER, PAGE_UP, PAGE_DOWN} = require('../constants/keyboard') {div, ul, button} = require 'react-dom-factories' ###& @props.results - REQUIRED - [Array] Array of items that are printed in the results list @props.value - REQUIRED - [String | Number] Value of input field @props.searchInterval - OPTIONAL - [Number] Number in milliseconds before firing onSearch, after user finishes typing @props.onChange - REQUIRED - [Function] Function that is fired when the value of the input changes @props.onSearch - REQUIRED - [Function] Function that is fired when the timer triggers after the set searchInterval @props.className - OPTIONAL - [String] CSS class applied to form wrapper @props.placeholder - OPTIONAL - [String | Number] Default placeholder value of text input @props.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. This passes the result object and close method callback @props.maxContainerHeight - OPTIONAL - [Number | String] The maximum height of the typeahead results container. The resultConfig.height property should be evenly divisible into maxContainerHeight @props.validation - OPTIONAL - [Function] a method that takes the value and returns an arry of validation objects always return an empty array for a valid value see the validation store for more documentation on validation objects @props.zIndex - OPTIONAL - [Number] Default style is 10. Optionally pass higher number to cover typeaheads on the same page. @props.disabled - OPTIONAL - [Boolean] @props.clearInput - OPTIONAL - [Function] a method that will clear out the input if passed display the clear btn in the input field &### InputTypeAhead = createClass displayName: 'InputTypeAhead' propTypes: resultConfig: PropTypes.shape { component: PropTypes.func.isRequired height: PropTypes.number.isRequired right: PropTypes.number left: PropTypes.number onResultSelect: PropTypes.func.isRequired } onSearch: PropTypes.func.isRequired onChange: PropTypes.func.isRequired className: PropTypes.string focusOnMount: PropTypes.bool results: PropTypes.array.isRequired minLength: PropTypes.number value: PropTypes.oneOfType([ PropTypes.number PropTypes.string ]).isRequired maxContainerHeight: PropTypes.oneOfType [ PropTypes.number PropTypes.string ] placeholder: PropTypes.oneOfType [ PropTypes.number PropTypes.string ] searchInterval: PropTypes.number zIndex: PropTypes.number disabled: PropTypes.bool 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 focusOnMount: false disabled: no 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) -> 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: (value, jsonPath = null) -> if jsonPath @props.onChange(value, jsonPath) else @props.onChange(value) @executeSearch(value) executeSearch: (term) -> {onSearch, minLength, searchInterval} = @props {length} = term 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?(term) , 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 render: -> {results, className, headerComponent, placeholder, showNoResults, resultConfig, focusOnMount, onKeyDown, validation, isInPopover, zIndex, disabled, clearInput, maxLength, jsonPath, tabIndex} = @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 style: zIndex: zIndex if zIndex? }, [ TextInput { key: 'input' ref: 'input' autoComplete: false value: value focusOnMount: focusOnMount placeholder: placeholder loading: loading onChange: @handleChange onKeyDown: onKeyDown className: if resultItems.length and not loading then 'no-radius' validation: validation isInPopover: isInPopover disabled: disabled maxLength: maxLength jsonPath: jsonPath tabIndex: tabIndex } button { className: 'search-clear' title: 'Clear Input' key: 'inputClearBtn' onClick: clearInput tabIndex: -1 }, [] if clearInput? headerComponent { key: 'header' } if resultItems.length and not loading and headerComponent? div { key: 'results' ref: 'results' className: 'type-ahead-results' style: height: @calculateMaxVisibleResults() * resultConfig.height + 2 right: "#{resultConfig.right}px" if resultConfig.right? left: "#{resultConfig.left}px" if resultConfig.left? }, [ 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