使用vue指令解決文本的溢出提示的問題
在我們項目開發(fā)中,經(jīng)常會有 超長文本溢出提示
,未溢出則不提示的場景。
筆者就遇到了比較復(fù)雜的場景,在一個表格中,總計約有 5000 個單元格,需要對每個單元格進行這個需求的校驗,剛開始開發(fā)的時候也是用 v-if
v-else
el-tooltip
一把梭。導(dǎo)致運行時存在大量的 <el-tooltip>
實例。這樣的操作,代碼重復(fù)性極高,而且不利于后期維護,關(guān)鍵我們發(fā)現(xiàn)性能極差,一拉伸就卡頓。和組長溝通后,開發(fā)出了這個指令。
接下來 讓我們一步步用 vue指令
實現(xiàn)這個需求
本文涉及到的技術(shù)棧
- vue2
- element-ui
在線體驗
彥祖?zhèn)?這個 codesandbox
好像用不了了,clone 到本地跑一下吧~
報錯 Error in render: "TypeError: (0 , _vue.resolveComponent) is not a function"
, 如果有知道怎么解決的彥祖,請評論區(qū) 留言。
常規(guī)開發(fā)
如下圖所示, 兩個寬度為 100px
的 div, 第一個不需要提示,第二個則需要提示
<template> <div class="parent"> <div> 天翼云 </div> <el-tooltip content="天翼云,海量數(shù)據(jù)"> <div class="tooltip"> 天翼云,海量數(shù)據(jù) </div> </el-tooltip> </div> </template> <style lang="scss" scoped> .parent{ margin:100px; >div{ border:1px solid lightblue; width:100px; margin-bottom:20px; &.tooltip{ overflow: hidden; //超出的文本隱藏 text-overflow: ellipsis; //溢出用省略號顯示 white-space: nowrap; // 默認不換行; } } } </style>
這樣的代碼比較復(fù)雜,而且復(fù)用性極低。如果在其他頁面也有類似的場景,我們就不得不做一個 cv 戰(zhàn)士
了
指令開發(fā)
如何判斷是否溢出?
這也算是一個知識點,首先我們需要判斷文本是否溢出了節(jié)點。后來在 element-ui/packages/table/src/table-body.js
上找到了這段代碼
const range = document.createRange(); range.setStart(cellChild, 0); range.setEnd(cellChild, cellChild.childNodes.length); const rangeWidth = range.getBoundingClientRect().width; const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) + (parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0); if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) { // 省略了不重要的邏輯 }
那我們就改造一下 直接拿來用吧
實現(xiàn)溢出指令
接下來,讓我們用這段代碼 實現(xiàn)一個判斷溢出的指令,非常簡單,直接上代碼
function getElStyleAttr (element, attr) { const styles = window.getComputedStyle(element) return styles[attr] } const isOverflow = (target) => { const scrollWidth = target.scrollWidth const offsetWidth = target.offsetWidth const range = document.createRange() range.setStart(target, 0) range.setEnd(target, target.childNodes.length) const rangeWidth = range.getBoundingClientRect().width const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0) return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth } export const ellipsisTooltip = { bind (el, binding, vnode, oldVnode) { // 避免用戶遺漏樣式,我們必須強制加上超出...樣式 el.style.overflow = 'hidden' el.style.textOverflow = 'ellipsis' el.style.whiteSpace = 'nowrap' const onMouseEnter = (e) => { if (isOverflow(el)) { console.log('溢出了') } else { console.log('未溢出') } } el.addEventListener('mouseenter', onMouseEnter) } }
來看下效果吧
如何把溢出節(jié)點掛載到 el-tooltip 上?
本文的難點也是核心代碼,我們該如何把這個節(jié)點掛載到 <el-tooltip>
上呢。筆者也在這個問題上卡了非常久,嘗試過復(fù)制一個新節(jié)點, 包裹一層 <el-tooltip>
去代替老的節(jié)點,也嘗試過用 template
常規(guī)渲染調(diào)用,直接生成。但是發(fā)現(xiàn)都比較麻煩,最終不得去看了它的源碼。發(fā)現(xiàn)了以下代碼
mounted() { this.referenceElm = this.$el; if (this.$el.nodeType === 1) { this.$el.setAttribute('aria-describedby', this.tooltipId); this.$el.setAttribute('tabindex', this.tabindex); on(this.referenceElm, 'mouseenter', this.show); on(this.referenceElm, 'mouseleave', this.hide); on(this.referenceElm, 'focus', () => { if (!this.$slots.default || !this.$slots.default.length) { this.handleFocus(); return; } const instance = this.$slots.default[0].componentInstance; if (instance && instance.focus) { instance.focus(); } else { this.handleFocus(); } }); on(this.referenceElm, 'blur', this.handleBlur); on(this.referenceElm, 'click', this.removeFocusing); } // fix issue https://github.com/ElemeFE/element/issues/14424 if (this.value && this.popperVM) { this.popperVM.$nextTick(() => { if (this.value) { this.updatePopper(); } }); } }
其實我們發(fā)現(xiàn) 核心點 就是這個 this.$el
, 它在 mounted
時對 this.$el
進行了一系列初始化操作
那么接下來就簡單了, 我們試著去替換這個 this.$el
,然后再執(zhí)行 mounted
邏輯不就行了?
但是我們最好不要去改變這個屬性
vue 官方解釋
改造 el-tooltip 源碼
替換 el-tooltip 的 $el
我們只需要把源碼的 this.$el
部分改成 this.target
也就是在源碼內(nèi)部,我們新增一個
setEl(el){ this.target = el }
替換 mounted 邏輯
同樣非常簡單,我們把 mounted
的邏輯重新封裝一個 init
方法
init () { this.referenceElm = this.target if (this.target.nodeType === 1) { this.target.setAttribute('aria-describedby', this.tooltipId) this.target.setAttribute('tabindex', this.tabindex) on(this.referenceElm, 'mouseenter', this.show) on(this.referenceElm, 'mouseleave', this.hide) on(this.referenceElm, 'focus', () => { if (!this.$slots.default || !this.$slots.default.length) { this.handleFocus() return } const instance = this.$slots.default[0].componentInstance if (instance && instance.focus) { instance.focus() } else { this.handleFocus() } }) on(this.referenceElm, 'blur', this.handleBlur) on(this.referenceElm, 'click', this.removeFocusing) } // fix issue https://github.com/ElemeFE/element/issues/14424 if (this.value && this.popperVM) { this.popperVM.$nextTick(() => { if (this.value) { this.updatePopper() } }) } }
引入改造后的 el-tooltip
我們看下此時的目錄結(jié)構(gòu)
- directive.js //指令代碼
- main.js //改造過的 el-tooltip 代碼
這里還有個很重要的知識點, 就是創(chuàng)建一個 vue 實例
我們在日常開發(fā)中, 一般只會在 main.js
進行一個 new Vue
的操作。
在閱讀了 element-ui
源碼后,我們會發(fā)現(xiàn) el-message
el-date-picker
中也用到了這個實例化的操作,更有 Vue.extend
等高階操作。
我們看下此時的 directive.js 代碼
import Vue from 'vue' import Tooltip from './main' function getElStyleAttr (element, attr) { const styles = window.getComputedStyle(element) return styles[attr] } const isOverflow = (target) => { const scrollWidth = target.scrollWidth const offsetWidth = target.offsetWidth const range = document.createRange() range.setStart(target, 0) range.setEnd(target, target.childNodes.length) const rangeWidth = range.getBoundingClientRect().width const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0) return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth } export const ellipsisTooltip = { bind (el, binding, vnode, oldVnode) { // 加上超出...樣式 el.style.overflow = 'hidden' el.style.textOverflow = 'ellipsis' el.style.whiteSpace = 'nowrap' const onMouseEnter = (e) => { // 需要展示 if (isOverflow(el)) { // 參考 https://v2.cn.vuejs.org/v2/api/#vm-mount const vm = new Vue(Tooltip).$mount() vm.setEl(el) vm.init() } } el.addEventListener('mouseenter', onMouseEnter) } }
看下此時的效果
已經(jīng)初步成效了,但是沒有任何顯示內(nèi)容?
填充顯示內(nèi)容
彥祖?zhèn)?這個可更簡單了 直接一個 setContent
搞定了
- main.js
setContent(content){ this.content = content }
- directive.js
if (isOverflow(el)) { // 參考 https://v2.cn.vuejs.org/v2/api/#vm-mount const vm = new Vue(Tooltip).$mount() vm.setEl(el) vm.init() vm.setContent('天翼云,海量數(shù)據(jù)') }
此時,可能有彥祖會說,這個 setContent
也太復(fù)雜了,難道我每次都需要手動傳入數(shù)據(jù)嗎?
當然不必, 我們 默認 vm.setContent(content || el.innerText)
就好了
此處只做演示說明
此時我們基本已經(jīng)實現(xiàn)了一個 簡單的 ellipsis-tooltip
開始進階
以下優(yōu)化代碼,只展示核心代碼,已去掉冗余代碼
防止重復(fù)實例化
如果我們不進行 實例化的檢測,那么我們可能會存在大量的 vue 實例,用戶操作久了,就可能導(dǎo)致頁面卡頓
const vmMap = new WeakMap() if (isOverflow(el)) { if(vmMap.get(el)) return const vm = new Vue(Tooltip).$mount() // ... vmMap.set(el,vm) }
兼容 el-tooltip 屬性
此時,還有很多 el-tooltip
的源生屬性待支持的。比如 placement
effect
...
其實我們只需要在代碼上加上
const vm = new Vue(Tooltip).$mount() vm.placement = placement || 'top-start' vm.effect = effect || 'dark' // ...其他屬性 不做贅述,自行拓展
兼容 文本 寬度改變的場景
業(yè)務(wù)中的文本寬度可能不是固定的
溢出的變成非溢出,非溢出也能變成溢出
所以我們需要在 mouseenter
的時候進行對應(yīng)判斷
const vm = vmMap.get(el) if (isOverflow(el)) { if(vm) return vm.disabled = false } else { vm.disabled = true // 沒有溢出,則應(yīng)該禁用 tooltip } }
移除大量實例化時候的節(jié)點
在開發(fā)中,筆者發(fā)現(xiàn), el-tooltip
在 render
的時候, 對應(yīng)的 dom
并不會從 文檔中移除,這個在表格或者樹這種大量節(jié)點的場景中, 性能開銷是我們不能接受的
我們開放一個 destroyOnLeave
配置,用于設(shè)置 移出時
是否銷毀對應(yīng) 提示節(jié)點
const onMouseLeave = () => { const elVm = vmMap.get(el) if (!elVm) return elVm.disabled = true elVm.$nextTick(elVm.unMount) //卸載 tooltip,在 main.js 做了注釋 vmMap.set(el, null) // 銷毀內(nèi)存 } if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave)
其他優(yōu)化項
其他一些常規(guī)的 eventListener
的移除操作,自我優(yōu)化,就不浪費彥祖?zhèn)兊那啻毫?/p>
完整代碼
- main.js
import Popper from 'element-ui/src/utils/vue-popper' import debounce from 'throttle-debounce/debounce' import { addClass, removeClass, on, off } from 'element-ui/src/utils/dom' import { generateId } from 'element-ui/src/utils/util' import Vue from 'vue' export default { name: 'TooltipWrapper', mixins: [Popper], props: { openDelay: { type: Number, default: 0 }, disabled: Boolean, manual: Boolean, effect: { type: String, default: 'dark' }, arrowOffset: { type: Number, default: 0 }, popperClass: String, content: String, visibleArrow: { default: true }, transition: { type: String, default: 'el-fade-in-linear' }, popperOptions: { default () { return { boundariesPadding: 10, gpuAcceleration: false } } }, enterable: { type: Boolean, default: true }, hideAfter: { type: Number, default: 0 }, tabindex: { type: Number, default: 0 } }, data () { return { tooltipId: `el-tooltip-${generateId()}`, timeoutPending: null, focusing: false } }, beforeCreate () { if (this.$isServer) return this.popperVM = new Vue({ data: { node: '' }, render (h) { return this.node } }).$mount() this.debounceClose = debounce(200, () => this.handleClosePopper()) }, render (h) { if (this.popperVM) { this.popperVM.node = ( <transition name={ this.transition } onAfterLeave={ this.doDestroy }> <div onMouseleave={ () => { this.setExpectedState(false); this.debounceClose() } } onMouseenter= { () => { this.setExpectedState(true) } } ref="popper" role="tooltip" id={this.tooltipId} aria-hidden={ (this.disabled || !this.showPopper) ? 'true' : 'false' } v-show={!this.disabled && this.showPopper} class={ ['el-tooltip__popper', 'is-' + this.effect, this.popperClass] }> { this.$slots.content || this.content } </div> </transition>) } const firstElement = this.getFirstElement() if (!firstElement) return null const data = firstElement.data = firstElement.data || {} data.staticClass = this.addTooltipClass(data.staticClass) return firstElement }, watch: { focusing (val) { if (val) { addClass(this.referenceElm, 'focusing') } else { removeClass(this.referenceElm, 'focusing') } } }, methods: { // 掛載目標節(jié)點 setEl (el) { this.target = el }, setContent (content) { this.content = content }, init () { this.referenceElm = this.target if (this.target.nodeType === 1) { this.target.setAttribute('aria-describedby', this.tooltipId) this.target.setAttribute('tabindex', this.tabindex) on(this.referenceElm, 'mouseenter', this.show) on(this.referenceElm, 'mouseleave', this.hide) on(this.referenceElm, 'focus', () => { if (!this.$slots.default || !this.$slots.default.length) { this.handleFocus() return } const instance = this.$slots.default[0].componentInstance if (instance && instance.focus) { instance.focus() } else { this.handleFocus() } }) on(this.referenceElm, 'blur', this.handleBlur) on(this.referenceElm, 'click', this.removeFocusing) } // fix issue https://github.com/ElemeFE/element/issues/14424 if (this.value && this.popperVM) { this.popperVM.$nextTick(() => { if (this.value) { this.updatePopper() } }) } }, show () { this.setExpectedState(true) this.handleShowPopper() }, hide () { this.setExpectedState(false) this.debounceClose() }, handleFocus () { this.focusing = true this.show() }, handleBlur () { this.focusing = false this.hide() }, removeFocusing () { this.focusing = false }, addTooltipClass (prev) { if (!prev) { return 'el-tooltip' } else { return 'el-tooltip ' + prev.replace('el-tooltip', '') } }, handleShowPopper () { if (!this.expectedState || this.manual) return clearTimeout(this.timeout) this.timeout = setTimeout(() => { this.showPopper = true }, this.openDelay) if (this.hideAfter > 0) { this.timeoutPending = setTimeout(() => { this.showPopper = false }, this.hideAfter) } }, handleClosePopper () { if (this.enterable && this.expectedState || this.manual) return clearTimeout(this.timeout) if (this.timeoutPending) { clearTimeout(this.timeoutPending) } this.showPopper = false if (this.disabled) { this.doDestroy() } }, setExpectedState (expectedState) { if (expectedState === false) { clearTimeout(this.timeoutPending) } this.expectedState = expectedState }, getFirstElement () { if (this.slotEl) return this.slotEl const slots = this.$slots.default if (!Array.isArray(slots)) return null let element = null for (let index = 0; index < slots.length; index++) { if (slots[index] && slots[index].tag) { element = slots[index] } } return element }, unMount () { if (this.popperVM) { this.popperVM.$destroy() // 銷毀 popperVM 實例 this.popperVM.node && this.popperVM.node.elm.remove() // 移除對應(yīng)的 tooltip節(jié)點 } const reference = this.referenceElm // 解綁事件 if (reference.nodeType === 1) { off(reference, 'mouseenter', this.show) off(reference, 'mouseleave', this.hide) off(reference, 'focus', this.handleFocus) off(reference, 'blur', this.handleBlur) off(reference, 'click', this.removeFocusing) } this.$nextTick(() => { this.doDestroy() // 調(diào)用 mixins的 Popper.doDestroy 銷毀 popper }) } }, beforeDestroy () { this.popperVM && this.popperVM.$destroy() }, destroyed () { const reference = this.referenceElm if (reference.nodeType === 1) { off(reference, 'mouseenter', this.show) off(reference, 'mouseleave', this.hide) off(reference, 'focus', this.handleFocus) off(reference, 'blur', this.handleBlur) off(reference, 'click', this.removeFocusing) } } }
- directive.js
import Vue from 'vue' import Tooltip from './main' const vmMap = new WeakMap() const listenerMap = new WeakMap() function getElStyleAttr (element, attr) { const styles = window.getComputedStyle(element) return styles[attr] } const isOverflow = (target) => { const scrollWidth = target.scrollWidth const offsetWidth = target.offsetWidth const range = document.createRange() range.setStart(target, 0) range.setEnd(target, target.childNodes.length) const rangeWidth = range.getBoundingClientRect().width const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0) return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth } export const ellipsisTooltip = { bind (el, binding, vnode, oldVnode) { const { value: { placement, disabled, content, effect, destroyOnLeave } = {} } = binding if (disabled) return // 加上超出...樣式 el.style.overflow = 'hidden' el.style.textOverflow = 'ellipsis' el.style.whiteSpace = 'nowrap' const onMouseLeave = () => { const elVm = vmMap.get(el) if (!elVm) return elVm.disabled = true elVm.$nextTick(elVm.unMount) vmMap.set(el, null) } const onMouseEnter = (e) => { const elVm = vmMap.get(el) // 需要展示 if (isOverflow(el)) { if (elVm) { elVm.disabled = false return } const vm = new Vue(Tooltip).$mount() vm.placement = placement || 'top-start' vm.effect = effect || 'dark' vm.setEl(el) vm.setContent(content || el.innerText) vm.init() vm.show() vmMap.set(el, vm) if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave) } else { if (elVm) elVm.disabled = true } } listenerMap.set(el, [ ['mouseenter', onMouseEnter] ]) // 用于拓展后續(xù)的監(jiān)聽 el.addEventListener('mouseenter', onMouseEnter) }, unbind (el) { const events = listenerMap.get(el) if (events?.length) { events.forEach(([name, event]) => el.removeEventListener(name, event)) } } }
以上就是使用vue指令解決文本的溢出提示的問題的詳細內(nèi)容,更多關(guān)于vue指令解決文本溢出提示的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解讀Vue3中keep-alive和動態(tài)組件的實現(xiàn)邏輯
這篇文章主要介紹了Vue3中keep-alive和動態(tài)組件的實現(xiàn)邏輯,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05Vue實現(xiàn)導(dǎo)出Excel表格文件提示“文件已損壞無法打開”的解決方法
xlsx用于讀取解析和寫入Excel文件的JavaScript庫,它提供了一系列的API處理Excel文件,使用該庫,可以將數(shù)據(jù)轉(zhuǎn)換Excel文件并下載到本地,適用于在前端直接生成Excel文件,這篇文章主要介紹了Vue實現(xiàn)導(dǎo)出Excel表格,提示文件已損壞,無法打開的解決方法,需要的朋友可以參考下2024-01-01Vue.set() this.$set()引發(fā)的視圖更新思考及注意事項
this.$set()和Vue.set()本質(zhì)方法一樣,前者可以用在methods中使用。這篇文章主要介紹了Vue.set() this.$set()引發(fā)的視圖更新思考及注意事項,需要的朋友可以參考下2018-08-08