使用JavaScript優(yōu)雅實現(xiàn)文本展開收起功能
前言
實現(xiàn)文本溢出的展開收起功能,純 CSS 方案在網(wǎng)頁中可行,但在小程序中存在兼容性問題。
最優(yōu)的解決方案就是使用 JavaScript 的二分截斷法。
看了下 vant 的 TextEllipsis 組件源碼。
理解了算法的實現(xiàn)原理后就寫了一個uniapp版本和vue3版本的展開收起組件。
算法步驟:
- 創(chuàng)建隱藏容器并渲染內(nèi)容。
- 計算最大行高(行數(shù) × 單行行高)。
- 使用遞歸算法,類似于 tail(left, content.length)。
- 取中間值,并將其寫入隱藏容器。
- 等待渲染完成后獲取最新高度。
- 如果隱藏容器的高度超過最大行高,則繼續(xù)調(diào)用 tail,使用 left = left,right = middle。
- 否則,可能是內(nèi)容太少了(或者無法再繼續(xù)截斷,那就返回截取的內(nèi)容)。使用 left = middle,right = right 繼續(xù)調(diào)用 tail。
這個算法通過不斷地二分截斷,尋找到最合適的截取內(nèi)容。
就算是1000多字,限定2行展示,截斷次數(shù)也只在10次左右。
擴展:canvas海報的文字溢出功能也可以用這個算法。
uniapp版本
下面是從源碼抽離出來單獨封裝的uniapp和vue3版本(網(wǎng)頁,小程序,app都測試過)
先上效果圖 300多ms:
uniapp版本有一些需要注意的點,如果兼容運行在小程序和app的話。
- 在小程序中,樣式計算是在渲染過程中異步進行的,必須nextTick后才能獲取容器最新高度(因為小程序樣式計算是異步的。所以性能比不上網(wǎng)頁的2ms,實測是300+ms)。
- 獲取元素節(jié)點信息的方法也不一樣。
- 行高如果是繼承的獲取的就是inherit。所以需要傳行高進去。
<template> <view :class="{root:true,visible:!show}" :style="{ lineHeight: props.lineHeight }" > {{ expanded ? props.content : text }} <text class="action" v-if="hasAction" @click="onClickAction" >{{ actionText }}</text > </view> <view :class="{hiddenText:true}" :style="{ lineHeight: props.lineHeight }" >{{ text }}</view > </template> <script lang="ts" setup> import { defineProps, ref, getCurrentInstance, nextTick, computed, onMounted } from 'vue'; const instance = getCurrentInstance(); // 獲取組件實例 const props = defineProps({ content: { type: String, default: '' }, rows: { type: Number, default: 2 }, lineHeight: { type: Number, default: '30rpx' } }); const expanded = ref(false); const text = ref(props.content); const hasAction = ref(false); const show= ref(false); const actionText = computed(() => { return expanded.value ? '收起' : '展開'; }); const onClickAction = () => { expanded.value = !expanded.value; }; // 查詢元素形狀信息 const qeuryRect = queryText => { let query = uni.createSelectorQuery().in(instance); return new Promise((resolve, reject) => { query .select(queryText) .boundingClientRect(rect => { resolve(rect); }) .exec(); }); }; // 查詢元素樣式屬性等信息 const qeuryRectProp = queryText => { let query = uni.createSelectorQuery().in(instance); return new Promise((resolve, reject) => { query .select(queryText) .fields({ computedStyle: ['lineHeight', 'height'], dataset: true, size: true }, rect => { resolve(rect); }) .exec(); }); }; let dots = '...'; let content = props.content; let end = content.length; const setHiddenText = val => { return new Promise((_, reject) => { text.value = val; console.error(val); nextTick(() => { _(val); }); }); }; // 計算截斷 const calcEllipsisText = maxHeight => { const tail = async (left, right) => { // 遞歸終止條件 if (right - left <= 1) { return content.slice(0, left) + dots; } const middle = Math.round((left + right) / 2); // 設(shè)置攔截位置(注意slice 0,middle,雖然left ,right不斷變,但是0是不變的) await setHiddenText(content.slice(0, middle) + dots + actionText.value); let result = await qeuryRectProp('.hiddenText'); if (parseInt(result.height) > maxHeight) { return tail(left, middle); } // 太往左了,內(nèi)容不夠,需要往右邊移動 return tail(middle, right); }; tail(0, end).then(res => { text.value = res; show.value=true console.timeEnd("完成計算") }); }; // 開始計算 onMounted(() => { console.time("完成計算") nextTick(async () => { let result = await qeuryRectProp('.hiddenText'); let maxHeight = parseInt(result.lineHeight) * props.rows; // 隱藏的行高大于限定行數(shù)高度 if (maxHeight < parseInt(result.height)) { hasAction.value = true; calcEllipsisText(maxHeight); } else { hasAction.value = false; text.value = props.content; show.value=true } }); }); </script> <style lang="scss" scoped> .visible { visibility: hidden; } .hiddenText { position: fixed; z-index: -999; top: -9999px; } .action{ color:#1989fa; } </style>
vue3版本
先上效果圖:2ms
<template> <div ref="root"> {{ expanded ? props.content : text }} <span v-if="hasAction" class="action" @click="onClickAction"> {{ actionText }} </span> </div> </template> <script setup> import { ref, watch, computed, onMounted, onUnmounted, onActivated, defineProps, defineEmits } from 'vue' const emit = defineEmits(['clickAction']) const props = defineProps({ rows: { type: Number, default: 2, }, dots: { type: String, default: '...', }, content: { type: String, default: '', }, expandText: { type: String, default: '展開', }, collapseText: { type: String, default: '收起', }, }) const useWindowResize = () => { const window_width = ref(window.innerWidth) onMounted(() => { window.addEventListener('resize', () => { windowWidth.value = window.innerWidth }) }) onUnmounted(() => { window.removeEventListener('resize', () => { windowWidth.value = window.innerWidth }) }) return window_width } const windowWidth = useWindowResize() const text = ref('') const expanded = ref(false) const hasAction = ref(false) const root = ref(null) let needRecalculate = false const actionText = computed(() => (expanded.value ? props.collapseText : props.expandText)) const pxToNum = (value) => { if (!value) return 0 const match = value.match(/^\d*(\.\d*)?/) return match ? Number(match[0]) : 0 } const cloneContainer = () => { if (!root.value || !root.value.isConnected) return const originStyle = window.getComputedStyle(root.value) const container = document.createElement('div') const styleNames = Array.from(originStyle) styleNames.forEach((name) => { container.style.setProperty(name, originStyle.getPropertyValue(name)) }) container.style.position = 'fixed' container.style.zIndex = '-9999' container.style.top = '-9999px' container.style.height = 'auto' container.style.minHeight = 'auto' container.style.maxHeight = 'auto' container.innerText = props.content document.body.appendChild(container) return container } const calcEllipsised = () => { console.time('完成計算') const calcEllipsisText = (container, maxHeight) => { const { content, dots } = props const end = content.length const calcEllipse = () => { const tail = (left, right) => { // 遞歸終止條件 if (right - left <= 1) { return content.slice(0, left) + dots } const middle = Math.round((left + right) / 2) // 設(shè)置攔截位置 container.innerText = content.slice(0, middle) + dots + actionText.value if (container.offsetHeight > maxHeight) { return tail(left, middle) } // 太往左了,內(nèi)容不夠,需要往右邊移動 return tail(middle, right) } container.innerText = tail(0, end) console.timeEnd('完成計算') } calcEllipse() return container.innerText } // 計算截斷文本 const container = cloneContainer() if (!container) { needRecalculate = true return } const { paddingBottom, paddingTop, lineHeight } = container.style const maxHeight = Math.ceil( (Number(props.rows) + 0.5) * pxToNum(lineHeight) + pxToNum(paddingTop) + pxToNum(paddingBottom) ) if (maxHeight < container.offsetHeight) { hasAction.value = true text.value = calcEllipsisText(container, maxHeight) } else { hasAction.value = false text.value = props.content } document.body.removeChild(container) } const toggle = (isExpanded = !expanded.value) => { expanded.value = isExpanded } const onClickAction = (event) => { toggle() emit('clickAction', event) } onMounted(calcEllipsised) onActivated(() => { if (needRecalculate) { needRecalculate = false calcEllipsised() } }) watch([windowWidth, () => [props.content, props.rows]], calcEllipsised) defineExpose({ toggle }) </script> <style scoped> .action { color: #1989fa; } </style>
到此這篇關(guān)于使用JavaScript優(yōu)雅實現(xiàn)文本展開收起功能的文章就介紹到這了,更多相關(guān)JavaScript文本展開收起內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用uni-app和uView實現(xiàn)多圖上傳功能全過程
最近在使用uniapp開發(fā)的微信小程序中使用了圖片上傳功能,下面這篇文章主要給大家介紹了關(guān)于利用uni-app和uView實現(xiàn)多圖上傳功能的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-03-03BootstrapValidator實現(xiàn)表單驗證功能
這篇文章主要為大家詳細介紹了BootstrapValidator實現(xiàn)表單驗證功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-11-11Bootstrap treeview實現(xiàn)動態(tài)加載數(shù)據(jù)并添加快捷搜索功能
本文實現(xiàn)了運用bootstrap treeview實現(xiàn)動態(tài)加載數(shù)據(jù),并且添加快捷搜索功能,需要的朋友參考下2018-01-01