|
|
@@ -1,98 +1,298 @@
|
|
|
-let t = null
|
|
|
-let r = null
|
|
|
-let l = null
|
|
|
+// useMouseTooltip.js
|
|
|
+
|
|
|
+let tooltipEl = null
|
|
|
+let currentTarget = null
|
|
|
+let isInitialized = false
|
|
|
+
|
|
|
+let moveHandler = null
|
|
|
+let overHandler = null
|
|
|
+let outHandler = null
|
|
|
+
|
|
|
+let rafId = null
|
|
|
+
|
|
|
+let showTimer = null
|
|
|
+let hideTimer = null
|
|
|
+
|
|
|
+let lastMouseX = 0
|
|
|
+let lastMouseY = 0
|
|
|
|
|
|
export function useMouseTooltip() {
|
|
|
- const TOOLTIP_WIDTH = 80
|
|
|
- const TOOLTIP_HEIGHT = 40
|
|
|
const PADDING = 16
|
|
|
+ const OFFSET = 15
|
|
|
+
|
|
|
+ const SHOW_DELAY = 300
|
|
|
+ const HIDE_DELAY = 80
|
|
|
+
|
|
|
+ function createTooltip() {
|
|
|
+ if (tooltipEl) return
|
|
|
+
|
|
|
+ tooltipEl = document.createElement('div')
|
|
|
+ tooltipEl.className = 'cursor-pointer-tooltip'
|
|
|
+
|
|
|
+ tooltipEl.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ z-index: 2147483647;
|
|
|
+ pointer-events: none;
|
|
|
+
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+
|
|
|
+ background: rgba(var(--bs-body-bg-rgb),1);
|
|
|
+ color: var(--bs-emphasis-color);
|
|
|
+
|
|
|
+ border-radius: .5rem;
|
|
|
+ box-shadow: 0 0 10px rgba(0,0,0,.1);
|
|
|
+
|
|
|
+ font-size: .8125rem;
|
|
|
+ line-height: 1.4;
|
|
|
+ padding: .375rem .5rem;
|
|
|
+
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ opacity: 0;
|
|
|
+ visibility: hidden;
|
|
|
+
|
|
|
+ transform: scale(.85);
|
|
|
+
|
|
|
+ transition:
|
|
|
+ opacity .18s ease,
|
|
|
+ transform .18s ease;
|
|
|
+
|
|
|
+ will-change:
|
|
|
+ opacity,
|
|
|
+ transform,
|
|
|
+ left,
|
|
|
+ top;
|
|
|
+ `
|
|
|
+
|
|
|
+ document.body.appendChild(tooltipEl)
|
|
|
+ }
|
|
|
+
|
|
|
+ function getTooltipSize() {
|
|
|
+ if (!tooltipEl) {
|
|
|
+ return {
|
|
|
+ width: 0,
|
|
|
+ height: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const rect = tooltipEl.getBoundingClientRect()
|
|
|
+
|
|
|
+ return {
|
|
|
+ width: rect.width,
|
|
|
+ height: rect.height
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updatePosition(clientX, clientY) {
|
|
|
+ if (!tooltipEl) return
|
|
|
|
|
|
- const updateTooltipPosition = (clientX, clientY) => {
|
|
|
const viewportWidth = window.innerWidth
|
|
|
const viewportHeight = window.innerHeight
|
|
|
|
|
|
- let x = clientX
|
|
|
- let y = clientY
|
|
|
- let flipX = false
|
|
|
- let flipY = false
|
|
|
+ const { width, height } = getTooltipSize()
|
|
|
|
|
|
- if (clientX + TOOLTIP_WIDTH + PADDING > viewportWidth) {
|
|
|
- x = clientX - TOOLTIP_WIDTH - PADDING
|
|
|
- flipX = true
|
|
|
+ let x = clientX + OFFSET
|
|
|
+ let y = clientY + OFFSET
|
|
|
+
|
|
|
+ if (x + width > viewportWidth - PADDING) {
|
|
|
+ x = clientX - width - OFFSET
|
|
|
}
|
|
|
|
|
|
- if (clientY + TOOLTIP_HEIGHT + PADDING > viewportHeight) {
|
|
|
- y = clientY - TOOLTIP_HEIGHT - PADDING
|
|
|
- flipY = true
|
|
|
+ if (y + height > viewportHeight - PADDING) {
|
|
|
+ y = clientY - height - OFFSET
|
|
|
}
|
|
|
|
|
|
x = Math.max(PADDING, x)
|
|
|
y = Math.max(PADDING, y)
|
|
|
|
|
|
- document.documentElement.style.setProperty('--mouse-x', x + 'px')
|
|
|
- document.documentElement.style.setProperty('--mouse-y', y + 'px')
|
|
|
- document.documentElement.style.setProperty('--tooltip-flip-x', flipX ? 'true' : 'false')
|
|
|
- document.documentElement.style.setProperty('--tooltip-flip-y', flipY ? 'true' : 'false')
|
|
|
+ tooltipEl.style.left = `${x}px`
|
|
|
+ tooltipEl.style.top = `${y}px`
|
|
|
}
|
|
|
|
|
|
- const u = (e, instant = false) => {
|
|
|
- if (instant) {
|
|
|
- updateTooltipPosition(e.clientX, e.clientY)
|
|
|
+ function showTooltip(e, target) {
|
|
|
+ const text = target.dataset.tooltip
|
|
|
+
|
|
|
+ if (!text) return
|
|
|
+
|
|
|
+ createTooltip()
|
|
|
+
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer)
|
|
|
+ hideTimer = null
|
|
|
}
|
|
|
|
|
|
- clearTimeout(t)
|
|
|
- t = setTimeout(() => {
|
|
|
- updateTooltipPosition(e.clientX, e.clientY)
|
|
|
- }, 200)
|
|
|
+ if (showTimer) {
|
|
|
+ clearTimeout(showTimer)
|
|
|
+ showTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ currentTarget = target
|
|
|
+
|
|
|
+ tooltipEl.textContent = text
|
|
|
+
|
|
|
+ updatePosition(
|
|
|
+ e.clientX,
|
|
|
+ e.clientY
|
|
|
+ )
|
|
|
+
|
|
|
+ tooltipEl.style.opacity = '0'
|
|
|
+ tooltipEl.style.visibility = 'hidden'
|
|
|
+ tooltipEl.style.transform = 'scale(.85)'
|
|
|
+
|
|
|
+ showTimer = setTimeout(() => {
|
|
|
+ if (currentTarget !== target) return
|
|
|
+
|
|
|
+ tooltipEl.style.visibility = 'visible'
|
|
|
+
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ tooltipEl.style.opacity = '1'
|
|
|
+ tooltipEl.style.transform = 'scale(1)'
|
|
|
+ })
|
|
|
+ }, SHOW_DELAY)
|
|
|
}
|
|
|
|
|
|
- const c = () => {
|
|
|
- clearTimeout(t)
|
|
|
- document.documentElement.style.removeProperty('--mouse-x')
|
|
|
- document.documentElement.style.removeProperty('--mouse-y')
|
|
|
- document.documentElement.style.removeProperty('--tooltip-flip-x')
|
|
|
- document.documentElement.style.removeProperty('--tooltip-flip-y')
|
|
|
+ function hideTooltip() {
|
|
|
+ if (!tooltipEl) return
|
|
|
+
|
|
|
+ if (showTimer) {
|
|
|
+ clearTimeout(showTimer)
|
|
|
+ showTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ hideTimer = setTimeout(() => {
|
|
|
+ tooltipEl.style.opacity = '0'
|
|
|
+ tooltipEl.style.transform = 'scale(.85)'
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (!tooltipEl) return
|
|
|
+
|
|
|
+ tooltipEl.style.visibility = 'hidden'
|
|
|
+ }, 180)
|
|
|
+
|
|
|
+ currentTarget = null
|
|
|
+ }, HIDE_DELAY)
|
|
|
}
|
|
|
|
|
|
- const i = () => {
|
|
|
- // 鼠标移动 → 节流更新
|
|
|
- r = (e) => {
|
|
|
- if (e.target.closest('.cursor-pointer')) {
|
|
|
- u(e)
|
|
|
- }
|
|
|
+ function init() {
|
|
|
+ if (isInitialized) return
|
|
|
+
|
|
|
+ isInitialized = true
|
|
|
+
|
|
|
+ moveHandler = (e) => {
|
|
|
+ if (!currentTarget) return
|
|
|
+
|
|
|
+ lastMouseX = e.clientX
|
|
|
+ lastMouseY = e.clientY
|
|
|
+
|
|
|
+ if (rafId) return
|
|
|
+
|
|
|
+ rafId = requestAnimationFrame(() => {
|
|
|
+ rafId = null
|
|
|
+
|
|
|
+ updatePosition(
|
|
|
+ lastMouseX,
|
|
|
+ lastMouseY
|
|
|
+ )
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- // 鼠标进入新元素 → 🔥 立刻刷新位置(解决你说的问题)
|
|
|
- const enter = (e) => {
|
|
|
- if (e.target.closest('.cursor-pointer')) {
|
|
|
- u(e, true) // 立刻更新
|
|
|
- }
|
|
|
+ overHandler = (e) => {
|
|
|
+ const target = e.target.closest('.cursor-pointer')
|
|
|
+
|
|
|
+ if (!target) return
|
|
|
+
|
|
|
+ if (target === currentTarget) return
|
|
|
+
|
|
|
+ showTooltip(e, target)
|
|
|
}
|
|
|
|
|
|
- // 鼠标离开 → 延迟清除
|
|
|
- l = (e) => {
|
|
|
- if (e.target.closest('.cursor-pointer')) {
|
|
|
- clearTimeout(t)
|
|
|
- t = setTimeout(() => {
|
|
|
- c()
|
|
|
- }, 100)
|
|
|
+ outHandler = (e) => {
|
|
|
+ const target = e.target.closest('.cursor-pointer')
|
|
|
+
|
|
|
+ if (!target) return
|
|
|
+
|
|
|
+ if (target !== currentTarget) return
|
|
|
+
|
|
|
+ // 在当前元素内部移动
|
|
|
+ if (
|
|
|
+ e.relatedTarget &&
|
|
|
+ target.contains(e.relatedTarget)
|
|
|
+ ) {
|
|
|
+ return
|
|
|
}
|
|
|
+
|
|
|
+ hideTooltip()
|
|
|
}
|
|
|
|
|
|
- document.addEventListener('mousemove', r, true)
|
|
|
- document.addEventListener('mouseenter', enter, true) // 新增
|
|
|
- document.addEventListener('mouseout', l, true)
|
|
|
- }
|
|
|
+ document.addEventListener(
|
|
|
+ 'mouseover',
|
|
|
+ overHandler,
|
|
|
+ true
|
|
|
+ )
|
|
|
+
|
|
|
+ document.addEventListener(
|
|
|
+ 'mouseout',
|
|
|
+ outHandler,
|
|
|
+ true
|
|
|
+ )
|
|
|
|
|
|
- const d = () => {
|
|
|
- clearTimeout(t)
|
|
|
- c()
|
|
|
- if (r) document.removeEventListener('mousemove', r, true)
|
|
|
- if (l) document.removeEventListener('mouseout', l, true)
|
|
|
- r = null
|
|
|
- l = null
|
|
|
+ document.addEventListener(
|
|
|
+ 'mousemove',
|
|
|
+ moveHandler,
|
|
|
+ true
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- i()
|
|
|
- return d
|
|
|
+ function cleanup() {
|
|
|
+ if (rafId) {
|
|
|
+ cancelAnimationFrame(rafId)
|
|
|
+ rafId = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (showTimer) {
|
|
|
+ clearTimeout(showTimer)
|
|
|
+ showTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hideTimer) {
|
|
|
+ clearTimeout(hideTimer)
|
|
|
+ hideTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ currentTarget = null
|
|
|
+
|
|
|
+ if (moveHandler) {
|
|
|
+ document.removeEventListener(
|
|
|
+ 'mousemove',
|
|
|
+ moveHandler,
|
|
|
+ true
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (overHandler) {
|
|
|
+ document.removeEventListener(
|
|
|
+ 'mouseover',
|
|
|
+ overHandler,
|
|
|
+ true
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (outHandler) {
|
|
|
+ document.removeEventListener(
|
|
|
+ 'mouseout',
|
|
|
+ outHandler,
|
|
|
+ true
|
|
|
+ )
|
|
|
+ }
|
|
|
+ moveHandler = null
|
|
|
+ overHandler = null
|
|
|
+ outHandler = null
|
|
|
+
|
|
|
+ isInitialized = false
|
|
|
+ }
|
|
|
+ init()
|
|
|
+ return cleanup
|
|
|
}
|