arrow.js.flow 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. // @flow
  2. import type { Modifier, ModifierArguments, Padding, Rect } from '../types';
  3. import type { Placement } from '../enums';
  4. import getBasePlacement from '../utils/getBasePlacement';
  5. import getLayoutRect from '../dom-utils/getLayoutRect';
  6. import contains from '../dom-utils/contains';
  7. import getOffsetParent from '../dom-utils/getOffsetParent';
  8. import getMainAxisFromPlacement from '../utils/getMainAxisFromPlacement';
  9. import { within } from '../utils/within';
  10. import mergePaddingObject from '../utils/mergePaddingObject';
  11. import expandToHashMap from '../utils/expandToHashMap';
  12. import { left, right, basePlacements, top, bottom } from '../enums';
  13. import { isHTMLElement } from '../dom-utils/instanceOf';
  14. // eslint-disable-next-line import/no-unused-modules
  15. export type Options = {
  16. element: HTMLElement | string | null,
  17. padding:
  18. | Padding
  19. | (({|
  20. popper: Rect,
  21. reference: Rect,
  22. placement: Placement,
  23. |}) => Padding),
  24. };
  25. const toPaddingObject = (padding, state) => {
  26. padding =
  27. typeof padding === 'function'
  28. ? padding({ ...state.rects, placement: state.placement })
  29. : padding;
  30. return mergePaddingObject(
  31. typeof padding !== 'number'
  32. ? padding
  33. : expandToHashMap(padding, basePlacements)
  34. );
  35. };
  36. function arrow({ state, name, options }: ModifierArguments<Options>) {
  37. const arrowElement = state.elements.arrow;
  38. const popperOffsets = state.modifiersData.popperOffsets;
  39. const basePlacement = getBasePlacement(state.placement);
  40. const axis = getMainAxisFromPlacement(basePlacement);
  41. const isVertical = [left, right].indexOf(basePlacement) >= 0;
  42. const len = isVertical ? 'height' : 'width';
  43. if (!arrowElement || !popperOffsets) {
  44. return;
  45. }
  46. const paddingObject = toPaddingObject(options.padding, state);
  47. const arrowRect = getLayoutRect(arrowElement);
  48. const minProp = axis === 'y' ? top : left;
  49. const maxProp = axis === 'y' ? bottom : right;
  50. const endDiff =
  51. state.rects.reference[len] +
  52. state.rects.reference[axis] -
  53. popperOffsets[axis] -
  54. state.rects.popper[len];
  55. const startDiff = popperOffsets[axis] - state.rects.reference[axis];
  56. const arrowOffsetParent = getOffsetParent(arrowElement);
  57. const clientSize = arrowOffsetParent
  58. ? axis === 'y'
  59. ? arrowOffsetParent.clientHeight || 0
  60. : arrowOffsetParent.clientWidth || 0
  61. : 0;
  62. const centerToReference = endDiff / 2 - startDiff / 2;
  63. // Make sure the arrow doesn't overflow the popper if the center point is
  64. // outside of the popper bounds
  65. const min = paddingObject[minProp];
  66. const max = clientSize - arrowRect[len] - paddingObject[maxProp];
  67. const center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;
  68. const offset = within(min, center, max);
  69. // Prevents breaking syntax highlighting...
  70. const axisProp: string = axis;
  71. state.modifiersData[name] = {
  72. [axisProp]: offset,
  73. centerOffset: offset - center,
  74. };
  75. }
  76. function effect({ state, options }: ModifierArguments<Options>) {
  77. let { element: arrowElement = '[data-popper-arrow]' } = options;
  78. if (arrowElement == null) {
  79. return;
  80. }
  81. // CSS selector
  82. if (typeof arrowElement === 'string') {
  83. arrowElement = state.elements.popper.querySelector(arrowElement);
  84. if (!arrowElement) {
  85. return;
  86. }
  87. }
  88. if (false) {
  89. if (!isHTMLElement(arrowElement)) {
  90. console.error(
  91. [
  92. 'Popper: "arrow" element must be an HTMLElement (not an SVGElement).',
  93. 'To use an SVG arrow, wrap it in an HTMLElement that will be used as',
  94. 'the arrow.',
  95. ].join(' ')
  96. );
  97. }
  98. }
  99. if (!contains(state.elements.popper, arrowElement)) {
  100. if (false) {
  101. console.error(
  102. [
  103. 'Popper: "arrow" modifier\'s `element` must be a child of the popper',
  104. 'element.',
  105. ].join(' ')
  106. );
  107. }
  108. return;
  109. }
  110. state.elements.arrow = arrowElement;
  111. }
  112. // eslint-disable-next-line import/no-unused-modules
  113. export type ArrowModifier = Modifier<'arrow', Options>;
  114. export default ({
  115. name: 'arrow',
  116. enabled: true,
  117. phase: 'main',
  118. fn: arrow,
  119. effect,
  120. requires: ['popperOffsets'],
  121. requiresIfExists: ['preventOverflow'],
  122. }: ArrowModifier);