UNPKG

ldx-widgets

Version:

widgets

278 lines (221 loc) 8.01 kB
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