ldx-widgets
Version:
widgets
305 lines (248 loc) • 9.3 kB
text/coffeescript
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