Blob Blame History Raw
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { isKeyIn } = require("../utils/key");

const Constants = require("../constants");
const Types = require("../types");

/**
 * Get the increment/decrement step to use for the provided key event.
 */
function getIncrement(event) {
  const defaultIncrement = 1;
  const largeIncrement = 100;
  const mediumIncrement = 10;

  let increment = 0;
  let key = event.keyCode;

  if (isKeyIn(key, "UP", "PAGE_UP")) {
    increment = 1 * defaultIncrement;
  } else if (isKeyIn(key, "DOWN", "PAGE_DOWN")) {
    increment = -1 * defaultIncrement;
  }

  if (event.shiftKey) {
    if (isKeyIn(key, "PAGE_UP", "PAGE_DOWN")) {
      increment *= largeIncrement;
    } else {
      increment *= mediumIncrement;
    }
  }

  return increment;
}

class ViewportDimension extends Component {
  static get propTypes() {
    return {
      viewport: PropTypes.shape(Types.viewport).isRequired,
      onChangeSize: PropTypes.func.isRequired,
      onRemoveDeviceAssociation: PropTypes.func.isRequired,
    };
  }

  constructor(props) {
    super(props);
    let { width, height } = props.viewport;

    this.state = {
      width,
      height,
      isEditing: false,
      isInvalid: false,
    };

    this.validateInput = this.validateInput.bind(this);
    this.onInputBlur = this.onInputBlur.bind(this);
    this.onInputChange = this.onInputChange.bind(this);
    this.onInputFocus = this.onInputFocus.bind(this);
    this.onInputKeyDown = this.onInputKeyDown.bind(this);
    this.onInputKeyUp = this.onInputKeyUp.bind(this);
    this.onInputSubmit = this.onInputSubmit.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    let { width, height } = nextProps.viewport;

    this.setState({
      width,
      height,
    });
  }

  validateInput(value) {
    let isInvalid = true;

    // Check the value is a number and greater than MIN_VIEWPORT_DIMENSION
    if (/^\d{3,4}$/.test(value) &&
        parseInt(value, 10) >= Constants.MIN_VIEWPORT_DIMENSION) {
      isInvalid = false;
    }

    this.setState({
      isInvalid,
    });
  }

  onInputBlur() {
    let { width, height } = this.props.viewport;

    if (this.state.width != width || this.state.height != height) {
      this.onInputSubmit();
    }

    this.setState({
      isEditing: false,
      inInvalid: false,
    });
  }

  onInputChange({ target }, callback) {
    if (target.value.length > 4) {
      return;
    }

    if (this.refs.widthInput == target) {
      this.setState({ width: target.value }, callback);
      this.validateInput(target.value);
    }

    if (this.refs.heightInput == target) {
      this.setState({ height: target.value }, callback);
      this.validateInput(target.value);
    }
  }

  onInputFocus() {
    this.setState({
      isEditing: true,
    });
  }

  onInputKeyDown(event) {
    let { target } = event;
    let increment = getIncrement(event);
    if (!increment) {
      return;
    }
    target.value = parseInt(target.value, 10) + increment;
    this.onInputChange(event, this.onInputSubmit);
  }

  onInputKeyUp({ target, keyCode }) {
    // On Enter, submit the input
    if (keyCode == 13) {
      this.onInputSubmit();
    }

    // On Esc, blur the target
    if (keyCode == 27) {
      target.blur();
    }
  }

  onInputSubmit() {
    if (this.state.isInvalid) {
      let { width, height } = this.props.viewport;

      this.setState({
        width,
        height,
        isInvalid: false,
      });

      return;
    }

    // Change the device selector back to an unselected device
    // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
    if (this.props.viewport.device) {
      this.props.onRemoveDeviceAssociation();
    }
    this.props.onChangeSize(parseInt(this.state.width, 10),
                            parseInt(this.state.height, 10));
  }

  render() {
    let editableClass = "viewport-dimension-editable";
    let inputClass = "viewport-dimension-input";

    if (this.state.isEditing) {
      editableClass += " editing";
      inputClass += " editing";
    }

    if (this.state.isInvalid) {
      editableClass += " invalid";
    }

    return dom.div(
      {
        className: "viewport-dimension",
      },
      dom.div(
        {
          className: editableClass,
        },
        dom.input({
          ref: "widthInput",
          className: inputClass,
          size: 4,
          value: this.state.width,
          onBlur: this.onInputBlur,
          onChange: this.onInputChange,
          onFocus: this.onInputFocus,
          onKeyDown: this.onInputKeyDown,
          onKeyUp: this.onInputKeyUp,
        }),
        dom.span({
          className: "viewport-dimension-separator",
        }, "×"),
        dom.input({
          ref: "heightInput",
          className: inputClass,
          size: 4,
          value: this.state.height,
          onBlur: this.onInputBlur,
          onChange: this.onInputChange,
          onFocus: this.onInputFocus,
          onKeyDown: this.onInputKeyDown,
          onKeyUp: this.onInputKeyUp,
        })
      )
    );
  }
}

module.exports = ViewportDimension;