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.

218 lines
6.6 KiB

2 months ago
  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 orderModifiers from './utils/orderModifiers';
  14. import debounce from './utils/debounce';
  15. import mergeByName from './utils/mergeByName';
  16. import detectOverflow from './utils/detectOverflow';
  17. import { isElement } from './dom-utils/instanceOf';
  18. const DEFAULT_OPTIONS: OptionsGeneric<any> = {
  19. placement: 'bottom',
  20. modifiers: [],
  21. strategy: 'absolute',
  22. };
  23. type PopperGeneratorArgs = {
  24. defaultModifiers?: Array<Modifier<any, any>>,
  25. defaultOptions?: $Shape<OptionsGeneric<any>>,
  26. };
  27. function areValidElements(...args: Array<any>): boolean {
  28. return !args.some(
  29. (element) =>
  30. !(element && typeof element.getBoundingClientRect === 'function')
  31. );
  32. }
  33. export function popperGenerator(generatorOptions: PopperGeneratorArgs = {}) {
  34. const { defaultModifiers = [], defaultOptions = DEFAULT_OPTIONS } =
  35. generatorOptions;
  36. return function createPopper<TModifier: $Shape<Modifier<any, any>>>(
  37. reference: Element | VirtualElement,
  38. popper: HTMLElement,
  39. options: $Shape<OptionsGeneric<TModifier>> = defaultOptions
  40. ): Instance {
  41. let state: $Shape<State> = {
  42. placement: 'bottom',
  43. orderedModifiers: [],
  44. options: { ...DEFAULT_OPTIONS, ...defaultOptions },
  45. modifiersData: {},
  46. elements: {
  47. reference,
  48. popper,
  49. },
  50. attributes: {},
  51. styles: {},
  52. };
  53. let effectCleanupFns: Array<() => void> = [];
  54. let isDestroyed = false;
  55. const instance = {
  56. state,
  57. setOptions(setOptionsAction) {
  58. const options =
  59. typeof setOptionsAction === 'function'
  60. ? setOptionsAction(state.options)
  61. : setOptionsAction;
  62. cleanupModifierEffects();
  63. state.options = {
  64. // $FlowFixMe[exponential-spread]
  65. ...defaultOptions,
  66. ...state.options,
  67. ...options,
  68. };
  69. state.scrollParents = {
  70. reference: isElement(reference)
  71. ? listScrollParents(reference)
  72. : reference.contextElement
  73. ? listScrollParents(reference.contextElement)
  74. : [],
  75. popper: listScrollParents(popper),
  76. };
  77. // Orders the modifiers based on their dependencies and `phase`
  78. // properties
  79. const orderedModifiers = orderModifiers(
  80. mergeByName([...defaultModifiers, ...state.options.modifiers])
  81. );
  82. // Strip out disabled modifiers
  83. state.orderedModifiers = orderedModifiers.filter((m) => m.enabled);
  84. runModifierEffects();
  85. return instance.update();
  86. },
  87. // Sync update – it will always be executed, even if not necessary. This
  88. // is useful for low frequency updates where sync behavior simplifies the
  89. // logic.
  90. // For high frequency updates (e.g. `resize` and `scroll` events), always
  91. // prefer the async Popper#update method
  92. forceUpdate() {
  93. if (isDestroyed) {
  94. return;
  95. }
  96. const { reference, popper } = state.elements;
  97. // Don't proceed if `reference` or `popper` are not valid elements
  98. // anymore
  99. if (!areValidElements(reference, popper)) {
  100. return;
  101. }
  102. // Store the reference and popper rects to be read by modifiers
  103. state.rects = {
  104. reference: getCompositeRect(
  105. reference,
  106. getOffsetParent(popper),
  107. state.options.strategy === 'fixed'
  108. ),
  109. popper: getLayoutRect(popper),
  110. };
  111. // Modifiers have the ability to reset the current update cycle. The
  112. // most common use case for this is the `flip` modifier changing the
  113. // placement, which then needs to re-run all the modifiers, because the
  114. // logic was previously ran for the previous placement and is therefore
  115. // stale/incorrect
  116. state.reset = false;
  117. state.placement = state.options.placement;
  118. // On each update cycle, the `modifiersData` property for each modifier
  119. // is filled with the initial data specified by the modifier. This means
  120. // it doesn't persist and is fresh on each update.
  121. // To ensure persistent data, use `${name}#persistent`
  122. state.orderedModifiers.forEach(
  123. (modifier) =>
  124. (state.modifiersData[modifier.name] = {
  125. ...modifier.data,
  126. })
  127. );
  128. for (let index = 0; index < state.orderedModifiers.length; index++) {
  129. if (state.reset === true) {
  130. state.reset = false;
  131. index = -1;
  132. continue;
  133. }
  134. const { fn, options = {}, name } = state.orderedModifiers[index];
  135. if (typeof fn === 'function') {
  136. state = fn({ state, options, name, instance }) || state;
  137. }
  138. }
  139. },
  140. // Async and optimistically optimized update – it will not be executed if
  141. // not necessary (debounced to run at most once-per-tick)
  142. update: debounce<$Shape<State>>(
  143. () =>
  144. new Promise<$Shape<State>>((resolve) => {
  145. instance.forceUpdate();
  146. resolve(state);
  147. })
  148. ),
  149. destroy() {
  150. cleanupModifierEffects();
  151. isDestroyed = true;
  152. },
  153. };
  154. if (!areValidElements(reference, popper)) {
  155. return instance;
  156. }
  157. instance.setOptions(options).then((state) => {
  158. if (!isDestroyed && options.onFirstUpdate) {
  159. options.onFirstUpdate(state);
  160. }
  161. });
  162. // Modifiers have the ability to execute arbitrary code before the first
  163. // update cycle runs. They will be executed in the same order as the update
  164. // cycle. This is useful when a modifier adds some persistent data that
  165. // other modifiers need to use, but the modifier is run after the dependent
  166. // one.
  167. function runModifierEffects() {
  168. state.orderedModifiers.forEach(({ name, options = {}, effect }) => {
  169. if (typeof effect === 'function') {
  170. const cleanupFn = effect({ state, name, instance, options });
  171. const noopFn = () => {};
  172. effectCleanupFns.push(cleanupFn || noopFn);
  173. }
  174. });
  175. }
  176. function cleanupModifierEffects() {
  177. effectCleanupFns.forEach((fn) => fn());
  178. effectCleanupFns = [];
  179. }
  180. return instance;
  181. };
  182. }
  183. export const createPopper = popperGenerator();
  184. // eslint-disable-next-line import/no-unused-modules
  185. export { detectOverflow };