<commit_before>import React, { Component, PropTypes } from 'react'

import classNames from 'classnames'
import isEqual from 'lodash/isEqual'
import isFunction from 'lodash/isFunction'
import isNil from 'lodash/isNil'
import uniqueId from 'lodash/uniqueId'

import makeString from '../utils/makeString'

import SelectDropdown from './SelectDropdown'
import SelectError from './SelectError'
import SelectSelection from './SelectSelection'

// TODO: multiselect
// TODO: label
// TODO: optgroups
// TODO: make slim version
// TODO: optimize isFunction calls
export class Select extends Component {
  static childContextTypes = {
    cssClassNamePrefix: PropTypes.string,
  }

  static propTypes = {
    /**
     * Whether to allow user to reset selected option
     */
    allowClear: PropTypes.bool,
    cssClassNamePrefix: PropTypes.string,
    /**
     * Whether to focus itself on mount
     */
    autoFocus: PropTypes.bool,
    closeOnClickOutside: PropTypes.bool, // Default: true
    defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    disabled: PropTypes.bool,
    /**
     * Provide error message to display or just boolean to highlight select container with error styles
     */
    error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
    layout: PropTypes.shape({
      /**
       * Container's width
       */
      width: PropTypes.string,
      /**
       * Defines whether SelectDropdown should be opened above or below the container.
       * default: 'below'
       */
      // TODO: define position automatically depends on SelectContainer position in the viewport
      dropdownVerticalPosition: PropTypes.oneOf(['above', 'below']),
    }),
    name: PropTypes.string,
    /**
     * Function to transform options' 'text' to display in the SelectDropdown if needed
     * @param {object} option
     * @returns React element
     */
    optionRenderer: PropTypes.func,
    /**
     * Array of option items
     */
    options: PropTypes.arrayOf(PropTypes.shape({
      id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
      isHidden: PropTypes.bool,
      text: PropTypes.string.isRequired,
    })),
    onSelect: PropTypes.func,
    placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
    search: PropTypes.shape({
      /**
       * Minimum results amount before showing search input
       */
      minimumResults: PropTypes.number,
      show: PropTypes.bool,
      status: PropTypes.string,
    }),
    /**
     * Search input change callback
     */
    onSearchTermChange: PropTypes.func,
    /**
     * Value can be set by providing option id
     */
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  }

  static defaultProps = {
    allowClear: false,
    closeOnClickOutside: true,
    cssClassNamePrefix: 'rs3',
    disabled: false,
    layout: { dropdownVerticalPosition: 'below', width: '245px' },
    name: uniqueId('reactSelect_'),
    options: null,
    search: { minimumResults: 20 },
  }

  static initialState = () => ({
    disabled: false,
    dropdownOpened: false,
    error: null,
    highlighted: null,
    options: [],
    searchTerm: '',
    value: null,
  })

  /**
   * Ref Interface methods
   */
  set options(options) {
    if (!Array.isArray(options)) {
      throw new Error('Invalid options were provided. Options must be an array.')
    }

    this.setState({
      options: this._setOptions(options, true),
      value: null,
    })
  }

  get value() {
    return this.state.value
  }

  get options() {
    return [].concat(this.state.options)
  }

  clear() {
    this._onClearSelection()
  }

  constructor(props) { // eslint-disable-line consistent-return
    super(props)

    const onArrowUp = this._setHighlightedOption.bind(null, -1)
    const onArrowDown = this._setHighlightedOption.bind(null, 1)

    this.KEY_FUNCTIONS = {
      ArrowUp: onArrowUp,
      38: onArrowUp,
      ArrowDown: onArrowDown,
      40: onArrowDown,
      Enter: this._selectHighlighted,
      13: this._selectHighlighted,
      ' ': this._selectHighlighted, // 'Space' key
      32: this._selectHighlighted, // 'Space' key
      Escape: this._closeDropdown,
      27: this._closeDropdown,
    }

    this.state = Select.initialState()
    const { disabled, error, options } = props

    /**
     * @type {{
     *  dropdownOpened: boolean,
     *  error: string|boolean,
     *  highlighted: {id, index},
     *  options: array,
     *  searchTerm: string,
     *  value: string,
     * }}
     */
    this.state = Object.assign(this.state, {
      disabled,
      error,
      options: this._setOptions(options),
      value: this._setValue(),
    })
  }

  getChildContext = () => ({
    cssClassNamePrefix: this.props.cssClassNamePrefix,
  })

  componentWillReceiveProps(newProps) {
    const { disabled, error, options, value } = newProps
    const isValueValid = this._isValidValue(value)

    if (isValueValid && typeof newProps.onSelect === 'undefined' && typeof this.props.onSelect === 'undefined') {
      /* eslint-disable */
      console.warn(`Warning: You're setting value for Select component throught props
                but not passing onSelect callback which can lead to unforeseen consequences(bugs).
                Please consider using onSelect callback or defaultValue instead of value`)
      /* eslint-enable */
    }

    if (disabled) {
      this._closeDropdown()
    }

    this.setState(state => {
      let newValue = state.value

      if (isValueValid) {
        newValue = makeString(value)
      }

      return {
        disabled,
        options: this._setOptions(options),
        value: newValue,
        error: typeof error !== 'undefined' ? error : state.error
      }
    })
  }

  shouldComponentUpdate = ({ error, disabled, value, children }, nextState) => (
    (error !== this.props.error && error !== this.state.error)
    || disabled !== this.props.disabled
    || value !== this.state.value
    || !isEqual(children, this.props.children)
    || !isEqual(nextState, this.state)
  )

  componentDidMount = () => {
    const { autoFocus, closeOnClickOutside } = this.props

    // Autofocus on mount if enabled
    if (autoFocus) this._focusContainer()

    // Add listener for click outside if enabled
    if (closeOnClickOutside) document.addEventListener('click', this._handleClickOutside, true)
  }

  componentWillUnmount() {
    document.removeEventListener('click', this._handleClickOutside, true);
  }

  /**
   * Close SelectDropdown on click outside
   */
  _handleClickOutside = (e) => {
    if (this.state.dropdownOpened && !this.selectContainer.contains(e.target)) {
      this._closeDropdown()
    }
  }

  _isValidValue = value => {
    const { options } = this.state
    let isValid = false

    if (typeof value === 'undefined') {
      isValid = false
    } else if (value === null) {
      isValid = true
    } else if (options && options.length) {
      isValid = options.some(({ id }) => id === String(value))
    }

    return isValid
  }

  _openDropdown = () => {
    this.setState({ dropdownOpened: true })
  }

  _closeDropdown = () => {
    this.setState({
      dropdownOpened: false,
      highlighted: null,
    })

    this._focusContainer()
  }

  _focusContainer = () => {
    const x = window.scrollX
    const y = window.scrollY

    window.scrollTo(x, y)
    if (this.selectContainer) {
      this.selectContainer.focus()
    }
  }

  _setValue = () => {
    const { value, defaultValue } = this.props

    if (value === null) {
      return null
    }

    return makeString(value || defaultValue)
  }

  _makeOption = (id, text) => {
    if (!((typeof id === 'string' || typeof id === 'number') && (typeof text === 'string' || typeof text === 'number'))) {
      throw new Error('Options array is not formatted properly, option object must have "id" and "text"')
    }

    return { id: String(id), text }
  }

  _setOptions = (options, isNotFromProps) => {
    if (!isNotFromProps) {
      if (options === this.initialOptions) {
        return this.state.options
      }

      this.initialOptions = options
    }

    let stateOptions = this.state.options || []

    if (Array.isArray(options) && options.length) {
      stateOptions = options.map(({ id, text }) => this._makeOption(id, text))
    }

    return stateOptions
  }

  _getOptionById = value => {
    const { options } = this.state

    if (options && options.length) {
      return options.find(({ id }) => id === value) // eslint-disable-line eqeqeq
    }

    return null
  }

  _onContainerClick = () => {
    if (this.state.disabled) {
      return
    }

    if (this.state.dropdownOpened) {
      this._closeDropdown()
    } else {
      this._openDropdown()
    }
  }

  /**
   * Handle keyboard controls
   * @param {object} event
   */
  _onContainerKeyDown = event => {
    if (this.props.disabled) return

    const key = event.key || event.keyCode
    if (!this.KEY_FUNCTIONS[key]) return

    event.preventDefault()
    // Handle key click
    this.KEY_FUNCTIONS[key]()
  }

  _onClearSelection = () => {
    const { disabled, value } = this.state

    // Dont clear when disabled && dont fire extra event when value is already cleared
    if (!disabled && value) {
      this._onSelect(null)
    }
  }

  /**
   * Setting selected value
   * @param {object} option - option object from data array
   */
  _onSelect = option => {
    const { name, onSelect } = this.props
    // Setup structure of selection event
    const value = option ? option.id : null
    const selectionEvent = {
      type: 'select',
      isClear: option === null,  // indicates that value being cleared by onClearSelection
      target: {
        name,
        option,
        value,
      }
    }

    this.setState({ value, searchTerm: '' }, () => {
      if (isFunction(onSelect)) {
        onSelect(selectionEvent)
      }
    })

    this._closeDropdown()
    this._focusContainer()
  }

  /**
   * Handle option selection via user click
   * @param {number} id - options id
   */
  _onSelectOption = id => {
    // Get selected option and pass it into onSelect method for further processing
    const selectedOption = this._getOptionById(id)

    this._onSelect(selectedOption)
  }

  static makeHighlightedObject = (index, options) => {
    if (!options.length) {
      return null
    }

    return ({ id: options[index].id, index })
  }

  _hasHighlighted = () => {
    const { highlighted } = this.state

    return !!highlighted && typeof highlighted.index !== 'undefined'
  }
  /**
   * Set next highlighted option via 'ArrowUp' or 'ArrowDown' key
   * @param {number} direction (can be -1 or 1)
   */
  _setHighlightedOption = direction => {
    this.setState(({ disabled, highlighted, dropdownOpened }) => {
      const options = this._getOptionsList()

      // do nothing if disabled or there are no options
      if (disabled || !options || !options.length) return

      const optionsLength = options.length - 1
      const hasHighlighted = this._hasHighlighted()
      const nextHighlighted = (hasHighlighted) ? highlighted.index + direction : 0
      let highlightIndex

      // TODO: scroll SelectDropdown block to show highlighted item when overflows
      // If dropdown not opened or there is no highlighted item yet
      if (!dropdownOpened || !(hasHighlighted)
        // highlight first option after click 'ArrowDown' on the last one
        || nextHighlighted > optionsLength) {
        highlightIndex = 0
      } else if (nextHighlighted < 0) {
        // Highlight last option after click 'ArrowUp' on the first one
        highlightIndex = optionsLength
      } else {
        // Highlight next option
        highlightIndex = nextHighlighted
      }

      return ({ // eslint-disable-line consistent-return
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(highlightIndex, options)
      })
    })
  }

  /**
   * Select current highlighted option
   */
    // @fixme: selects invalid option when options list filtered by searchTerm
  _selectHighlighted = () => {
    const { options, highlighted, dropdownOpened } = this.state

    // If dropdown not opened or there is no highlighted item yet
    if (!dropdownOpened || !this._hasHighlighted()) {
      // Open dropdown and hightlight first item
      this.setState({
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(0, this._getOptionsList()),
      })
    } else {
      // Select highlighted item
      this._onSelect(options.find(({ id }) => id === highlighted.id))
    }
  }

  _getSelectContainerClassName = () => {
    const {
      className,
      cssClassNamePrefix,
      disabled,
      error,
      layout: { dropdownVerticalPosition },
    } = this.props
    const { dropdownOpened, value } = this.state

    return classNames(`${cssClassNamePrefix}__container ${className || ''}`, {
      [`${cssClassNamePrefix}--above`]: dropdownVerticalPosition === 'above',
      [`${cssClassNamePrefix}--below`]: dropdownVerticalPosition !== 'above',
      [`${cssClassNamePrefix}--disabled`]: disabled,
      [`${cssClassNamePrefix}--error`]: !!error || !!this.state.error,
      [`${cssClassNamePrefix}--open`]: dropdownOpened,
      [`${cssClassNamePrefix}--selected`]: !isNil(value),
    })
  }

  _isClearable = () => {
    const { allowClear } = this.props
    const { value, disabled } = this.state

    return (allowClear && !disabled && !isNil(value))
  }

  _getOptionsList = () => {
    const { options, searchTerm } = this.state
    let optionsList = options || []

    if (searchTerm && optionsList.length) {
      optionsList = options.filter(({ text }) => new RegExp(searchTerm, 'gi').test(text))
    }

    return optionsList
  }

  _onSearchTermChange = e => {
    const { target: { value: term } } = e
    const { onSearchTermChange } = this.props

    // If size of text is increases
    // const isTextIncreasing = term && (!stateSearchTerm || term.length > stateSearchTerm.length)
    const searchTerm = term || ''

    // if callback were passed in props
    if (isFunction(onSearchTermChange)) {
      onSearchTermChange(e)
    }

    this.setState({ searchTerm })
  }

  _isShowSearch = () => {
    const { options } = this.state
    const { search: { show, minimumResults = 20 } } = this.props

    return show || (!!options.length && (minimumResults <= options.length))
  }

  _setContainerRef = (node) => {
    this.selectContainer = node
  }

  render() {
    const { cssClassNamePrefix, optionRenderer, layout: { width }, placeholder, search: { status }, name } = this.props
    const { disabled, dropdownOpened, error, highlighted, searchTerm, value } = this.state
    const selectedOption = this._getOptionById(value)

    return (
      <span ref={ this._setContainerRef }
            className={ this._getSelectContainerClassName() }
            style={{ width }}
            disabled={ disabled }
            tabIndex='0'
            role='combobox'
            onClick={ this._onContainerClick }
            onKeyDown={ this._onContainerKeyDown }>
      <select name={ name } value={ value } className={`${cssClassNamePrefix}__select-node`}>
        { !isNil(value) && <option value={value}/> }
      </select>
      <SelectSelection clearable={ this._isClearable() }
                       onClearSelection={ this._onClearSelection }
                       placeholder={ placeholder }
                       formatter={ optionRenderer }
                       selection={ selectedOption }
                       onKeyDown={ this._onContainerKeyDown }/>
        { dropdownOpened && (
          <SelectDropdown options={ this._getOptionsList() }
                          status={ status }
                          showSearch={ this._isShowSearch() }
                          highlighted={ highlighted ? highlighted.id : null }
                          searchTerm={ searchTerm }
                          formatter={ optionRenderer }
                          onSelect={ this._onSelectOption }
                          onSearchInputChange={ this._onSearchTermChange }
                          onSearchInputKeyDown={ this._onContainerKeyDown }
                          selected={ value }/>
        ) }
        <SelectError error={ error }/>
      </span>
    )
  }
}

export default Select
<commit_msg>dont pass null to the select node<commit_after>import React, { Component, PropTypes } from 'react'

import classNames from 'classnames'
import isEqual from 'lodash/isEqual'
import isFunction from 'lodash/isFunction'
import isNil from 'lodash/isNil'
import uniqueId from 'lodash/uniqueId'

import makeString from '../utils/makeString'

import SelectDropdown from './SelectDropdown'
import SelectError from './SelectError'
import SelectSelection from './SelectSelection'

// TODO: multiselect
// TODO: label
// TODO: optgroups
// TODO: make slim version
// TODO: optimize isFunction calls
export class Select extends Component {
  static childContextTypes = {
    cssClassNamePrefix: PropTypes.string,
  }

  static propTypes = {
    /**
     * Whether to allow user to reset selected option
     */
    allowClear: PropTypes.bool,
    cssClassNamePrefix: PropTypes.string,
    /**
     * Whether to focus itself on mount
     */
    autoFocus: PropTypes.bool,
    closeOnClickOutside: PropTypes.bool, // Default: true
    defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    disabled: PropTypes.bool,
    /**
     * Provide error message to display or just boolean to highlight select container with error styles
     */
    error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
    layout: PropTypes.shape({
      /**
       * Container's width
       */
      width: PropTypes.string,
      /**
       * Defines whether SelectDropdown should be opened above or below the container.
       * default: 'below'
       */
      // TODO: define position automatically depends on SelectContainer position in the viewport
      dropdownVerticalPosition: PropTypes.oneOf(['above', 'below']),
    }),
    name: PropTypes.string,
    /**
     * Function to transform options' 'text' to display in the SelectDropdown if needed
     * @param {object} option
     * @returns React element
     */
    optionRenderer: PropTypes.func,
    /**
     * Array of option items
     */
    options: PropTypes.arrayOf(PropTypes.shape({
      id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
      isHidden: PropTypes.bool,
      text: PropTypes.string.isRequired,
    })),
    onSelect: PropTypes.func,
    placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
    search: PropTypes.shape({
      /**
       * Minimum results amount before showing search input
       */
      minimumResults: PropTypes.number,
      show: PropTypes.bool,
      status: PropTypes.string,
    }),
    /**
     * Search input change callback
     */
    onSearchTermChange: PropTypes.func,
    /**
     * Value can be set by providing option id
     */
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  }

  static defaultProps = {
    allowClear: false,
    closeOnClickOutside: true,
    cssClassNamePrefix: 'rs3',
    disabled: false,
    layout: { dropdownVerticalPosition: 'below', width: '245px' },
    name: uniqueId('reactSelect_'),
    options: null,
    search: { minimumResults: 20 },
  }

  static initialState = () => ({
    disabled: false,
    dropdownOpened: false,
    error: null,
    highlighted: null,
    options: [],
    searchTerm: '',
    value: null,
  })

  /**
   * Ref Interface methods
   */
  set options(options) {
    if (!Array.isArray(options)) {
      throw new Error('Invalid options were provided. Options must be an array.')
    }

    this.setState({
      options: this._setOptions(options, true),
      value: null,
    })
  }

  get value() {
    return this.state.value
  }

  get options() {
    return [].concat(this.state.options)
  }

  clear() {
    this._onClearSelection()
  }

  constructor(props) { // eslint-disable-line consistent-return
    super(props)

    const onArrowUp = this._setHighlightedOption.bind(null, -1)
    const onArrowDown = this._setHighlightedOption.bind(null, 1)

    this.KEY_FUNCTIONS = {
      ArrowUp: onArrowUp,
      38: onArrowUp,
      ArrowDown: onArrowDown,
      40: onArrowDown,
      Enter: this._selectHighlighted,
      13: this._selectHighlighted,
      ' ': this._selectHighlighted, // 'Space' key
      32: this._selectHighlighted, // 'Space' key
      Escape: this._closeDropdown,
      27: this._closeDropdown,
    }

    this.state = Select.initialState()
    const { disabled, error, options } = props

    /**
     * @type {{
     *  dropdownOpened: boolean,
     *  error: string|boolean,
     *  highlighted: {id, index},
     *  options: array,
     *  searchTerm: string,
     *  value: string,
     * }}
     */
    this.state = Object.assign(this.state, {
      disabled,
      error,
      options: this._setOptions(options),
      value: this._setValue(),
    })
  }

  getChildContext = () => ({
    cssClassNamePrefix: this.props.cssClassNamePrefix,
  })

  componentWillReceiveProps(newProps) {
    const { disabled, error, options, value } = newProps
    const isValueValid = this._isValidValue(value)

    if (isValueValid && typeof newProps.onSelect === 'undefined' && typeof this.props.onSelect === 'undefined') {
      /* eslint-disable */
      console.warn(`Warning: You're setting value for Select component throught props
                but not passing onSelect callback which can lead to unforeseen consequences(bugs).
                Please consider using onSelect callback or defaultValue instead of value`)
      /* eslint-enable */
    }

    if (disabled) {
      this._closeDropdown()
    }

    this.setState(state => {
      let newValue = state.value

      if (isValueValid) {
        newValue = makeString(value)
      }

      return {
        disabled,
        options: this._setOptions(options),
        value: newValue,
        error: typeof error !== 'undefined' ? error : state.error
      }
    })
  }

  shouldComponentUpdate = ({ error, disabled, value, children }, nextState) => (
    (error !== this.props.error && error !== this.state.error)
    || disabled !== this.props.disabled
    || value !== this.state.value
    || !isEqual(children, this.props.children)
    || !isEqual(nextState, this.state)
  )

  componentDidMount = () => {
    const { autoFocus, closeOnClickOutside } = this.props

    // Autofocus on mount if enabled
    if (autoFocus) this._focusContainer()

    // Add listener for click outside if enabled
    if (closeOnClickOutside) document.addEventListener('click', this._handleClickOutside, true)
  }

  componentWillUnmount() {
    document.removeEventListener('click', this._handleClickOutside, true);
  }

  /**
   * Close SelectDropdown on click outside
   */
  _handleClickOutside = (e) => {
    if (this.state.dropdownOpened && !this.selectContainer.contains(e.target)) {
      this._closeDropdown()
    }
  }

  _isValidValue = value => {
    const { options } = this.state
    let isValid = false

    if (typeof value === 'undefined') {
      isValid = false
    } else if (value === null) {
      isValid = true
    } else if (options && options.length) {
      isValid = options.some(({ id }) => id === String(value))
    }

    return isValid
  }

  _openDropdown = () => {
    this.setState({ dropdownOpened: true })
  }

  _closeDropdown = () => {
    this.setState({
      dropdownOpened: false,
      highlighted: null,
    })

    this._focusContainer()
  }

  _focusContainer = () => {
    const x = window.scrollX
    const y = window.scrollY

    window.scrollTo(x, y)
    if (this.selectContainer) {
      this.selectContainer.focus()
    }
  }

  _setValue = () => {
    const { value, defaultValue } = this.props

    if (value === null) {
      return null
    }

    return makeString(value || defaultValue)
  }

  _makeOption = (id, text) => {
    if (!((typeof id === 'string' || typeof id === 'number') && (typeof text === 'string' || typeof text === 'number'))) {
      throw new Error('Options array is not formatted properly, option object must have "id" and "text"')
    }

    return { id: String(id), text }
  }

  _setOptions = (options, isNotFromProps) => {
    if (!isNotFromProps) {
      if (options === this.initialOptions) {
        return this.state.options
      }

      this.initialOptions = options
    }

    let stateOptions = this.state.options || []

    if (Array.isArray(options) && options.length) {
      stateOptions = options.map(({ id, text }) => this._makeOption(id, text))
    }

    return stateOptions
  }

  _getOptionById = value => {
    const { options } = this.state

    if (options && options.length) {
      return options.find(({ id }) => id === value) // eslint-disable-line eqeqeq
    }

    return null
  }

  _onContainerClick = () => {
    if (this.state.disabled) {
      return
    }

    if (this.state.dropdownOpened) {
      this._closeDropdown()
    } else {
      this._openDropdown()
    }
  }

  /**
   * Handle keyboard controls
   * @param {object} event
   */
  _onContainerKeyDown = event => {
    if (this.props.disabled) return

    const key = event.key || event.keyCode
    if (!this.KEY_FUNCTIONS[key]) return

    event.preventDefault()
    // Handle key click
    this.KEY_FUNCTIONS[key]()
  }

  _onClearSelection = () => {
    const { disabled, value } = this.state

    // Dont clear when disabled && dont fire extra event when value is already cleared
    if (!disabled && value) {
      this._onSelect(null)
    }
  }

  /**
   * Setting selected value
   * @param {object} option - option object from data array
   */
  _onSelect = option => {
    const { name, onSelect } = this.props
    // Setup structure of selection event
    const value = option ? option.id : null
    const selectionEvent = {
      type: 'select',
      isClear: option === null,  // indicates that value being cleared by onClearSelection
      target: {
        name,
        option,
        value,
      }
    }

    this.setState({ value, searchTerm: '' }, () => {
      if (isFunction(onSelect)) {
        onSelect(selectionEvent)
      }
    })

    this._closeDropdown()
    this._focusContainer()
  }

  /**
   * Handle option selection via user click
   * @param {number} id - options id
   */
  _onSelectOption = id => {
    // Get selected option and pass it into onSelect method for further processing
    const selectedOption = this._getOptionById(id)

    this._onSelect(selectedOption)
  }

  static makeHighlightedObject = (index, options) => {
    if (!options.length) {
      return null
    }

    return ({ id: options[index].id, index })
  }

  _hasHighlighted = () => {
    const { highlighted } = this.state

    return !!highlighted && typeof highlighted.index !== 'undefined'
  }
  /**
   * Set next highlighted option via 'ArrowUp' or 'ArrowDown' key
   * @param {number} direction (can be -1 or 1)
   */
  _setHighlightedOption = direction => {
    this.setState(({ disabled, highlighted, dropdownOpened }) => {
      const options = this._getOptionsList()

      // do nothing if disabled or there are no options
      if (disabled || !options || !options.length) return

      const optionsLength = options.length - 1
      const hasHighlighted = this._hasHighlighted()
      const nextHighlighted = (hasHighlighted) ? highlighted.index + direction : 0
      let highlightIndex

      // TODO: scroll SelectDropdown block to show highlighted item when overflows
      // If dropdown not opened or there is no highlighted item yet
      if (!dropdownOpened || !(hasHighlighted)
        // highlight first option after click 'ArrowDown' on the last one
        || nextHighlighted > optionsLength) {
        highlightIndex = 0
      } else if (nextHighlighted < 0) {
        // Highlight last option after click 'ArrowUp' on the first one
        highlightIndex = optionsLength
      } else {
        // Highlight next option
        highlightIndex = nextHighlighted
      }

      return ({ // eslint-disable-line consistent-return
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(highlightIndex, options)
      })
    })
  }

  /**
   * Select current highlighted option
   */
    // @fixme: selects invalid option when options list filtered by searchTerm
  _selectHighlighted = () => {
    const { options, highlighted, dropdownOpened } = this.state

    // If dropdown not opened or there is no highlighted item yet
    if (!dropdownOpened || !this._hasHighlighted()) {
      // Open dropdown and hightlight first item
      this.setState({
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(0, this._getOptionsList()),
      })
    } else {
      // Select highlighted item
      this._onSelect(options.find(({ id }) => id === highlighted.id))
    }
  }

  _getSelectContainerClassName = () => {
    const {
      className,
      cssClassNamePrefix,
      disabled,
      error,
      layout: { dropdownVerticalPosition },
    } = this.props
    const { dropdownOpened, value } = this.state

    return classNames(`${cssClassNamePrefix}__container ${className || ''}`, {
      [`${cssClassNamePrefix}--above`]: dropdownVerticalPosition === 'above',
      [`${cssClassNamePrefix}--below`]: dropdownVerticalPosition !== 'above',
      [`${cssClassNamePrefix}--disabled`]: disabled,
      [`${cssClassNamePrefix}--error`]: !!error || !!this.state.error,
      [`${cssClassNamePrefix}--open`]: dropdownOpened,
      [`${cssClassNamePrefix}--selected`]: !isNil(value),
    })
  }

  _isClearable = () => {
    const { allowClear } = this.props
    const { value, disabled } = this.state

    return (allowClear && !disabled && !isNil(value))
  }

  _getOptionsList = () => {
    const { options, searchTerm } = this.state
    let optionsList = options || []

    if (searchTerm && optionsList.length) {
      optionsList = options.filter(({ text }) => new RegExp(searchTerm, 'gi').test(text))
    }

    return optionsList
  }

  _onSearchTermChange = e => {
    const { target: { value: term } } = e
    const { onSearchTermChange } = this.props

    // If size of text is increases
    // const isTextIncreasing = term && (!stateSearchTerm || term.length > stateSearchTerm.length)
    const searchTerm = term || ''

    // if callback were passed in props
    if (isFunction(onSearchTermChange)) {
      onSearchTermChange(e)
    }

    this.setState({ searchTerm })
  }

  _isShowSearch = () => {
    const { options } = this.state
    const { search: { show, minimumResults = 20 } } = this.props

    return show || (!!options.length && (minimumResults <= options.length))
  }

  _setContainerRef = (node) => {
    this.selectContainer = node
  }

  render() {
    const { cssClassNamePrefix, optionRenderer, layout: { width }, placeholder, search: { status }, name } = this.props
    const { disabled, dropdownOpened, error, highlighted, searchTerm, value } = this.state
    const selectedOption = this._getOptionById(value)

    return (
      <span ref={ this._setContainerRef }
            className={ this._getSelectContainerClassName() }
            style={{ width }}
            disabled={ disabled }
            tabIndex='0'
            role='combobox'
            onClick={ this._onContainerClick }
            onKeyDown={ this._onContainerKeyDown }>
      <select name={ name } value={ !isNil(value) ? value : '' } readOnly className={`${cssClassNamePrefix}__select-node`}>
        { !isNil(value) && <option value={value}/> }
      </select>
      <SelectSelection clearable={ this._isClearable() }
                       onClearSelection={ this._onClearSelection }
                       placeholder={ placeholder }
                       formatter={ optionRenderer }
                       selection={ selectedOption }
                       onKeyDown={ this._onContainerKeyDown }/>
        { dropdownOpened && (
          <SelectDropdown options={ this._getOptionsList() }
                          status={ status }
                          showSearch={ this._isShowSearch() }
                          highlighted={ highlighted ? highlighted.id : null }
                          searchTerm={ searchTerm }
                          formatter={ optionRenderer }
                          onSelect={ this._onSelectOption }
                          onSearchInputChange={ this._onSearchTermChange }
                          onSearchInputKeyDown={ this._onContainerKeyDown }
                          selected={ value }/>
        ) }
        <SelectError error={ error }/>
      </span>
    )
  }
}

export default Select
<|endoftext|>


<commit_before>App = {
    debug: true,
    init: function() {
        this.log('App initialized');
        this.bind();
    },
    bind: function() {
        $('#parse').click(function(e) {
            var s = $('#stage').val()
            if (s == '')
                return;
            parse(s);
        });
    },
    log: function(msg) {
        $('#log').append(msg + '
');
    },
};

function Node() {
    this.value = null;
};

function parse(s) {
    App.log('Parsing ' + s + '');
    var i = 1;

    while ((s[i] != ' ') && (s[i] != '[') && (s[i] != ']')) i++;
    App.log('Parsed first token: ' + s.substring(1, i+1));

    while (s[i] == ' ') i++;
    var level = 1,
        start = 1;
    for (; i < s.length; i++) {
        var outer_level = level;
        if (s[i] == '[') level++;
        if (s[i] == ']') level--;

        if ((outer_level == 1) && (level == 2))
            start = i;
        if ((outer_level == 2) && (level == 1))
            parse(s.substring(start, i+1));
    }
};

$(function() {
    App.init();
});
<commit_msg>parsing text elements<commit_after>App = {
    debug: true,
    init: function() {
        this.log('App initialized');
        this.bind();
    },
    bind: function() {
        $('#parse').click(function(e) {
            var s = $('#stage').val()
            if (s == '')
                return;
            parse(s);
        });
    },
    log: function(msg) {
        $('#log').append(msg + '
');
    },
};

function Node() {
    this.value = null;
};

function parse(s) {
    App.log('Parsing ' + s + '');

    if (s[0] != '[') {
        return;
    }

    var i = 1;
    while ((s[i] != ' ') && (s[i] != '[') && (s[i] != ']')) i++;
    App.log('Parsed first token: ' + s.substring(1, i+1));

    while (s[i] == ' ') i++;
    var level = 1,
        start = i;
    for (; i < s.length; i++) {
        var outer_level = level;
        if (s[i] == '[') level++;
        if (s[i] == ']') level--;

        if (((outer_level == 1) && (level == 2)) || ((outer_level == 1) && (level == 0))) {
            /* Final token inside a bracket group (e.g., "Alice" in [N Alice]) */
            if (s.substring(start, i).search(/\w+/) > -1)
               parse(s.substring(start, i));
            start = i;
        }
        if ((outer_level == 2) && (level == 1)) {
            parse(s.substring(start, i+1));
            /* Otherwise we get the closing ] again and an extra level-- */
            start = i+1;
        }
    }
};

$(function() {
    App.init();
});
<|endoftext|>


<commit_before>var gulp = require('gulp');
var chai = require('chai');
var Server = require('karma').Server;
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var ngAnnotate = require('gulp-ng-annotate');
var path = require('path');
var plumber = require('gulp-plumber');
var jshint = require('gulp-jshint');
var eventStream = require('event-stream');

// Root directory
var rootDirectory = path.resolve('./');

// Source directory for build process
var sourceDirectory = path.join(rootDirectory, './src');

var sourceFiles = [
  path.join(sourceDirectory, '/**/*.js')
];

var lintFiles = [
  'gulpfile.js',
  // Karma configuration
  // 'karma-*.conf.js'
].concat(sourceFiles);

// Build JavaScript distribution files
gulp.task('build', function() {
  return eventStream.merge(gulp.src(sourceFiles))
    .pipe(plumber())
    .pipe(concat('angular-actioncable.js'))
    .pipe(gulp.dest('./dist/'))
    .pipe(ngAnnotate())
    .pipe(uglify({mangle: false}))
    .pipe(rename('angular-actioncable.min.js'))
    .pipe(gulp.dest('./dist/'));
});

// Validate source JavaScript
gulp.task('jshint', function () {
  gulp.src(lintFiles)
    .pipe(plumber())
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// watch for changes
gulp.task('watch', function () {
  gulp.watch([sourceFiles], ['build']);
});

// Run test once and exit
gulp.task('test', function (done) {
  new Server({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('test-dist', ['build'], function (done) {
  new Server({
    configFile: __dirname + '/karma-dist-concatenated.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('test-min', ['build'], function (done) {
  new Server({
    configFile: __dirname + '/karma-dist-minified.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('default', ['test', 'watch', 'build']);
gulp.task('release', ['test', 'jshint', 'test-dist', 'test-min']);
<commit_msg>changes default gulp task to 'gulp serve'<commit_after>var gulp = require('gulp');
var chai = require('chai');
var Server = require('karma').Server;
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
var ngAnnotate = require('gulp-ng-annotate');
var path = require('path');
var plumber = require('gulp-plumber');
var jshint = require('gulp-jshint');
var eventStream = require('event-stream');

// Root directory
var rootDirectory = path.resolve('./');

// Source directory for build process
var sourceDirectory = path.join(rootDirectory, './src');

var sourceFiles = [
  path.join(sourceDirectory, '/**/*.js')
];

var lintFiles = [
  'gulpfile.js',
  // Karma configuration
  // 'karma-*.conf.js'
].concat(sourceFiles);

// Build JavaScript distribution files
gulp.task('build', function() {
  return eventStream.merge(gulp.src(sourceFiles))
    .pipe(plumber())
    .pipe(concat('angular-actioncable.js'))
    .pipe(gulp.dest('./dist/'))
    .pipe(ngAnnotate())
    .pipe(uglify({mangle: false}))
    .pipe(rename('angular-actioncable.min.js'))
    .pipe(gulp.dest('./dist/'));
});

// Validate source JavaScript
gulp.task('jshint', function () {
  gulp.src(lintFiles)
    .pipe(plumber())
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// watch for changes
gulp.task('watch', function () {
  gulp.watch([sourceFiles], ['build']);
});

// Run test once and exit
gulp.task('test', function (done) {
  new Server({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('test-dist', ['build'], function (done) {
  new Server({
    configFile: __dirname + '/karma-dist-concatenated.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('test-min', ['build'], function (done) {
  new Server({
    configFile: __dirname + '/karma-dist-minified.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('serve', ['test', 'watch', 'build']);
gulp.task('release', ['test', 'jshint', 'test-dist', 'test-min']); //runs `build` too as task dependency
<|endoftext|>


<commit_before>var path = require('path');
var child = require('child_process');
var which = require('which');
var util = require('util');
var fs = require('fs');

var debug = util.debuglog('browser_process');

var browsers = {
  chrome: {
    type: 'chrome',
    paths: {
      darwin: [
        '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
      ],
      linux: [
        'google-chrome'
      ],
      win32: [
        process.env['LOCALAPPDATA'] + '\\Google\\Chrome\\Application\\chrome.exe',
        process.env['PROGRAMFILES'] + '\\Google\\Chrome\\Application\\chrome.exe',
        process.env['PROGRAMFILES(X86)'] + '\\Google\\Chrome\\Application\\chrome.exe',
      ],
    }[process.platform],
  },

  chromium: {
    type: 'chrome',
    paths: {
      darwin: [
        '/Applications/Chromium.app/Contents/MacOS/Chromium',
      ],
      linux: [
        'chromium-browser',
      ],
      win32: [
        process.env['LOCALAPPDATA'] + '\\Chromium\\Application\\chrome.exe',
        process.env['PROGRAMFILES'] + '\\Chromium\\Application\\chrome.exe',
        process.env['PROGRAMFILES(X86)'] + '\\Chromium\\Application\\chrome.exe',
      ]
    }[process.platform],
  },

  electron: {
    type: 'chrome',
    paths: {
      darwin: [
        'electron',
      ],
      linux: [
        'electron',
      ],
      win32: [
        'electron',
        'electron.cmd',
      ]
    }[process.platform],
  },
  firefox: {
    type: 'firefox',
    paths: {
      darwin: [
        '/Applications/Firefox.app/Contents/MacOS/Firefox',
      ],
      linux: [
        'firefox',
      ],
      win32: [
        process.env['LOCALAPPDATA'] + '\\Mozilla\ Firefox\\firefox.exe',
        process.env['PROGRAMFILES'] + '\\Mozilla\ Firefox\\firefox.exe',
        process.env['PROGRAMFILES(X86)'] + '\\Mozilla\ Firefox\\firefox.exe',
      ],
    }[process.platform],
  },
};

var browserOptions = {
  chrome: {
    debug: '--remote-debugging-port=',
    profile: '--user-data-dir=',
    window: '--new-window',
    private: '--incognito',
  },

  firefox: {
    debug: '--start-debugging-server',
    profile: '--profile',
    window: '--new-window',
    private: '--private',
  }
};

function type(command) {
  if (browsers[command]) {
    return browsers[command].type;
  }

  var basename = path.basename(command);

  var key = Object.keys(browsers).filter(function (key) {
    return browsers[key].paths.some(function (exename) {
      return basename === path.basename(exename);
    });
  })[0];

  if (key) {
    return browsers[key].type;
  }

  throw Error('Bad browser command ' + command);
}

module.exports.type = type;

function find(command, callback) {
  if (path.isAbsolute(command)) {
    return callback(null, command);
  }

  var browser = browsers[command];
  if (browser === undefined) {
    var error = new Error('Invalid browser command');
    return callback(error);
  }

  var done = false;
  browser.paths.forEach(function (exename) {
    which(exename, function (error, command) {
      if (error || done) {
        return;
      }

      done = true;
      callback(null, command);
    });
  });
}

module.exports.find = find;

function detect(callback) {
  var results = [];

  var pending = 0;
  Object.keys(browsers).forEach(function(name) {
    var browser = browsers[name];

    pending += browser.paths.length;
    browser.paths.forEach(function(filename) {
      fs.stat(filename, function(error, stat) {
        if (stat) {
          results.push({
            name: name,
            type: browser.type,
            command: filename
          });
        }

        if (--pending == 0) {
          callback(results);
        }
      });
    });
  });
}

module.exports.detect = detect;

function options(command, options) {
  var browserType = type(command);
  var args = [];

  var keys = Object.keys(options);
  keys.forEach(function (key) {
    var value = options[key];

    if (value !== undefined) {
      var option = browserOptions[browserType][key];

      if (key === 'url') {
        args.push(value);
      } else if (option.match(/\S=$/)) {
        args.push(option + value);
      } else if (value === true) {
        args.push(option);
      } else {
        args.push(option, value.toString());
      }
    }
  });

  return args;
}

module.exports.options = options;

function spawn(command, args, options, callback) {
  if (typeof args === 'function') {
    callback = args;
    args = undefined;
  } else if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  find(command, function (error, command) {
    debug('spawn %s %s', command, util.inspect(args));

    if (error) {
      return callback(error);
    }

    var exe = child.spawn(command, args, options);
    exe.once('error', function (error) {
      callback(error);
    });

    process.nextTick(function () {
      callback(null, exe);
    });
  });
}

module.exports.spawn = spawn;
<commit_msg>lib: resolve browser commands with realpath<commit_after>var path = require('path');
var child = require('child_process');
var which = require('which');
var util = require('util');
var fs = require('fs');

var debug = util.debuglog('browser_process');

var browsers = {
  chrome: {
    type: 'chrome',
    paths: {
      darwin: [
        '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
      ],
      linux: [
        'google-chrome'
      ],
      win32: [
        process.env['LOCALAPPDATA'] + '\\Google\\Chrome\\Application\\chrome.exe',
        process.env['PROGRAMFILES'] + '\\Google\\Chrome\\Application\\chrome.exe',
        process.env['PROGRAMFILES(X86)'] + '\\Google\\Chrome\\Application\\chrome.exe',
      ],
    }[process.platform],
  },

  chromium: {
    type: 'chrome',
    paths: {
      darwin: [
        '/Applications/Chromium.app/Contents/MacOS/Chromium',
      ],
      linux: [
        'chromium-browser',
      ],
      win32: [
        process.env['LOCALAPPDATA'] + '\\Chromium\\Application\\chrome.exe',
        process.env['PROGRAMFILES'] + '\\Chromium\\Application\\chrome.exe',
        process.env['PROGRAMFILES(X86)'] + '\\Chromium\\Application\\chrome.exe',
      ]
    }[process.platform],
  },

  electron: {
    type: 'chrome',
    paths: {
      darwin: [
        'electron',
      ],
      linux: [
        'electron',
      ],
      win32: [
        'electron',
        'electron.cmd',
      ]
    }[process.platform],
  },
  firefox: {
    type: 'firefox',
    paths: {
      darwin: [
        '/Applications/Firefox.app/Contents/MacOS/Firefox',
      ],
      linux: [
        'firefox',
      ],
      win32: [
        process.env['LOCALAPPDATA'] + '\\Mozilla\ Firefox\\firefox.exe',
        process.env['PROGRAMFILES'] + '\\Mozilla\ Firefox\\firefox.exe',
        process.env['PROGRAMFILES(X86)'] + '\\Mozilla\ Firefox\\firefox.exe',
      ],
    }[process.platform],
  },
};

var browserOptions = {
  chrome: {
    debug: '--remote-debugging-port=',
    profile: '--user-data-dir=',
    window: '--new-window',
    private: '--incognito',
  },

  firefox: {
    debug: '--start-debugging-server',
    profile: '--profile',
    window: '--new-window',
    private: '--private',
  }
};

function type(command) {
  if (browsers[command]) {
    return browsers[command].type;
  }

  var basename = path.basename(command);

  var key = Object.keys(browsers).filter(function (key) {
    return browsers[key].paths.some(function (exename) {
      return basename === path.basename(exename);
    });
  })[0];

  if (key) {
    return browsers[key].type;
  }

  throw Error('Bad browser command ' + command);
}

module.exports.type = type;

function find(command, callback) {
  if (path.isAbsolute(command)) {
    return callback(null, command);
  }

  var browser = browsers[command];
  if (browser === undefined) {
    var error = new Error('Invalid browser command');
    return callback(error);
  }

  var done = false;
  browser.paths.forEach(function (exename) {
    which(exename, function (error, command) {
      if (error || done) {
        return;
      }

      done = true;
      callback(null, command);
    });
  });
}

module.exports.find = find;

function detect(callback) {
  var results = [];

  var pending = 0;
  Object.keys(browsers).forEach(function(name) {
    var browser = browsers[name];

    pending += browser.paths.length;
    browser.paths.forEach(function(filename) {
      fs.stat(filename, function(error, stat) {
        if (stat) {
          results.push({
            name: name,
            type: browser.type,
            command: filename
          });
        }

        if (--pending == 0) {
          callback(results);
        }
      });
    });
  });
}

module.exports.detect = detect;

function options(command, options) {
  var browserType = type(command);
  var args = [];

  var keys = Object.keys(options);
  keys.forEach(function (key) {
    var value = options[key];

    if (value !== undefined) {
      var option = browserOptions[browserType][key];

      if (key === 'url') {
        args.push(value);
      } else if (option.match(/\S=$/)) {
        args.push(option + value);
      } else if (value === true) {
        args.push(option);
      } else {
        args.push(option, value.toString());
      }
    }
  });

  return args;
}

module.exports.options = options;

function spawn(name, args, options, callback) {
  if (typeof args === 'function') {
    callback = args;
    args = undefined;
  } else if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  find(name, function (error, command) {
    if (error) {
      return callback(error);
    }

    fs.realpath(command, function(error, command) {
      if (error) {
        return callback(error);
      }

      debug('spawn %s %s', command, util.inspect(args));
      var exe = child.spawn(command, args, options);
      exe.once('error', function (error) {
        callback(error);
      });

      process.nextTick(function () {
        callback(null, exe);
      });
    });
  });
}

module.exports.spawn = spawn;
<|endoftext|>


<commit_before>'use strict';

var postal = require('postal');
var raf = require('raf');
var FluzoStore = require('fluzo-store')(postal);

var fluzo_channel = postal.channel('fluzo');

var render_pending = false;
var render_suscriptions = [];
var updating = false;

var Fluzo = {

   Store: FluzoStore,
   action: FluzoStore.action,

   clearRenderRequests: function () {
      render_pending = false;
   },

   requestRender: function () {
      render_pending = true;
   },

   onRender: function (cb) {
      render_suscriptions.push(cb);
      var index = render_suscriptions.length - 1;
      return function () {
         render_suscriptions.splice(index, 1);
      };
   },

   renderIfRequested: function (delta) {
      if (render_pending) {
         var now = Date.now();
         render_suscriptions.forEach(function (cb) {
            cb(now, delta);
         });
         this.clearRenderRequests();
      }
   },

   startUpdating: function () {
      updating = true;
      (function tick(delta) {
         if (updating) {
            raf(tick);
         }
         Fluzo.renderIfRequested(delta);
      })(0);
   },

   stopUpdating: function () {
      updating = false;
   }

};

fluzo_channel.subscribe('store.changed.*', Fluzo.requestRender);

module.exports = Fluzo;
<commit_msg>Check if not already started before starting updates<commit_after>'use strict';

var postal = require('postal');
var raf = require('raf');
var FluzoStore = require('fluzo-store')(postal);

var fluzo_channel = postal.channel('fluzo');

var render_pending = false;
var render_suscriptions = [];
var updating = false;

var Fluzo = {

   Store: FluzoStore,
   action: FluzoStore.action,

   clearRenderRequests: function () {
      render_pending = false;
   },

   requestRender: function () {
      render_pending = true;
   },

   onRender: function (cb) {
      render_suscriptions.push(cb);
      var index = render_suscriptions.length - 1;
      return function () {
         render_suscriptions.splice(index, 1);
      };
   },

   renderIfRequested: function (delta) {
      if (render_pending) {
         var now = Date.now();
         render_suscriptions.forEach(function (cb) {
            cb(now, delta);
         });
         this.clearRenderRequests();
      }
   },

   startUpdating: function () {
      if (!updating) {
         updating = true;
         (function tick(delta) {
            if (updating) {
               raf(tick);
            }
            Fluzo.renderIfRequested(delta);
         })(0);
      }
   },

   stopUpdating: function () {
      updating = false;
   }

};

fluzo_channel.subscribe('store.changed.*', Fluzo.requestRender);

module.exports = Fluzo;
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import find from 'lodash/find'
import isEqual from 'lodash/isEqual'
import isFunction from 'lodash/isFunction'
import isNil from 'lodash/isNil'
import uniqueId from 'lodash/uniqueId'
import makeString from '../utils/makeString'
import SelectDropdown from './SelectDropdown'
import SelectError from './SelectError'
import SelectSelection from './SelectSelection'

// TODO: isPending prop that renders loader
// TODO: multiselect
// TODO: label
// TODO: optgroups
// TODO: make slim version
// TODO: optimize isFunction calls
export class Select extends Component {
  constructor(props) { // eslint-disable-line consistent-return
    super(props);

    const onArrowUp = this.setHighlightedOption.bind(null, -1);
    const onArrowDown = this.setHighlightedOption.bind(null, 1);

    this.KEY_FUNCTIONS = {
      ArrowUp: onArrowUp,
      38: onArrowUp,
      ArrowDown: onArrowDown,
      40: onArrowDown,
      Enter: this.selectHighlighted,
      13: this.selectHighlighted,
      ' ': this.selectHighlighted, // 'Space' key
      32: this.selectHighlighted, // 'Space' key
      Escape: this.closeDropdown.bind(this, true),
      27: this.closeDropdown.bind(this, true),
    };

    this.state = Select.initialState();
    const { disabled, error, options } = props;

    /**
     * @type {{
     *  dropdownOpened: boolean,
     *  error: string|boolean,
     *  highlighted: {id, index},
     *  options: array,
     *  searchTerm: string,
     *  value: string,
     * }}
     */
    this.state = Object.assign(this.state, {
      disabled,
      error,
      options: this.setOptions(options),
      value: this.setValue(),
    })
  }

  static childContextTypes = {
    cssClassNamePrefix: PropTypes.string,
  };

  static propTypes = {
    /**
     * Whether to allow user to reset selected option
     */
    allowClear: PropTypes.bool,
    cssClassNamePrefix: PropTypes.string,
    /**
     * Whether to focus itself on mount
     */
    autoFocus: PropTypes.bool,
    closeOnClickOutside: PropTypes.bool, // Default: true
    defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    disabled: PropTypes.bool,
    /**
     * Provide error message to display or just boolean to highlight select container with error styles
     */
    error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
    layout: PropTypes.shape({
      /**
       * Container's width
       */
      width: PropTypes.string,
      /**
       * Defines whether SelectDropdown should be opened above or below the container.
       * default: 'below'
       */
      // TODO: define position automatically depends on SelectContainer position in the viewport
      dropdownVerticalPosition: PropTypes.oneOf(['above', 'below']),
      emptyOptionsLabel: PropTypes.string,
    }),
    name: PropTypes.string,
    /**
     * Function to transform options' 'text' to display in the SelectDropdown if needed
     * @param {object} option
     * @returns React element
     */
    optionRenderer: PropTypes.func,
    /**
     * Array of option items
     */
    options: PropTypes.arrayOf(PropTypes.shape({
      id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
      isHidden: PropTypes.bool,
      text: PropTypes.string.isRequired,
    })),
    onSelect: PropTypes.func,
    placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
    required: PropTypes.bool,
    search: PropTypes.shape({
      /**
       * Minimum results amount before showing search input
       */
      minimumResults: PropTypes.number,
      show: PropTypes.bool,
      status: PropTypes.string,
    }),
    selectionRenderer: PropTypes.func,
    /**
     * Search input change callback
     */
    onSearchTermChange: PropTypes.func,
    /**
     * Value can be set by providing option id
     */
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  };

  static defaultProps = {
    allowClear: false,
    closeOnClickOutside: true,
    cssClassNamePrefix: 'rs3',
    disabled: false,
    layout: {
      dropdownVerticalPosition: 'below',
      width: '245px',
    },
    name: uniqueId('reactSelect_'),
    options: null,
    search: {
      minimumResults: 20,
    },
  };

  static initialState = () => ({
    disabled: false,
    dropdownOpened: false,
    error: null,
    highlighted: null,
    options: [],
    searchTerm: '',
    value: null,
  });

  /**
   * Ref Interface methods
   */

  clear() {
    this.onClearSelection()
  }

  get options() {
    return [].concat(this.state.options)
  }

  set options(options) {
    if (!Array.isArray(options)) {
      throw new Error('Invalid options were provided. Options must be an array.')
    }

    this.setState({
      options: this.setOptions(options, true),
      value: null,
    })
  }

  get valid() {
    const { error, value } = this.state;
    const { required } = this.props;

    return !!error || (required && !value)
  }

  get value() {
    return this.state.value
  }

  getChildContext = () => ({
    cssClassNamePrefix: this.props.cssClassNamePrefix,
  });

  componentWillReceiveProps(newProps) {
    const { disabled, error, options, value } = newProps;
    const isValueValid = this.isValidValue(value);

    if (isValueValid && typeof newProps.onSelect === 'undefined' && typeof this.props.onSelect === 'undefined') {
      /* eslint-disable */
      console.warn(`Warning: You're setting value for Select component throught props
                but not passing onSelect callback which can lead to unforeseen consequences(bugs).
                Please consider using onSelect callback or defaultValue instead of value`);
      /* eslint-enable */
    }

    if (disabled) {
      this.closeDropdown(true)
    }

    this.setState(state => {
      let newValue = state.value;

      if (isValueValid) {
        newValue = makeString(value)
      }

      return {
        disabled,
        options: this.setOptions(options),
        value: newValue,
        error: typeof error !== 'undefined' ?
          error
          : state.error,
      }
    })
  }

  shouldComponentUpdate = ({ error, disabled, value, children }, nextState) => (
    (error !== this.props.error && error !== this.state.error)
    || disabled !== this.props.disabled
    || value !== this.state.value
    || !isEqual(children, this.props.children)
    || !isEqual(nextState, this.state)
  );

  componentDidMount = () => {
    const { autoFocus, closeOnClickOutside } = this.props;

    // Autofocus on mount if enabled
    if (autoFocus) this.focusContainer();

    // Add listener for click outside if enabled
    if (closeOnClickOutside) document.addEventListener('click', this.handleClickOutside, true);
    this.selectContainer.addEventListener('focusout', this.handleContainerBlur, true)
  };

  componentWillUnmount() {
    document.removeEventListener('click', this.handleClickOutside, true);
    this.selectContainer.removeEventListener('focusout', this.handleContainerBlur, true);
  }

  /**
   * Close SelectDropdown on click outside
   */
  handleClickOutside = (e) => {
    if (this.state.dropdownOpened && !this.selectContainer.contains(e.target)) {
      this.closeDropdown(false)
    }
  };

  handleContainerBlur = (e) => {
    if (!this.selectContainer.contains(e.relatedTarget)) {
      this.closeDropdown(false)
    }
  };

  isValidValue = value => {
    const { options } = this.state;
    let isValid = false;

    if (typeof value === 'undefined') {
      isValid = false
    } else if (value === null) {
      isValid = true
    } else if (options && options.length) {
      isValid = options.some(({ id }) => id === String(value))
    }

    return isValid
  };

  openDropdown = () => {
    this.setState({ dropdownOpened: true })
  };

  closeDropdown = (focus) => {
    this.setState({
      dropdownOpened: false,
      highlighted: null,
    });

    if (focus) {
      this.focusContainer()
    }
  };

  focusContainer = () => {
    const x = window.scrollX;
    const y = window.scrollY;

    window.scrollTo(x, y);
    if (this.selectContainer) {
      this.selectContainer.focus()
    }
  };

  setValue = () => {
    const { value, defaultValue } = this.props;

    if (value === null) {
      return null
    }

    return makeString(value || defaultValue)
  };

  makeOption = ({ id, text, ...rest }) => {
    // validate option object
    if (!((typeof id === 'string' || typeof id === 'number')
      && (typeof text === 'string' || typeof text === 'number'))) {
      throw new Error('Options array is not formatted properly, option object must have "id" and "text"'); // eslint-disable-line max-len
    }

    return {
      id: String(id),
      text,
      ...rest,
    }
  };

  setOptions = (options, isNotFromProps) => {
    if (!isNotFromProps) {
      if (options === this.initialOptions) {
        return this.state.options
      }

      this.initialOptions = options
    }

    let stateOptions = this.state.options || [];

    if (Array.isArray(options)) {
      if (options.length) {
        stateOptions = options.map((option) => this.makeOption(option))
      } else {
        stateOptions = []
      }
    }

    return stateOptions
  };

  getOptionById = value => {
    const { options } = this.state;

    if (options && options.length) {
      return find(options, ({ id }) => id === value);
    }

    return null
  };

  onContainerClick = () => {
    if (this.state.disabled) {
      return
    }

    if (this.state.dropdownOpened) {
      this.closeDropdown(true)
    } else {
      this.openDropdown()
    }
  };

  /**
   * Handle keyboard controls
   * @param {object} event
   */
  onContainerKeyDown = event => {
    if (this.props.disabled) return;

    const key = event.key || event.keyCode;
    if (!this.KEY_FUNCTIONS[key]) return;

    event.preventDefault();
    // Handle key click
    this.KEY_FUNCTIONS[key]()
  };

  onClearSelection = () => {
    const { disabled, value } = this.state;

    // Dont clear when disabled && dont fire extra event when value is already cleared
    if (!disabled && value) {
      this.onSelect(null)
    }
  };

  /**
   * Setting selected value
   * @param {object} option - option object from data array
   */
  onSelect = option => {
    const { name, onSelect } = this.props;
    // Setup structure of selection event
    const value = option ? option.id : null;
    const selectionEvent = {
      type: 'select',
      isClear: option === null, // indicates that value being cleared by onClearSelection
      target: {
        name,
        option,
        value,
      },
    };

    this.setState({
      value,
      searchTerm: '',
    }, () => {
      if (isFunction(onSelect)) {
        onSelect(selectionEvent)
      }
    });

    this.closeDropdown(true);
    this.focusContainer()
  };

  /**
   * Handle option selection via user click
   * @param {number} id - options id
   */
  onSelectOption = id => {
    // Get selected option and pass it into onSelect method for further processing
    const selectedOption = this.getOptionById(id);

    this.onSelect(selectedOption)
  };

  static makeHighlightedObject = (index, options) => {
    if (!options.length) {
      return null
    }

    return ({
      id: options[index].id,
      index,
    })
  };

  hasHighlighted = () => {
    const { highlighted } = this.state;

    return !!highlighted && typeof highlighted.index !== 'undefined'
  };
  /**
   * Set next highlighted option via 'ArrowUp' or 'ArrowDown' key
   * @param {number} direction (can be -1 or 1)
   */
  setHighlightedOption = direction => {
    this.setState(({ disabled, highlighted, dropdownOpened }) => {
      const options = this.getOptionsList();

      // do nothing if disabled or there are no options
      if (disabled || !options || !options.length) return;

      const optionsLength = options.length - 1;
      const hasHighlighted = this.hasHighlighted();
      const nextHighlighted = hasHighlighted ? highlighted.index + direction : 0;
      let highlightIndex;

      // TODO: scroll SelectDropdown block to show highlighted item when overflows
      // If dropdown not opened or there is no highlighted item yet
      if (!dropdownOpened || !hasHighlighted
        // highlight first option after click 'ArrowDown' on the last one
        || nextHighlighted > optionsLength) {
        highlightIndex = 0
      } else if (nextHighlighted < 0) {
        // Highlight last option after click 'ArrowUp' on the first one
        highlightIndex = optionsLength
      } else {
        // Highlight next option
        highlightIndex = nextHighlighted
      }

      return ({ // eslint-disable-line consistent-return
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(highlightIndex, options),
      })
    })
  };

  /**
   * Select current highlighted option
   */
    // @fixme: selects invalid option when options list filtered by searchTerm
  selectHighlighted = () => {
    const { options, highlighted, dropdownOpened } = this.state;

    // If dropdown not opened or there is no highlighted item yet
    if (!dropdownOpened || !this.hasHighlighted()) {
      // Open dropdown and hightlight first item
      this.setState({
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(0, this.getOptionsList()),
      })
    } else {
      // Select highlighted item
      this.onSelect(find(options, ({ id }) => id === highlighted.id))
    }
  };

  getSelectContainerClassName = () => {
    const {
      className,
      cssClassNamePrefix,
      disabled,
      error,
      layout: {
        dropdownVerticalPosition,
      },
    } = this.props;
    const { dropdownOpened, value } = this.state;

    return classNames(`${cssClassNamePrefix}__container ${className || ''}`, {
      [`${cssClassNamePrefix}--above`]: dropdownVerticalPosition === 'above',
      [`${cssClassNamePrefix}--below`]: dropdownVerticalPosition !== 'above',
      [`${cssClassNamePrefix}--disabled`]: disabled,
      [`${cssClassNamePrefix}--error`]: !!error || !!this.state.error,
      [`${cssClassNamePrefix}--open`]: dropdownOpened,
      [`${cssClassNamePrefix}--selected`]: !isNil(value),
    })
  };

  isClearable = () => {
    const { allowClear } = this.props;
    const { value, disabled } = this.state;

    return allowClear && !disabled && !isNil(value)
  };

  getOptionsList = () => {
    const { options, searchTerm } = this.state;
    let optionsList = options || [];

    if (searchTerm && optionsList.length) {
      optionsList = options.filter(({ text }) => new RegExp(searchTerm, 'gi').test(text))
    }

    return optionsList
  };

  onSearchTermChange = e => {
    const { target: { value: term } } = e;
    const { onSearchTermChange } = this.props;

    // If size of text is increases
    // const isTextIncreasing = term && (!stateSearchTerm || term.length > stateSearchTerm.length)
    const searchTerm = term || '';

    // if callback were passed in props
    if (isFunction(onSearchTermChange)) {
      onSearchTermChange(e)
    }

    this.setState({ searchTerm })
  };

  isShowSearch = () => {
    const { options } = this.state;
    const { search: { show, minimumResults = 20 } } = this.props;

    return show || (!!options.length && (minimumResults <= options.length))
  };

  setContainerRef = (node) => {
    this.selectContainer = node
  };

  render() {
    const {
      cssClassNamePrefix,
      layout: { width, emptyOptionsLabel },
      name,
      optionRenderer,
      placeholder,
      search: { status },
      selectionRenderer,
    } = this.props;
    const { disabled, dropdownOpened, error, highlighted, searchTerm, value } = this.state;
    const selectedOption = this.getOptionById(value);

    return (
      <span
        ref={this.setContainerRef}
        className={this.getSelectContainerClassName()}
        style={{ width }}
        disabled={disabled}
        tabIndex='0'
        role='combobox'
        onClick={this.onContainerClick}
        onKeyDown={this.onContainerKeyDown}
      >
        <select
          tabIndex='-1'
          name={name}
          value={!isNil(value) ? value : ''}
          readOnly
          className={`${cssClassNamePrefix}__select-node`}
        >
          { !isNil(value) && <option value={value}/> }
        </select>
        <SelectSelection
          clearable={this.isClearable()}
          onClearSelection={this.onClearSelection}
          placeholder={placeholder}
          formatter={selectionRenderer || optionRenderer}
          selection={selectedOption}
          onKeyDown={this.onContainerKeyDown}
        />
        { dropdownOpened && (
          <SelectDropdown
            options={this.getOptionsList()}
            status={status}
            emptyOptionsLabel={emptyOptionsLabel}
            showSearch={this.isShowSearch()}
            highlighted={highlighted ? highlighted.id : null}
            searchTerm={searchTerm}
            formatter={optionRenderer}
            onSelect={this.onSelectOption}
            onSearchInputChange={this.onSearchTermChange}
            onSearchInputKeyDown={this.onContainerKeyDown}
            selected={value}
          />)
        }
        <SelectError error={error}/>
      </span>
    )
  }
}

export default Select
<commit_msg>fix window.scrollTo on container focus for IE<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import find from 'lodash/find'
import isEqual from 'lodash/isEqual'
import isFunction from 'lodash/isFunction'
import isNil from 'lodash/isNil'
import uniqueId from 'lodash/uniqueId'
import makeString from '../utils/makeString'
import SelectDropdown from './SelectDropdown'
import SelectError from './SelectError'
import SelectSelection from './SelectSelection'

// TODO: isPending prop that renders loader
// TODO: multiselect
// TODO: label
// TODO: optgroups
// TODO: make slim version
// TODO: optimize isFunction calls
export class Select extends Component {
  constructor(props) { // eslint-disable-line consistent-return
    super(props);

    const onArrowUp = this.setHighlightedOption.bind(null, -1);
    const onArrowDown = this.setHighlightedOption.bind(null, 1);

    this.KEY_FUNCTIONS = {
      ArrowUp: onArrowUp,
      38: onArrowUp,
      ArrowDown: onArrowDown,
      40: onArrowDown,
      Enter: this.selectHighlighted,
      13: this.selectHighlighted,
      ' ': this.selectHighlighted, // 'Space' key
      32: this.selectHighlighted, // 'Space' key
      Escape: this.closeDropdown.bind(this, true),
      27: this.closeDropdown.bind(this, true),
    };

    this.state = Select.initialState();
    const { disabled, error, options } = props;

    /**
     * @type {{
     *  dropdownOpened: boolean,
     *  error: string|boolean,
     *  highlighted: {id, index},
     *  options: array,
     *  searchTerm: string,
     *  value: string,
     * }}
     */
    this.state = Object.assign(this.state, {
      disabled,
      error,
      options: this.setOptions(options),
      value: this.setValue(),
    })
  }

  static childContextTypes = {
    cssClassNamePrefix: PropTypes.string,
  };

  static propTypes = {
    /**
     * Whether to allow user to reset selected option
     */
    allowClear: PropTypes.bool,
    cssClassNamePrefix: PropTypes.string,
    /**
     * Whether to focus itself on mount
     */
    autoFocus: PropTypes.bool,
    closeOnClickOutside: PropTypes.bool, // Default: true
    defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    disabled: PropTypes.bool,
    /**
     * Provide error message to display or just boolean to highlight select container with error styles
     */
    error: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
    layout: PropTypes.shape({
      /**
       * Container's width
       */
      width: PropTypes.string,
      /**
       * Defines whether SelectDropdown should be opened above or below the container.
       * default: 'below'
       */
      // TODO: define position automatically depends on SelectContainer position in the viewport
      dropdownVerticalPosition: PropTypes.oneOf(['above', 'below']),
      emptyOptionsLabel: PropTypes.string,
    }),
    name: PropTypes.string,
    /**
     * Function to transform options' 'text' to display in the SelectDropdown if needed
     * @param {object} option
     * @returns React element
     */
    optionRenderer: PropTypes.func,
    /**
     * Array of option items
     */
    options: PropTypes.arrayOf(PropTypes.shape({
      id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
      isHidden: PropTypes.bool,
      text: PropTypes.string.isRequired,
    })),
    onSelect: PropTypes.func,
    placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
    required: PropTypes.bool,
    search: PropTypes.shape({
      /**
       * Minimum results amount before showing search input
       */
      minimumResults: PropTypes.number,
      show: PropTypes.bool,
      status: PropTypes.string,
    }),
    selectionRenderer: PropTypes.func,
    /**
     * Search input change callback
     */
    onSearchTermChange: PropTypes.func,
    /**
     * Value can be set by providing option id
     */
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  };

  static defaultProps = {
    allowClear: false,
    closeOnClickOutside: true,
    cssClassNamePrefix: 'rs3',
    disabled: false,
    layout: {
      dropdownVerticalPosition: 'below',
      width: '245px',
    },
    name: uniqueId('reactSelect_'),
    options: null,
    search: {
      minimumResults: 20,
    },
  };

  static initialState = () => ({
    disabled: false,
    dropdownOpened: false,
    error: null,
    highlighted: null,
    options: [],
    searchTerm: '',
    value: null,
  });

  /**
   * Ref Interface methods
   */

  clear() {
    this.onClearSelection()
  }

  get options() {
    return [].concat(this.state.options)
  }

  set options(options) {
    if (!Array.isArray(options)) {
      throw new Error('Invalid options were provided. Options must be an array.')
    }

    this.setState({
      options: this.setOptions(options, true),
      value: null,
    })
  }

  get valid() {
    const { error, value } = this.state;
    const { required } = this.props;

    return !!error || (required && !value)
  }

  get value() {
    return this.state.value
  }

  getChildContext = () => ({
    cssClassNamePrefix: this.props.cssClassNamePrefix,
  });

  componentWillReceiveProps(newProps) {
    const { disabled, error, options, value } = newProps;
    const isValueValid = this.isValidValue(value);

    if (isValueValid && typeof newProps.onSelect === 'undefined' && typeof this.props.onSelect === 'undefined') {
      /* eslint-disable */
      console.warn(`Warning: You're setting value for Select component throught props
                but not passing onSelect callback which can lead to unforeseen consequences(bugs).
                Please consider using onSelect callback or defaultValue instead of value`);
      /* eslint-enable */
    }

    if (disabled) {
      this.closeDropdown(true)
    }

    this.setState(state => {
      let newValue = state.value;

      if (isValueValid) {
        newValue = makeString(value)
      }

      return {
        disabled,
        options: this.setOptions(options),
        value: newValue,
        error: typeof error !== 'undefined' ?
          error
          : state.error,
      }
    })
  }

  shouldComponentUpdate = ({ error, disabled, value, children }, nextState) => (
    (error !== this.props.error && error !== this.state.error)
    || disabled !== this.props.disabled
    || value !== this.state.value
    || !isEqual(children, this.props.children)
    || !isEqual(nextState, this.state)
  );

  componentDidMount = () => {
    const { autoFocus, closeOnClickOutside } = this.props;

    // Autofocus on mount if enabled
    if (autoFocus) this.focusContainer();

    // Add listener for click outside if enabled
    if (closeOnClickOutside) document.addEventListener('click', this.handleClickOutside, true);
    this.selectContainer.addEventListener('focusout', this.handleContainerBlur, true)
  };

  componentWillUnmount() {
    document.removeEventListener('click', this.handleClickOutside, true);
    this.selectContainer.removeEventListener('focusout', this.handleContainerBlur, true);
  }

  /**
   * Close SelectDropdown on click outside
   */
  handleClickOutside = (e) => {
    if (this.state.dropdownOpened && !this.selectContainer.contains(e.target)) {
      this.closeDropdown(false)
    }
  };

  handleContainerBlur = (e) => {
    if (!this.selectContainer.contains(e.relatedTarget)) {
      this.closeDropdown(false)
    }
  };

  isValidValue = value => {
    const { options } = this.state;
    let isValid = false;

    if (typeof value === 'undefined') {
      isValid = false
    } else if (value === null) {
      isValid = true
    } else if (options && options.length) {
      isValid = options.some(({ id }) => id === String(value))
    }

    return isValid
  };

  openDropdown = () => {
    this.setState({ dropdownOpened: true })
  };

  closeDropdown = (focus) => {
    this.setState({
      dropdownOpened: false,
      highlighted: null,
    });

    if (focus) {
      this.focusContainer()
    }
  };

  focusContainer = () => {
    const x = window.pageXOffset;
    const y = window.pageYOffset;

    window.scrollTo(x, y);
    if (this.selectContainer) {
      this.selectContainer.focus()
    }
  };

  setValue = () => {
    const { value, defaultValue } = this.props;

    if (value === null) {
      return null
    }

    return makeString(value || defaultValue)
  };

  makeOption = ({ id, text, ...rest }) => {
    // validate option object
    if (!((typeof id === 'string' || typeof id === 'number')
      && (typeof text === 'string' || typeof text === 'number'))) {
      throw new Error('Options array is not formatted properly, option object must have "id" and "text"'); // eslint-disable-line max-len
    }

    return {
      id: String(id),
      text,
      ...rest,
    }
  };

  setOptions = (options, isNotFromProps) => {
    if (!isNotFromProps) {
      if (options === this.initialOptions) {
        return this.state.options
      }

      this.initialOptions = options
    }

    let stateOptions = this.state.options || [];

    if (Array.isArray(options)) {
      if (options.length) {
        stateOptions = options.map((option) => this.makeOption(option))
      } else {
        stateOptions = []
      }
    }

    return stateOptions
  };

  getOptionById = value => {
    const { options } = this.state;

    if (options && options.length) {
      return find(options, ({ id }) => id === value);
    }

    return null
  };

  onContainerClick = () => {
    if (this.state.disabled) {
      return
    }

    if (this.state.dropdownOpened) {
      this.closeDropdown(true)
    } else {
      this.openDropdown()
    }
  };

  /**
   * Handle keyboard controls
   * @param {object} event
   */
  onContainerKeyDown = event => {
    if (this.props.disabled) return;

    const key = event.key || event.keyCode;
    if (!this.KEY_FUNCTIONS[key]) return;

    event.preventDefault();
    // Handle key click
    this.KEY_FUNCTIONS[key]()
  };

  onClearSelection = () => {
    const { disabled, value } = this.state;

    // Dont clear when disabled && dont fire extra event when value is already cleared
    if (!disabled && value) {
      this.onSelect(null)
    }
  };

  /**
   * Setting selected value
   * @param {object} option - option object from data array
   */
  onSelect = option => {
    const { name, onSelect } = this.props;
    // Setup structure of selection event
    const value = option ? option.id : null;
    const selectionEvent = {
      type: 'select',
      isClear: option === null, // indicates that value being cleared by onClearSelection
      target: {
        name,
        option,
        value,
      },
    };

    this.setState({
      value,
      searchTerm: '',
    }, () => {
      if (isFunction(onSelect)) {
        onSelect(selectionEvent)
      }
    });

    this.closeDropdown(true);
    this.focusContainer()
  };

  /**
   * Handle option selection via user click
   * @param {number} id - options id
   */
  onSelectOption = id => {
    // Get selected option and pass it into onSelect method for further processing
    const selectedOption = this.getOptionById(id);

    this.onSelect(selectedOption)
  };

  static makeHighlightedObject = (index, options) => {
    if (!options.length) {
      return null
    }

    return ({
      id: options[index].id,
      index,
    })
  };

  hasHighlighted = () => {
    const { highlighted } = this.state;

    return !!highlighted && typeof highlighted.index !== 'undefined'
  };
  /**
   * Set next highlighted option via 'ArrowUp' or 'ArrowDown' key
   * @param {number} direction (can be -1 or 1)
   */
  setHighlightedOption = direction => {
    this.setState(({ disabled, highlighted, dropdownOpened }) => {
      const options = this.getOptionsList();

      // do nothing if disabled or there are no options
      if (disabled || !options || !options.length) return;

      const optionsLength = options.length - 1;
      const hasHighlighted = this.hasHighlighted();
      const nextHighlighted = hasHighlighted ? highlighted.index + direction : 0;
      let highlightIndex;

      // TODO: scroll SelectDropdown block to show highlighted item when overflows
      // If dropdown not opened or there is no highlighted item yet
      if (!dropdownOpened || !hasHighlighted
        // highlight first option after click 'ArrowDown' on the last one
        || nextHighlighted > optionsLength) {
        highlightIndex = 0
      } else if (nextHighlighted < 0) {
        // Highlight last option after click 'ArrowUp' on the first one
        highlightIndex = optionsLength
      } else {
        // Highlight next option
        highlightIndex = nextHighlighted
      }

      return ({ // eslint-disable-line consistent-return
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(highlightIndex, options),
      })
    })
  };

  /**
   * Select current highlighted option
   */
    // @fixme: selects invalid option when options list filtered by searchTerm
  selectHighlighted = () => {
    const { options, highlighted, dropdownOpened } = this.state;

    // If dropdown not opened or there is no highlighted item yet
    if (!dropdownOpened || !this.hasHighlighted()) {
      // Open dropdown and hightlight first item
      this.setState({
        dropdownOpened: true,
        highlighted: Select.makeHighlightedObject(0, this.getOptionsList()),
      })
    } else {
      // Select highlighted item
      this.onSelect(find(options, ({ id }) => id === highlighted.id))
    }
  };

  getSelectContainerClassName = () => {
    const {
      className,
      cssClassNamePrefix,
      disabled,
      error,
      layout: {
        dropdownVerticalPosition,
      },
    } = this.props;
    const { dropdownOpened, value } = this.state;

    return classNames(`${cssClassNamePrefix}__container ${className || ''}`, {
      [`${cssClassNamePrefix}--above`]: dropdownVerticalPosition === 'above',
      [`${cssClassNamePrefix}--below`]: dropdownVerticalPosition !== 'above',
      [`${cssClassNamePrefix}--disabled`]: disabled,
      [`${cssClassNamePrefix}--error`]: !!error || !!this.state.error,
      [`${cssClassNamePrefix}--open`]: dropdownOpened,
      [`${cssClassNamePrefix}--selected`]: !isNil(value),
    })
  };

  isClearable = () => {
    const { allowClear } = this.props;
    const { value, disabled } = this.state;

    return allowClear && !disabled && !isNil(value)
  };

  getOptionsList = () => {
    const { options, searchTerm } = this.state;
    let optionsList = options || [];

    if (searchTerm && optionsList.length) {
      optionsList = options.filter(({ text }) => new RegExp(searchTerm, 'gi').test(text))
    }

    return optionsList
  };

  onSearchTermChange = e => {
    const { target: { value: term } } = e;
    const { onSearchTermChange } = this.props;

    // If size of text is increases
    // const isTextIncreasing = term && (!stateSearchTerm || term.length > stateSearchTerm.length)
    const searchTerm = term || '';

    // if callback were passed in props
    if (isFunction(onSearchTermChange)) {
      onSearchTermChange(e)
    }

    this.setState({ searchTerm })
  };

  isShowSearch = () => {
    const { options } = this.state;
    const { search: { show, minimumResults = 20 } } = this.props;

    return show || (!!options.length && (minimumResults <= options.length))
  };

  setContainerRef = (node) => {
    this.selectContainer = node
  };

  render() {
    const {
      cssClassNamePrefix,
      layout: { width, emptyOptionsLabel },
      name,
      optionRenderer,
      placeholder,
      search: { status },
      selectionRenderer,
    } = this.props;
    const { disabled, dropdownOpened, error, highlighted, searchTerm, value } = this.state;
    const selectedOption = this.getOptionById(value);

    return (
      <span
        ref={this.setContainerRef}
        className={this.getSelectContainerClassName()}
        style={{ width }}
        disabled={disabled}
        tabIndex='0'
        role='combobox'
        onClick={this.onContainerClick}
        onKeyDown={this.onContainerKeyDown}
      >
        <select
          tabIndex='-1'
          name={name}
          value={!isNil(value) ? value : ''}
          readOnly
          className={`${cssClassNamePrefix}__select-node`}
        >
          { !isNil(value) && <option value={value}/> }
        </select>
        <SelectSelection
          clearable={this.isClearable()}
          onClearSelection={this.onClearSelection}
          placeholder={placeholder}
          formatter={selectionRenderer || optionRenderer}
          selection={selectedOption}
          onKeyDown={this.onContainerKeyDown}
        />
        { dropdownOpened && (
          <SelectDropdown
            options={this.getOptionsList()}
            status={status}
            emptyOptionsLabel={emptyOptionsLabel}
            showSearch={this.isShowSearch()}
            highlighted={highlighted ? highlighted.id : null}
            searchTerm={searchTerm}
            formatter={optionRenderer}
            onSelect={this.onSelectOption}
            onSearchInputChange={this.onSearchTermChange}
            onSearchInputKeyDown={this.onContainerKeyDown}
            selected={value}
          />)
        }
        <SelectError error={error}/>
      </span>
    )
  }
}

export default Select
<|endoftext|>


<commit_before>var context = new AudioContext(),
    volume = context.createGain(),
    squareOscillators = {},
    sawtoothOscillators = {},
    analyser = context.createAnalyser(),
    dataArray = new Uint8Array(analyser.frequencyBinCount),
    canvas = document.getElementById('oscilloscope'),
    canvasCntxt = canvas.getContext('2d'),
    canvasHeight = 210,
    canvasWidth = 587;

//load and play a sample using Fetch API and web Audio API
var button = document.querySelector('#bling'),
    audioContext = new AudioContext(),
    audioBuffer,
    bufferSource;

//lower the output volume cause oscillators are loud
volume.gain.value = 0.5;

//route to output (context.destionation), speakers
volume.connect(context.destination);
//route output to analyser
volume.connect(analyser);

var keyboard = new QwertyHancock({
  id: 'keyboard',
  width: 600,
  height: 180,
  octaves: 2,
  startNote: 'A2'
});

keyboard.keyDown = function(note, frequency) {
  var squareOsc = context.createOscillator(),
      sawtoothOsc = context.createOscillator();

  squareOscillators[note] = squareOsc;
  sawtoothOscillators[note] = sawtoothOsc;

  squareOsc.connect(volume);
  sawtoothOsc.connect(volume);

  squareOsc.type = 'square';
  sawtoothOsc.type = 'sawtooth';

  squareOsc.frequency.value = frequency;
  squareOsc.detune.value = -10;

  sawtoothOsc.frequency.value = frequency;
  sawtoothOsc.detune.value = 5;

  squareOsc.start(context.currentTime);
  sawtoothOsc.start(context.currentTime);

};

keyboard.keyUp = function(note, frequency) {
  squareOscillators[note].stop(context.currentTime);
  squareOscillators[note].disconnect();

  sawtoothOscillators[note].stop(context.currentTime);
  sawtoothOscillators[note].disconnect();

};

var xWidth = canvasWidth / analyser.frequencyBinCount;

var draw = function() {
  requestAnimationFrame(function () {
    //reset canvas width to clear canvas after each frame
    canvasCntxt.clearRect(0, 0, canvasWidth, canvasHeight);
    canvasCntxt.beginPath();

    //analyze dataArray from output
    analyser.getByteTimeDomainData(dataArray);

    for(var i = 0; i < dataArray.length; i++) {
      var yPos = dataArray[i] / 256,
          xPos = i * xWidth;

      yPos = yPos * canvasHeight;
      canvasCntxt.lineTo(xPos,yPos);
    }
      canvasCntxt.strokeStyle = 'green';
      canvasCntxt.stroke();
      draw();
  });
};

draw();

//fetch the file and return an array buffer. Then do something with it.
fetch('/audio/Sandel-OpenCulture.mp3')
  .then(function(response) {
    return response.arrayBuffer();
  })
  .then(function(arrayBuffer) {
    //decode teh array buffer into something the audio api can read, ie AudioBuffer

    audioContext.decodeAudioData(arrayBuffer)
      .then(function(decodedBuffer) {
        //audio buffer is served up;
        console.log('decoded Buffer ', decodedBuffer);
        audioBuffer = decodedBuffer;
        //return audioBuffer;
      });
  });

var playSample = function() {
  //create new node to do stuff with the audio
  bufferSource = audioContext.createBufferSource();

  //point that at the returned decoded Audio buffer
  bufferSource.buffer = audioBuffer;

  //make it louder
  volume.gain.value = .1;

  //connect it to output (destination)
  bufferSource.connect(audioContext.destination);
  bufferSource.start(audioContext.currentTime);
};

//set eventlistener to the play button:
button.onclick = playSample;
<commit_msg>Refactor to use callback instead of promise syntax<commit_after>var context = new AudioContext(),
    volume = context.createGain(),
    squareOscillators = {},
    sawtoothOscillators = {},
    analyser = context.createAnalyser(),
    dataArray = new Uint8Array(analyser.frequencyBinCount),
    canvas = document.getElementById('oscilloscope'),
    canvasCntxt = canvas.getContext('2d'),
    canvasHeight = 210,
    canvasWidth = 587;

//load and play a sample using Fetch API and web Audio API
var button = document.querySelector('#bling'),
    audioContext = new AudioContext(),
    audioBuffer,
    bufferSource;

//lower the output volume cause oscillators are loud
volume.gain.value = 0.5;

//route to output (context.destionation), speakers
volume.connect(context.destination);
//route output to analyser
volume.connect(analyser);

var keyboard = new QwertyHancock({
  id: 'keyboard',
  width: 600,
  height: 180,
  octaves: 2,
  startNote: 'A2'
});

keyboard.keyDown = function(note, frequency) {
  var squareOsc = context.createOscillator(),
      sawtoothOsc = context.createOscillator();

  squareOscillators[note] = squareOsc;
  sawtoothOscillators[note] = sawtoothOsc;

  squareOsc.connect(volume);
  sawtoothOsc.connect(volume);

  squareOsc.type = 'square';
  sawtoothOsc.type = 'sawtooth';

  squareOsc.frequency.value = frequency;
  squareOsc.detune.value = -10;

  sawtoothOsc.frequency.value = frequency;
  sawtoothOsc.detune.value = 5;

  squareOsc.start(context.currentTime);
  sawtoothOsc.start(context.currentTime);

};

keyboard.keyUp = function(note, frequency) {
  squareOscillators[note].stop(context.currentTime);
  squareOscillators[note].disconnect();

  sawtoothOscillators[note].stop(context.currentTime);
  sawtoothOscillators[note].disconnect();

};

var xWidth = canvasWidth / analyser.frequencyBinCount;

var draw = function() {
  requestAnimationFrame(function () {
    //clear canvas after each frame
    canvasCntxt.clearRect(0, 0, canvasWidth, canvasHeight);
    canvasCntxt.beginPath();

    //analyze dataArray from output
    analyser.getByteTimeDomainData(dataArray);

    for(var i = 0; i < dataArray.length; i++) {
      var yPos = dataArray[i] / 256,
          xPos = i * xWidth;

      yPos = yPos * canvasHeight;
      canvasCntxt.lineTo(xPos,yPos);
    }
      canvasCntxt.strokeStyle = 'green';
      canvasCntxt.stroke();
      draw();
  });
};

draw();

//fetch the file and return an array buffer. Then do something with it.
fetch('/audio/Sandel-OpenCulture.mp3')
  .then(function(response) {
    return response.arrayBuffer();
  })
  .then(function(arrayBuffer) {

    //decode teh array buffer into something the audio api can read, ie AudioBuffer
    audioContext.decodeAudioData(arrayBuffer, function(decodedBuffer) {

      //audio buffer is served up;
      audioBuffer = decodedBuffer;
    });
    return audioBuffer;
  });

var playSample = function() {
  //create new node to do stuff with the audio
  bufferSource = audioContext.createBufferSource();

  //point that at the returned decoded Audio buffer
  bufferSource.buffer = audioBuffer;

  //lower osc gain while sample is playing
  volume.gain.value = .1;

  //connect it to output (destination)
  bufferSource.connect(audioContext.destination);
  bufferSource.start(audioContext.currentTime);
};

button.onclick = playSample;
<|endoftext|>


<commit_before>var hoek = require('hoek');
var path = require('path');

// Declare internals

var internals = {};

// Defaults

internals.defaults = {
    whitelist: []
};

// Version
internals.version = hoek.loadPackage(path.join(__dirname, '..')).version;

exports.register = function(plugin, options, next) {
    function shouldForward(request) {
        if (options.handler && request.response.output && request.response.output.errorCode == 404) {
            return options.whitelist.reduce(function (previousValue, currentValue) {
                return previousValue || currentValue.test(request.path);
            }, false);
        }
    }

    plugin.ext('onPreResponse', function (request, extNext) {
        if (shouldForward(request)) {
            options.handler(request, extNext);
        } else {
            extNext();
        }
    });
};<commit_msg>misc fixes<commit_after>var path = require('path');

// Declare internals

var internals = {};

// Defaults

internals.defaults = {
    whitelist: []
};

// Version
internals.version = hoek.loadPackage(path.join(__dirname, '..')).version;

exports.register = function(plugin, options, next) {
    function shouldForward(request) {
        if (options.handler && request.response.output && request.response.output.statusCode == 404) {
            return !options.whitelist.reduce(function (previousValue, currentValue) {
                return previousValue || currentValue.test(request.path);
            }, false);
        }
    }

    plugin.ext('onPreResponse', function (request, extNext) {
        if (shouldForward(request)) {
            options.handler(request, extNext);
        } else {
            extNext();
        }
    });
    next();
};<|endoftext|>