HurricaneJames/react-item-box-input

View on GitHub
src/ItemBox.js

Summary

Maintainability
C
1 day
Test Coverage
/*eslint-disable react/no-did-mount-set-state, react/no-did-update-set-state, dot-notation */

var React = require('react')
  , ReactDOM = require('react-dom')
  , Immutable = require('immutable')
  , ImmutablePropTypes = require('react-immutable-proptypes')
  , ItemList = require('./ItemList')
  , DefaultTemplate = require('./DefaultTemplate')
  , ResizeDetector = require('./ResizeDetector')
  , KeyCodes = require('./KeyCodes');

const TEST_AREA_STYLE = {
  position: 'absolute',
  visibility: 'hidden',
  height: 'auto',
  width: 'auto',
  whiteSpace: 'nowrap'
};

var ItemBox = React.createClass({
  displayName: 'ItemBox',
  propTypes: {
    value: React.PropTypes.string,
    onChange: React.PropTypes.func,
    items: ImmutablePropTypes.list.isRequired,
    itemTemplate: React.PropTypes.func.isRequired,
    onRemove: React.PropTypes.func,
    defaultWidth: React.PropTypes.number,       // the default width (in px) of the component if it cannot be determined by the DOM
    onKeyUp: React.PropTypes.func,
    onKeyDown: React.PropTypes.func,
    onCopy: React.PropTypes.func,
    onCut: React.PropTypes.func,
    onPaste: React.PropTypes.func,
    onInputFocus: React.PropTypes.func,
    onInputBlur: React.PropTypes.func,
    onFocus: React.PropTypes.func,
    onBlur: React.PropTypes.func
  },
  getDefaultProps: function() {
    return {
      items: new Immutable.List(),
      itemTemplate: DefaultTemplate,
      defaultWidth: 500
    };
  },
  getInitialState: function() {
    return {
      width: '1em',
      lastItemRightBoundary: 0,
      focus: false
    };
  },
  componentDidMount: function() {
    this.resizeEntryWidth(this.props.value);
  },
  componentDidUpdate: function() {
    this.resizeEntryWidth(this.props.value);
  },
  isFocused: function() {
    return this.state.focused || this.refs.entry === document.activeElement;
  },
  updateRightBoundary: function(newRightBoundary) {
    if(this.state.lastItemRightBoundary !== newRightBoundary) {
      this.setState({lastItemRightBoundary: newRightBoundary});
    }
  },
  resizeEntryWidth: function(entryText) {
    var newWidth = this.getCorrectEntryWidth(entryText);
    if(newWidth !== this.state.width) {
      this.setState({ width: newWidth });
    }
  },
  getCaretPosition: function (inputElement) {
    if('selectionStart' in inputElement) {
      return inputElement.selectionStart;
    }else {
      var selection = document.selection.createRange();
      var selectionLength = selection.text.length;
      selection.moveStart('character', -inputElement.value.length);
      return selection.text.length - selectionLength;
    }
  },
  getCorrectEntryWidth: function(entryText) {
    var node = ReactDOM.findDOMNode(this.refs['entry']);
    var entryOffset = (this.state.lastItemRightBoundary > 0 ? this.state.lastItemRightBoundary : node.offsetLeft) || 0;
    var maxWidth = node.parentNode.clientWidth || this.props.defaultWidth;
    var textWidth = this.getTextWidth(entryText) || 0;
    return (textWidth + entryOffset > maxWidth) ? maxWidth : maxWidth - entryOffset;
  },
  getTextWidth: function(text) {
    var node = ReactDOM.findDOMNode(this.refs['testarea']);
    node.innerHTML = text;
    return node.offsetWidth;
  },
  keyboardSelectLastItem: function(e) {
    var position = this.getCaretPosition(e.target);
    if(position === 0) {
      this.refs['itemList'].selectLast();
    }
  },
  checkComponentFocus: function(e) {
    if(!this.state.focused) {
      if(this.props.onFocus) { this.props.onFocus(e); }
      this.setState({ focused: true });
    }
  },
  triggerComponentBlur: function(e) {
    if(this.state.focused) {
      if(this.props.onBlur) { this.props.onBlur(e); }
      this.setState({ focused: false });
    }
  },
  onEntryKeyDown: function(e) {
    e.stopPropagation();
    if(e.keyCode === KeyCodes.LEFT_ARROW) {
      this.keyboardSelectLastItem(e);
    }
    if(this.props.onKeyDown) { this.props.onKeyDown(e); }
  },
  onEntryKeyUp: function(e) {
    e.stopPropagation();
    if(e.keyCode === KeyCodes.BACKSPACE) {
      this.keyboardSelectLastItem(e);
    }
    if(this.props.onKeyUp) { this.props.onKeyUp(e); }
  },
  onInputFocus: function(e) {
    if(this.props.onInputFocus) { this.props.onInputFocus(e); }
    this.ignoreInputBlur = false;
    this.ignoreListItemBlur = true;
    this.checkComponentFocus(e);
  },
  handleInputBlur: function() {
    if(!this.ignoreInputBlur) {
      this.triggerComponentBlur();
    }
    this.ignoreInputBlur = false;
  },
  onInputBlur: function(e) {
    if(this.props.onInputBlur) { this.props.onInputBlur(e); }
    setTimeout(this.handleInputBlur, 0);
  },
  onItemListFocus: function(e) {
    this.ignoreInputBlur = true;
    this.ignoreListItemBlur = false;
    this.checkComponentFocus(e);
  },
  handleListItemBlur: function() {
    if(!this.ignoreListItemBlur) {
      this.triggerComponentBlur();
    }
    this.ignoreListItemBlur = false;
  },
  onItemListBlur: function() {
    setTimeout(this.handleListItemBlur, 0);
  },
  onItemListSelectNextField: function() {
    var entry = this.refs['entry'];
    if(entry) { ReactDOM.findDOMNode(entry).focus(); }
  },
  onResize: function() {
    this.resizeEntryWidth(this.props.value);
  },
  render: function() {
    var inputStyle = {
      display: 'inline-block',
      width: this.state.width,
      boxShadow: 'none',
      border: 'none',
      background: 'transparent',
      margin: 0
    };
    var componentStyle = {
      border: '1px solid #ccc',
      backgroundColor: this.isFocused() ? '#fafafa' : 'transparent',
    }
    return (
      <div style={componentStyle}>
        <ResizeDetector onResize={this.onResize} />
        <div ref="testarea" className="testarea" style={TEST_AREA_STYLE} />
        <ItemList
          ref="itemList"
          items={this.props.items}
          defaultTemplate={this.props.itemTemplate}
          onLastItemRightBoundaryChange={this.updateRightBoundary}
          onSelectNextField={this.onItemListSelectNextField}
          onRemove={this.props.onRemove}
          onFocus={this.onItemListFocus}
          onBlur={this.onItemListBlur}
        />
        <input
          ref="entry"
          type="text"
          value={this.props.value}
          onChange={this.props.onChange}
          onKeyDown={this.onEntryKeyDown}
          onKeyUp={this.onEntryKeyUp}
          style={inputStyle}
          onCopy={this.props.onCopy}
          onCut={this.props.onCut}
          onPaste={this.props.onPaste}
          onFocus={this.onInputFocus}
          onBlur={this.onInputBlur}
        />
      </div>
    );
  }
});

module.exports = ItemBox;