pet/uni_modules/uview-next/components/u-ellipsis/u-ellipsis.vue

272 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="u-ellipsis" :style="customStyle">
<view class="u-ellipsis__content">
<text :style="[textStyle]">
{{ expanded ? content : text }}
<text v-if="hasAction" class="u-ellipsis__action" @click="handleToggle"
:style="[{
color: actionColor
}]">
{{ expanded ? collapseText : expandText }}
</text>
</text>
</view>
<text class="measureBox" :class="[measureId]" :style="[textStyle]"> {{ measureContent }}</text>
</view>
</template>
<script>
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom')
// #endif
import props from './props.js';
import mixin from '../../libs/mixin/mixin'
import mpMixin from '../../libs/mixin/mpMixin';
/**
* ellipsis 文本省略
* @description 文本过长时,自动省略多余的文本。支持展开/收起功能。
* @tutorial https://uview.d3u.cn/components/ellipsis.html
* @property {String} content 文本内容
* @property {String} position 省略位置,可选值为 start、end、middle (默认 'end'
* @property {String | Number} rows 行数 (默认 1
* @property {String} expandText 展开文本
* @property {String} collapseText 收起文本
* @property {String} symbol 省略符号 (默认 '...'
* @property {String} color 文本颜色
* @property {String | Number} fontSize 文本大小 (默认 14
* @property {String} actionColor 展开/收起按钮颜色
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击文本时触发
* @event {Function} change 展开/收起状态改变时触发
* @example <u-ellipsis content="这是一段很长的文本内容..." :rows="2" expand-text="展开" collapse-text="收起"></u-ellipsis>
*/
export default {
name: "u-ellipsis",
mixins: [mpMixin, mixin, props],
data() {
return {
text: '',
expanded: false,
hasAction: false,
measureBoxVisible: false,
measureContent: '',
measureId: uni.$u.guid() // 生成唯一class
}
},
computed: {
textStyle() {
return {
color: this.color,
fontSize: this.$u.addUnit(this.fontSize),
lineHeight: this.$u.addUnit(this.lineHeight)
}
}
},
mounted() {
this.$nextTick(() => {
this.calcEllipsised()
})
},
// #ifdef VUE3
emits: ["click", "change"],
// #endif
methods: {
getMeasureBox() {
return new Promise(resolve => {
// #ifndef APP-NVUE
this.$uGetRect('.' + this.measureId).then(res => {
resolve(res)
})
// #endif
// #ifdef APP-NVUE
const ref = this.$refs['measureBox']
if (ref) {
dom.getComponentRect(ref, (res) => {
resolve({
height: res.size.height,
width: res.size.width
})
})
} else {
resolve({ height: 0, width: 0 })
}
// #endif
})
},
async calcEllipsised() {
if (!this.content || !this.content.length) return
// 初始化测量容器
this.measureBoxVisible = true
try {
// 计算基准行高和最大允许高度
const sampleText = this.content.slice(0, 1)
const { height, width } = await this.measureTextHeight(sampleText)
const maxAllowedHeight = (height || 20) * Number(this.rows)
// 检查原文本是否需要省略
const originalTextHeight = await this.measureTextHeight(this.content)
if (originalTextHeight <= maxAllowedHeight) {
this.text = this.content
this.hasAction = false
return
}
// 执行文本截断处理
this.hasAction = true
this.text = await this.performTextTruncation(maxAllowedHeight)
} catch (error) {
this.text = this.content
this.hasAction = false
} finally {
this.measureBoxVisible = false
}
},
// 测量文本高度
async measureTextHeight(text) {
this.measureContent = text
await this.$nextTick()
return this.getMeasureBox()
},
// 执行文本截断处理
async performTextTruncation(maxHeight) {
if (this.position === 'middle') {
return await this.truncateMiddle(maxHeight, this.symbol, this.expandText)
} else {
return await this.truncateEdge(maxHeight, this.symbol, this.expandText, this.position)
}
},
// 中间截断处理
async truncateMiddle(maxHeight, symbol, actionText) {
const text = this.content
const textLength = text.length
const centerPoint = Math.floor(textLength / 2)
let leftBoundary = [0, centerPoint]
let rightBoundary = [centerPoint, textLength]
while (this.shouldContinueTruncation(leftBoundary, rightBoundary)) {
const leftMidpoint = Math.floor((leftBoundary[0] + leftBoundary[1]) / 2)
const rightMidpoint = Math.ceil((rightBoundary[0] + rightBoundary[1]) / 2)
const testText = text.slice(0, leftMidpoint) + symbol + text.slice(rightMidpoint) + actionText
const { height } = await this.measureTextHeight(testText)
if (height >= maxHeight) {
// 文本过长,缩小范围
leftBoundary = [leftBoundary[0], leftMidpoint]
rightBoundary = [rightMidpoint, rightBoundary[1]]
} else {
// 文本适中,扩大范围
leftBoundary = [leftMidpoint, leftBoundary[1]]
rightBoundary = [rightBoundary[0], rightMidpoint]
}
}
return text.slice(0, leftBoundary[0]) + symbol + text.slice(rightBoundary[1])
},
// 边缘截断处理(开始或结束)
async truncateEdge(maxHeight, symbol, actionText, direction) {
const text = this.content
const isStartTruncation = direction === 'start'
let searchStart = 0
let searchEnd = text.length
let bestPosition = -1
while (searchStart <= searchEnd) {
const midPosition = Math.floor((searchStart + searchEnd) / 2)
const testText = isStartTruncation
? symbol + text.slice(midPosition) + actionText
: text.slice(0, midPosition) + symbol + actionText
const { height } = await this.measureTextHeight(testText)
if (height <= maxHeight) {
bestPosition = midPosition
if (isStartTruncation) {
searchEnd = midPosition - 1
} else {
searchStart = midPosition + 1
}
} else {
if (isStartTruncation) {
searchStart = midPosition + 1
} else {
searchEnd = midPosition - 1
}
}
}
return isStartTruncation
? symbol + text.slice(bestPosition)
: text.slice(0, bestPosition) + symbol
},
shouldContinueTruncation(leftBoundary, rightBoundary) {
return (leftBoundary[1] - leftBoundary[0] > 1) || (rightBoundary[1] - rightBoundary[0] > 1)
},
handleToggle(event) {
this.expanded = this.expanded ? false : true;
this.$emit('change', {
expanded: this.expanded,
content: this.expanded ? this.content : this.text
})
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-ellipsis {
position: relative;
.measureBox {
position: absolute;
top: -9999px;
left: -9999px;
/* #ifndef APP-NVUE */
white-space: normal;
word-wrap: break-word;
visibility: hidden;
width: 100%;
/* #endif */
}
&__content {
display: flex;
flex-direction: row;
align-items: flex-end;
flex-wrap: wrap;
}
&__action {
/* #ifndef APP-NVUE */
flex-shrink: 0;
cursor: pointer;
user-select: none;
/* #endif */
&:hover {
opacity: 0.8;
}
}
}
</style>