index.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { computed, onBeforeUnmount, ref, shallowRef, unref, watch } from 'vue'
  2. import { createPopper } from '@popperjs/core'
  3. import { fromPairs } from 'lodash-unified'
  4. import type { Ref } from 'vue'
  5. import type {
  6. Instance,
  7. Modifier,
  8. Options,
  9. State,
  10. VirtualElement,
  11. } from '@popperjs/core'
  12. type ElementType = HTMLElement | undefined
  13. type ReferenceElement = ElementType | VirtualElement
  14. export type PartialOptions = Partial<Options>
  15. export const usePopper = (
  16. referenceElementRef: Ref<ReferenceElement>,
  17. popperElementRef: Ref<ElementType>,
  18. opts: Ref<PartialOptions> | PartialOptions = {} as PartialOptions
  19. ) => {
  20. const stateUpdater = {
  21. name: 'updateState',
  22. enabled: true,
  23. phase: 'write',
  24. fn: ({ state }) => {
  25. const derivedState = deriveState(state)
  26. Object.assign(states.value, derivedState)
  27. },
  28. requires: ['computeStyles'],
  29. } as Modifier<'updateState', any>
  30. const options = computed<Options>(() => {
  31. const { onFirstUpdate, placement, strategy, modifiers } = unref(opts)
  32. return {
  33. onFirstUpdate,
  34. placement: placement || 'bottom',
  35. strategy: strategy || 'absolute',
  36. modifiers: [
  37. ...(modifiers || []),
  38. stateUpdater,
  39. { name: 'applyStyles', enabled: false },
  40. ],
  41. }
  42. })
  43. const instanceRef = shallowRef<Instance | undefined>()
  44. const states = ref<Pick<State, 'styles' | 'attributes'>>({
  45. styles: {
  46. popper: {
  47. position: unref(options).strategy,
  48. left: '0',
  49. top: '0',
  50. },
  51. arrow: {
  52. position: 'absolute',
  53. },
  54. },
  55. attributes: {},
  56. })
  57. const destroy = () => {
  58. if (!instanceRef.value) return
  59. instanceRef.value.destroy()
  60. instanceRef.value = undefined
  61. }
  62. watch(
  63. options,
  64. (newOptions) => {
  65. const instance = unref(instanceRef)
  66. if (instance) {
  67. instance.setOptions(newOptions)
  68. }
  69. },
  70. {
  71. deep: true,
  72. }
  73. )
  74. watch(
  75. [referenceElementRef, popperElementRef],
  76. ([referenceElement, popperElement]) => {
  77. destroy()
  78. if (!referenceElement || !popperElement) return
  79. instanceRef.value = createPopper(
  80. referenceElement,
  81. popperElement,
  82. unref(options)
  83. )
  84. }
  85. )
  86. onBeforeUnmount(() => {
  87. destroy()
  88. })
  89. return {
  90. state: computed(() => ({ ...(unref(instanceRef)?.state || {}) })),
  91. styles: computed(() => unref(states).styles),
  92. attributes: computed(() => unref(states).attributes),
  93. update: () => unref(instanceRef)?.update(),
  94. forceUpdate: () => unref(instanceRef)?.forceUpdate(),
  95. // Preventing end users from modifying the instance.
  96. instanceRef: computed(() => unref(instanceRef)),
  97. }
  98. }
  99. function deriveState(state: State) {
  100. const elements = Object.keys(state.elements) as unknown as Array<
  101. keyof State['elements']
  102. >
  103. const styles = fromPairs(
  104. elements.map(
  105. (element) =>
  106. [element, state.styles[element] || {}] as [
  107. string,
  108. State['styles'][keyof State['styles']]
  109. ]
  110. )
  111. )
  112. const attributes = fromPairs(
  113. elements.map(
  114. (element) =>
  115. [element, state.attributes[element]] as [
  116. string,
  117. State['attributes'][keyof State['attributes']]
  118. ]
  119. )
  120. )
  121. return {
  122. styles,
  123. attributes,
  124. }
  125. }
  126. export type UsePopperReturn = ReturnType<typeof usePopper>