'use strict';

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

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

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);

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

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

// 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 = 10;

var Rect = _styledComponents2.default.div.withConfig({
  displayName: 'DragBox__Rect'
})(['border-width:2px;border-color:black;border-style:dotted;z-index:100000;position:fixed;pointer-events:none;']);

// Div that allows us to disable pointer events on children - this helps to avoid the hoverable behavior on children.
var DragBoxDiv = _styledComponents2.default.div.withConfig({
  displayName: 'DragBox__DragBoxDiv'
})(['', ';'], function (_ref) {
  var disablePointerEvents = _ref.disablePointerEvents;
  return disablePointerEvents ? (0, _styledComponents.css)(['& > *{pointer-events:none;}']) : null;
});

var DragBox = function (_React$Component) {
  _inherits(DragBox, _React$Component);

  function DragBox() {
    var _ref2;

    var _temp, _this, _ret;

    _classCallCheck(this, DragBox);

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

    return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref2 = DragBox.__proto__ || Object.getPrototypeOf(DragBox)).call.apply(_ref2, [this].concat(args))), _this), _this.state = {
      mouseDown: false,
      start: null,
      end: null,
      collisions: new Set(),
      scrollLeft: 0,
      scrollTop: 0
    }, _this.childElements = {}, _this.componentDidMount = function () {
      var el = window.document.querySelector(_this.props.scrollSelector) || window.document;
      el.addEventListener('scroll', _this.debouncedScroll, 16);
      window.document.addEventListener('mousemove', _this.onMouseMove);
      window.document.addEventListener('mouseup', _this.onMouseUp);
    }, _this.componentWillUnmount = function () {
      var el = window.document.querySelector(_this.props.scrollSelector) || window.document;
      el.removeEventListener('scroll', _this.debouncedScroll);
      window.document.removeEventListener('mousemove', _this.onMouseMove);
      window.document.removeEventListener('mouseup', _this.onMouseUp);
    }, _this.getMousePosition = function (ev) {
      return _this.addScroll({ x: ev.pageX, y: ev.pageY });
    }, _this.__handleScroll = function (ev) {
      var _this2 = _this,
          _this2$state = _this2.state,
          lastScrollLeft = _this2$state.scrollLeft,
          lastScrollTop = _this2$state.scrollTop,
          lastEnd = _this2$state.end;

      var el = ev.target;
      var scrollLeft = el.scrollLeft,
          scrollTop = el.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;
      _this.setState({ end: end, scrollLeft: scrollLeft, scrollTop: scrollTop });
      _this.checkChildrenBoxCollisions();
    }, _this.debouncedScroll = (0, _lodash.debounce)(_this.__handleScroll, 16), _this.addScroll = function (_ref3) {
      var x = _ref3.x,
          y = _ref3.y;
      return {
        x: x + _this.state.scrollLeft,
        y: y + _this.state.scrollTop
      };
    }, _this.onMouseDown = function (ev) {
      var _this3 = _this,
          onMouseDown = _this3.props.onMouseDown,
          getMousePosition = _this3.getMousePosition;
      // Left click

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

      if (!start || !end) return {};
      var parent = _this.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)
      };
    }, _this.checkChildrenBoxCollisions = function () {
      var _this5 = _this,
          _this5$props = _this5.props,
          onCollisionBegin = _this5$props.onCollisionBegin,
          onCollisionEnd = _this5$props.onCollisionEnd,
          onCollisionChange = _this5$props.onCollisionChange,
          collisions = _this5.state.collisions;

      var collisionBox = _this.calcRect();
      // Divide all clickable refs into two arrays based on whether they are colliding or not.

      var _partition = (0, _lodash.partition)(Object.keys(_this.childElements), function (key) {
        var node = _reactDom2.default.findDOMNode(_this.childElements[key]);
        var childRect = node.getBoundingClientRect();
        // We are looking for intersections of the edges of the rectangle.
        return _this.checkBoxCollision(childRect, collisionBox) && !_this.checkBoxContains(childRect, collisionBox);
      }),
          _partition2 = _slicedToArray(_partition, 2),
          colliding = _partition2[0],
          notColliding = _partition2[1];

      // 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 });
      }
      _this.setState({ collisions: collisions });
    }, _this.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;
    }, _this.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;
    }, _this.shouldDrawRect = function (rect) {
      var _this6 = _this,
          mouseDown = _this6.state.mouseDown;

      return mouseDown && rect.width * rect.height >= MIN_SIZE_DRAGBOX;
    }, _this.renderRect = function (rect) {
      var _this7 = _this,
          renderRect = _this7.props.renderRect,
          mouseDown = _this7.state.mouseDown,
          shouldDrawRect = _this7.shouldDrawRect;

      if (!shouldDrawRect(rect)) return;
      return renderRect({
        left: rect.x,
        top: rect.y,
        width: rect.width,
        height: rect.height
      });
    }, _this.getRef = function (key) {
      return function (el) {
        if (el === null) delete _this.childElements[el];else _this.childElements[key] = el;
      };
    }, _this.renderContents = function () {
      var _this8 = _this,
          _this8$props = _this8.props,
          onCollisionChange = _this8$props.onCollisionChange,
          onCollisionBegin = _this8$props.onCollisionBegin,
          onCollisionEnd = _this8$props.onCollisionEnd,
          onMouseUp = _this8$props.onMouseUp,
          onMouseDown = _this8$props.onMouseDown,
          renderContents = _this8$props.renderContents,
          collisions = _this8.state.collisions,
          getRef = _this8.getRef;

      return renderContents({
        onMouseDown: onMouseDown,
        onMouseUp: onMouseUp,
        onCollisionChange: onCollisionChange,
        onCollisionBegin: onCollisionBegin,
        onCollisionEnd: onCollisionEnd,
        collisions: collisions,
        getRef: getRef
      });
    }, _this.refDragElement = function (el) {
      return _this.dragElement = el;
    }, _temp), _possibleConstructorReturn(_this, _ret);
  }

  // Add scroll delta to our mouse position


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


  // 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


  // Checks if b1 contains b2


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


  _createClass(DragBox, [{
    key: 'render',
    value: function render() {
      var refDragElement = this.refDragElement,
          onMouseDown = this.onMouseDown,
          renderRect = this.renderRect,
          renderContents = this.renderContents,
          calcRect = this.calcRect;

      var rect = calcRect();
      return _react2.default.createElement(
        DragBoxDiv,
        {
          ref: refDragElement,
          onMouseDown: onMouseDown,
          disablePointerEvents: 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
};
DragBox.defaultProps = {
  onMouseDown: _lodash.noop,
  onMouseUp: _lodash.noop,
  onCollisionChange: _lodash.noop,
  onCollisionBegin: _lodash.noop,
  onCollisionEnd: _lodash.noop,
  renderContents: _lodash.noop,
  scrollSelector: 'body',
  renderRect: function renderRect(_ref4) {
    var left = _ref4.left,
        top = _ref4.top,
        width = _ref4.width,
        height = _ref4.height;
    return _react2.default.createElement(Rect, {
      style: {
        left: left,
        top: top,
        width: width,
        height: height
      }
    });
  }
};


DragBox.Select = _DragBoxSelect2.default;

exports.default = DragBox;