You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

296 lines
9.2 KiB

  1. // @flow
  2. import type {
  3. State,
  4. OptionsGeneric,
  5. Modifier,
  6. Instance,
  7. VirtualElement,
  8. } from './types';
  9. import getCompositeRect from './dom-utils/getCompositeRect';
  10. import getLayoutRect from './dom-utils/getLayoutRect';
  11. import listScrollParents from './dom-utils/listScrollParents';
  12. import getOffsetParent from './dom-utils/getOffsetParent';
  13. import getComputedStyle from './dom-utils/getComputedStyle';
  14. import orderModifiers from './utils/orderModifiers';
  15. import debounce from './utils/debounce';
  16. import validateModifiers from './utils/validateModifiers';
  17. import uniqueBy from './utils/uniqueBy';
  18. import getBasePlacement from './utils/getBasePlacement';
  19. import mergeByName from './utils/mergeByName';
  20. import detectOverflow from './utils/detectOverflow';
  21. import { isElement } from './dom-utils/instanceOf';
  22. import { auto } from './enums';
  23. const INVALID_ELEMENT_ERROR =
  24. 'Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.';
  25. const INFINITE_LOOP_ERROR =
  26. 'Popper: An infinite loop in the modifiers cycle has been detected! The cycle has been interrupted to prevent a browser crash.';
  27. const DEFAULT_OPTIONS: OptionsGeneric<any> = {
  28. placement: 'bottom',
  29. modifiers: [],
  30. strategy: 'absolute',
  31. };
  32. type PopperGeneratorArgs = {
  33. defaultModifiers?: Array<Modifier<any, any>>,
  34. defaultOptions?: $Shape<OptionsGeneric<any>>,
  35. };
  36. function areValidElements(...args: Array<any>): boolean {
  37. return !args.some(
  38. (element) =>
  39. !(element && typeof element.getBoundingClientRect === 'function')
  40. );
  41. }
  42. export function popperGenerator(generatorOptions: PopperGeneratorArgs = {}) {
  43. const {
  44. defaultModifiers = [],
  45. defaultOptions = DEFAULT_OPTIONS,
  46. } = generatorOptions;
  47. return function createPopper<TModifier: $Shape<Modifier<any, any>>>(
  48. reference: Element | VirtualElement,
  49. popper: HTMLElement,
  50. options: $Shape<OptionsGeneric<TModifier>> = defaultOptions
  51. ): Instance {
  52. let state: $Shape<State> = {
  53. placement: 'bottom',
  54. orderedModifiers: [],
  55. options: { ...DEFAULT_OPTIONS, ...defaultOptions },
  56. modifiersData: {},
  57. elements: {
  58. reference,
  59. popper,
  60. },
  61. attributes: {},
  62. styles: {},
  63. };
  64. let effectCleanupFns: Array<() => void> = [];
  65. let isDestroyed = false;
  66. const instance = {
  67. state,
  68. setOptions(setOptionsAction) {
  69. const options =
  70. typeof setOptionsAction === 'function'
  71. ? setOptionsAction(state.options)
  72. : setOptionsAction;
  73. cleanupModifierEffects();
  74. state.options = {
  75. // $FlowFixMe[exponential-spread]
  76. ...defaultOptions,
  77. ...state.options,
  78. ...options,
  79. };
  80. state.scrollParents = {
  81. reference: isElement(reference)
  82. ? listScrollParents(reference)
  83. : reference.contextElement
  84. ? listScrollParents(reference.contextElement)
  85. : [],
  86. popper: listScrollParents(popper),
  87. };
  88. // Orders the modifiers based on their dependencies and `phase`
  89. // properties
  90. const orderedModifiers = orderModifiers(
  91. mergeByName([...defaultModifiers, ...state.options.modifiers])
  92. );
  93. // Strip out disabled modifiers
  94. state.orderedModifiers = orderedModifiers.filter((m) => m.enabled);
  95. // Validate the provided modifiers so that the consumer will get warned
  96. // if one of the modifiers is invalid for any reason
  97. if (false) {
  98. const modifiers = uniqueBy(
  99. [...orderedModifiers, ...state.options.modifiers],
  100. ({ name }) => name
  101. );
  102. validateModifiers(modifiers);
  103. if (getBasePlacement(state.options.placement) === auto) {
  104. const flipModifier = state.orderedModifiers.find(
  105. ({ name }) => name === 'flip'
  106. );
  107. if (!flipModifier) {
  108. console.error(
  109. [
  110. 'Popper: "auto" placements require the "flip" modifier be',
  111. 'present and enabled to work.',
  112. ].join(' ')
  113. );
  114. }
  115. }
  116. const {
  117. marginTop,
  118. marginRight,
  119. marginBottom,
  120. marginLeft,
  121. } = getComputedStyle(popper);
  122. // We no longer take into account `margins` on the popper, and it can
  123. // cause bugs with positioning, so we'll warn the consumer
  124. if (
  125. [marginTop, marginRight, marginBottom, marginLeft].some((margin) =>
  126. parseFloat(margin)
  127. )
  128. ) {
  129. console.warn(
  130. [
  131. 'Popper: CSS "margin" styles cannot be used to apply padding',
  132. 'between the popper and its reference element or boundary.',
  133. 'To replicate margin, use the `offset` modifier, as well as',
  134. 'the `padding` option in the `preventOverflow` and `flip`',
  135. 'modifiers.',
  136. ].join(' ')
  137. );
  138. }
  139. }
  140. runModifierEffects();
  141. return instance.update();
  142. },
  143. // Sync update – it will always be executed, even if not necessary. This
  144. // is useful for low frequency updates where sync behavior simplifies the
  145. // logic.
  146. // For high frequency updates (e.g. `resize` and `scroll` events), always
  147. // prefer the async Popper#update method
  148. forceUpdate() {
  149. if (isDestroyed) {
  150. return;
  151. }
  152. const { reference, popper } = state.elements;
  153. // Don't proceed if `reference` or `popper` are not valid elements
  154. // anymore
  155. if (!areValidElements(reference, popper)) {
  156. if (false) {
  157. console.error(INVALID_ELEMENT_ERROR);
  158. }
  159. return;
  160. }
  161. // Store the reference and popper rects to be read by modifiers
  162. state.rects = {
  163. reference: getCompositeRect(
  164. reference,
  165. getOffsetParent(popper),
  166. state.options.strategy === 'fixed'
  167. ),
  168. popper: getLayoutRect(popper),
  169. };
  170. // Modifiers have the ability to reset the current update cycle. The
  171. // most common use case for this is the `flip` modifier changing the
  172. // placement, which then needs to re-run all the modifiers, because the
  173. // logic was previously ran for the previous placement and is therefore
  174. // stale/incorrect
  175. state.reset = false;
  176. state.placement = state.options.placement;
  177. // On each update cycle, the `modifiersData` property for each modifier
  178. // is filled with the initial data specified by the modifier. This means
  179. // it doesn't persist and is fresh on each update.
  180. // To ensure persistent data, use `${name}#persistent`
  181. state.orderedModifiers.forEach(
  182. (modifier) =>
  183. (state.modifiersData[modifier.name] = {
  184. ...modifier.data,
  185. })
  186. );
  187. let __debug_loops__ = 0;
  188. for (let index = 0; index < state.orderedModifiers.length; index++) {
  189. if (false) {
  190. __debug_loops__ += 1;
  191. if (__debug_loops__ > 100) {
  192. console.error(INFINITE_LOOP_ERROR);
  193. break;
  194. }
  195. }
  196. if (state.reset === true) {
  197. state.reset = false;
  198. index = -1;
  199. continue;
  200. }
  201. const { fn, options = {}, name } = state.orderedModifiers[index];
  202. if (typeof fn === 'function') {
  203. state = fn({ state, options, name, instance }) || state;
  204. }
  205. }
  206. },
  207. // Async and optimistically optimized update – it will not be executed if
  208. // not necessary (debounced to run at most once-per-tick)
  209. update: debounce<$Shape<State>>(
  210. () =>
  211. new Promise<$Shape<State>>((resolve) => {
  212. instance.forceUpdate();
  213. resolve(state);
  214. })
  215. ),
  216. destroy() {
  217. cleanupModifierEffects();
  218. isDestroyed = true;
  219. },
  220. };
  221. if (!areValidElements(reference, popper)) {
  222. if (false) {
  223. console.error(INVALID_ELEMENT_ERROR);
  224. }
  225. return instance;
  226. }
  227. instance.setOptions(options).then((state) => {
  228. if (!isDestroyed && options.onFirstUpdate) {
  229. options.onFirstUpdate(state);
  230. }
  231. });
  232. // Modifiers have the ability to execute arbitrary code before the first
  233. // update cycle runs. They will be executed in the same order as the update
  234. // cycle. This is useful when a modifier adds some persistent data that
  235. // other modifiers need to use, but the modifier is run after the dependent
  236. // one.
  237. function runModifierEffects() {
  238. state.orderedModifiers.forEach(({ name, options = {}, effect }) => {
  239. if (typeof effect === 'function') {
  240. const cleanupFn = effect({ state, name, instance, options });
  241. const noopFn = () => {};
  242. effectCleanupFns.push(cleanupFn || noopFn);
  243. }
  244. });
  245. }
  246. function cleanupModifierEffects() {
  247. effectCleanupFns.forEach((fn) => fn());
  248. effectCleanupFns = [];
  249. }
  250. return instance;
  251. };
  252. }
  253. export const createPopper = popperGenerator();
  254. // eslint-disable-next-line import/no-unused-modules
  255. export { detectOverflow };