UNPKG

ldx-widgets

Version:

widgets

392 lines (308 loc) 11.7 kB
React = require 'react' PropTypes = require 'prop-types' createClass = require 'create-react-class' ReactDOM = require 'react-dom' _ = require 'lodash' MultiSelectOption = React.createFactory(require './multi_select_option') SearchInput = React.createFactory(require './search_input') {makeGuid} = require '../utils' {ESCAPE} = require '../constants/keyboard' {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 options array 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 facing 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 = createClass displayName: 'MultiSelect' contextTypes: clearValidationError: PropTypes.func addValidationError: PropTypes.func getValidationStatus: PropTypes.func toggleValidationError: PropTypes.func getDefaultProps: -> { filter: 'auto' allowDefault: false editPlaceholder: 'Edit Items' searchPlaceholder: 'Filter Items' tabIndex: -1 returnFullObjects: no isInPopover: no onRemove: -> } render: -> {selected, notSelected, isActive, theDefault, filterTerm, valueHasChanged} = @state {editPlaceholder, searchPlaceholder, allowDefault, filter, tabIndex, disabled} = @props {getValidationStatus} = @context {error, forceShowAllErrors} = getValidationStatus(@inputId) isValid = not error? outerClass = 'multiselect field-wrap' outerClass += ' invalid' if not isValid and (valueHasChanged or forceShowAllErrors) selectedOptions = [] otherOptions = [] selectedOptions.push(MultiSelectOption { key: option.value ref: option.value option: option allowDefault: allowDefault isActive: isActive setValues: @setValues disabled: disabled setDefault: @setDefault tabIndex: tabIndex isTheDefault: option is theDefault onRemove: @props.onRemove onBlur: if i is 0 then @btnBlur else null }) for option, i in selected if selectedOptions.length is 0 selectedOptions.push(li { key: 'none' className: 'multiselect-none' }, ['None']) visible = _.filter notSelected, {isVisible: yes} if isActive otherOptions.push(MultiSelectOption { key: option.value ref: option.value option: option allowDefault: allowDefault isActive: isActive tabIndex: tabIndex setValues: @setValues onBlur: if i is visible.length - 1 then @btnBlur else null }) for option, i in visible buttonText = "+ #{editPlaceholder}" div { className: outerClass 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 tabIndex: tabIndex term: filterTerm } if @filterShouldBeShown() button { key: 'addButton' ref: 'addButton' className: 'multiselect-edit' tabIndex: tabIndex }, buttonText unless isActive or disabled ul { key: 'notSelectedList' ref: 'notSelectedList' className: 'multiselect-list-out' }, otherOptions if isActive div { key: 'errors' className: 'field-errors-show' ref: 'errorAnchor' onMouseOver: @handleErrorMouseOver onMouseOut: @handleErrorMouseOut } ] componentWillMount: -> # create a unique id for this input # used by the validation framework to group validation errors @inputId = makeGuid() componentDidMount: -> document.addEventListener('keydown', @handleKeyDown) document.addEventListener('click', @blur, no) {selected} = @state {validation} = @props @validate validation, selected componentWillReceiveProps: (nextProps) -> {validation} = nextProps {selected} = @state validationChanged = ( if typeof validation is 'function' then no else not _.isEqual(validation?.messages, @props.validation?.messages) ) # Run validaiton if the validation changes if validationChanged then @validate(validation, selected) if nextProps.options isnt @props.options stateOptions = @setOptions(nextProps) @setState selected: stateOptions.selected or [] notSelected: _.filter(@allOptions, {isSelected: false}) or [] theDefault: stateOptions.theDefault componentWillUnmount: -> document.removeEventListener('keydown', @handleKeyDown) document.removeEventListener('click', @blur, no) @context.clearValidationError groupId: @inputId isInPopover: @props.isInPopover getInitialState: -> stateOptions = @setOptions(@props) { selected: stateOptions.selected or [] notSelected: _.filter(@allOptions, {isSelected: false}) or [] isActive: false theDefault: stateOptions.theDefault filterTerm: '' valueHasChanged: no } setOptions: (params) -> {options, values, labelField, valueField, valueOfDefault, allowDefault} = params @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 = _.find(@allOptions, {value: valueOfDefault}) selected = _.filter(@allOptions, {isSelected: true}) unless theDefault? if selected.length? then theDefault = selected[0] data = selected: selected theDefault: theDefault return data getValue: -> @getFormData() getFormData: -> selectedValues = _.map(_.filter(@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() unless @filterShouldBeShown() 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() # Run by multi select option after a 15ms delay # Will check if any element is focused before calling @blur btnBlur: -> for refname, ref of @refs continue if refname is 'addButton' return if ref.refs?.toggleBtn is document.activeElement return if ref is document.activeElement do @blur blur: (cb) -> @setState isActive: false , -> cb?() setDefault: (newDefault) -> @setState theDefault: newDefault setValues: (option) -> {theDefault} = @state {validation} = @props option.isSelected = not option.isSelected selected = _.filter(@allOptions, {isSelected: true}) if selected.indexOf(theDefault) is -1 if selected.length then theDefault = selected[0] @setState selected: _.filter(@allOptions, {isSelected: true}) notSelected: _.filter(@allOptions, {isSelected: false}) theDefault: theDefault valueHasChanged: yes , -> if @filterShouldBeShown() then @refs.filterField.focus() else @focusFirstOption() @props.onChange?() @validate validation, @state.selected handlefilter: (term) -> notSelected = _.filter(@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 filterTerm: term notSelected: notSelected filterShouldBeShown: -> @state.isActive and (@props.filter is true or (@props.filter is 'auto' and @allOptions.length > 4)) validate: (validation, value) -> return if validation is off # Run validation and show any auto show messages if typeof validation is 'function' then validationError = validation(value) # validation can also be passed as a static array else validationError = validation {isInPopover, tabId} = @props {addValidationError, clearValidationError} = @context if validationError? addValidationError anchor: @refs.errorAnchor error: validationError groupId: @inputId isInPopover: isInPopover tabId: tabId else clearValidationError groupId: @inputId isInPopover: isInPopover handleErrorMouseOver: -> {isInPopover} = @props {toggleValidationError} = @context toggleValidationError @inputId, on, isInPopover handleErrorMouseOut: -> {isInPopover} = @props {toggleValidationError} = @context toggleValidationError @inputId, off, isInPopover handleKeyDown: (e) -> if e.keyCode is ESCAPE if @state.isActive @blur => @refs.addButton.focus() module.exports = MultiSelect