一个展开收起组件
效果如下图:
如何实现呢?
- 利用
canvas
动态计算每行文字的宽度; - 文字收起的时候,去掉字符串首尾的空格和回车;保证第一行有文字;同时保证最后一行也有文字;
- 文字展开的时候,文字末尾加上“收起”:一个
visibility: hidden
保证占据文字位置;一个使用position: absolute
绝对定位在文字最后。
模板:
<template>
<div
class="more-show-wrapper"
:style="{ font: fontStyle }"
>
<div
id="moreShowWrapper"
ref="moreShowWrapper"
class="more-show__inner"
>
<template v-if="!isFold">
<p
v-for="(str,index) in textList"
:key="index"
:class="['more-show__str-line', index === textList.length - 1 && showBtn ? 'more-show-last-str' : '']"
>{{ str }}</p>
</template>
<template v-else>
<span class="more-show__all-context">{{ showContent }}</span>
<span class="more-show__not-show">收起</span>
</template>
<span
v-if="showBtn"
class="more-show__button"
:style="{ bottom: `${paddingBottom}px`, right: `${paddingRight}px` }"
@click="isFold = !isFold"
>{{ isFold ? '收起' : '展开' }}</span>
</div>
</div>
</template>
逻辑处理:
export default defineComponent({
name: 'ShowMore',
props: {
showContent: String,
maxLine: {
type: Number,
default: 3,
},
fontStyle: {
type: String,
default: '14px PingFang SC',
},
paddingBottom: {
type: Number,
default: 0,
},
paddingRight: {
type: Number,
default: 0,
},
},
setup(props){
const lineWidth = ref(0);
const showBtn = ref(false);
const textList = ref([])
const isFold = ref(false)
const content = ref('')
const displayTextWidth = (text, font)=> {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = font
const metrics = context.measureText(text)
return metrics.width
}
const maxLineWidth = computed(()=> lineWidth.value - displayTextWidth('...展开', props.fontStyle))
const handleShowContent = (newContent) => {
const newVal = newContent.trim()
// 会把 \n 也去掉
let lineTmp = ''
// 循环结束完,指针位置
let lastIndex = 0
let tmpIndex = 0
const tmpList = []
for (let i = 0; i < newVal.length; i++) {
const cur = newVal[i]
if (cur !== '\n') {
lineTmp += cur
const curWidth = displayTextWidth(lineTmp, props.fontStyle)
if (tmpIndex <= props.maxLine - 1) {
// 换行处理,否则不处理
if (curWidth > lineWidth.value) {
const pushStr = lineTmp.substr(0, lineTmp.length - 1)
tmpList[tmpIndex] = pushStr
tmpIndex += 1
lineTmp = cur
// 最后一行已经满了
if (tmpIndex === props.maxLine) {
lastIndex = i - 1
break
}
}
}
} else if (cur === '\n') {
if (lineTmp) {
if (tmpIndex !== props.maxLine - 1) {
tmpList[tmpIndex] = lineTmp + cur
} else {
tmpList[tmpIndex] = lineTmp
}
lineTmp = ''
tmpIndex += 1
} else if (!lineTmp && tmpIndex !== props.maxLine - 1 && tmpIndex !== 0) {
// 空行处理
tmpList[tmpIndex] = cur
tmpIndex += 1
}
// 换行处理后,到最大行数结束
if (tmpIndex === props.maxLine) {
lastIndex = i
break
}
}
// 循环到最后一个,结束
if (i === newVal.length - 1) {
tmpList[tmpIndex] = lineTmp
lastIndex = i
}
}
textList.value = tmpList
// 循环结束,最后一行单独处理
if (textList.value.length === props.maxLine) {
let lastLine = textList.value[props.maxLine - 1]
if (lastIndex < newVal.length - 1) {
// 要有省略号 console.log("this.lineWidth", this.lineWidth, this.maxLineWidth)
let curWidth = displayTextWidth(lastLine, props.fontStyle)
while (curWidth > maxLineWidth.value) {
lastLine = lastLine.substr(0, lastLine.length - 1)
curWidth = displayTextWidth(lastLine, props.fontStyle)
}
showBtn.value = true
textList.value[props.maxLine - 1] = `${lastLine}...`
} else {
showBtn.value = false
}
} else {
showBtn.value = false
}
}
onMounted(()=>{
const dom = document.getElementById("moreShowWrapper")
if (dom) {
lineWidth.value = dom.getBoundingClientRect().width
handleShowContent(props.showContent)
}
})
return{
lineWidth,
showBtn,
textList,
isFold,
content,
maxLineWidth,
handleShowContent,
}
}
})
样式:
.more-show-wrapper {
width: 100%;
position: relative;
}
.more-show__all-context {
white-space: pre-wrap;
line-height: 22px;
font-size: 14px;
color: #262626;
word-break: break-all;
}
.more-show__button {
position: absolute;
color: #3974c7;
line-height: 22px;
font-size: 14px;
}
.more-show__str-line {
white-space: pre-line;
word-break: break-all;
line-height: 22px;
font-size: 14px;
color: #262626;
}
.more-show__not-show {
line-height: 22px;
font-size: 14px;
visibility: hidden;
}
转载自:https://juejin.cn/post/7035545441605255182