ldx-widgets
Version:
widgets
273 lines (210 loc) • 8.12 kB
text/coffeescript
React = require 'react'
_ = require 'lodash'
MultiSelectOption = React.createFactory(require './multi_select_option')
SearchInput = React.createFactory(require './search_input')
FormValidation = require '../mixins/form_validation'
{div, button, ul, li} = React.DOM
###
Multi Select Props
@props.options - REQUIRED - Array
An array of options for the user to click - can be a flat array (where each entry is both the value and label)
or an array of objects, where the value is the `valueField` and the label is the `labelField` (see below)
@props.values - OPTIONAL - Array
A flat array of values that the control initializes to. If the optons arrya is objects, each entry should be the
`valueField` value. Will default to an empty array
@props.labelField - OPTIONAL - String
The attribute name from each option object that is the 'value`
@props.valueField - OPTIONAL - String
The attribute name from each option object that is the user faciing label
@props.onChange - OPTIONAL - function
Function/method to fire when the data changes
@props.filter - OPTIONAL - Boolean - default: 'auto'
Show text filter? auto will show it whenever the options list is longer than 4
@props.allowDefault - OPTIONAL - Boolean - default: false
Allow one entry to be set as the default
@props.valueOfDefault - OPTIONAL - used in conjunction w/ allowDefault
The value of the option that is currently set as the default
@props.searchPlaceholder - OPTIONAL
placeholder text for the add button
@props.editPlaceholder - OPTIONAL
placeholder text for the filter field
@props.tabIndex - OPTIONAL
tab order of the edi button
@props.returnFullObjects - OPTIONAL - Boolean - default: false
whether or not the getFormData method should return a collection of selected objects or a flat array
@props.onRemove - OPTIONAL - function
function call when an item is removed, will pass the removed item
###
MultiSelect = React.createClass
displayName: 'MultiSelect'
# Note - See form_validation for required props when using with this mixin
mixins: [FormValidation]
getDefaultProps: ->
{
filter: 'auto'
allowDefault: false
editPlaceholder: 'Edit Items'
searchPlaceholder: 'Filter Items'
tabIndex: -1
returnFullObjects: no
onRemove: ->
}
render: ->
{selected, notSelected, isActive, theDefault, filterTerm} = @state
{editPlaceholder, searchPlaceholder, allowDefault, filter, tabIndex, disabled} = @props
selectedOptions = []
otherOptions = []
@getErrors()
selectedOptions.push(MultiSelectOption {
key: option.value
option: option
allowDefault: allowDefault
isActive: isActive
setValues: @setValues
disabled: disabled
setDefault: @setDefault
tabIndex: tabIndex
isTheDefault: option is theDefault
onRemove: @props.onRemove
}) for option in selected
if selectedOptions.length is 0
selectedOptions.push(li {
key: 'none'
className: 'multiselect-none'
}, ['None'])
if isActive
otherOptions.push(MultiSelectOption {
key: option.value
option: option
allowDefault: allowDefault
isActive: isActive
tabIndex: tabIndex
setValues: @setValues
}) for option in notSelected when option.isVisible
buttonText = "+ #{editPlaceholder}"
div {
className: 'multiselect field-wrap' + @invalidClass
onClick: @toggleOn
}, [
ul {
key: 'selectedList'
ref: 'selectedList'
className: 'multiselect-list-in'
}, selectedOptions
SearchInput {
ref: 'filterField'
key: 'filter-input'
placeholder: searchPlaceholder
handleChange: @handlefilter
wrapClass: 'multi-select-filter'
width: '100%'
focusOnMount: yes
disabled: disabled
} if @filterShouldBeShown()
button {
key: 'addButton'
className: 'multiselect-edit'
tabIndex: tabIndex
onFocus: @toggleOn
}, buttonText unless isActive or disabled
ul {
key: 'notSelectedList'
ref: 'notSelectedList'
className: 'multiselect-list-out'
}, otherOptions if isActive
div({
className: 'field-errors-show'
key: 'textInputErrorsShow'
}, [
div {
className: 'field-errors'
key: 'textInputErrors'
},
ul {
className: 'field-errors-list'
}, @validationErrors
]) if @validationErrors.length
]
componentDidMount: -> document.addEventListener('click', @blur)
componentWillUnmount: -> document.removeEventListener('click', @blur)
getInitialState: ->
{options, values, labelField, valueField, valueOfDefault, allowDefault} = @props
@allOptions = []
for option in options
newOption =
isSelected: false
isVisible: true
if typeof option is 'object'
unless (valueField? and labelField?) or (option.value? and options.label?)
return console?.error 'MultiSelect requires labelField and valueField props when the options array is made up of objects'
newOption.value = option[valueField]
newOption.label = option[labelField]
else
newOption.label = option.toString()
newOption.value = option
newOption.isSelected = true if values? and values.indexOf(newOption.value) isnt -1
@allOptions.push newOption
theDefault = _.findWhere(@allOptions, {value: valueOfDefault})
selected = _.where(@allOptions, {isSelected: true})
unless theDefault?
if selected.length? then theDefault = selected[0]
{
selected: selected or []
notSelected: _.where(@allOptions, {isSelected: false}) or []
isActive: false
theDefault: theDefault
filterTerm: ''
}
getFormData: ->
selectedValues = _.pluck(_.where(@allOptions, {isSelected: true}), 'value')
unless @props.returnFullObjects then return selectedValues
(option for option in @props.options when selectedValues.indexOf(option[@props.valueField]) isnt -1)
toggleOn: (e) ->
if @props.disabled then return
e.nativeEvent.stopImmediatePropagation?()
if not @state.isActive
@setState
isActive: true
, =>
@focusFirstOption()
focusFirstOption: ->
# Find the first option in the list and focus it
# This is done because of keyboard navigation with TAB key
# When tabbing around, the dom re-renders and resets the tab order once the options are visible
firstUnselected = @refs.notSelectedList.getElementsByTagName('button')[0]
firstSelected = @refs.selectedList.getElementsByTagName('button')[0]
if firstUnselected? then firstUnselected.focus()
# If there's no unselected first item, focus the first selected item
else firstSelected.focus()
blur: ->
@setState
isActive: false
setDefault: (newDefault) ->
@setState
theDefault: newDefault
setValues: () ->
{theDefault} = @state
selected = _.where(@allOptions, {isSelected: true})
if selected.indexOf(theDefault) is -1
if selected.length then theDefault = selected[0]
@setState
selected: _.where(@allOptions, {isSelected: true})
notSelected: _.where(@allOptions, {isSelected: false})
theDefault: theDefault
, ->
if @filterShouldBeShown() then @refs.filterField.focus()
else @focusFirstOption()
@props.onChange?()
handlefilter: (term) ->
notSelected = _.where(@allOptions, {isSelected: false})
if term is ''
item.isVisible = true for item in @allOptions
else
for item in notSelected
if item.label.toLowerCase().search(term.toLowerCase()) isnt -1 then item.isVisible = true
else item.isVisible = false
@setState
notSelected: notSelected
filterShouldBeShown: ->
@state.isActive and (@props.filter is true or (@props.filter is 'auto' and @allOptions.length > 4))
module.exports = MultiSelect