kumabook/bebop

View on GitHub
src/containers/Options.jsx

Summary

Maintainability
B
6 hrs
Test Coverage
import browser from 'webextension-polyfill';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import getMessage from '../utils/i18n';
import { defaultOrder } from '../reducers/options';

const dragIcon = browser.extension.getURL('images/drag.png');

const SortableItem = SortableElement(({ value }) => ((
  <li className="sortableListItem">
    <img src={dragIcon} className="dragIcon" alt="drag" />
    {value}
  </li>
)));

const SortableList = SortableContainer(({ items }) => ((
  <ul className="sortableList">
    {items.map((value, index) => (
      <SortableItem key={`item-${value}`} index={index} value={value} />
    ))}
  </ul>
)));

class Options extends React.Component {
  static get propTypes() {
    return {
      popupWidth:                     PropTypes.number.isRequired,
      orderOfCandidates:              PropTypes.arrayOf(PropTypes.string).isRequired,
      maxResultsForEmpty:             PropTypes.objectOf(PropTypes.number).isRequired,
      enabledCJKMove:                 PropTypes.bool.isRequired,
      hatenaUserName:                 PropTypes.string.isRequired,
      theme:                          PropTypes.string.isRequired,
      handlePopupWidthChange:         PropTypes.func.isRequired,
      handleMaxResultsForEmptyChange: PropTypes.func.isRequired,
      handleCJKMoveChange:            PropTypes.func.isRequired,
      handleSortEnd:                  PropTypes.func.isRequired,
      handleHatenaUserNameChange:     PropTypes.func.isRequired,
      handleThemeChange:              PropTypes.func.isRequired,
    };
  }

  handlePopupWidthChange(e) {
    if (!Number.isNaN(e.target.valueAsNumber)) {
      this.props.handlePopupWidthChange(e.target.valueAsNumber);
    }
  }

  renderInputsOfCandidates() {
    return defaultOrder.map((type) => {
      const n = this.props.maxResultsForEmpty[type];
      return (
        <div key={`maxResultsFor-${type}`}>
          <span className="maxResultsInputLabel">{type}</span>
          <input
            className="maxResultsInput"
            type="number"
            min="0"
            max="20"
            step="1"
            value={n}
            onChange={e => this.props.handleMaxResultsForEmptyChange({
              [type]: parseInt(e.target.value, 10),
            })}
          />
        </div>
      );
    });
  }

  renderPopupWidthInput() {
    return (
      <div>
        <h4 className="optionsLabel">Popup width</h4>
        <input
          className="optionsValueInput popupWidthInput"
          type="number"
          min="200"
          max="5000"
          step="50"
          placeholder={getMessage('optionsUI_width')}
          value={this.props.popupWidth}
          onChange={e => this.handlePopupWidthChange(e)}
        />
      </div>
    );
  }

  renderOrderOfCandidates() {
    return (
      <div>
        <h4 className="optionsLabel">Order of candidates</h4>
        <p className="optionsDescription">You can change order by drag</p>
        <SortableList items={this.props.orderOfCandidates} onSortEnd={this.props.handleSortEnd} />
      </div>
    );
  }

  renderMaxResultsForEmpty() {
    return (
      <div>
        <h4 className="optionsLabel">Max results of candidates for empty query</h4>
        <div className="optionsValue">
          {this.renderInputsOfCandidates()}
        </div>
      </div>
    );
  }

  renderKeyBindings() {
    return (
      <div>
        <h4 className="optionsLabel">key-bindings</h4>
        <input
          className="optionsValueInput cjkMoveCheckbox"
          type="checkbox"
          checked={this.props.enabledCJKMove}
          onChange={e => this.props.handleCJKMoveChange(e.target.checked)}
        />
        C-j ... next-candidate, C-k ... previous-candidate
      </div>
    );
  }

  renderHatenaUserName() {
    return (
      <div>
        <h4 className="optionsLabel">Hatena User Name</h4>
        <input
          className="optionsValueTextInput"
          type="text"
          value={this.props.hatenaUserName}
          onChange={e => this.props.handleHatenaUserNameChange(e.target.value)}
        />
      </div>
    );
  }

  renderTheme() {
    return (
      <div>
        <h4 className="optionsLabel">Select theme</h4>
        <select
          className="themeSelect"
          value={this.props.theme}
          onChange={e => this.props.handleThemeChange(e.target.value)}
        >
          <option value="">Default</option>
          <option value="simple-dark">Simple Dark</option>
        </select>
      </div>
    );
  }

  render() {
    return (
      <div className="options">
        <h3 className="optionsTitle">Options</h3>
        {this.renderPopupWidthInput()}
        {this.renderOrderOfCandidates()}
        {this.renderMaxResultsForEmpty()}
        {this.renderKeyBindings()}
        {this.renderHatenaUserName()}
        {this.renderTheme()}
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    popupWidth:         state.popupWidth,
    orderOfCandidates:  state.orderOfCandidates,
    maxResultsForEmpty: state.maxResultsForEmpty,
    enabledCJKMove:     state.enabledCJKMove,
    hatenaUserName:     state.hatenaUserName,
    theme:              state.theme,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    handlePopupWidthChange:         payload => dispatch({ type: 'POPUP_WIDTH', payload }),
    handleSortEnd:                  payload => dispatch({ type: 'CHANGE_ORDER', payload }),
    handleMaxResultsForEmptyChange: payload => dispatch({
      type: 'UPDATE_MAX_RESULTS_FOR_EMPTY',
      payload,
    }),
    handleCJKMoveChange: payload => dispatch({
      type: 'ENABLE_CJK_MOVE',
      payload,
    }),
    handleHatenaUserNameChange: payload => dispatch({ type: 'HATENA_USER_NAME', payload }),
    handleThemeChange:          payload => dispatch({ type: 'SET_THEME', payload }),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Options);