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 |
|
---|
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 |
|
---|
26 | const toPaddingObject = (padding, state) => {
|
---|
27 | padding =
|
---|
28 | typeof padding === 'function'
|
---|
29 | ? padding({ ...state.rects, placement: state.placement })
|
---|
30 | : padding;
|
---|
31 |
|
---|
32 | return mergePaddingObject(
|
---|
33 | typeof padding !== 'number'
|
---|
34 | ? padding
|
---|
35 | : expandToHashMap(padding, basePlacements)
|
---|
36 | );
|
---|
37 | };
|
---|
38 |
|
---|
39 | function arrow({ state, name, options }: ModifierArguments<Options>) {
|
---|
40 | const arrowElement = state.elements.arrow;
|
---|
41 | const popperOffsets = state.modifiersData.popperOffsets;
|
---|
42 | const basePlacement = getBasePlacement(state.placement);
|
---|
43 | const axis = getMainAxisFromPlacement(basePlacement);
|
---|
44 | const isVertical = [left, right].indexOf(basePlacement) >= 0;
|
---|
45 | const len = isVertical ? 'height' : 'width';
|
---|
46 |
|
---|
47 | if (!arrowElement || !popperOffsets) {
|
---|
48 | return;
|
---|
49 | }
|
---|
50 |
|
---|
51 | const paddingObject = toPaddingObject(options.padding, state);
|
---|
52 | const arrowRect = getLayoutRect(arrowElement);
|
---|
53 | const minProp = axis === 'y' ? top : left;
|
---|
54 | const maxProp = axis === 'y' ? bottom : right;
|
---|
55 |
|
---|
56 | const endDiff =
|
---|
57 | state.rects.reference[len] +
|
---|
58 | state.rects.reference[axis] -
|
---|
59 | popperOffsets[axis] -
|
---|
60 | state.rects.popper[len];
|
---|
61 | const startDiff = popperOffsets[axis] - state.rects.reference[axis];
|
---|
62 |
|
---|
63 | const arrowOffsetParent = getOffsetParent(arrowElement);
|
---|
64 | const clientSize = arrowOffsetParent
|
---|
65 | ? axis === 'y'
|
---|
66 | ? arrowOffsetParent.clientHeight || 0
|
---|
67 | : arrowOffsetParent.clientWidth || 0
|
---|
68 | : 0;
|
---|
69 |
|
---|
70 | const centerToReference = endDiff / 2 - startDiff / 2;
|
---|
71 |
|
---|
72 | // Make sure the arrow doesn't overflow the popper if the center point is
|
---|
73 | // outside of the popper bounds
|
---|
74 | const min = paddingObject[minProp];
|
---|
75 | const max = clientSize - arrowRect[len] - paddingObject[maxProp];
|
---|
76 | const center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;
|
---|
77 | const offset = within(min, center, max);
|
---|
78 |
|
---|
79 | // Prevents breaking syntax highlighting...
|
---|
80 | const axisProp: string = axis;
|
---|
81 | state.modifiersData[name] = {
|
---|
82 | [axisProp]: offset,
|
---|
83 | centerOffset: offset - center,
|
---|
84 | };
|
---|
85 | }
|
---|
86 |
|
---|
87 | function effect({ state, options }: ModifierArguments<Options>) {
|
---|
88 | let { element: arrowElement = '[data-popper-arrow]' } = options;
|
---|
89 |
|
---|
90 | if (arrowElement == null) {
|
---|
91 | return;
|
---|
92 | }
|
---|
93 |
|
---|
94 | // CSS selector
|
---|
95 | if (typeof arrowElement === 'string') {
|
---|
96 | arrowElement = state.elements.popper.querySelector(arrowElement);
|
---|
97 |
|
---|
98 | if (!arrowElement) {
|
---|
99 | return;
|
---|
100 | }
|
---|
101 | }
|
---|
102 |
|
---|
103 | if (!contains(state.elements.popper, arrowElement)) {
|
---|
104 | return;
|
---|
105 | }
|
---|
106 |
|
---|
107 | state.elements.arrow = arrowElement;
|
---|
108 | }
|
---|
109 |
|
---|
110 | // eslint-disable-next-line import/no-unused-modules
|
---|
111 | export type ArrowModifier = Modifier<'arrow', Options>;
|
---|
112 | export default ({
|
---|
113 | name: 'arrow',
|
---|
114 | enabled: true,
|
---|
115 | phase: 'main',
|
---|
116 | fn: arrow,
|
---|
117 | effect,
|
---|
118 | requires: ['popperOffsets'],
|
---|
119 | requiresIfExists: ['preventOverflow'],
|
---|
120 | }: ArrowModifier);
|
---|