使用vue指令解決文本的溢出提示的問(wèn)題
在我們項(xiàng)目開(kāi)發(fā)中,經(jīng)常會(huì)有 超長(zhǎng)文本溢出提示
,未溢出則不提示的場(chǎng)景。
筆者就遇到了比較復(fù)雜的場(chǎng)景,在一個(gè)表格中,總計(jì)約有 5000 個(gè)單元格,需要對(duì)每個(gè)單元格進(jìn)行這個(gè)需求的校驗(yàn),剛開(kāi)始開(kāi)發(fā)的時(shí)候也是用 v-if
v-else
el-tooltip
一把梭。導(dǎo)致運(yùn)行時(shí)存在大量的 <el-tooltip>
實(shí)例。這樣的操作,代碼重復(fù)性極高,而且不利于后期維護(hù),關(guān)鍵我們發(fā)現(xiàn)性能極差,一拉伸就卡頓。和組長(zhǎng)溝通后,開(kāi)發(fā)出了這個(gè)指令。
接下來(lái) 讓我們一步步用 vue指令
實(shí)現(xiàn)這個(gè)需求
本文涉及到的技術(shù)棧
- vue2
- element-ui
在線體驗(yàn)
彥祖?zhèn)?這個(gè) codesandbox
好像用不了了,clone 到本地跑一下吧~
報(bào)錯(cuò) Error in render: "TypeError: (0 , _vue.resolveComponent) is not a function"
, 如果有知道怎么解決的彥祖,請(qǐng)?jiān)u論區(qū) 留言。
常規(guī)開(kāi)發(fā)
如下圖所示, 兩個(gè)寬度為 100px
的 div, 第一個(gè)不需要提示,第二個(gè)則需要提示
<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; //溢出用省略號(hào)顯示 white-space: nowrap; // 默認(rèn)不換行; } } } </style>
這樣的代碼比較復(fù)雜,而且復(fù)用性極低。如果在其他頁(yè)面也有類(lèi)似的場(chǎng)景,我們就不得不做一個(gè) cv 戰(zhàn)士
了
指令開(kāi)發(fā)
如何判斷是否溢出?
這也算是一個(gè)知識(shí)點(diǎn),首先我們需要判斷文本是否溢出了節(jié)點(diǎn)。后來(lái)在 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) { // 省略了不重要的邏輯 }
那我們就改造一下 直接拿來(lái)用吧
實(shí)現(xiàn)溢出指令
接下來(lái),讓我們用這段代碼 實(shí)現(xiàn)一個(gè)判斷溢出的指令,非常簡(jiǎ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) { // 避免用戶遺漏樣式,我們必須強(qiáng)制加上超出...樣式 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) } }
來(lái)看下效果吧
如何把溢出節(jié)點(diǎn)掛載到 el-tooltip 上?
本文的難點(diǎn)也是核心代碼,我們?cè)撊绾伟堰@個(gè)節(jié)點(diǎn)掛載到 <el-tooltip>
上呢。筆者也在這個(gè)問(wèn)題上卡了非常久,嘗試過(guò)復(fù)制一個(gè)新節(jié)點(diǎn), 包裹一層 <el-tooltip>
去代替老的節(jié)點(diǎn),也嘗試過(guò)用 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(); } }); } }
其實(shí)我們發(fā)現(xiàn) 核心點(diǎn) 就是這個(gè) this.$el
, 它在 mounted
時(shí)對(duì) this.$el
進(jìn)行了一系列初始化操作
那么接下來(lái)就簡(jiǎn)單了, 我們?cè)囍ヌ鎿Q這個(gè) this.$el
,然后再執(zhí)行 mounted
邏輯不就行了?
但是我們最好不要去改變這個(gè)屬性
vue 官方解釋
改造 el-tooltip 源碼
替換 el-tooltip 的 $el
我們只需要把源碼的 this.$el
部分改成 this.target
也就是在源碼內(nèi)部,我們新增一個(gè)
setEl(el){ this.target = el }
替換 mounted 邏輯
同樣非常簡(jiǎn)單,我們把 mounted
的邏輯重新封裝一個(gè) 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
我們看下此時(shí)的目錄結(jié)構(gòu)
- directive.js //指令代碼
- main.js //改造過(guò)的 el-tooltip 代碼
這里還有個(gè)很重要的知識(shí)點(diǎn), 就是創(chuàng)建一個(gè) vue 實(shí)例
我們?cè)谌粘i_(kāi)發(fā)中, 一般只會(huì)在 main.js
進(jìn)行一個(gè) new Vue
的操作。
在閱讀了 element-ui
源碼后,我們會(huì)發(fā)現(xiàn) el-message
el-date-picker
中也用到了這個(gè)實(shí)例化的操作,更有 Vue.extend
等高階操作。
我們看下此時(shí)的 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) } }
看下此時(shí)的效果
已經(jīng)初步成效了,但是沒(méi)有任何顯示內(nèi)容?
填充顯示內(nèi)容
彥祖?zhèn)?這個(gè)可更簡(jiǎn)單了 直接一個(gè) 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ù)') }
此時(shí),可能有彥祖會(huì)說(shuō),這個(gè) setContent
也太復(fù)雜了,難道我每次都需要手動(dòng)傳入數(shù)據(jù)嗎?
當(dāng)然不必, 我們 默認(rèn) vm.setContent(content || el.innerText)
就好了
此處只做演示說(shuō)明
此時(shí)我們基本已經(jīng)實(shí)現(xiàn)了一個(gè) 簡(jiǎn)單的 ellipsis-tooltip
開(kāi)始進(jìn)階
以下優(yōu)化代碼,只展示核心代碼,已去掉冗余代碼
防止重復(fù)實(shí)例化
如果我們不進(jìn)行 實(shí)例化的檢測(cè),那么我們可能會(huì)存在大量的 vue 實(shí)例,用戶操作久了,就可能導(dǎo)致頁(yè)面卡頓
const vmMap = new WeakMap() if (isOverflow(el)) { if(vmMap.get(el)) return const vm = new Vue(Tooltip).$mount() // ... vmMap.set(el,vm) }
兼容 el-tooltip 屬性
此時(shí),還有很多 el-tooltip
的源生屬性待支持的。比如 placement
effect
...
其實(shí)我們只需要在代碼上加上
const vm = new Vue(Tooltip).$mount() vm.placement = placement || 'top-start' vm.effect = effect || 'dark' // ...其他屬性 不做贅述,自行拓展
兼容 文本 寬度改變的場(chǎng)景
業(yè)務(wù)中的文本寬度可能不是固定的
溢出的變成非溢出,非溢出也能變成溢出
所以我們需要在 mouseenter
的時(shí)候進(jìn)行對(duì)應(yīng)判斷
const vm = vmMap.get(el) if (isOverflow(el)) { if(vm) return vm.disabled = false } else { vm.disabled = true // 沒(méi)有溢出,則應(yīng)該禁用 tooltip } }
移除大量實(shí)例化時(shí)候的節(jié)點(diǎn)
在開(kāi)發(fā)中,筆者發(fā)現(xiàn), el-tooltip
在 render
的時(shí)候, 對(duì)應(yīng)的 dom
并不會(huì)從 文檔中移除,這個(gè)在表格或者樹(shù)這種大量節(jié)點(diǎn)的場(chǎng)景中, 性能開(kāi)銷(xiāo)是我們不能接受的
我們開(kāi)放一個(gè) destroyOnLeave
配置,用于設(shè)置 移出時(shí)
是否銷(xiāo)毀對(duì)應(yīng) 提示節(jié)點(diǎn)
const onMouseLeave = () => { const elVm = vmMap.get(el) if (!elVm) return elVm.disabled = true elVm.$nextTick(elVm.unMount) //卸載 tooltip,在 main.js 做了注釋 vmMap.set(el, null) // 銷(xiāo)毀內(nèi)存 } if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave)
其他優(yōu)化項(xiàng)
其他一些常規(guī)的 eventListener
的移除操作,自我優(yōu)化,就不浪費(fèi)彥祖?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: { // 掛載目標(biāo)節(jié)點(diǎn) 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() // 銷(xiāo)毀 popperVM 實(shí)例 this.popperVM.node && this.popperVM.node.elm.remove() // 移除對(duì)應(yīng)的 tooltip節(jié)點(diǎn) } 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 銷(xiāo)毀 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)聽(tīng) el.addEventListener('mouseenter', onMouseEnter) }, unbind (el) { const events = listenerMap.get(el) if (events?.length) { events.forEach(([name, event]) => el.removeEventListener(name, event)) } } }
以上就是使用vue指令解決文本的溢出提示的問(wèn)題的詳細(xì)內(nèi)容,更多關(guān)于vue指令解決文本溢出提示的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+elemet實(shí)現(xiàn)表格手動(dòng)合并行列
這篇文章主要為大家詳細(xì)介紹了vue+elemet實(shí)現(xiàn)表格手動(dòng)合并行列,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08vue 實(shí)現(xiàn)基礎(chǔ)組件的自動(dòng)化全局注冊(cè)
這篇文章主要介紹了vue 實(shí)現(xiàn)基礎(chǔ)組件的自動(dòng)化全局注冊(cè)的方法,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2020-12-12解讀Vue3中keep-alive和動(dòng)態(tài)組件的實(shí)現(xiàn)邏輯
這篇文章主要介紹了Vue3中keep-alive和動(dòng)態(tài)組件的實(shí)現(xiàn)邏輯,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05vue項(xiàng)目運(yùn)行或打包時(shí),頻繁內(nèi)存溢出情況問(wèn)題
這篇文章主要介紹了vue項(xiàng)目運(yùn)行或打包時(shí),頻繁內(nèi)存溢出情況的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04Vue實(shí)現(xiàn)導(dǎo)出Excel表格文件提示“文件已損壞無(wú)法打開(kāi)”的解決方法
xlsx用于讀取解析和寫(xiě)入Excel文件的JavaScript庫(kù),它提供了一系列的API處理Excel文件,使用該庫(kù),可以將數(shù)據(jù)轉(zhuǎn)換Excel文件并下載到本地,適用于在前端直接生成Excel文件,這篇文章主要介紹了Vue實(shí)現(xiàn)導(dǎo)出Excel表格,提示文件已損壞,無(wú)法打開(kāi)的解決方法,需要的朋友可以參考下2024-01-01Vue.set() this.$set()引發(fā)的視圖更新思考及注意事項(xiàng)
this.$set()和Vue.set()本質(zhì)方法一樣,前者可以用在methods中使用。這篇文章主要介紹了Vue.set() this.$set()引發(fā)的視圖更新思考及注意事項(xiàng),需要的朋友可以參考下2018-08-08vue.js 實(shí)現(xiàn)點(diǎn)擊div標(biāo)簽時(shí)改變樣式
這篇文章主要介紹了vue.js 實(shí)現(xiàn)點(diǎn)擊div標(biāo)簽時(shí)改變樣式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08