'use strict';

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

var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray');

var _slicedToArray3 = _interopRequireDefault(_slicedToArray2);

var _set = require('babel-runtime/core-js/set');

var _set2 = _interopRequireDefault(_set);

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 _reactDom = require('react-dom');

var _reactDom2 = _interopRequireDefault(_reactDom);

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

var _propTypes2 = _interopRequireDefault(_propTypes);

var _styledComponents = require('styled-components');

var _styledComponents2 = _interopRequireDefault(_styledComponents);

var _lodash = require('lodash');

var _DragBoxSelect = require('./DragBoxSelect');

var _DragBoxSelect2 = _interopRequireDefault(_DragBoxSelect);

var _DragBoxRect = require('./styles/DragBoxRect');

var _DragBoxRect2 = _interopRequireDefault(_DragBoxRect);

var _DragBoxDiv = require('./styles/DragBoxDiv');

var _DragBoxDiv2 = _interopRequireDefault(_DragBoxDiv);

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

// Minimum size for the dragbox to become active and prevent click through. This value is based on feel - it helps
// avoid missed clicks on buttons when moving the mouse inaccurately.
// TODO: We should probably turn on drag box once this size is met and not turn it off until
// the mouse button is released
var MIN_SIZE_DRAGBOX = 16;

//Empty class to attach ref to.

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

  function DragBoxItem() {
    (0, _classCallCheck3.default)(this, DragBoxItem);
    return (0, _possibleConstructorReturn3.default)(this, (DragBoxItem.__proto__ || (0, _getPrototypeOf2.default)(DragBoxItem)).apply(this, arguments));
  }

  (0, _createClass3.default)(DragBoxItem, [{
    key: 'render',
    value: function render() {
      return this.props.children;
    }
  }]);
  return DragBoxItem;
}(_react2.default.Component);

/**
 * **DragBox** creates an area where the user can drag a box that can then control other types of behavior. It is intended to
 * be used with the [Selectable](/#!/Selectable) behavior, though it can be used for general purposes. For a prebuilt implementation
 * of **DragBox** with [Selectable](/#!/Selectable), use [DragBox.Select](#!/DragBoxSelect).
 *
 * **DragBox** uses render props, which means that it takes functions that it passes several arguments and uses the result
 * of that function to perform renders. This allows it to be easily composed with other types of components.
 *
 * **DragBox** requires that the `getRef()` function be passed down and called in the `ref` property of each React
 * component that should be selectable. Note that this requires that the component have an instance - it cannot be a
 * purely functional component. **DragBox.Item** is provided as a simple wrapper component for this purpose.
 */


var DragBox = function (_React$Component2) {
  (0, _inherits3.default)(DragBox, _React$Component2);

  function DragBox() {
    var _ref;

    var _temp, _this2, _ret;

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

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

    return _ret = (_temp = (_this2 = (0, _possibleConstructorReturn3.default)(this, (_ref = DragBox.__proto__ || (0, _getPrototypeOf2.default)(DragBox)).call.apply(_ref, [this].concat(args))), _this2), _this2.state = {
      mouseDown: false,
      start: null,
      end: null,
      collisions: new _set2.default(),
      scrollLeft: 0,
      scrollTop: 0
    }, _this2.getScrollElement = function () {
      return _this2.props.scrollSelector && window.document.querySelector(_this2.props.scrollSelector) || window.document.documentElement;
    }, _this2.getScroll = function () {
      return (0, _lodash.pick)(_this2.getScrollElement(), ['scrollLeft', 'scrollTop']);
    }, _this2.attachScrollSelector = function () {
      var el = _this2.getScrollElement();
      el.addEventListener('scroll', _this2.throttledScroll);
      window.document.addEventListener('mousemove', _this2.onMouseMove);
      window.document.addEventListener('mouseup', _this2.onMouseUp);
    }, _this2.detachScrollSelector = function () {
      var el = _this2.getScrollElement();
      el.removeEventListener('scroll', _this2.throttledScroll);
      window.document.removeEventListener('mousemove', _this2.onMouseMove);
      window.document.removeEventListener('mouseup', _this2.onMouseUp);
    }, _this2.getMousePosition = function (ev) {
      var customScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;

      if (_this2.getScrollElement() === window.document.documentElement) {
        return { x: ev.pageX, y: ev.pageY };
      }
      return _this2.addScroll({ x: ev.pageX, y: ev.pageY }, customScroll);
    }, _this2.__handleScroll = function (ev) {
      var _this3 = _this2,
          _this3$state = _this3.state,
          lastScrollLeft = _this3$state.scrollLeft,
          lastScrollTop = _this3$state.scrollTop,
          lastEnd = _this3$state.end;

      var _this2$getScroll = _this2.getScroll(),
          scrollLeft = _this2$getScroll.scrollLeft,
          scrollTop = _this2$getScroll.scrollTop;
      // If the mouse is being held down as we scroll, we need to adjust the rectangle


      var end = lastEnd ? {
        x: lastEnd.x + (scrollLeft - lastScrollLeft),
        y: lastEnd.y + (scrollTop - lastScrollTop)
      } : null;
      _this2.setState({ end: end, scrollLeft: scrollLeft, scrollTop: scrollTop });
      _this2.checkChildrenBoxCollisions();
    }, _this2.throttledScroll = (0, _lodash.throttle)(_this2.__handleScroll, 50), _this2.addScroll = function (_ref2) {
      var x = _ref2.x,
          y = _ref2.y;
      var customScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
      return {
        x: x + (customScroll || _this2.state).scrollLeft,
        y: y + (customScroll || _this2.state).scrollTop
      };
    }, _this2.onMouseDown = function (ev) {
      var _this4 = _this2,
          onMouseDown = _this4.props.onMouseDown,
          getMousePosition = _this4.getMousePosition;
      // Left click

      if (ev.button !== 0) return;
      _this2.attachScrollSelector();
      // When we start dragging, it's possible that we dragged a bunch
      // and now our scroll state is invalid. We update the scroll state
      // while using the new scroll state to calculate the mouse positions

      var _this2$getScroll2 = _this2.getScroll(),
          scrollLeft = _this2$getScroll2.scrollLeft,
          scrollTop = _this2$getScroll2.scrollTop;

      _this2.setState({
        scrollLeft: scrollLeft,
        scrollTop: scrollTop,
        mouseDown: true,
        start: _this2.getMousePosition(ev, { scrollLeft: scrollLeft, scrollTop: scrollTop }),
        end: _this2.getMousePosition(ev, { scrollLeft: scrollLeft, scrollTop: scrollTop })
      });
      _this2.props.onMouseDown();
    }, _this2.onMouseUp = function (ev) {
      if (!_this2.state.mouseDown) {
        return;
      }
      if (ev.button !== 0) return;
      _this2.detachScrollSelector();
      _this2.props.onMouseUp(_this2.state.collisions);
      _this2.setState({
        mouseDown: false,
        start: null,
        end: null,
        collisions: new _set2.default()
      });
    }, _this2.onMouseMove = function (ev) {
      if (!_this2.state.mouseDown) {
        return;
      }
      ev.preventDefault();
      _this2.setState({ end: _this2.getMousePosition(ev) });
      _this2.checkChildrenBoxCollisions();
    }, _this2.calcRect = function () {
      var _this5 = _this2,
          _this5$state = _this5.state,
          start = _this5$state.start,
          end = _this5$state.end,
          scrollLeft = _this5$state.scrollLeft,
          scrollTop = _this5$state.scrollTop;

      if (!start || !end) return {};
      var parent = _this2.dragElement;
      return {
        x: Math.min(start.x, end.x) - scrollLeft,
        y: Math.min(start.y, end.y) - scrollTop,
        width: Math.abs(start.x - end.x),
        height: Math.abs(start.y - end.y)
      };
    }, _this2.checkChildrenBoxCollisions = function () {
      var _this6 = _this2,
          _this6$props = _this6.props,
          onCollisionBegin = _this6$props.onCollisionBegin,
          onCollisionEnd = _this6$props.onCollisionEnd,
          onCollisionChange = _this6$props.onCollisionChange,
          collisions = _this6.state.collisions;

      var collisionBox = _this2.calcRect();
      // Divide all clickable refs into two arrays based on whether they are colliding or not.
      var itemElements = _this2.dragElement.querySelectorAll('[data-drag-box-key]');

      var _partition = (0, _lodash.partition)(itemElements, function (node) {
        var childRect = node.getBoundingClientRect();
        // We are looking for intersections of the edges of the rectangle.
        return _this2.checkBoxCollision(childRect, collisionBox) && !_this2.checkBoxContains(childRect, collisionBox);
      }),
          _partition2 = (0, _slicedToArray3.default)(_partition, 2),
          collidingNodes = _partition2[0],
          notCollidingNodes = _partition2[1];

      var colliding = collidingNodes.map(function (node) {
        return node.getAttribute('data-drag-box-key');
      });
      var notColliding = notCollidingNodes.map(function (node) {
        return node.getAttribute('data-drag-box-key');
      });

      // Pull out elements that haven't changed their collision state.
      var added = colliding.filter(function (key) {
        return !collisions.has(key);
      });
      var removed = notColliding.filter(function (key) {
        return collisions.has(key);
      });

      //Update collisions set
      added.forEach(function (i) {
        return collisions.add(i);
      });
      removed.forEach(function (i) {
        return collisions.delete(i);
      });

      //Invoke callbacks if necessary
      if (added.length > 0) {
        onCollisionBegin(added);
      }
      if (removed.length > 0) {
        onCollisionEnd(removed);
      }
      if (added.length > 0 || removed.length > 0) {
        onCollisionChange({ added: added, removed: removed });
      }
      _this2.setState({ collisions: collisions });
    }, _this2.checkBoxCollision = function (b1, b2) {
      return b1.x < b2.x + b2.width && b1.x + b1.width > b2.x && b1.y < b2.y + b2.height && b1.height + b1.y > b2.y;
    }, _this2.checkBoxContains = function (b1, b2) {
      return b1.x <= b2.x && b2.x <= b1.x + b1.width && b1.x <= b2.x + b2.width && b2.x + b2.width <= b1.x + b1.width && b1.y <= b2.y && b2.y <= b1.y + b1.height && b1.y <= b2.y + b2.height && b2.y + b2.height <= b1.y + b1.height;
    }, _this2.shouldDrawRect = function (rect) {
      var _this7 = _this2,
          mouseDown = _this7.state.mouseDown;

      return mouseDown && (rect.width + 1) * (rect.height + 1) >= MIN_SIZE_DRAGBOX;
    }, _this2.renderRect = function (rect) {
      var _this8 = _this2,
          renderRect = _this8.props.renderRect,
          mouseDown = _this8.state.mouseDown,
          shouldDrawRect = _this8.shouldDrawRect;

      if (!shouldDrawRect(rect)) return;
      return renderRect({
        left: rect.x,
        top: rect.y,
        width: rect.width,
        height: rect.height
      });
    }, _this2.getRef = function (key) {
      return function (el) {
        if (el !== null) {
          el.setAttribute('data-drag-box-key', key);
        }
      };
    }, _this2.itemRef = function (el) {
      if (el === null) {
        return;
      }

      var key = el.getAttribute('data-drag-box-key');
      if (!key) {
        throw new Error("You used the `ref` mode for DragBox, but did not provide a `data-drag-box-key` attribute to the referenced element. This is not allowed. If you can't provide a custom data prop to the element, please use the `getRef(key)` mode");
      }
    }, _this2.renderContents = function () {
      var _this9 = _this2,
          _this9$props = _this9.props,
          onCollisionChange = _this9$props.onCollisionChange,
          onCollisionBegin = _this9$props.onCollisionBegin,
          onCollisionEnd = _this9$props.onCollisionEnd,
          onMouseUp = _this9$props.onMouseUp,
          onMouseDown = _this9$props.onMouseDown,
          renderContents = _this9$props.renderContents,
          collisions = _this9.state.collisions,
          getRef = _this9.getRef,
          itemRef = _this9.itemRef;

      return renderContents({
        onMouseDown: onMouseDown,
        onMouseUp: onMouseUp,
        onCollisionChange: onCollisionChange,
        onCollisionBegin: onCollisionBegin,
        onCollisionEnd: onCollisionEnd,
        collisions: collisions,
        getRef: getRef,
        ref: itemRef
      });
    }, _this2.refDragElement = function (el) {
      return _this2.dragElement = el;
    }, _temp), (0, _possibleConstructorReturn3.default)(_this2, _ret);
  }

  // Add scroll delta to our mouse position if the scroll context element
  // is not already the window.


  // We can't get the new mouse position on scroll, so we determine the scroll delta
  // and recalculate the mouse position from that. Use throttledScroll instead of using this function directly.


  // Scrolling is throttled so that we avoid doing costly collision calculations
  // and rerenders on every frame. Instead, we limit it to 20 times a second,
  // which still gives high visual fidelity.


  // Checks the collisions with the draggable box against the children. The collision deltas are then used
  // to invoke callbacks that consumers can bind to.


  // Checks if b1 collides with b2
  // prettier-ignore


  // Checks if b1 contains b2
  // prettier-ignore


  // Keep track of collidable child refs; ref functions are called with null when they are unmounted.


  (0, _createClass3.default)(DragBox, [{
    key: 'render',
    value: function render() {
      var refDragElement = this.refDragElement,
          onMouseDown = this.onMouseDown,
          renderRect = this.renderRect,
          renderContents = this.renderContents,
          calcRect = this.calcRect,
          renderItem = this.renderItem,
          _props = this.props,
          disablePointerEventsWhileDragging = _props.disablePointerEventsWhileDragging,
          children = _props.children;

      var rect = calcRect();
      return _react2.default.createElement(
        _DragBoxDiv2.default,
        {
          innerRef: refDragElement,
          onMouseDown: onMouseDown,
          disablePointerEvents: disablePointerEventsWhileDragging && this.shouldDrawRect(rect)
        },
        renderRect(rect),
        renderContents()
      );
    }
  }]);
  return DragBox;
}(_react2.default.Component);

DragBox.propTypes = {
  /**
   * Callback invoked when the mouse is clicked in the **DragBox** area
   */
  onMouseDown: _propTypes2.default.func,
  /**
   * Callback invoked when the mouse is released in the **DragBox** area.
   * Passed the **Set** of collision keys as its argument.
   */
  onMouseUp: _propTypes2.default.func,
  /**
   * Callback invoked when the **DragBox** box set of collisions changes.
   * Called with an object that has two keys, `added` and `removed`, each
   * with an array of items that were just added or removed respectively
   * to the collision set.
   */
  onCollisionChange: _propTypes2.default.func,
  /**
   * Callback invoked when the **DragBox** box collides with new items
   * that it wasn't colliding with at the previous mouse position.
   * Called with an array of items that have just been collided with.
   */
  onCollisionBegin: _propTypes2.default.func,
  /**
   * Callback invoked when the **DragBox** box stops colliding with items
   * that it was colliding with at the previous mouse position.
   * Called with an array of items that have just stopped colliding.
   */
  onCollisionEnd: _propTypes2.default.func,
  /**
   * Render prop to render contents inside **DragBox**.
   * Supports all callbacks that **DragBox** itself supports as well as the following props:
   *
   * *collisions*: The **Set** of keys that are currently being collided with
   *
   * *getRef*: A function that should be passed into the `ref` property of each child
   * that can be selected by **DragBox**. It should be called with a unique key
   * for each component. For example, `<button ref={getRef("uniquekey")} key="uniquekey" />`.
   */
  renderContents: _propTypes2.default.func,
  /**
   * Render prop to render the draggable rectangle. It is passed an object with the left, top, width, and height of
   * the rectangle to be drawn should return the component to render.
   */
  renderRect: _propTypes2.default.func,
  /**
   * The selector for the element to attach the scroll listener to. Defaults to page body. Do not change after initial call.
   */
  scrollSelector: _propTypes2.default.string,
  /**
   * Disables mouse events (such as hover) on children while dragging.
   */
  disablePointerEventsWhileDragging: _propTypes2.default.bool
};
DragBox.defaultProps = {
  onMouseDown: _lodash.noop,
  onMouseUp: _lodash.noop,
  onCollisionChange: _lodash.noop,
  onCollisionBegin: _lodash.noop,
  onCollisionEnd: _lodash.noop,
  renderContents: _lodash.noop,
  scrollSelector: null,
  disablePointerEventsWhileDragging: false,
  renderRect: function renderRect(_ref3) {
    var left = _ref3.left,
        top = _ref3.top,
        width = _ref3.width,
        height = _ref3.height;
    return _react2.default.createElement(_DragBoxRect2.default, {
      style: {
        left: left,
        top: top,
        width: width,
        height: height
      }
    });
  }
};
DragBox.Item = DragBoxItem;


DragBox.Select = _DragBoxSelect2.default;

exports.default = DragBox;