"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = exports.ResponsiveBox = exports.Droppable = exports.DropManager = exports.Draggable = void 0;
var _hoc = _interopRequireDefault(require("@enact/core/hoc"));
var _kind = _interopRequireDefault(require("@enact/core/kind"));
var _Changeable = _interopRequireDefault(require("@enact/ui/Changeable"));
var _Slottable = _interopRequireDefault(require("@enact/ui/Slottable"));
var _classnames = _interopRequireDefault(require("classnames"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _ramda = require("ramda");
var _react = require("react");
var _Rearrangeable = _interopRequireDefault(require("../Rearrangeable"));
var _DropManagerModule = _interopRequireDefault(require("./DropManager.module.css"));
var _jsxRuntime = require("react/jsx-runtime");
var _excluded = ["arrangement", "arrangeable", "className"],
  _excluded2 = ["containerShape", "draggable", "name", "slot"],
  _excluded3 = ["arrangementProp", "slots"];
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var s = Object.getOwnPropertySymbols(e); for (r = 0; r < s.length; r++) o = s[r], t.includes(o) || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (e.includes(n)) continue; t[n] = r[n]; } return t; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _possibleConstructorReturn(t, e) { if (e && ("object" == typeof e || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); }
function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } /**
 * Components to support drag and drop.
 *
 * @module agate/DropManager
 * @exports Droppable
 * @exports DropManager
 * @exports Draggable
 * @exports ResponsiveBox
 * @private
 */ // https://stackoverflow.com/questions/9907419/how-to-get-a-key-in-a-javascript-object-by-its-value
// By: ZER0 - Mar 28 '12 at 12:51
var getKeyByValue = function getKeyByValue(obj, value) {
  return Object.keys(obj).find(function (key) {
    return obj[key] === value;
  });
};

// Establish the base container shape, to be shared with all components as a consistent starting point.
var containerShapePropTypes = _propTypes["default"].shape({
  edge: _propTypes["default"].bool,
  edges: _propTypes["default"].shape({
    bottom: _propTypes["default"].bool,
    left: _propTypes["default"].bool,
    right: _propTypes["default"].bool,
    top: _propTypes["default"].bool
  }),
  horizontalEdge: _propTypes["default"].bool,
  orientation: _propTypes["default"].string,
  size: _propTypes["default"].shape({
    relative: _propTypes["default"].string // Relative size: small, medium, large, full
    // proposed - height: PropTypes.number,
    // proposed - width: PropTypes.number
  }),
  verticalEdge: _propTypes["default"].bool
});
var defaultContainerShape = {
  edge: false,
  edges: {
    top: false,
    right: false,
    bottom: false,
    left: false
  },
  horizontalEdge: false,
  verticalEdge: false,
  orientation: null,
  size: {
    relative: null // Relative size: small, medium, large, full
    // proposed - height: 0,
    // proposed - width: 0
  }
};

/**
 * Default config for DropManager.
 *
 * @memberof agate/DropManager.DropManager
 * @hocconfig
 */
var defaultConfig = {
  // The prop name for the boolean send to the Wrapped component that indicates whether we are
  // able to arrange things. Useful in case we need to show some boxes on the screen or
  // something. The default/common value for this is "arrangeable".
  arrangeableProp: null,
  // The prop name for the boolean send to the Wrapped component that indicates whether we are
  // currently arranging things. Useful in case we need to add an overlay or hide content during
  // dragging. The default/common value for this is "arranging".
  arrangingProp: null,
  // The prop name for the object sent to the Wrapped component containing the arrangement object.
  // This also applies to the incoming arrangement prop name.
  // This will typically directly feed into Rearrangeable.
  // The default/common value for this is "arrangement".
  arrangementProp: null
};

// The arrangementProp above is allowed to be empty. When it's empty, though, and the user doesn't
// want the prop delivered to their Wrapped component, we still need to have some prop name to
// communicate to Rearrangeable. That's where this comes in; it's used if the user didn't specify a
// prop, so we can send an arrangement to Rearrangeable, exclusively.
var fallbackArrangementProp = 'arrangement';
var DropManagerContext = /*#__PURE__*/(0, _react.createContext)(defaultContainerShape);

/**
 * TBD.
 *
 * @class DropManager
 * @hoc
 * @memberof agate/DropManager
 * @private
 */
var DropManager = exports.DropManager = (0, _hoc["default"])(defaultConfig, function (configHoc, Wrapped) {
  var _Class;
  var ArrangementState = (0, _Changeable["default"])({
    prop: configHoc.arrangementProp || fallbackArrangementProp
  });
  var DropManagerBase = (_Class = /*#__PURE__*/function (_Component) {
    function DropManagerBase() {
      var _this;
      _classCallCheck(this, DropManagerBase);
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
        args[_key] = arguments[_key];
      }
      _this = _callSuper(this, DropManagerBase, [].concat(args));
      _this.state = {
        dragging: false,
        touchOverElement: null
      };
      _this.addDropTarget = function (target) {
        target.classList.add('dropTarget');
      };
      _this.removeDropTarget = function (target) {
        target.classList.remove('dropTarget');
      };
      _this.handleDragStart = function (ev) {
        ev.dataTransfer.setData('text/plain', ev.target.dataset.slot);
        ev.dataTransfer.effectAllowed = 'move';
        _this.dragOriginNode = ev.target;
        _this.setState({
          dragging: true
        });
      };
      _this.handleDragEnter = function (ev) {
        _this.addDropTarget(ev.target);
      };
      _this.handleDragLeave = function (ev) {
        _this.removeDropTarget(ev.target);
      };
      _this.handleDragOver = function (ev) {
        ev.preventDefault();
        // Set the dropEffect to move
        ev.dataTransfer.dropEffect = 'move';
      };
      _this.handleDrop = function (ev) {
        ev.preventDefault();
        ev.dataTransfer.clearData();

        // Bail early if the drag started from some unknown location.
        if (!_this.dragOriginNode) {
          _this.setState({
            dragging: false
          });
          return;
        }

        // Get the id of the target and add the moved element to the target's DOM
        // const dragOrigin = ev.dataTransfer.getData('text/plain');
        var dragOrigin = _this.dragOriginNode.dataset.slot;
        var dragDropNode;
        // If we dropped directly on an element with a slot defined, just use that directly
        if (ev.target.dataset.slot) {
          dragDropNode = ev.target;
        } else {
          // If we dropped on a child of a slotted element (like an icon or a div or other
          // nested component), find the closest ancestor with a slot and use that as the drop element.
          var closestSlot = ev.target.closest('[data-slot]');
          if (closestSlot && closestSlot.dataset.slot) {
            dragDropNode = closestSlot;
          } else {
            // We didn't actually find anything, just bail out. The component was dropped in
            // a place that is unknown.
            _this.setState({
              dragging: false
            });
            return;
          }
        }
        _this.removeDropTarget(dragDropNode);

        // Get the destination element's slot value, or find its ancestor that has one (in case we drop this on a child or grandchild of the slotted item).
        // const dragDestination = ev.target.dataset.slot || (ev.target.closest('[data-slot]') && ev.target.closest('[data-slot]').dataset.slot);
        var dragDestination = dragDropNode.dataset.slot;

        // If the dragged element was dropped back on itself, do nothing and exit.
        if (dragDestination === dragOrigin) {
          _this.setState({
            dragging: false
          });
          return;
        }
        _this.dragOriginNode.dataset.slot = dragDestination;
        dragDropNode.dataset.slot = dragOrigin;

        // console.log('from:', dragOrigin, 'to:', dragDestination);

        // We successfully completed the drag, blank-out the node.
        _this.dragOriginNode = null;
        var _this$props = _this.props,
          arrangement = _this$props.arrangement,
          onArrange = _this$props.onArrange;
        var oldD = getKeyByValue(arrangement, dragDestination);
        var oldO = getKeyByValue(arrangement, dragOrigin);
        arrangement[oldD || dragDestination] = dragOrigin;
        arrangement[oldO || dragOrigin] = dragDestination;
        if (onArrange) {
          onArrange({
            arrangement: arrangement
          });
        }
        _this.setState({
          dragging: false
        });
      };
      _this.handleTouchStart = function (ev) {
        _this.dragOriginNode = ev.target;
        _this.setState({
          dragging: true
        });
      };
      _this.handleTouchMove = function (ev) {
        var touchMoveOverElem = document.elementFromPoint(ev.changedTouches[0].clientX, ev.changedTouches[0].clientY);
        if (_this.state.touchOverElement && _this.state.touchOverElement !== touchMoveOverElem) {
          _this.removeDropTarget(_this.state.touchOverElement);
          _this.addDropTarget(touchMoveOverElem);
          _this.setState(function () {
            return {
              touchOverElement: touchMoveOverElem
            };
          });
        } else {
          _this.addDropTarget(touchMoveOverElem);
          _this.setState(function () {
            return {
              touchOverElement: touchMoveOverElem
            };
          });
        }
      };
      _this.handleTouchEnd = function (ev) {
        ev.preventDefault();

        // Bail early if the drag started from some unknown location.
        if (!_this.dragOriginNode || !_this.dragOriginNode.dataset.slot) {
          _this.setState({
            dragging: false
          });
          return;
        }
        if (_this.state.touchOverElement) {
          _this.removeDropTarget(_this.state.touchOverElement);
        }

        // Get the id of the target and add the moved element to the target's DOM
        var dragOrigin = _this.dragOriginNode.dataset.slot;
        var dropOverElem = document.elementFromPoint(ev.changedTouches[0].clientX, ev.changedTouches[0].clientY);
        var dragDropNode;
        // If we dropped directly on an element with a slot defined, just use that directly
        if (dropOverElem.dataset.slot) {
          dragDropNode = dropOverElem;
        } else {
          // If we dropped on a child of a slotted element (like an icon or a div or other
          // nested component), find the closest ancestor with a slot and use that as the drop element.
          var closestSlot = ev.target.closest('[data-slot]');
          if (closestSlot && closestSlot.dataset.slot) {
            dragDropNode = closestSlot;
          } else {
            // We didn't actually find anything, just bail out. The component was dropped in
            // a place that is unknown.
            _this.setState({
              dragging: false
            });
            return;
          }
        }

        // Get the destination element's slot value, or find its ancestor that has one (in case we drop this on a child or grandchild of the slotted item).
        // const dragDestination = ev.target.dataset.slot || (ev.target.closest('[data-slot]') && ev.target.closest('[data-slot]').dataset.slot);
        var dragDestination = dragDropNode.dataset.slot;

        // If the dragged element was dropped back on itself, do nothing and exit.
        if (dragDestination === dragOrigin) {
          _this.setState({
            dragging: false
          });
          return;
        }
        _this.dragOriginNode.dataset.slot = dragDestination;
        dragDropNode.dataset.slot = dragOrigin;

        // We successfully completed the drag, blank-out the node.
        _this.dragOriginNode = null;
        var _this$props2 = _this.props,
          arrangement = _this$props2.arrangement,
          onArrange = _this$props2.onArrange;
        var oldD = getKeyByValue(arrangement, dragDestination);
        var oldO = getKeyByValue(arrangement, dragOrigin);
        arrangement[oldD || dragDestination] = dragOrigin;
        arrangement[oldO || dragOrigin] = dragDestination;
        if (onArrange) {
          onArrange({
            arrangement: arrangement
          });
        }
        _this.setState({
          dragging: false
        });
      };
      return _this;
    }
    _inherits(DropManagerBase, _Component);
    return _createClass(DropManagerBase, [{
      key: "render",
      value:
      // handleDragEnd = () => {
      // 	if (this.props.onArrange) {
      // 		this.props.onArrange({arrangement: this.state.arrangement});
      // 	}
      // };

      function render() {
        var _this$props3 = _objectSpread({}, this.props),
          arrangement = _this$props3.arrangement,
          arrangeable = _this$props3.arrangeable,
          className = _this$props3.className,
          rest = _objectWithoutProperties(_this$props3, _excluded);
        delete rest.onArrange;
        rest[configHoc.arrangementProp || fallbackArrangementProp] = arrangement;
        if (configHoc.arrangeableProp) rest[configHoc.arrangeableProp] = arrangeable;
        if (configHoc.arrangingProp) rest[configHoc.arrangingProp] = this.state.dragging;
        if (arrangeable) {
          // Add all of the necessary events, but only if we're in edit mode
          rest.onDragStart = this.handleDragStart;
          rest.onDragEnter = this.handleDragEnter;
          rest.onDragLeave = this.handleDragLeave;
          rest.onDragOver = this.handleDragOver;
          rest.onDrop = this.handleDrop;
          rest.onTouchStart = this.handleTouchStart;
          rest.onTouchMove = this.handleTouchMove;
          rest.onTouchEnd = this.handleTouchEnd;
          // rest.onDragEnd = this.handleDragEnd;
        }
        return /*#__PURE__*/(0, _jsxRuntime.jsx)(DropManagerContext.Provider, {
          value: {
            arrangement: arrangement,
            arranging: this.state.dragging,
            arrangeable: arrangeable
          },
          children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Wrapped, _objectSpread(_objectSpread({}, rest), {}, {
            className: (0, _classnames["default"])(className, _DropManagerModule["default"].dropManager, arrangeable ? _DropManagerModule["default"].arrangeable : '', {
              dragging: this.state.dragging
            })
            // draggable="true"
          }))
        });
      }
    }]);
  }(_react.Component), _Class.displayName = 'DropManager', _Class.propTypes = /** @lends agate/DropManager.DropManager.prototype */{
    /**
     * The ready-state to indicate that the contents are allowed to be rearranged
     *
     * @type {Boolean}
     * @default false
     * @public
     */
    arrangeable: _propTypes["default"].bool,
    /**
     * The arrangement object, consumable by `Rearrangeable`. This represents the initial
     * arrangement and will be forwarded to `Rearrangeable` as well as the `Draggable`
     * children. This is ingested using `Changeable`.
     *
     * @type {Object}
     * @public
     */
    arrangement: _propTypes["default"].object,
    /**
     * Executed when an arranging (dragging) action has completed. Included in the payload
     * is a new arrangement object, which can be saved for later arrangement recall.
     *
     * @type {Function}
     * @public
     */
    onArrange: _propTypes["default"].func
  }, _Class.defaultProps = {
    arrangeable: false
  }, _Class);
  return ArrangementState(DropManagerBase);
});

// Draw conclusions from supplied shape information and auto-set some values based on logical
// conclusions to save time and improve consistency for consumers.
var logicallyPopulateContainerShape = function logicallyPopulateContainerShape(cs) {
  if (cs.edges.left || cs.edges.right) {
    cs.horizontalEdge = true;
    cs.edge = true;
  }
  if (cs.edges.top || cs.edges.bottom) {
    cs.verticalEdge = true;
    cs.edge = true;
  }
  // return cs;
};
var DraggableContainerContext = /*#__PURE__*/(0, _react.createContext)(null);

/**
 * TBD.
 *
 * @class Draggable
 * @memberof agate/DropManager
 * @ui
 * @private
 */
var Draggable = exports.Draggable = function Draggable(Wrapped) {
  return (0, _kind["default"])({
    name: 'Draggable',
    propTypes: /** @lends agate/DropManager.Draggable.prototype */{
      containerShape: containerShapePropTypes,
      draggable: _propTypes["default"].bool,
      name: _propTypes["default"].string,
      slot: _propTypes["default"].string
    },
    // If something is marked explicitly as *not* draggable (everything using this is normally)
    // don't assign it a data-slot, so it can't be dragged(A) or dropped on(B).
    render: function render(_ref) {
      var _ref$containerShape = _ref.containerShape,
        containerShape = _ref$containerShape === void 0 ? {} : _ref$containerShape,
        _ref$draggable = _ref.draggable,
        draggable = _ref$draggable === void 0 ? true : _ref$draggable,
        name = _ref.name,
        slot = _ref.slot,
        rest = _objectWithoutProperties(_ref, _excluded2);
      // Apply a consistent (predictable) set of object keys for consumers
      // defaultProps is a shallow merge only, so we aren't using it :(
      containerShape = (0, _ramda.mergeDeepRight)(defaultContainerShape, containerShape);

      // Draw conclusions from supplied shape information and auto-set some values.
      logicallyPopulateContainerShape(containerShape);
      Object.freeze(containerShape);
      return /*#__PURE__*/(0, _jsxRuntime.jsx)(DropManagerContext.Consumer, {
        children: function children(_ref2) {
          var arrangement = _ref2.arrangement,
            arrangeable = _ref2.arrangeable;
          return /*#__PURE__*/(0, _jsxRuntime.jsx)(DraggableContainerContext.Provider, {
            value: {
              containerShape: containerShape
            },
            children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Wrapped, _objectSpread(_objectSpread({}, rest), {}, {
              draggable: arrangeable && draggable,
              "data-slot": draggable ? arrangement && (arrangement[name] || arrangement[slot]) || name || slot : null,
              "data-slot-name": slot
            }))
          });
        }
      });
    }
  });
};

/**
 * TBD.
 *
 * @class ResponsiveBox
 * @memberof agate/DropManager
 * @ui
 * @private
 */
var ResponsiveBox = exports.ResponsiveBox = function ResponsiveBox(Wrapped) {
  var Decorator = function Decorator(props) {
    return /*#__PURE__*/(0, _jsxRuntime.jsx)(DraggableContainerContext.Consumer, {
      children: function children(ctx) {
        var containerShape = ctx ? ctx.containerShape : defaultContainerShape;
        return /*#__PURE__*/(0, _jsxRuntime.jsx)(Wrapped, _objectSpread({
          containerShape: containerShape
        }, props));
      }
    });
  };
  Decorator.displayName = 'ResponsiveBox';
  return Decorator;
};

//
//
// DEV TO-DO LATER:
//
// Simplify draggable usage, maybe include a "hide with no content" prop
//
// Relocate data-slot junk to context child->parent relay of child identification information
//
//

// Consolidate usage pattern into a simple reusable piece
/**
 * TBD.
 *
 * @class Droppable
 * @hoc
 * @memberof agate/DropManager
 * @private
 */
var Droppable = exports.Droppable = (0, _hoc["default"])(function (configHoc, Wrapped) {
  var arrangementProp = configHoc.arrangementProp,
    slots = configHoc.slots,
    rest = _objectWithoutProperties(configHoc, _excluded3);
  return DropManager(_objectSpread({
    arrangementProp: arrangementProp
  }, rest), (0, _Slottable["default"])({
    slots: slots
  }, (0, _Rearrangeable["default"])({
    arrangementProp: arrangementProp,
    slots: slots
  }, Wrapped)));
});
var _default = exports["default"] = Droppable;