'use strict';

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

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

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 _propTypes = require('prop-types');

var _propTypes2 = _interopRequireDefault(_propTypes);

var _consts = require('./consts');

var _consts2 = _interopRequireDefault(_consts);

var _utils = require('./utils');

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

var SCROLL_RANGE = 150;
var SCROLL_ACC_PX = 10;

/* eslint-disable no-restricted-globals */

var DragNDropContainer = function DragNDropContainer(WrappedComponent) {
  var Wrapper = function (_React$Component) {
    _inherits(Wrapper, _React$Component);

    function Wrapper(props) {
      _classCallCheck(this, Wrapper);

      var _this = _possibleConstructorReturn(this, (Wrapper.__proto__ || Object.getPrototypeOf(Wrapper)).call(this, props));

      _this.state = {
        isDragging: false,
        sourceIndex: -1,
        lastOverIndex: -1, // as drag over does not happen sequentially, we need to store last over index and render it when new element is being dragged over
        overIndex: -1,
        isKeyboardMoving: false,
        curPreview: '',
        keyInsertIndex: -1
      };

      _this.setClientY = function (e) {
        _this.clientY = e.clientY;
      };

      _this.leaveKeyboardMoving = function () {
        if (_this.state.isKeyboardMoving) {
          _this.setState({
            isDragging: false,
            isKeyboardMoving: false,
            sourceIndex: -1,
            keyInsertIndex: -1
          });
        }
      };

      _this.onDrag = function (e, dragPreviewRef) {
        // position move is out of control of react render, so we use id instead of ref
        if (_this.props.boundingElementId && !(0, _utils.isEdge)()) {
          // In some browsers (e.g. Chrome, Safari) the preview item does not get fixed position if an ancestor element uses translateZ
          // See: https://stackoverflow.com/questions/15194313/transform3d-not-working-with-position-fixed-children
          // boundingElementId represents an alternate ancestor/container element that can be used to calculate the position while dragging
          dragPreviewRef.style.top = _this.clientY - document.getElementById(_this.props.boundingElementId).getBoundingClientRect().top + 'px'; // eslint-disable-line
        } else {
          dragPreviewRef.style.top = _this.clientY + 'px'; // eslint-disable-line no-param-reassign
        }

        // increase scroll area
        if (_this.clientY < SCROLL_RANGE) {
          if (_this.scrollContainer) {
            _this.scrollContainer.scrollTop -= SCROLL_ACC_PX;
          } else {
            scrollBy(0, 0 - SCROLL_ACC_PX);
          }
        } else if (innerHeight - _this.clientY < SCROLL_RANGE) {
          if (_this.scrollContainer) {
            _this.scrollContainer.scrollTop += SCROLL_ACC_PX;
          } else {
            scrollBy(0, SCROLL_ACC_PX);
          }
        }
      };

      _this.onDragStart = function (e, index) {
        /**
         * Updating state causes the dnd item to re-render thus manipulating the DOM. In some cases
         * this causes dragend event to fire immediately. To prevent this wrap in a timeout.
         *
         * See https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
         */
        setTimeout(function () {
          _this.setState({
            isDragging: true,
            isKeyboardMoving: false,
            sourceIndex: index,
            overIndex: -1
          });
        });
      };

      _this.onDragEnd = function (e) {
        if (_this.containerRef) {
          e.preventDefault();
          _this.setState({
            isDragging: false,
            isKeyboardMoving: false,
            sourceIndex: -1,
            overIndex: -1
          });
        }
      };

      _this.onDrop = function (e, targetIndex) {
        var items = _this.props.items;

        e.preventDefault();
        var newOrderItems = (0, _utils.moveItem)(items, targetIndex, _this.state.sourceIndex);
        var sourceDragItem = items[_this.state.sourceIndex];
        _this.props.onReorderItem(newOrderItems, sourceDragItem);
      };

      _this.onDragOver = function (e, index) {
        e.preventDefault();
        _this.setState({
          lastOverIndex: index,
          overIndex: index
        });
      };

      _this.onDragLeave = function (e, index) {
        e.preventDefault();
        _this.setState({
          lastOverIndex: index,
          overIndex: -1
        });
      };

      _this.onTouchMove = function (e, dragPreviewRef) {
        var touchPoint = e.touches[0];
        var dropZone = document.elementFromPoint(touchPoint.clientX, touchPoint.clientY);
        var position = dropZone.dataset.position;

        if (position) {
          _this.setState({
            overIndex: parseInt(position, 10)
          });
        } else {
          _this.onDragLeave(e);
        }

        // position move is out of control of react render, so we use id instead of ref
        dragPreviewRef.style.top = touchPoint.clientY + 'px'; // eslint-disable-line no-param-reassign

        // increase scroll area
        if (touchPoint.clientY < SCROLL_RANGE) {
          if (_this.scrollContainer) {
            _this.scrollContainer.scrollTop -= SCROLL_ACC_PX;
          } else {
            scrollBy(0, 0 - SCROLL_ACC_PX);
          }
        } else if (innerHeight - touchPoint.clientY < SCROLL_RANGE) {
          if (_this.scrollContainer) {
            _this.scrollContainer.scrollTop += SCROLL_ACC_PX;
          } else {
            scrollBy(0, SCROLL_ACC_PX);
          }
        }
      };

      _this.onTouchDrop = function (e) {
        var touchPoint = e.changedTouches[0];
        var dropZone = document.elementFromPoint(touchPoint.clientX, touchPoint.clientY);
        var position = dropZone.dataset.position;
        if (position) {
          _this.onDrop(e, parseInt(position, 10), 0);
          _this.onDragEnd(e);
        } else {
          _this.onDragEnd(e);
        }
      };

      _this.onClickDrag = function (e, index, preview) {
        e.stopPropagation(); // as binded a event on window to reset isKeyboardMoving state, need to stop propagation here to avoid mis-triggering it
        _this.setState({
          isDragging: false,
          isKeyboardMoving: true,
          sourceIndex: index,
          keyInsertIndex: index + 1,
          curPreview: preview
        });
      };

      _this.onKeyChangeOrder = function (e) {
        var items = _this.props.items;

        if (_this.state.isKeyboardMoving) {
          e.preventDefault();
          e.stopPropagation();
          switch (e.keyCode) {
            case _consts2.default.ESC:
              {
                _this.leaveKeyboardMoving();
                break;
              }
            case _consts2.default.ENTER:
              {
                var newOrderItems = (0, _utils.moveItem)(items, _this.state.keyInsertIndex, _this.state.sourceIndex);
                var sourceDragItem = items[_this.state.sourceIndex];
                _this.props.onReorderItem(newOrderItems, sourceDragItem);
                _this.setState({
                  isKeyboardMoving: false,
                  sourceIndex: -1,
                  keyInsertIndex: -1,
                  curPreview: ''
                });
                break;
              }
            case _consts2.default.ARROW_UP:
              {
                // skip displaying of item which is above the source cause its insert place holder should never be displayed
                if (_this.state.keyInsertIndex - 1 === _this.state.sourceIndex) {
                  if (_this.state.keyInsertIndex - 2 >= 0) {
                    _this.setState({ keyInsertIndex: _this.state.keyInsertIndex - 2 });
                  }
                } else if (_this.state.keyInsertIndex - 1 >= 0) {
                  _this.setState({ keyInsertIndex: _this.state.keyInsertIndex - 1 });
                }
                break;
              }
            case _consts2.default.ARROW_DOWN:
              {
                // skip displaying of item which is above the source cause its insert place holder should never be displayed
                if (_this.state.keyInsertIndex + 1 === _this.state.sourceIndex) {
                  if (_this.state.keyInsertIndex + 2 <= items.length) {
                    _this.setState({ keyInsertIndex: _this.state.keyInsertIndex + 2 });
                  }
                } else if (_this.state.keyInsertIndex + 1 <= items.length) {
                  _this.setState({ keyInsertIndex: _this.state.keyInsertIndex + 1 });
                }
                break;
              }
            default:
              break;
          }
        }
      };

      _this.actions = {
        onDrag: _this.onDrag,
        onDragStart: _this.onDragStart,
        onDragEnd: _this.onDragEnd,
        leaveKeyboardMoving: _this.leaveKeyboardMoving,
        onDrop: _this.onDrop,
        onDragOver: _this.onDragOver,
        onDragLeave: _this.onDragLeave,
        onTouchMove: _this.onTouchMove,
        onTouchDrop: _this.onTouchDrop,
        onClickDrag: _this.onClickDrag,
        onKeyChangeOrder: _this.onKeyChangeOrder
      };

      _this.clientY = 0;
      return _this;
    }

    _createClass(Wrapper, [{
      key: 'componentDidMount',
      value: function componentDidMount() {
        var _this2 = this;

        // to retrieve clientY in FF
        // https://stackoverflow.com/questions/887316/how-do-i-get-clientx-and-clienty-to-work-inside-my-drag-event-handler-on-firef
        window.addEventListener('dragover', this.setClientY);

        window.addEventListener('click', this.leaveKeyboardMoving);

        setTimeout(function () {
          _this2.scrollContainer = document.getElementById(_this2.props.scrollContainerId);
        }, 0);
      }
    }, {
      key: 'componentWillUnmount',
      value: function componentWillUnmount() {
        window.removeEventListener('dragover', this.setClientY);
        window.removeEventListener('click', this.leaveKeyboardMoving);
      }

      // we don't have to render the component for every onDragOver callback

    }, {
      key: 'shouldComponentUpdate',
      value: function shouldComponentUpdate(nextProps, nextState) {
        if (nextState.isDragging !== this.state.isDragging) {
          // deal with dragOver delay
          return true;
        }
        if (nextState.overIndex === this.state.overIndex && this.state.isDragging) {
          return false;
        }
        return true;
      }
    }, {
      key: 'render',
      value: function render() {
        var _this3 = this;

        return _react2.default.createElement(
          'div',
          { ref: function ref(_ref) {
              _this3.containerRef = _ref;
            } },
          _react2.default.createElement('div', { className: 'dnd-drag-placeholder' }),
          _react2.default.createElement(WrappedComponent, _extends({}, this.props, {
            state: this.state,
            actions: this.actions,
            keyInsertIndex: state.keyInsertIndex,
            isKeyboardMoving: state.isKeyboardMoving
          }))
        );
      }
    }]);

    return Wrapper;
  }(_react2.default.Component);

  Wrapper.propTypes = {
    items: _propTypes2.default.arrayOf(_propTypes2.default.object).isRequired,
    onReorderItem: _propTypes2.default.func.isRequired,
    boundingElementId: _propTypes2.default.string,
    scrollContainerId: _propTypes2.default.string
  };

  Wrapper.displayName = 'DragNDropContainer(' + (0, _utils.getDisplayName)(WrappedComponent) + ')';

  return Wrapper;
};

exports.default = DragNDropContainer;