useMouseTooltip.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // useMouseTooltip.js
  2. let tooltipEl = null
  3. let currentTarget = null
  4. let isInitialized = false
  5. let moveHandler = null
  6. let overHandler = null
  7. let outHandler = null
  8. let rafId = null
  9. let showTimer = null
  10. let hideTimer = null
  11. let lastMouseX = 0
  12. let lastMouseY = 0
  13. export function useMouseTooltip() {
  14. const PADDING = 16
  15. const OFFSET = 15
  16. const SHOW_DELAY = 300
  17. const HIDE_DELAY = 80
  18. function createTooltip() {
  19. if (tooltipEl) return
  20. tooltipEl = document.createElement('div')
  21. tooltipEl.className = 'cursor-pointer-tooltip'
  22. tooltipEl.style.cssText = `
  23. position: fixed;
  24. z-index: 2147483647;
  25. pointer-events: none;
  26. left: 0;
  27. top: 0;
  28. background: rgba(var(--bs-body-bg-rgb),1);
  29. color: var(--bs-emphasis-color);
  30. border-radius: .5rem;
  31. box-shadow: 0 0 10px rgba(0,0,0,.1);
  32. font-size: .8125rem;
  33. line-height: 1.4;
  34. padding: .375rem .5rem;
  35. white-space: nowrap;
  36. opacity: 0;
  37. visibility: hidden;
  38. transform: scale(.85);
  39. transition:
  40. opacity .18s ease,
  41. transform .18s ease;
  42. will-change:
  43. opacity,
  44. transform,
  45. left,
  46. top;
  47. `
  48. document.body.appendChild(tooltipEl)
  49. }
  50. function getTooltipSize() {
  51. if (!tooltipEl) {
  52. return {
  53. width: 0,
  54. height: 0
  55. }
  56. }
  57. const rect = tooltipEl.getBoundingClientRect()
  58. return {
  59. width: rect.width,
  60. height: rect.height
  61. }
  62. }
  63. function updatePosition(clientX, clientY) {
  64. if (!tooltipEl) return
  65. const viewportWidth = window.innerWidth
  66. const viewportHeight = window.innerHeight
  67. const { width, height } = getTooltipSize()
  68. let x = clientX + OFFSET
  69. let y = clientY + OFFSET
  70. if (x + width > viewportWidth - PADDING) {
  71. x = clientX - width - OFFSET
  72. }
  73. if (y + height > viewportHeight - PADDING) {
  74. y = clientY - height - OFFSET
  75. }
  76. x = Math.max(PADDING, x)
  77. y = Math.max(PADDING, y)
  78. tooltipEl.style.left = `${x}px`
  79. tooltipEl.style.top = `${y}px`
  80. }
  81. function showTooltip(e, target) {
  82. const text = target.dataset.tooltip
  83. if (!text) return
  84. createTooltip()
  85. if (hideTimer) {
  86. clearTimeout(hideTimer)
  87. hideTimer = null
  88. }
  89. if (showTimer) {
  90. clearTimeout(showTimer)
  91. showTimer = null
  92. }
  93. currentTarget = target
  94. tooltipEl.textContent = text
  95. updatePosition(
  96. e.clientX,
  97. e.clientY
  98. )
  99. tooltipEl.style.opacity = '0'
  100. tooltipEl.style.visibility = 'hidden'
  101. tooltipEl.style.transform = 'scale(.85)'
  102. showTimer = setTimeout(() => {
  103. if (currentTarget !== target) return
  104. tooltipEl.style.visibility = 'visible'
  105. requestAnimationFrame(() => {
  106. tooltipEl.style.opacity = '1'
  107. tooltipEl.style.transform = 'scale(1)'
  108. })
  109. }, SHOW_DELAY)
  110. }
  111. function hideTooltip() {
  112. if (!tooltipEl) return
  113. if (showTimer) {
  114. clearTimeout(showTimer)
  115. showTimer = null
  116. }
  117. if (hideTimer) {
  118. clearTimeout(hideTimer)
  119. }
  120. hideTimer = setTimeout(() => {
  121. tooltipEl.style.opacity = '0'
  122. tooltipEl.style.transform = 'scale(.85)'
  123. setTimeout(() => {
  124. if (!tooltipEl) return
  125. tooltipEl.style.visibility = 'hidden'
  126. }, 180)
  127. currentTarget = null
  128. }, HIDE_DELAY)
  129. }
  130. function init() {
  131. if (isInitialized) return
  132. isInitialized = true
  133. moveHandler = (e) => {
  134. if (!currentTarget) return
  135. lastMouseX = e.clientX
  136. lastMouseY = e.clientY
  137. if (rafId) return
  138. rafId = requestAnimationFrame(() => {
  139. rafId = null
  140. updatePosition(
  141. lastMouseX,
  142. lastMouseY
  143. )
  144. })
  145. }
  146. overHandler = (e) => {
  147. const target = e.target.closest('.cursor-pointer')
  148. if (!target) return
  149. if (target === currentTarget) return
  150. showTooltip(e, target)
  151. }
  152. outHandler = (e) => {
  153. const target = e.target.closest('.cursor-pointer')
  154. if (!target) return
  155. if (target !== currentTarget) return
  156. // 在当前元素内部移动
  157. if (
  158. e.relatedTarget &&
  159. target.contains(e.relatedTarget)
  160. ) {
  161. return
  162. }
  163. hideTooltip()
  164. }
  165. document.addEventListener(
  166. 'mouseover',
  167. overHandler,
  168. true
  169. )
  170. document.addEventListener(
  171. 'mouseout',
  172. outHandler,
  173. true
  174. )
  175. document.addEventListener(
  176. 'mousemove',
  177. moveHandler,
  178. true
  179. )
  180. }
  181. function cleanup() {
  182. if (rafId) {
  183. cancelAnimationFrame(rafId)
  184. rafId = null
  185. }
  186. if (showTimer) {
  187. clearTimeout(showTimer)
  188. showTimer = null
  189. }
  190. if (hideTimer) {
  191. clearTimeout(hideTimer)
  192. hideTimer = null
  193. }
  194. currentTarget = null
  195. if (moveHandler) {
  196. document.removeEventListener(
  197. 'mousemove',
  198. moveHandler,
  199. true
  200. )
  201. }
  202. if (overHandler) {
  203. document.removeEventListener(
  204. 'mouseover',
  205. overHandler,
  206. true
  207. )
  208. }
  209. if (outHandler) {
  210. document.removeEventListener(
  211. 'mouseout',
  212. outHandler,
  213. true
  214. )
  215. }
  216. moveHandler = null
  217. overHandler = null
  218. outHandler = null
  219. isInitialized = false
  220. }
  221. init()
  222. return cleanup
  223. }