'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _extends2 = require('babel-runtime/helpers/extends');

var _extends3 = _interopRequireDefault(_extends2);

var _objectWithoutProperties2 = require('babel-runtime/helpers/objectWithoutProperties');

var _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2);

var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');

var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

var _createClass2 = require('babel-runtime/helpers/createClass');

var _createClass3 = _interopRequireDefault(_createClass2);

var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');

var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);

var _inherits2 = require('babel-runtime/helpers/inherits');

var _inherits3 = _interopRequireDefault(_inherits2);

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _propTypes = require('prop-types');

var _propTypes2 = _interopRequireDefault(_propTypes);

var _components = require('./components');

var components = _interopRequireWildcard(_components);

var _moment = require('moment');

var _moment2 = _interopRequireDefault(_moment);

var _lodash = require('lodash');

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var disabledRange = [-1, -1];
var roundMinutes = function roundMinutes(minutes) {
  return Math.round(minutes / 15.0) * 15;
};
var toHours = function toHours(momentTime) {
  return momentTime.hours() + momentTime.minutes() / 60.0;
};

/**
 * An intuitive time selector which works in 15 minute increments. Allows setting
 * a valid range of time to pick from. Please supply all time values as moment objects
 * which are rounded to 15 minutes. If your provided value is not rounded, an onChange
 * event will be immediately fired with the rounded value.
 */

var TimeScrubber = function (_React$Component) {
  (0, _inherits3.default)(TimeScrubber, _React$Component);

  function TimeScrubber() {
    var _ref;

    var _temp, _this, _ret;

    (0, _classCallCheck3.default)(this, TimeScrubber);

    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    return _ret = (_temp = (_this = (0, _possibleConstructorReturn3.default)(this, (_ref = TimeScrubber.__proto__ || (0, _getPrototypeOf2.default)(TimeScrubber)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
      offset: 0,
      dragging: false,
      grabPoint: null,
      potentialTime: null,
      internalValue: _this.props.value
    }, _this.viewportRef = (0, _react.createRef)(), _this.timelineElement = null, _this.offset = 0, _this.capToRange = function (position) {
      var _this$props = _this.props,
          tickSpacing = _this$props.tickSpacing,
          allowedRange = _this$props.allowedRange;

      var rangeSpan = (allowedRange[1] - allowedRange[0]) * 4 * tickSpacing;
      var minimum = allowedRange[0] * 4 * tickSpacing;
      var maximum = minimum + rangeSpan;
      return Math.min(maximum, Math.max(minimum, position));
    }, _this.enforceTimeRounding = function () {
      var _this$props2 = _this.props,
          value = _this$props2.value,
          onChange = _this$props2.onChange;

      if (onChange && value.minutes() % 15 !== 0) {
        var rounded = value.clone().minutes(roundMinutes(value.minutes())).seconds(0);
        onChange(rounded);
      }
    }, _this.getPosition = function (timeValue) {
      var tickSpacing = _this.props.tickSpacing;
      var internalValue = _this.state.internalValue;
      var _this2 = _this,
          offset = _this2.offset;

      var value = timeValue || internalValue;
      var hours = toHours(value);
      var rawPosition = tickSpacing * hours * 4 - offset;

      return _this.capToRange(rawPosition);
    }, _this.getTime = function (timeValue) {
      var tickSpacing = _this.props.tickSpacing;
      var internalValue = _this.state.internalValue;

      var realPosition = _this.getPosition(timeValue);
      var hoursWithPartial = realPosition / 4 / tickSpacing;
      var hours = Math.floor(hoursWithPartial);
      var minutes = Math.floor((hoursWithPartial - hours) * 60);
      var roundedMinutes = roundMinutes(minutes);
      return timeValue || internalValue.clone().hours(hours).minutes(roundedMinutes);
    }, _this.stepTime = function (stepMinutes) {
      var _this$props3 = _this.props,
          onChange = _this$props3.onChange,
          allowedRange = _this$props3.allowedRange;
      var internalValue = _this.state.internalValue;

      var newValue = internalValue.clone().add(stepMinutes, 'minutes');
      var hoursWithPartial = toHours(newValue);
      if (hoursWithPartial < allowedRange[0] || hoursWithPartial > allowedRange[1]) {
        return;
      }
      _this.setState({ internalValue: newValue });
      if (onChange) {
        onChange(newValue);
      }
    }, _this.incrementTime = function () {
      return _this.stepTime(15);
    }, _this.decrementTime = function () {
      return _this.stepTime(-15);
    }, _this.canIncrementTime = function () {
      return toHours(_this.state.internalValue) + 0.25 <= _this.props.allowedRange[1];
    }, _this.canDecrementTime = function () {
      return toHours(_this.state.internalValue) - 0.25 >= _this.props.allowedRange[0];
    }, _this.syncTimelinePosition = function (timeValue) {
      if (_this.timelineElement) {
        _this.timelineElement.style.left = '-' + _this.getPosition(timeValue) + 'px';
      }
    }, _this.getEventPosition = function (ev) {
      if (ev.touches) {
        return ev.touches.item(0).clientX;
      }
      return ev.clientX;
    }, _this.handleGrab = function (ev) {
      ev.preventDefault();
      var grabPoint = _this.getEventPosition(ev);
      _this.setState(function (_ref2) {
        var offset = _ref2.offset;
        return {
          dragging: true,
          grabPoint: grabPoint
        };
      });
    }, _this.handleDrag = function (ev) {
      if (!_this.state.dragging) {
        return;
      }

      var dragPoint = _this.getEventPosition(ev);
      _this.offset = dragPoint - _this.state.grabPoint;
      _this.syncTimelinePosition();

      _this.setState({ potentialTime: _this.getTime() });
    }, _this.handleRelease = function () {
      var tickSpacing = _this.props.tickSpacing;

      if (!_this.state.dragging) {
        return;
      }

      var newTime = _this.getTime();

      if (_this.props.onChange) {
        _this.props.onChange(newTime);
      }
      _this.setState({
        dragging: false,
        grabPoint: null,
        potentialTime: null,
        internalValue: newTime
      });

      _this.offset = 0;
      _this.syncTimelinePosition();

      if (_this.viewportRef.current) {
        _this.viewportRef.current.focus();
      }
    }, _this.handleKey = function (ev) {
      // setting dragging: true turns off the smoothing on the
      // timeline movement, which makes the component feel more
      // responsive
      if (ev.key === 'ArrowLeft') {
        _this.setState({ dragging: true });
        _this.decrementTime();
      } else if (ev.key === 'ArrowRight') {
        _this.setState({ dragging: true });
        _this.incrementTime();
      }
    }, _this.handleKeyUp = function () {
      _this.setState({ dragging: false });
    }, _this.timelineRef = function (el) {
      if (!el) {
        return;
      }

      _this.timelineElement = el;
      _this.syncTimelinePosition();
    }, _temp), (0, _possibleConstructorReturn3.default)(_this, _ret);
  }

  (0, _createClass3.default)(TimeScrubber, [{
    key: 'componentDidMount',
    value: function componentDidMount() {
      window.addEventListener('mousemove', this.handleDrag);
      window.addEventListener('mouseup', this.handleRelease);
      window.addEventListener('touchmove', this.handleDrag);
      window.addEventListener('touchend', this.handleRelease);
      this.enforceTimeRounding();
    }
  }, {
    key: 'componentDidUpdate',
    value: function componentDidUpdate(prevProps, prevState) {
      this.enforceTimeRounding();
      if (this.props.value !== prevProps.value) {
        this.setState({ internalValue: this.props.value });
      }
      if (this.state.internalValue !== prevState.internalValue) {
        this.syncTimelinePosition();
      }
    }
  }, {
    key: 'componentWillUnmount',
    value: function componentWillUnmount() {
      window.removeEventListener('mousemove', this.handleDrag);
      window.removeEventListener('mouseup', this.handleRelease);
      window.removeEventListener('touchmove', this.handleDrag);
      window.removeEventListener('touchend', this.handleRelease);
    }

    /**
     * Restricts a position to only be within the allowed range
     */


    /**
     * Takes the current time value and rounds it to 15 minutes.
     * Called whenever a user-provided value is supplied to the component
     * to ensure it is always rounded.
     */


    /**
     * Returns the physical timeline position based on the current
     * scrubbed time. Optionally, you can provide your own base time value
     * as the first parameter. In both cases, any offset from user interaction
     * will be added to the final position.
     *
     * You can pass a time object to override the internal value.
     */


    /**
     * Converts the current real scrubber position into a time value.
     */


    /**
     * Bumps the time up or down by the provided number of minutes.
     */


    /**
     * Forces the timeline element's position to be synchronized with
     * the current calculated position value based on the time value
     * and user input.
     *
     * You can pass a time object to override the current internal value
     */


    /**
     * Called when the user first starts manipulating the timeline.
     */


    /**
     * We modify the element position on the timeline directly
     * rather than store it in state. Since it's a Pure component,
     * it doesn't get re-rendered as we move, which is critical
     * for performance.
     */


    /**
     * Called when the user releases the timeline. Sets the time
     * value and calls onChange.
     */


    /**
     * Handles keyboard events on the component so users can
     * scrub time with arrow keys.
     */


    /**
     * Resets the dragging state after the user releases
     * a key
     */

  }, {
    key: 'render',
    value: function render() {
      var _state = this.state,
          offset = _state.offset,
          dragging = _state.dragging,
          potentialTime = _state.potentialTime,
          internalValue = _state.internalValue;
      var _props = this.props,
          tickSpacing = _props.tickSpacing,
          allowedRange = _props.allowedRange,
          disabled = _props.disabled,
          Container = _props.Container,
          NudgeButton = _props.NudgeButton,
          Viewport = _props.Viewport,
          Timeline = _props.Timeline,
          rest = (0, _objectWithoutProperties3.default)(_props, ['tickSpacing', 'allowedRange', 'disabled', 'Container', 'NudgeButton', 'Viewport', 'Timeline']);


      var maximum = this.props.tickSpacing * 4 * 25;
      var minimum = 0;

      var position = this.getPosition();

      return _react2.default.createElement(
        Container,
        (0, _extends3.default)({ onKeyDown: this.handleKey, onKeyUp: this.handleKeyUp }, rest),
        _react2.default.createElement(NudgeButton, {
          direction: 'left',
          onClick: this.decrementTime,
          disabled: disabled || !this.canDecrementTime(),
          type: 'button'
        }),
        _react2.default.createElement(
          Viewport,
          {
            onMouseDown: this.handleGrab,
            onTouchStart: this.handleGrab,
            disabled: disabled,
            time: potentialTime || internalValue,
            innerRef: this.viewportRef
          },
          _react2.default.createElement(Timeline, {
            dragging: dragging,
            tickSpacing: tickSpacing,
            allowedRange: disabled ? disabledRange : allowedRange,
            innerRef: this.timelineRef
          })
        ),
        _react2.default.createElement(NudgeButton, {
          direction: 'right',
          onClick: this.incrementTime,
          disabled: disabled || !this.canIncrementTime(),
          type: 'button'
        })
      );
    }
  }]);
  return TimeScrubber;
}(_react2.default.Component);

TimeScrubber.propTypes = {
  /**
   * A MomentJS object which represents the current time. You can
   * provide an object with a date included, and the date will not
   * be modified.
   */
  value: _propTypes2.default.object.isRequired,
  /**
   * Called when the time value is changed with the new value. This
   * is not called while the user is scrubbing; only when they release.
   */
  onChange: _propTypes2.default.func.isRequired,
  /**
   * An array with two values. The first is the time (in hours) which
   * represents an *inclusive* start to the allowed time range. The
   * second represents the *inclusive* end of the range. Values should
   * be decimals representing hour values between 0 and 24
   * (like "13.5" for 1:30PM)
   */
  allowedRange: _propTypes2.default.arrayOf(_propTypes2.default.number),
  /**
   * Determines whether the user can interact with the control
   */
  disabled: _propTypes2.default.bool,
  /**
   * Adjusts the spacing between tickmarks. Include the width
   * of the mark itself (1px) in the provided spacing total.
   */
  tickSpacing: _propTypes2.default.number,
  /**
   * A component that can be provided to customize
   * the rendering behavior of the outer container
   */
  Container: _propTypes2.default.func,
  /**
   * A component that can be provided to customize
   * the rendering behavior of the buttons
   */
  NudgeButton: _propTypes2.default.func,
  /**
   * A component that can be provided to customize
   * the rendering behavior of the viewport window
   */
  Viewport: _propTypes2.default.func,
  /**
   * A component that can be provided to customize
   * the rendering behavior of the timeline bar
   */
  Timeline: _propTypes2.default.func
};
TimeScrubber.defaultProps = {
  tickSpacing: 11,
  value: (0, _moment2.default)().hour(12).minute(0).second(0),
  disabled: false,
  allowedRange: [0, 23.75],
  Container: components.Container,
  NudgeButton: components.NudgeButton,
  Viewport: components.Viewport,
  Timeline: components.Timeline
};
exports.default = TimeScrubber;