"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.useThemeScroller = exports["default"] = void 0;

var _spotlight = _interopRequireDefault(require("@enact/spotlight"));

var _utils = require("@enact/spotlight/src/utils");

var _resolution = _interopRequireDefault(require("@enact/ui/resolution"));

var _utilDOM = _interopRequireDefault(require("@enact/ui/useScroll/utilDOM"));

var _react = require("react");

var _useEvent = require("./useEvent");

var _useSpotlight = require("./useSpotlight");

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

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

var useSpottable = function useSpottable(props, instances) {
  var scrollContainerRef = instances.scrollContainerRef,
      scrollContentHandle = instances.scrollContentHandle,
      scrollContentRef = instances.scrollContentRef; // Hooks

  (0, _useSpotlight.useSpotlightConfig)(props, instances);

  var _useEventKey = (0, _useEvent.useEventKey)(),
      addGlobalKeyDownEventListener = _useEventKey.addGlobalKeyDownEventListener,
      removeGlobalKeyDownEventListener = _useEventKey.removeGlobalKeyDownEventListener;

  var setContainerDisabled = (0, _react.useCallback)(function (bool) {
    if (scrollContainerRef.current) {
      scrollContainerRef.current.dataset.spotlightContainerDisabled = bool;

      if (bool) {
        addGlobalKeyDownEventListener(function () {
          setContainerDisabled(false);
        });
      } else {
        removeGlobalKeyDownEventListener();
      }
    }
  }, [addGlobalKeyDownEventListener, removeGlobalKeyDownEventListener, scrollContainerRef]);
  (0, _react.useEffect)(function () {
    return function () {
      return setContainerDisabled(false);
    };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  (0, _react.useEffect)(function () {
    var onUpdate = props.onUpdate;

    if (onUpdate) {
      onUpdate();
    }
  }); // Functions

  /**
   * Returns the first spotlight container between `node` and the scroller
   *
   * @param {Node} node A DOM node
   *
   * @returns {Node|Null} Spotlight container for `node`
   * @private
   */

  function getSpotlightContainerForNode(node) {
    do {
      if (node.dataset.spotlightId && node.dataset.spotlightContainer && !node.dataset.expandableContainer) {
        return node;
      }
    } while ((node = node.parentNode) && node !== scrollContentRef.current);
  }
  /**
   * Calculates the "focus bounds" of a node. If the node is within a spotlight container, that
   * container is scrolled into view rather than just the element.
   *
   * @param {Node} node Focused node
   *
   * @returns {Object} Bounds as returned by `getBoundingClientRect`
   * @private
   */


  function getFocusedItemBounds(node) {
    node = getSpotlightContainerForNode(node) || node;
    return node.getBoundingClientRect();
  }
  /**
   * Calculates the new `scrollTop`.
   *
   * @param {Node} item Focused item node
   *
   * @returns {Number} Calculated `scrollTop`
   * @private
   */


  function calculateScrollTop(item) {
    var threshold = _resolution["default"].scale(48);

    var roundToStart = function roundToStart(sb, st) {
      // round to start
      if (st < threshold) return 0;
      return st;
    };

    var roundToEnd = function roundToEnd(sb, st, sh) {
      // round to end
      if (sh - (st + sb.height) < threshold) return sh - sb.height;
      return st;
    }; // adding threshold into these determinations ensures that items that are within that are
    // near the bounds of the scroller cause the edge to be scrolled into view even when the
    // item itself is in view (e.g. due to margins)


    var isItemBeforeView = function isItemBeforeView(ib, sb, d) {
      return ib.top + d - threshold < sb.top;
    };

    var isItemAfterView = function isItemAfterView(ib, sb, d) {
      return ib.top + d + ib.height + threshold > sb.top + sb.height;
    };

    var canItemFit = function canItemFit(ib, sb) {
      return ib.height <= sb.height;
    };

    var calcItemAtStart = function calcItemAtStart(ib, sb, st, d) {
      return ib.top + st + d - sb.top;
    };

    var calcItemAtEnd = function calcItemAtEnd(ib, sb, st, d) {
      return ib.top + ib.height + st + d - (sb.top + sb.height);
    };

    var calcItemInView = function calcItemInView(ib, sb, st, sh, d) {
      if (isItemBeforeView(ib, sb, d)) {
        return roundToStart(sb, calcItemAtStart(ib, sb, st, d));
      } else if (isItemAfterView(ib, sb, d)) {
        return roundToEnd(sb, calcItemAtEnd(ib, sb, st, d), sh);
      }

      return st;
    };

    var container = getSpotlightContainerForNode(item);
    var scrollerBounds = scrollContentRef.current.getBoundingClientRect();
    var _scrollContentRef$cur = scrollContentRef.current,
        scrollHeight = _scrollContentRef$cur.scrollHeight,
        scrollTop = _scrollContentRef$cur.scrollTop;
    var scrollTopDelta = 0;

    var adjustScrollTop = function adjustScrollTop(v) {
      scrollTopDelta = scrollTop - v;
      scrollTop = v;
    };

    if (container) {
      var containerBounds = container.getBoundingClientRect(); // if the entire container fits in the scroller, scroll it into view

      if (canItemFit(containerBounds, scrollerBounds)) {
        return calcItemInView(containerBounds, scrollerBounds, scrollTop, scrollHeight, scrollTopDelta);
      } // if the container doesn't fit, adjust the scroll top ...


      if (containerBounds.top > scrollerBounds.top) {
        // ... to the top of the container if the top is below the top of the scroller
        adjustScrollTop(calcItemAtStart(containerBounds, scrollerBounds, scrollTop, scrollTopDelta));
      } // removing support for "snap to bottom" for 2.2.8
      // } else if (containerBounds.top + containerBounds.height < scrollerBounds.top + scrollerBounds.height) {
      // 	// ... to the bottom of the container if the bottom is above the bottom of the
      // 	// scroller
      // 	adjustScrollTop(calcItemAtEnd(containerBounds, scrollerBounds, scrollTop, scrollTopDelta));
      // }
      // N.B. if the container covers the scrollable area (its top is above the top of the
      // scroller and its bottom is below the bottom of the scroller), we need not adjust the
      // scroller to ensure the container is wholly in view.

    }

    var itemBounds = item.getBoundingClientRect();
    return calcItemInView(itemBounds, scrollerBounds, scrollTop, scrollHeight, scrollTopDelta);
  }
  /**
   * Calculates the new `scrollLeft`.
   *
   * @param {Node} focusedItem node
   * @param {Number} scrollPosition last target position, passed when scroll animation is ongoing
   *
   * @returns {Number} Calculated `scrollLeft`
   * @private
   */


  function calculateScrollLeft(item, scrollPosition) {
    var scrollContentNode = scrollContentRef.current;

    var _getFocusedItemBounds = getFocusedItemBounds(item),
        itemLeft = _getFocusedItemBounds.left,
        itemWidth = _getFocusedItemBounds.width;

    var rtl = props.rtl,
        clientWidth = scrollContentHandle.current.scrollBounds.clientWidth,
        rtlDirection = rtl ? -1 : 1,
        _scrollContentNode$ge = scrollContentNode.getBoundingClientRect(),
        containerLeft = _scrollContentNode$ge.left,
        scrollLastPosition = scrollPosition ? scrollPosition : scrollContentHandle.current.scrollPos.left,
        currentScrollLeft = rtl ? scrollContentHandle.current.scrollBounds.maxLeft - scrollLastPosition : scrollLastPosition,
        newItemLeft = scrollContentNode.scrollLeft + (itemLeft - containerLeft);

    var nextScrollLeft = scrollContentHandle.current.scrollPos.left;

    if (newItemLeft + itemWidth > clientWidth + currentScrollLeft && itemWidth < clientWidth) {
      // If focus is moved to an element outside of view area (to the right), scroller will move
      // to the right just enough to show the current `focusedItem`. This does not apply to
      // `focusedItem` that has a width that is bigger than `scrollBounds.clientWidth`.
      nextScrollLeft += rtlDirection * (newItemLeft + itemWidth - (clientWidth + currentScrollLeft));
    } else if (newItemLeft < currentScrollLeft) {
      // If focus is outside of the view area to the left, move scroller to the left accordingly.
      nextScrollLeft += rtlDirection * (newItemLeft - currentScrollLeft);
    }

    return nextScrollLeft;
  }
  /**
   * Calculates the new top and left position for scroller based on focusedItem.
   *
   * @param {Node} item node
   * @param {Number} scrollPosition last target position, passed scroll animation is ongoing
   *
   * @returns {Object} with keys {top, left} containing calculated top and left positions for scroll.
   * @private
   */


  function calculatePositionOnFocus(_ref) {
    var item = _ref.item,
        scrollPosition = _ref.scrollPosition;
    var containerNode = scrollContentRef.current;
    var horizontal = scrollContentHandle.current.isHorizontal();
    var vertical = scrollContentHandle.current.isVertical();

    if (!vertical && !horizontal || !item || !_utilDOM["default"].containsDangerously(containerNode, item)) {
      return;
    }

    var containerRect = (0, _utils.getRect)(containerNode);
    var itemRect = (0, _utils.getRect)(item);

    if (horizontal && !(itemRect.left >= containerRect.left && itemRect.right <= containerRect.right)) {
      scrollContentHandle.current.scrollPos.left = calculateScrollLeft(item, scrollPosition);
    }

    if (vertical && !(itemRect.top >= containerRect.top && itemRect.bottom <= containerRect.bottom)) {
      scrollContentHandle.current.scrollPos.top = calculateScrollTop(item);
    }

    return scrollContentHandle.current.scrollPos;
  }

  function focusOnNode(node) {
    if (node) {
      _spotlight["default"].focus(node);
    }
  } // Return


  return {
    calculatePositionOnFocus: calculatePositionOnFocus,
    focusOnNode: focusOnNode,
    setContainerDisabled: setContainerDisabled
  };
};

var useThemeScroller = function useThemeScroller(props) {
  var scrollContainerRef = props.scrollContainerRef,
      scrollContentHandle = props.scrollContentHandle,
      rest = _objectWithoutProperties(props, ["scrollContainerRef", "scrollContentHandle"]);

  var scrollContentRef = rest.scrollContentRef;
  delete rest.onUpdate;
  delete rest.scrollAndFocusScrollbarButton;
  delete rest.scrollContainerContainsDangerously;
  delete rest.scrollContainerHandle;
  delete rest.setThemeScrollContentHandle;
  delete rest.spotlightId; // Hooks

  var _useSpottable = useSpottable(props, {
    scrollContainerRef: scrollContainerRef,
    scrollContentHandle: scrollContentHandle,
    scrollContentRef: scrollContentRef
  }),
      calculatePositionOnFocus = _useSpottable.calculatePositionOnFocus,
      focusOnNode = _useSpottable.focusOnNode,
      setContainerDisabled = _useSpottable.setContainerDisabled;

  props.setThemeScrollContentHandle({
    calculatePositionOnFocus: calculatePositionOnFocus,
    focusOnNode: focusOnNode,
    setContainerDisabled: setContainerDisabled
  }); // Render

  return rest;
};

exports.useThemeScroller = useThemeScroller;
var _default = useThemeScroller;
exports["default"] = _default;