ldx-widgets
Version:
widgets
278 lines (221 loc) • 8.01 kB
text/coffeescript
React = require 'react'
createClass = require 'create-react-class'
PropTypes = require 'prop-types'
find = require 'lodash/find'
{ DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, ENTER, SPACE, ESCAPE, TAB } = require '../constants/keyboard'
{alphaNumericKeyCode} = require '../utils'
InputMixin = require '../mixins/input_mixin'
CircleXButton = React.createFactory(require './circle_x_button')
SelectInputCustomOptions = React.createFactory(require './select_input_custom_options')
Pvr = React.createFactory(require './pvr')
{div, select, option} = require 'react-dom-factories'
OPTION_PADDING = 10
###&
@general
Filterable select menu. This component lives on the overlay layer, and requires integrated context methods closeOverlay and openOverlay within the application.
@props.options - [Array] - Required
Full list of options to display on component
@props.value - [String|Object] - Optional
The value that corresponds to the option object with the matching value
@props.selectText - [String] - Optional
Text displayed as the default value
@props.onChange - [Function] - Required
Function fired when a change is made to the selection
@props.tabIndex - [Number] - Optional
Tab order index
@props.disabled - [Boolean] - Optional
Disabled state of the component
@props.isFilter - [Boolean] - Optional
Show/hide the filter typeahead input. Default is no
@props.returnFullObjects - [Boolean] - Optional
Determine whether `getValue` returns the full option object, or just the default value string
@props.placeholder - [String] - Optional
Placeholder text for the filter input
@props.valueField - [String] - Optional
The name of the key used to reference the value on the option object
@props.labelField - [String] - Optional
The name of the key used to reference the label on the option object
@props.width - [Number] - Optional
The width of the menu popover
@props.height - [Number] - Optional
The height of the menu popover
@props.optionHeight - [Number] - Optional
The fixed height of each menu option
&###
SelectInputCustom = createClass
displayName: 'SelectInputCustom'
mixins: [InputMixin]
contextTypes:
openOverlay: PropTypes.func
closeOverlay: PropTypes.func
clearValidationError: PropTypes.func
addValidationError: PropTypes.func
getValidationStatus: PropTypes.func
toggleValidationError: PropTypes.func
propTypes:
options: PropTypes.array.isRequired
selectText: PropTypes.string
onChange: PropTypes.func.isRequired
tabIndex: PropTypes.number
disabled: PropTypes.bool
isFilter: PropTypes.bool
returnFullObjects: PropTypes.bool
placeholder: PropTypes.string
valueField: PropTypes.string
labelField: PropTypes.string
width: PropTypes.number
height: PropTypes.number
optionHeight: PropTypes.number
value: PropTypes.oneOfType [
PropTypes.string
PropTypes.object
]
nibColor: PropTypes.string
getDefaultProps: ->
options: []
selectText: 'Select from list...'
placeholder: 'Filter options'
valueField: 'value'
labelField: 'title'
width: 250
height: 400
isFilter: no
nibColor: 'white'
optionHeight: 20
returnFullObjects: no
componentWillMount: ->
{value} = @props
@value = value
@overlayId = null
componentWillUnmount: ->
clearInterval @timer
render: ->
{options, id, selectText, valueField, labelField, tabIndex, disabled, isFilter, value, returnFullObjects} = @props
{valueHasChanged} = @state
{error, forceShowAllErrors} = @context.getValidationStatus(@inputId)
isValid = not error?
outerClass = 'field-wrap filter-select'
outerClass += " #{wrapperClass}" if wrapperClass?
outerClass += ' invalid shrink' if not isValid and (valueHasChanged or forceShowAllErrors)
outerClass += ' x-room' if value and not disabled
optionItems = [
option {
key: "none"
value: ""
}, selectText
]
options.forEach (o, i) =>
optionItems.push option {
key: i
value: o[valueField]
}, o[labelField]
# When returnFullObjects is on, then overwrite the value (which will be ab object),
# with it's 'valueField' attribute
if returnFullObjects and value? then value = value[valueField] or ''
@value = value
div {
className: outerClass
}, [
select {
key: 'select'
ref: 'input'
tabIndex: tabIndex
id: id
disabled: disabled
value: value
onChange: -> # So React doesn't complain about value with no onChange handler
onMouseDown: @handleNativeEvent
onKeyDown: @handleNativeEvent
}, optionItems
CircleXButton {
key: 'clear'
tabIndex: -1
ref: (clearBtn) => @clearBtn = clearBtn
onClick: @clearValue
} if value and not disabled
div {
className: 'field-errors-show'
key: 'textInputErrorsShow'
ref: 'errorAnchor'
onMouseOver: @handleErrorMouseOver
onMouseOut: @handleErrorMouseOut
}
]
handleNativeEvent: (e) ->
{keyCode, currentTarget} = e
{options} = @props
openKeys = [DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, ENTER, SPACE]
closeKeys = [ESCAPE, TAB]
# Handle the keyboard
if keyCode?
isOpenKey = openKeys.indexOf(keyCode) > -1
isCloseKey = closeKeys.indexOf(keyCode) > -1
alphaNum = alphaNumericKeyCode(keyCode)
if isOpenKey or alphaNum
e.preventDefault()
e.stopPropagation()
# For alphanumeric input, start filtering the element
if alphaNum then filter = e.key
@openOptionsList(currentTarget, filter)
else if isCloseKey
e.preventDefault() if keyCode isnt TAB
if @overlayId?
@context.closeOverlay {@overlayId}
@overlayId = null
return
# Handle mouse clicks
e.preventDefault()
@openOptionsList(currentTarget)
# Attempt to reload the overlay if the list load after the user opens it
if options.length is 0
@timer = setInterval =>
if @props.options.length > 0
@context.closeOverlay {@overlayId}
@openOptionsList(currentTarget)
clearInterval @timer
, 100
openOptionsList: (anchor, filter) ->
{value, options, labelField, valueField, height, width, placeholder, isFilter, nibColor, optionHeight, returnFullObjects} = @props
if ((options.length + 1) * optionHeight) < height
height = options.length * (optionHeight + OPTION_PADDING)
if returnFullObjects and value? then value = value[valueField] or ''
# Construct the options component
@overlayId = @context.openOverlay
component: Pvr
noBackdrop: true
id: "popover"
props:
direction: 'below'
width: width
height: if height is 0 then 58 else height
anchor: anchor
nibColor: nibColor
animateIn: no
element: SelectInputCustomOptions {
key: 'fso'
options: @props.options
onChange: @handleValueChange
labelField: labelField
valueField: valueField
placeholder: placeholder
value: value
SelectEl: @
isFilter: isFilter
searchWidth: width - 10
optionHeight: optionHeight
OPTION_PADDING: OPTION_PADDING
filter: filter
}
# Note: using slightly non-standard names for the next 2 methods in order to not conflict with the mixin
handleValueChange: (value, cb) ->
{onChange, validation, jsonPath} = @props
{valueHasChanged} = @state
@setState {valueHasChanged: yes}, =>
@validate(validation, value)
@value = value
onChange?(@getValue(), jsonPath)
cb?()
@focus()
@fireDelayedAction()
clearValue: -> @handleValueChange('')
module.exports = SelectInputCustom