'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.findPopoverPosition = findPopoverPosition;
exports.getPopoverScreenCoordinates = getPopoverScreenCoordinates;
exports.getElementBoundingBox = getElementBoundingBox;
exports.getAvailableSpace = getAvailableSpace;
exports.getVisibleFit = getVisibleFit;
exports.intersectBoundingBoxes = intersectBoundingBoxes;
exports.getElementZIndex = getElementZIndex;

var _reactDom = require('react-dom');

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var relatedDimension = {
  top: 'height',
  right: 'width',
  bottom: 'height',
  left: 'width'
};

var dimensionPositionAttribute = {
  height: 'top',
  width: 'left'
};

var positionComplements = {
  top: 'bottom',
  right: 'left',
  bottom: 'top',
  left: 'right'
};

// always resolving to top/left is taken advantage of by knowing they are the
// minimum edges of the bounding box
var positionSubstitutes = {
  top: 'left',
  right: 'top',
  bottom: 'left',
  left: 'top'
};

/**
 * Calculates the absolute positioning (relative to document.body) to place a popover element
 *
 * @param anchor {HTMLElement|React.Component} Element to anchor the popover to
 * @param popover {HTMLElement|React.Component} Element containing the popover content
 * @param position {string} Position the user wants. One of ["top", "right", "bottom", "left"]
 * @param [align] {string} Cross-axis alignment. One of ["top", "right", "bottom", "left"]
 * @param [buffer=16] {number} Minimum distance between the popover and the bounding container
 * @param [offset=0] {number} Distance between the popover and the anchor
 * @param [allowCrossAxis=true] {boolean} Whether to allow the popover to be positioned on the cross-axis
 * @param [container] {HTMLElement|React.Component} Element the popover must be constrained to fit within
 * @param [arrowConfig] {{arrowWidth: number, arrowBuffer: number}} If present, describes the size & constraints for an arrow element, and the function return value will include an `arrow` param with position details
 *
 * @returns {{top: number, left: number, position: string, fit: number, arrow?: {left: number, top: number}}|null} absolute page coordinates for the popover,
 * and the placements's relation to the anchor; if there's no room this returns null
 */
function findPopoverPosition(_ref) {
  var anchor = _ref.anchor,
      popover = _ref.popover,
      align = _ref.align,
      position = _ref.position,
      _ref$buffer = _ref.buffer,
      buffer = _ref$buffer === undefined ? 16 : _ref$buffer,
      _ref$offset = _ref.offset,
      offset = _ref$offset === undefined ? 0 : _ref$offset,
      _ref$allowCrossAxis = _ref.allowCrossAxis,
      allowCrossAxis = _ref$allowCrossAxis === undefined ? true : _ref$allowCrossAxis,
      container = _ref.container,
      arrowConfig = _ref.arrowConfig;

  container = (0, _reactDom.findDOMNode)(container); // resolve any React abstractions

  // find the screen-relative bounding boxes of the anchor, popover, and container
  var anchorBoundingBox = getElementBoundingBox(anchor);
  var popoverBoundingBox = getElementBoundingBox(popover);

  // calculate the window's bounds
  // window.(innerWidth|innerHeight) do not account for scrollbars
  // so prefer the clientWidth/clientHeight of the DOM if available
  var documentWidth = document.documentElement.clientWidth || window.innerWidth;
  var documentHeight = document.documentElement.clientHeight || window.innerHeight;
  var windowBoundingBox = {
    top: 0,
    right: documentWidth,
    bottom: documentHeight,
    left: 0,
    height: documentHeight,
    width: documentWidth
  };

  // if no container element is given fall back to using the window viewport
  var containerBoundingBox = container ? getElementBoundingBox(container) : windowBoundingBox;

  /**
   * `position` was specified by the function caller and is a strong hint
   * as to the preferred location of the popover relative to the anchor.
   * However, we strongly prefer showing all of the popover content within
   * the window+container boundary and will iterate over the four
   * possible sides until a perfect fit is located. If none of the locations
   * fully contain popover, the location with the best fit is selected.
   *
   * This approach first checks the preferred `position`, then its opposite
   * along the same axis, next a location on the cross-axis, and finally it
   * tests the remaining position.
   *
   * e.g.
   * if position = "top" the order is top, bottom, left right
   * if position = "right" the order is right, left, top, bottom
   */

  var iterationPositions = [position, // Try the user-desired position first.
  positionComplements[position]];
  if (allowCrossAxis) {
    iterationPositions.push(positionSubstitutes[position], // Switch to the cross axis.
    positionComplements[positionSubstitutes[position]] // Try the complementary position on the cross axis.
    );
  }

  var _iterationPositions$r = iterationPositions.reduce(function (_ref2, iterationPosition) {
    var bestFit = _ref2.bestFit,
        bestPosition = _ref2.bestPosition;

    // If we've already found the ideal fit, use that position.
    if (bestFit === 1) {
      return { bestFit: bestFit, bestPosition: bestPosition };
    }

    // Otherwise, see if we can find a position with a better fit than we've found so far.
    var screenCoordinates = getPopoverScreenCoordinates({
      position: iterationPosition,
      align: align,
      anchorBoundingBox: anchorBoundingBox,
      popoverBoundingBox: popoverBoundingBox,
      windowBoundingBox: windowBoundingBox,
      containerBoundingBox: containerBoundingBox,
      offset: offset,
      buffer: buffer,
      arrowConfig: arrowConfig
    });

    if (screenCoordinates.fit > bestFit) {
      return {
        bestFit: screenCoordinates.fit,
        bestPosition: {
          fit: screenCoordinates.fit,
          position: iterationPosition,
          top: screenCoordinates.top + window.pageYOffset,
          left: screenCoordinates.left + window.pageXOffset,
          arrow: screenCoordinates.arrow
        }
      };
    }

    // If we haven't improved the fit, then continue on and try a new position.
    return {
      bestFit: bestFit,
      bestPosition: bestPosition
    };
  }, {
    bestFit: -Infinity,
    bestPosition: null
  }),
      bestPosition = _iterationPositions$r.bestPosition;

  return bestPosition;
}

/**
 * Given a target position and the popover's surrounding context, returns either an
 * object with {top, left} screen coordinates or `null` if it's not possible to show
 * content in the target position
 * @param position {string} the target position, one of ["top", "right", "bottom", "left"]
 * @param [align] {string} target alignment on the cross-axis, one of ["top", "right", "bottom", "left"]
 * @param anchorBoundingBox {Object} bounding box of the anchor element
 * @param popoverBoundingBox {Object} bounding box of the popover element
 * @param windowBoundingBox {Object} bounding box of the window
 * @param containerBoundingBox {Object} bounding box of the container
 * @param [arrowConfig] {{arrowWidth: number, arrowBuffer: number}} If present, describes the size &
 *  constraints for an arrow element, and the function return value will include an `arrow` param
 *  with position details
 * @param [offset=0] {number} Distance between the popover and the anchor
 * @param [buffer=0] {number} Minimum distance between the popover's placement and the container edge
 *
 * @returns {{top: number, left: number, relativePlacement: string, fit: number, arrow?: {top: number, left: number}}|null}
 *  object with top/left coordinates, the popover's relative position to the anchor, and how well the
 *  popover fits in the location (0.0 -> 1.0) oordinates and the popover's relative position, if
 *  there is no room in this placement then null
 */
function getPopoverScreenCoordinates(_ref3) {
  var _popoverPlacement, _ref4;

  var position = _ref3.position,
      align = _ref3.align,
      anchorBoundingBox = _ref3.anchorBoundingBox,
      popoverBoundingBox = _ref3.popoverBoundingBox,
      windowBoundingBox = _ref3.windowBoundingBox,
      containerBoundingBox = _ref3.containerBoundingBox,
      arrowConfig = _ref3.arrowConfig,
      _ref3$offset = _ref3.offset,
      offset = _ref3$offset === undefined ? 0 : _ref3$offset,
      _ref3$buffer = _ref3.buffer,
      buffer = _ref3$buffer === undefined ? 0 : _ref3$buffer;

  /**
   * The goal is to find the best way to align the popover content
   * on the given side of the anchor element. The popover prefers
   * centering on the anchor but can shift along the cross-axis as needed.
   *
   * We return the top/left coordinates that best fit the popover inside
   * the given boundaries, and also return the `fit` value which indicates
   * what percentage of the popover is within the bounds.
   *
   * e.g. finding a location when position=top
   * the preferred location is directly over the anchor
   *
   *        +----------------------+
   *        |       popover        |
   *        +----------------------+
   *                   v
   *            +--------------+
   *            |    anchor    |
   *            +--------------+
   *
   * but if anchor doesn't have much (or any) room on its ride side
   * the popover will shift to the left
   *
   *    +----------------------+
   *    |       popover        |
   *    +----------------------+
   *                   v
   *            +--------------+
   *            |    anchor    |
   *            +--------------+
   *
   */

  var crossAxisFirstSide = positionSubstitutes[position]; // "top" -> "left"
  var crossAxisSecondSide = positionComplements[crossAxisFirstSide]; // "left" -> "right"
  var crossAxisDimension = relatedDimension[crossAxisFirstSide]; // "left" -> "width"

  var _getCrossAxisPosition = getCrossAxisPosition({
    crossAxisFirstSide: crossAxisFirstSide,
    crossAxisSecondSide: crossAxisSecondSide,
    crossAxisDimension: crossAxisDimension,
    position: position,
    align: align,
    buffer: buffer,
    offset: offset,
    windowBoundingBox: windowBoundingBox,
    containerBoundingBox: containerBoundingBox,
    popoverBoundingBox: popoverBoundingBox,
    anchorBoundingBox: anchorBoundingBox,
    arrowConfig: arrowConfig
  }),
      crossAxisPosition = _getCrossAxisPosition.crossAxisPosition,
      crossAxisArrowPosition = _getCrossAxisPosition.crossAxisArrowPosition;

  var primaryAxisDimension = relatedDimension[position]; // "top" -> "height"
  var primaryAxisPositionName = dimensionPositionAttribute[primaryAxisDimension]; // "height" -> "top"

  var _getPrimaryAxisPositi = getPrimaryAxisPosition({
    position: position,
    offset: offset,
    popoverBoundingBox: popoverBoundingBox,
    anchorBoundingBox: anchorBoundingBox,
    arrowConfig: arrowConfig
  }),
      primaryAxisPosition = _getPrimaryAxisPositi.primaryAxisPosition,
      primaryAxisArrowPosition = _getPrimaryAxisPositi.primaryAxisArrowPosition;

  var popoverPlacement = (_popoverPlacement = {}, _defineProperty(_popoverPlacement, crossAxisFirstSide, crossAxisPosition), _defineProperty(_popoverPlacement, primaryAxisPositionName, primaryAxisPosition), _popoverPlacement);

  // calculate the fit of the popover in this location
  // fit is in range 0.0 -> 1.0 and is the percentage of the popover which is visible in this location
  var combinedBoundingBox = intersectBoundingBoxes(windowBoundingBox, containerBoundingBox);

  // shrink the visible bounding box by `buffer`
  // to compute a fit value
  combinedBoundingBox.top += buffer;
  combinedBoundingBox.right -= buffer;
  combinedBoundingBox.bottom -= buffer;
  combinedBoundingBox.left += buffer;

  var fit = getVisibleFit({
    top: popoverPlacement.top,
    right: popoverPlacement.left + popoverBoundingBox.width,
    bottom: popoverPlacement.top + popoverBoundingBox.height,
    left: popoverPlacement.left,
    width: popoverBoundingBox.width,
    height: popoverBoundingBox.height
  }, combinedBoundingBox);

  var arrow = arrowConfig ? (_ref4 = {}, _defineProperty(_ref4, crossAxisFirstSide, crossAxisArrowPosition - popoverPlacement[crossAxisFirstSide]), _defineProperty(_ref4, primaryAxisPositionName, primaryAxisArrowPosition), _ref4) : undefined;

  return {
    fit: fit,
    top: popoverPlacement.top,
    left: popoverPlacement.left,
    arrow: arrow
  };
}

function getCrossAxisPosition(_ref5) {
  var crossAxisFirstSide = _ref5.crossAxisFirstSide,
      crossAxisSecondSide = _ref5.crossAxisSecondSide,
      crossAxisDimension = _ref5.crossAxisDimension,
      position = _ref5.position,
      align = _ref5.align,
      buffer = _ref5.buffer,
      offset = _ref5.offset,
      windowBoundingBox = _ref5.windowBoundingBox,
      containerBoundingBox = _ref5.containerBoundingBox,
      popoverBoundingBox = _ref5.popoverBoundingBox,
      anchorBoundingBox = _ref5.anchorBoundingBox,
      arrowConfig = _ref5.arrowConfig;

  // how much of the popover overflows past either side of the anchor if its centered
  var popoverSizeOnCrossAxis = popoverBoundingBox[crossAxisDimension];
  var anchorSizeOnCrossAxis = anchorBoundingBox[crossAxisDimension];
  var anchorHalfSize = anchorSizeOnCrossAxis / 2;

  // the popover's original position on the cross-axis is determined by:
  var crossAxisPositionOriginal = anchorBoundingBox[crossAxisFirstSide] // where the anchor is located
  + anchorHalfSize // plus half anchor dimension
  - popoverSizeOnCrossAxis / 2; // less half the popover dimension

  // To fit the content within both the window and container,
  // compute the smaller of the two spaces along each edge
  var combinedBoundingBox = intersectBoundingBoxes(windowBoundingBox, containerBoundingBox);
  var availableSpace = getAvailableSpace(anchorBoundingBox, combinedBoundingBox, buffer, offset, position);
  var minimumSpace = arrowConfig ? arrowConfig.arrowBuffer : 0;
  availableSpace[crossAxisFirstSide] = Math.max(availableSpace[crossAxisFirstSide], minimumSpace);
  availableSpace[crossAxisSecondSide] = Math.max(availableSpace[crossAxisSecondSide], minimumSpace);

  var contentOverflowSize = (popoverSizeOnCrossAxis - anchorSizeOnCrossAxis) / 2;

  var alignAmount = 0;
  var alignDirection = 1;
  var amountOfShiftNeeded = 0;
  var shiftDirection = 1;

  if (align != null) {
    // no alignment, find how much the container boundary requires the content to shift
    alignDirection = align === 'top' || align === 'left' ? 1 : -1;
    alignAmount = contentOverflowSize;

    var alignedOverflowAmount = contentOverflowSize + alignAmount;
    var needsShift = alignedOverflowAmount > availableSpace[positionComplements[align]];
    amountOfShiftNeeded = needsShift ? alignedOverflowAmount - availableSpace[positionComplements[align]] : 0;
    shiftDirection = -1 * alignDirection;
  } else {
    // shifting the popover to one side may yield a better fit
    var spaceAvailableOnFirstSide = availableSpace[crossAxisFirstSide];
    var spaceAvailableOnSecondSide = availableSpace[crossAxisSecondSide];

    var isShiftTowardFirstSide = spaceAvailableOnFirstSide > spaceAvailableOnSecondSide;
    shiftDirection = isShiftTowardFirstSide ? -1 : 1;

    // determine which direction has more room and the popover should shift to
    var leastAvailableSpace = Math.min(spaceAvailableOnFirstSide, spaceAvailableOnSecondSide);

    var _needsShift = contentOverflowSize > leastAvailableSpace;
    amountOfShiftNeeded = _needsShift ? contentOverflowSize - leastAvailableSpace : 0;
  }

  // shift over the popover if necessary
  var shiftAmount = amountOfShiftNeeded * shiftDirection;
  var crossAxisPosition = crossAxisPositionOriginal + shiftAmount + alignAmount * alignDirection;

  // if an `arrowConfig` is specified, find where to position the arrow
  var crossAxisArrowPosition = void 0;
  if (arrowConfig) {
    var arrowWidth = arrowConfig.arrowWidth;

    var _anchorSizeOnCrossAxis = anchorBoundingBox[crossAxisDimension];
    var _anchorHalfSize = _anchorSizeOnCrossAxis / 2;
    crossAxisArrowPosition = anchorBoundingBox[crossAxisFirstSide] + _anchorHalfSize - arrowWidth / 2;

    // make sure there's enough buffer around the arrow
    // by calculating how how much the arrow would need to move
    // but instead of moving the arrow, shift the popover content
    if (crossAxisArrowPosition < crossAxisPosition + minimumSpace) {
      // arrow is too close to the minimum side
      var difference = crossAxisPosition + minimumSpace - crossAxisArrowPosition;
      crossAxisPosition -= difference;
    } else if (crossAxisArrowPosition + minimumSpace + arrowWidth > crossAxisPosition + popoverSizeOnCrossAxis) {
      // arrow is too close to the maximum side
      var edge = crossAxisPosition + popoverSizeOnCrossAxis;
      var _difference = crossAxisArrowPosition - (edge - minimumSpace - arrowWidth);
      crossAxisPosition += _difference;
    }
  }

  return {
    crossAxisPosition: crossAxisPosition,
    crossAxisArrowPosition: crossAxisArrowPosition
  };
}

function getPrimaryAxisPosition(_ref6) {
  var position = _ref6.position,
      offset = _ref6.offset,
      popoverBoundingBox = _ref6.popoverBoundingBox,
      anchorBoundingBox = _ref6.anchorBoundingBox,
      arrowConfig = _ref6.arrowConfig;

  // if positioning to the top or left, the target position decreases
  // from the anchor's top or left, otherwise the position adds to the anchor's
  var isOffsetDecreasing = position === 'top' || position === 'left';

  var primaryAxisDimension = relatedDimension[position]; // "top" -> "height"
  var popoverSizeOnPrimaryAxis = popoverBoundingBox[primaryAxisDimension];

  // start at the top or left edge of the anchor element
  var primaryAxisPositionName = dimensionPositionAttribute[primaryAxisDimension]; // "height" -> "top"
  var anchorEdgeOrigin = anchorBoundingBox[primaryAxisPositionName];

  // find the popover position on the primary axis
  var anchorSizeOnPrimaryAxis = anchorBoundingBox[primaryAxisDimension];
  var primaryAxisOffset = isOffsetDecreasing ? popoverSizeOnPrimaryAxis : anchorSizeOnPrimaryAxis;
  var contentOffset = (offset + primaryAxisOffset) * (isOffsetDecreasing ? -1 : 1);
  var primaryAxisPosition = anchorEdgeOrigin + contentOffset;

  var primaryAxisArrowPosition = void 0;

  if (arrowConfig) {
    primaryAxisArrowPosition = isOffsetDecreasing ? popoverSizeOnPrimaryAxis : 0;
  }

  return {
    primaryAxisPosition: primaryAxisPosition,
    primaryAxisArrowPosition: primaryAxisArrowPosition
  };
}

/**
 * Finds the client pixel coordinate of each edge for the element's bounding box,
 * and the bounding box's width & height
 *
 * @param {HTMLElement|React.Component} element
 * @returns {{top: number, right: number, bottom: number, left: number, height: number, width: number}}
 */
function getElementBoundingBox(element) {
  element = (0, _reactDom.findDOMNode)(element); // resolve any React abstractions

  var rect = element.getBoundingClientRect();
  return {
    top: rect.top,
    right: rect.right,
    bottom: rect.bottom,
    left: rect.left,
    height: rect.height,
    width: rect.width
  };
}

/**
 * Calculates the available content space between anchor and container
 *
 * @param {Object} anchorBoundingBox Client bounding box of the anchor element
 * @param {Object} containerBoundingBox Client bounding box of the container element
 * @param {number} buffer Minimum distance between the popover and the bounding container
 * @param {number} offset Distance between the popover and the anchor
 * @param {string} offsetSide Side the offset needs to be applied to, one of ["top", "right", "bottom", "left"]
 * @returns {{top: number, right: number, bottom: number, left: number}}
 */
function getAvailableSpace(anchorBoundingBox, containerBoundingBox, buffer, offset, offsetSide) {
  return {
    top: anchorBoundingBox.top - containerBoundingBox.top - buffer - (offsetSide === 'top' ? offset : 0),
    right: containerBoundingBox.right - anchorBoundingBox.right - buffer - (offsetSide === 'right' ? offset : 0),
    bottom: containerBoundingBox.bottom - anchorBoundingBox.bottom - buffer - (offsetSide === 'bottom' ? offset : 0),
    left: anchorBoundingBox.left - containerBoundingBox.left - buffer - (offsetSide === 'left' ? offset : 0)
  };
}

/**
 * Computes the fit (overlap) of the content within the container, fit is in range 0.0 => 1.0
 * @param contentBoundingBox bounding box of content to calculate fit for
 * @param containerBoundingBox bounding box of container
 * @returns {number}
 */
function getVisibleFit(contentBoundingBox, containerBoundingBox) {
  var intersection = intersectBoundingBoxes(contentBoundingBox, containerBoundingBox);

  if (intersection.left > intersection.right || intersection.top > intersection.top) {
    // there is no intersection, the boxes are completely separated on at least one axis
    return 0;
  }

  var intersectionArea = (intersection.right - intersection.left) * (intersection.bottom - intersection.top);
  var contentArea = (contentBoundingBox.right - contentBoundingBox.left) * (contentBoundingBox.bottom - contentBoundingBox.top);

  return intersectionArea / contentArea;
}

/**
 * Calculates the intersection space between two bounding boxes
 *
 * @param firstBox
 * @param secondBox
 * @returns {{top: number, right: number, bottom: number, left: number, height: number, width: number}}
 */
function intersectBoundingBoxes(firstBox, secondBox) {
  var intersection = {
    top: Math.max(firstBox.top, secondBox.top),
    right: Math.min(firstBox.right, secondBox.right),
    bottom: Math.min(firstBox.bottom, secondBox.bottom),
    left: Math.max(firstBox.left, secondBox.left)
  };

  intersection.height = Math.max(intersection.bottom - intersection.top, 0);
  intersection.width = Math.max(intersection.right - intersection.left, 0);

  return intersection;
}

/**
 * Returns the top-most defined z-index in the element's ancestor hierarchy
 * relative to the `target` element; if no z-index is defined, returns "0"
 * @param element {HTMLElement|React.Component}
 * @param cousin {HTMLElement|React.Component}
 * @returns {string}
 */
function getElementZIndex(element, cousin) {
  element = (0, _reactDom.findDOMNode)(element);
  cousin = (0, _reactDom.findDOMNode)(cousin);

  /**
   * finding the z-index of `element` is not the full story
   * its the CSS stacking context that is important
   * take this DOM for example:
   * body
   *   section[z-index: 1000]
   *     p[z-index: 500]
   *       button
   *   div
   *
   * what z-index does the `div` need to display next to `button`?
   * the `div` and `section` are where the stacking context splits
   * so `div` needs to copy `section`'s z-index in order to
   * appear next to / over `button`
   *
   * calculate this by starting at `button` and finding its offsetParents
   * then walk the parents from top -> down until the stacking context
   * split is found, or if there is no split then a specific z-index is unimportant
   */

  // build the array of the element + its offset parents
  var nodesToInspect = [];
  while (true) {
    nodesToInspect.push(element);

    element = element.offsetParent;

    // stop if there is no parent
    if (element == null) break;

    // stop if the parent contains the related element
    // as this is the z-index ancestor
    if (element.contains(cousin)) break;
  }

  // reverse the nodes to walk from top -> element
  nodesToInspect.reverse();

  return nodesToInspect.reduce(function (foundZIndex, node) {
    if (foundZIndex != null) return foundZIndex;

    // get this node's z-index css value
    var zIndex = window.document.defaultView.getComputedStyle(node).getPropertyValue('z-index');

    // if the z-index is not a number (e.g. "auto") return null, else the value
    return isNaN(zIndex) ? null : zIndex;
  }, null) || '0';
}