floating.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <template>
  2. <teleport :to="mount">
  3. <div class="ui-floating" :style="style" :class="props.class" @mouseenter="emit('enter')" @mouseleave="emit('leave')">
  4. <slot />
  5. </div>
  6. </teleport>
  7. </template>
  8. <script setup lang="ts">
  9. // onUpdated
  10. import { computed, defineExpose, defineProps, onActivated, onUnmounted, reactive, watch } from 'vue'
  11. import { getPostionByTarget, getScrollParents } from '@kankan-components/utils'
  12. import { useZIndex } from '@kankan-components/hooks'
  13. const { currentZIndex } = useZIndex()
  14. defineOptions({
  15. name: 'UIFloating',
  16. })
  17. const Horizontal = {
  18. center: 'center',
  19. right: 'right',
  20. left: 'left',
  21. }
  22. const Vertical = {
  23. center: 'center',
  24. top: 'top',
  25. bottom: 'bottom',
  26. }
  27. const Divide = '-'
  28. const props = defineProps({
  29. mount: {
  30. require: true,
  31. default: document.body,
  32. },
  33. class: { type: String },
  34. refer: { type: Object },
  35. dire: { type: String },
  36. width: { type: [Number, String] },
  37. height: { type: [Number, String] },
  38. })
  39. const emit = defineEmits(['leave', 'enter'])
  40. // 确定方向
  41. const dires = computed(() => {
  42. const dire = props.dire || `${Vertical.bottom}${Divide}${Horizontal.left}`
  43. const isPreset = (preset, val) => Object.keys(preset).some(key => preset[key] === val)
  44. let [horizontal, vertical] = dire.split(Divide)
  45. if (!horizontal || !isPreset(Horizontal, horizontal)) {
  46. horizontal = Horizontal.left
  47. }
  48. if (!vertical || !isPreset(Vertical, vertical)) {
  49. vertical = Vertical.bottom
  50. }
  51. return [horizontal, vertical]
  52. })
  53. const normalizeUnit = (unit: number | string, total: number): number => {
  54. if (unit === 0) {
  55. return 0
  56. } else if (typeof unit === 'number') {
  57. return unit ? (unit <= 1 && unit >= 0 ? total * unit : unit) : 0
  58. } else if (unit.includes('px')) {
  59. return normalizeUnit(Number.parseFloat(unit), total)
  60. } else if (unit.includes('%')) {
  61. return normalizeUnit(Number.parseFloat(unit) / 100, total)
  62. }
  63. return 0
  64. }
  65. const width = computed(() => props.refer && normalizeUnit(props.width, props.refer.offsetWidth))
  66. const height = computed(() => props.refer && normalizeUnit(props.height, props.refer.offsetHeight))
  67. const location = reactive({ x: 0, y: 0 })
  68. const scrollParents = computed(() => (props.refer ? getScrollParents(props.refer, props.mount) : []))
  69. watch(
  70. [scrollParents, props],
  71. ([newParents], [oldParents]) => {
  72. oldParents && oldParents.forEach(dom => dom.removeEventListener('scroll', updateLocation))
  73. newParents.forEach(dom => dom.addEventListener('scroll', updateLocation))
  74. if (props.refer) {
  75. setTimeout(() => updateLocation())
  76. }
  77. },
  78. { immediate: true }
  79. )
  80. // const zIndex = currentZIndex
  81. const style = computed(() => {
  82. const style = {
  83. width: width.value && `${width.value}px`,
  84. height: height.value && `${height.value}px`,
  85. left: `${location.x}px`,
  86. top: `${location.y}px`,
  87. zIndex: currentZIndex,
  88. }
  89. if (location.x > 0 && location.y > 0) {
  90. return style
  91. }
  92. return {}
  93. })
  94. const updateLocation = () => {
  95. const pos = getPostionByTarget(props.refer, props.mount)
  96. const screenInfo = scrollParents.value.reduce(
  97. (t, c) => {
  98. t.y += c.scrollTop
  99. t.x += c.scrollLeft
  100. return t
  101. },
  102. { x: 0, y: 0 }
  103. )
  104. const [horizontal, vertical] = dires.value
  105. const start = {
  106. x: pos.x - screenInfo.x,
  107. y: pos.y - screenInfo.y,
  108. }
  109. switch (horizontal) {
  110. case Horizontal.left:
  111. location.x = start.x
  112. break
  113. case Horizontal.right:
  114. location.x = start.x + pos.width
  115. break
  116. case Horizontal.center:
  117. location.x = start.x + pos.width / 2
  118. break
  119. }
  120. switch (vertical) {
  121. case Vertical.top:
  122. location.y = start.y
  123. break
  124. case Vertical.bottom:
  125. location.y = start.y + pos.height
  126. break
  127. case Vertical.center:
  128. location.y = start.y + pos.height / 2
  129. break
  130. }
  131. }
  132. window.addEventListener('resize', updateLocation)
  133. onUnmounted(() => {
  134. scrollParents.value.forEach(dom => dom.removeEventListener('scroll', updateLocation))
  135. window.removeEventListener('resize', updateLocation)
  136. })
  137. onActivated(() => {
  138. if (props.refer) {
  139. updateLocation()
  140. }
  141. })
  142. defineExpose({
  143. updateLocation,
  144. })
  145. </script>