使用JavaScript優(yōu)雅實(shí)現(xiàn)文本展開收起功能
前言
實(shí)現(xiàn)文本溢出的展開收起功能,純 CSS 方案在網(wǎng)頁(yè)中可行,但在小程序中存在兼容性問題。
最優(yōu)的解決方案就是使用 JavaScript 的二分截?cái)喾ā?/p>
看了下 vant 的 TextEllipsis 組件源碼。
理解了算法的實(shí)現(xiàn)原理后就寫了一個(gè)uniapp版本和vue3版本的展開收起組件。
算法步驟:
- 創(chuàng)建隱藏容器并渲染內(nèi)容。
- 計(jì)算最大行高(行數(shù) × 單行行高)。
- 使用遞歸算法,類似于 tail(left, content.length)。
- 取中間值,并將其寫入隱藏容器。
- 等待渲染完成后獲取最新高度。
- 如果隱藏容器的高度超過最大行高,則繼續(xù)調(diào)用 tail,使用 left = left,right = middle。
- 否則,可能是內(nèi)容太少了(或者無(wú)法再繼續(xù)截?cái)?,那就返回截取的?nèi)容)。使用 left = middle,right = right 繼續(xù)調(diào)用 tail。
這個(gè)算法通過不斷地二分截?cái)?,尋找到最合適的截取內(nèi)容。
就算是1000多字,限定2行展示,截?cái)啻螖?shù)也只在10次左右。
擴(kuò)展:canvas海報(bào)的文字溢出功能也可以用這個(gè)算法。
uniapp版本
下面是從源碼抽離出來(lái)單獨(dú)封裝的uniapp和vue3版本(網(wǎng)頁(yè),小程序,app都測(cè)試過)
先上效果圖 300多ms:


uniapp版本有一些需要注意的點(diǎn),如果兼容運(yùn)行在小程序和app的話。
- 在小程序中,樣式計(jì)算是在渲染過程中異步進(jìn)行的,必須nextTick后才能獲取容器最新高度(因?yàn)樾〕绦驑邮接?jì)算是異步的。所以性能比不上網(wǎng)頁(yè)的2ms,實(shí)測(cè)是300+ms)。
- 獲取元素節(jié)點(diǎn)信息的方法也不一樣。
- 行高如果是繼承的獲取的就是inherit。所以需要傳行高進(jìn)去。
<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(); // 獲取組件實(shí)例
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;
};
// 查詢?cè)匦螤钚畔?
const qeuryRect = queryText => {
let query = uni.createSelectorQuery().in(instance);
return new Promise((resolve, reject) => {
query
.select(queryText)
.boundingClientRect(rect => {
resolve(rect);
})
.exec();
});
};
// 查詢?cè)貥邮綄傩缘刃畔?
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);
});
});
};
// 計(jì)算截?cái)?
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)容不夠,需要往右邊移動(dòng)
return tail(middle, right);
};
tail(0, end).then(res => {
text.value = res;
show.value=true
console.timeEnd("完成計(jì)算")
});
};
// 開始計(jì)算
onMounted(() => {
console.time("完成計(jì)算")
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('完成計(jì)算')
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)容不夠,需要往右邊移動(dòng)
return tail(middle, right)
}
container.innerText = tail(0, end)
console.timeEnd('完成計(jì)算')
}
calcEllipse()
return container.innerText
}
// 計(jì)算截?cái)辔谋?
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)雅實(shí)現(xiàn)文本展開收起功能的文章就介紹到這了,更多相關(guān)JavaScript文本展開收起內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用uni-app和uView實(shí)現(xiàn)多圖上傳功能全過程
最近在使用uniapp開發(fā)的微信小程序中使用了圖片上傳功能,下面這篇文章主要給大家介紹了關(guān)于利用uni-app和uView實(shí)現(xiàn)多圖上傳功能的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03
原生JS檢測(cè)CSS3動(dòng)畫是否結(jié)束的方法詳解
這篇文章主要介紹了原生JS檢測(cè)CSS3動(dòng)畫是否結(jié)束的方法,結(jié)合實(shí)例形式分析了javascript事件響應(yīng)及針對(duì)css3屬性檢測(cè)相關(guān)操作技巧,需要的朋友可以參考下2019-01-01
BootstrapValidator實(shí)現(xiàn)表單驗(yàn)證功能
這篇文章主要為大家詳細(xì)介紹了BootstrapValidator實(shí)現(xiàn)表單驗(yàn)證功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
Bootstrap treeview實(shí)現(xiàn)動(dòng)態(tài)加載數(shù)據(jù)并添加快捷搜索功能
本文實(shí)現(xiàn)了運(yùn)用bootstrap treeview實(shí)現(xiàn)動(dòng)態(tài)加載數(shù)據(jù),并且添加快捷搜索功能,需要的朋友參考下2018-01-01
微信小程序左滑動(dòng)顯示菜單功能的實(shí)現(xiàn)
這篇文章主要介紹了微信小程序左滑動(dòng)顯示菜單功能的實(shí)現(xiàn),代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06

