问题描述:
今天碰见一个关于自定义指令的问题,在项目中关于el-tooltip组件封装了一个自定义组件,功能就是常规的悬浮窗显示鼠标选中的文本,做一个具体的全部展示。问题是这个数据是会更新的,但是悬浮窗中的文本数据仍然是前一次的数据,所以鼠标悬浮之后显示为上一次的数据,造成了这个问题,当然只要是定位到了问题,并且可以复现,那么还是老规矩,先交给cursor去试试。
修改前:
import type { DirectiveBinding } from 'vue'
import { ElTooltip } from 'element-plus'
import { h, render } from 'vue'
function setupTruncateTooltip(el: HTMLElement, binding: DirectiveBinding) {
const updateTooltip = () => {
const text = el.innerText
const classes = el.className
const inputWidth = el.offsetWidth
const scrollWidth = el.scrollWidth
// 内容长度大于元素长度时,显示tooltip
if (scrollWidth > inputWidth) {
if (!el.parentElement?.classList.contains('el-tooltip__trigger')) {
// 创建tooltip vnode
const tooltipVNode = h(ElTooltip, {
content: text,
placement: 'top-end',
effect: 'dark',
}, () => [h('span', { class: classes }, { default: () => el.cloneNode(true) })])
// 渲染tooltip
const container = document.createElement('div')
render(tooltipVNode, container)
// 找到tooltip元素
const tooltipElement = container.querySelector('.el-tooltip__trigger') || container.firstElementChild
if (tooltipElement) {
el.parentNode?.insertBefore(tooltipElement, el)
// el 增加一个style 样式为宽度100%
el.style.width = '100%'
tooltipElement.appendChild(el)
}
}
} else {
const tooltipTrigger = el.closest('.el-tooltip__trigger')
// 清除style为100%
el.style.cssText = ''
if (tooltipTrigger && tooltipTrigger !== el) {
tooltipTrigger.parentNode?.insertBefore(el, tooltipTrigger)
tooltipTrigger.parentNode?.removeChild(tooltipTrigger)
}
}
}
// 初始设置
updateTooltip()
// 监听元素内容变化
const observer = new MutationObserver(updateTooltip)
observer.observe(el, {
childList: true,
characterData: true,
subtree: true,
})
// 监听窗口大小变化
window.addEventListener('resize', updateTooltip)
// 清理函数
el._cleanup = () => {
observer.disconnect()
window.removeEventListener('resize', updateTooltip)
}
}
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
setupTruncateTooltip(el, binding)
},
updated(el: HTMLElement, binding: DirectiveBinding) {
setupTruncateTooltip(el, binding)
},
}
修改后:
import type { DirectiveBinding } from 'vue'
import { ElTooltip } from 'element-plus'
import { h, render, nextTick } from 'vue'
// 扩展 HTMLElement 类型,添加自定义属性
declare global {
interface HTMLElement {
_tooltipInstance?: any
_cleanup?: () => void
_tooltipContainer?: HTMLElement
}
}
function setupTruncateTooltip(el: HTMLElement, binding: DirectiveBinding) {
const updateTooltip = async () => {
// 等待 DOM 更新完成
await nextTick()
const text = el.innerText
const classes = el.className
const inputWidth = el.offsetWidth
const scrollWidth = el.scrollWidth
// 内容长度大于元素长度时,显示tooltip
if (scrollWidth > inputWidth) {
if (!el.parentElement?.classList.contains('el-tooltip__trigger')) {
// 创建tooltip vnode
const tooltipVNode = h(ElTooltip, {
content: text,
placement: 'top-end',
effect: 'dark',
}, () => [h('span', { class: classes }, { default: () => el.cloneNode(true) })])
// 渲染tooltip
const container = document.createElement('div')
render(tooltipVNode, container)
// 找到tooltip元素
const tooltipElement = container.querySelector('.el-tooltip__trigger') || container.firstElementChild
if (tooltipElement) {
el.parentNode?.insertBefore(tooltipElement, el)
// el 增加一个style 样式为宽度100%
el.style.width = '100%'
tooltipElement.appendChild(el)
// 保存 tooltip 实例和容器引用,用于后续更新
el._tooltipInstance = tooltipVNode
el._tooltipContainer = container
}
} else {
// 如果已经存在 tooltip,更新其内容
updateExistingTooltip(el, text)
}
} else {
const tooltipTrigger = el.closest('.el-tooltip__trigger')
// 清除style为100%
el.style.cssText = ''
if (tooltipTrigger && tooltipTrigger !== el) {
tooltipTrigger.parentNode?.insertBefore(el, tooltipTrigger)
tooltipTrigger.parentNode?.removeChild(tooltipTrigger)
// 清理引用
el._tooltipInstance = undefined
el._tooltipContainer = undefined
}
}
}
// 更新已存在的 tooltip 内容
const updateExistingTooltip = (el: HTMLElement, newText: string) => {
if (el._tooltipInstance && el._tooltipContainer) {
// 重新创建 tooltip vnode 并更新内容
const newTooltipVNode = h(ElTooltip, {
content: newText,
placement: 'top-end',
effect: 'dark',
}, () => [h('span', { class: el.className }, { default: () => el.cloneNode(true) })])
// 重新渲染
render(newTooltipVNode, el._tooltipContainer)
// 更新引用
el._tooltipInstance = newTooltipVNode
}
}
// 初始设置
updateTooltip()
// 监听元素内容变化
const observer = new MutationObserver(() => {
// 使用防抖,避免频繁更新
clearTimeout(el._updateTimer)
el._updateTimer = setTimeout(updateTooltip, 50)
})
observer.observe(el, {
childList: true,
characterData: true,
subtree: true,
})
// 监听窗口大小变化
const handleResize = () => {
clearTimeout(el._resizeTimer)
el._resizeTimer = setTimeout(updateTooltip, 100)
}
window.addEventListener('resize', handleResize)
// 清理函数
el._cleanup = () => {
observer.disconnect()
window.removeEventListener('resize', handleResize)
clearTimeout(el._updateTimer)
clearTimeout(el._resizeTimer)
// 清理 tooltip 相关引用
if (el._tooltipContainer) {
el._tooltipContainer.remove()
}
el._tooltipInstance = undefined
el._tooltipContainer = undefined
}
}
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
setupTruncateTooltip(el, binding)
},
updated(el: HTMLElement, binding: DirectiveBinding) {
// 在 updated 钩子中重新设置,确保数据更新后 tooltip 内容也更新
setupTruncateTooltip(el, binding)
},
unmounted(el: HTMLElement) {
// 组件卸载时清理
if (el._cleanup) {
el._cleanup()
}
},
}
结果:
cursor不负众望,完成了我想要的结果。还是感叹AI的强大辅助能力,在这里记录一下。