说起多行溢出省略号,用CSS实现最简单
.one-line {
display: -webkit-box !important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/*clip 修剪文本。*/
}
.more-line {
display: -webkit-box !important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
下面就摸索下用JS如何实现:
- Github DEMO
先看两个API:
getClientRects获取元素占据页面的所有矩形区域 :
getClientRects
返回一个TextRectangle集合,就是TextRectangleList对象。TextRectangle对象包含了, top left bottom right width height 六个属性TextRectangle对于文本对象,W3C提供了一个 TextRectangle 对象,这个对象是对文本区域的一个解释。这里的文本区域只针对inline 元素,比如:a, span, em这类标签元素。浏览器差异getClientRects() 最先由MS IE提出,后被W3C引入并制订了标准。目前主流浏览器都支持该标准,而IE只支持TextRectangle的top left bottom right四个属性。IE下可以通过right-left来计算width、bottom-top来计算height。ie 和非ie浏览器在使用getClientRects还是有些差别的,ie获取TextRectangleList的范围很大。而非ie获取的范围比较小, 只有display:inline的对象才能获取到TextRectangleList,例如em i span 等标签。应用场景getClientRects常用于获取鼠标的位置,如放大镜效果。微博的用户信息卡也是通过改方法获得的。
总结:只能用于行内元素,返回每一行的信息,返回信息和getBoundingClientRect返回类似。
DEMO:
html>
<html>
<head>
<meta charset="UTF-8">
<title>title>
<style type="text/css">
* {padding: 0px;margin: 0px;
}div {width: 80%;height: 90px;border: 1px solid red;
}#test{width:400px;height: 10px;background: red;
}style>
head>
<body>
<div>
<span id="main">返回值是ClientRect对象集合,该对象是与该元素相关的CSS边框。每个ClientRect对象包含一组描述该边框的只读属性——left、top、right和bottom,单位为像素,这些属性值是相对于视口的top-left的。即使当表格的标题在表格的边框外面,该标题仍会被计算在内。span>
div>
<div id="test">div>
body>
<script type="text/javascript">console.log(document.getElementById("main").getClientRects());script>
html>
getBoundingClientRect
获取元素位置 getBoundingClientRect
用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。getBoundingClientRect
是DOM元素到浏览器可视范围的距离(不包含文档卷起的部分)。该函数返回一个Object对象,该对象有6个属性:top
,lef
,right
,bottom
,width
,height
;这里的top
、left
和css中的理解很相似,width、height是元素自身的宽高,但是right
,bottom
和css中的理解有点不一样。right是指元素右边界距窗口最左边的距离,bottom是指元素下边界距窗口最上面的距离。
getBoundingClientRect()
最先是IE的私有属性,现在已经是一个W3C标准。所以你不用当心浏览器兼容问题,不过还是有区别的:IE只返回top
,lef
,right
,bottom
四个值,
返回差异:
getClientRects
和 getBoundingClientRect
的区别返回类型差异:getClientRects
返回一个TextRectangle
集合,就是TextRectangleList
对象。getBoundingClientRect
返回 一个TextRectangle
对象,即使DOM里没有文本也能返回TextRectangle对象.浏览器差异:除了safari,firefox2.0外所有浏览器都支持getClientRects
和getBoundingClientRect
,firefox 3.1给TextRectangle增加了 width 和 height。ie 和非ie浏览器在使用getClientRects
还是有些差别的,ie获取TextRectangleList
的范围很大。而非ie获取的范围比较小, 只有display:inline的对象才能获取到TextRectangleList
,例如em i span 等标签。通过测试,至少Chrome 2+\Safari 4\Firefox3.5\0pera 9.63+已经支持getBoundingClientRect
方法。使用场景差异:出于浏览器兼容的考虑,现在用得最多的是getBoundingClientRect
,经常用来获取一个element
元素的viewport
坐标。
Vue多行溢出省略号
监听DOM尺寸变化
import { addListener, removeListener } from 'resize-detector'
if (this.autoresize) {
let resizeCallback = () => {
this.update()
}
addListener(this.$el, resizeCallback) //监听
this.unregisterResizeCallback = () => { //移除
removeListener(this.$el, resizeCallback)
}
}
判断是否溢出
isOverflow () {
if (!this.maxLines && !this.maxHeight) {
return false
}
if (this.maxLines) {
//获取全部显示的行数
let actualLines = this.$refs.content.getClientRects().length
if (actualLines > this.maxLines) {
return true
}
}
if (this.maxHeight) {
if (this.$el.scrollHeight > this.$el.offsetHeight) {
return true
}
}
return false
},
二分查找多行截取字符临界值
moveEdge (steps) {
this.clampAt(this.offset + steps)
},
clampAt (offset) {
this.offset = offset
this.applyChange()
},
applyChange () {
this.$refs.text.textContent = this.realText
},
stepToFit () {
this.fill()
this.clamp()
},
fill () {
while (!this.isOverflow() && this.offset this.text.length) {
this.moveEdge(1)
}
},
clamp () {
while (this.isOverflow() && this.offset > 0) {
this.moveEdge(-1)
}
},
search (...range) {
let [from = 0, to = this.offset] = range
if (to - from <= 3) {
this.stepToFit()
return
}
let target = Math.floor((to + from) / 2) //从中间找临界值
this.clampAt(target)
if (this.isOverflow()) {
this.search(from, target)
} else {
this.search(target, to)
}
}
完整代码:
import { addListener, removeListener } from 'resize-detector'
import Vue from 'vue'
const UPDATE_TRIGGERS = ['maxLines', 'maxHeight', 'ellipsis']
const INIT_TRIGGERS = ['tag', 'text', 'autoresize']
export default {
name: 'vue-clamper',
props: {
tag: {
type: String,
default: 'div'
},
autoresize: {
type: Boolean,
default: false
},
maxLines: Number,
maxHeight: [String, Number],
ellipsis: {
type: String,
default: '…'
},
expanded: Boolean
},
data () {
return {
offset: null,
text: this.getText(),
localExpanded: !!this.expanded
}
},
computed: {
clampedText () {
return this.text.slice(0, this.offset) + this.ellipsis
},
isClamped () {
if (!this.text) {
return false
}
return this.offset !== this.text.length
},
realText () {
return this.isClamped ? this.clampedText : this.text
},
realMaxHeight () {
if (this.localExpanded) {
return null
}
let { maxHeight } = this
if (!maxHeight) {
return null
}
return typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight
}
},
watch: {
expanded (val) {
this.localExpanded = val
},
localExpanded (val) {
if (val) {
this.clampAt(this.text.length)
} else {
this.update()
}
if (this.expanded !== val) {
this.$emit('update:expanded', val)
}
}
},
mounted () {
this.init()
INIT_TRIGGERS.forEach(prop => {
this.$watch(prop, this.init)
})
UPDATE_TRIGGERS.forEach(prop => {
this.$watch(prop, this.update)
})
},
updated () {
this.text = this.getText()
this.applyChange()
},
beforeDestroy () {
this.cleanUp()
},
methods: {
init () {
let contents = this.$slots.default
if (!contents) {
return
}
if (Array.isArray(contents) && contents.length > 1) {
Vue.util.warn(
'VueClamper only supports clamping plain text content.',
this
)
return
}
let [content] = contents
if (content && content.tag) {
Vue.util.warn(
'VueClamper only supports clamping plain text content.',
this
)
return
}
this.offset = this.text.length
this.cleanUp()
if (this.autoresize) {
let resizeCallback = () => {
this.update()
}
addListener(this.$el, resizeCallback)
this.unregisterResizeCallback = () => {
removeListener(this.$el, resizeCallback)
}
}
this.update()
},
update () {
if (this.localExpanded) {
return
}
this.applyChange()
if (this.isOverflow() || this.isClamped) {
this.search()
}
},
expand () {
this.localExpanded = true
},
collapse () {
this.localExpanded = false
},
toggle () {
this.localExpanded = !this.localExpanded
},
isOverflow () {
if (!this.maxLines && !this.maxHeight) {
return false
}
if (this.maxLines) {
let actualLines = this.$refs.content.getClientRects().length
if (actualLines > this.maxLines) {
return true
}
}
if (this.maxHeight) {
if (this.$el.scrollHeight > this.$el.offsetHeight) {
return true
}
}
return false
},
getText () {
let [content] = this.$slots.default || []
return content ? content.text : ''
},
moveEdge (steps) {
this.clampAt(this.offset + steps)
},
clampAt (offset) {
this.offset = offset
this.applyChange()
},
applyChange () {
this.$refs.text.textContent = this.realText
},
stepToFit () {
this.fill()
this.clamp()
},
fill () {
while (!this.isOverflow() && this.offset this.text.length) {
this.moveEdge(1)
}
},
clamp () {
while (this.isOverflow() && this.offset > 0) {
this.moveEdge(-1)
}
},
search (...range) {
let [from = 0, to = this.offset] = range
if (to - from <= 3) {
this.stepToFit()
return
}
let target = Math.floor((to + from) / 2)
this.clampAt(target)
if (this.isOverflow()) {
this.search(from, target)
} else {
this.search(target, to)
}
},
cleanUp () {
if (this.unregisterResizeCallback) {
this.unregisterResizeCallback()
}
}
},
render (h) {
let contents = [
h(
'span',
{
ref: 'text',
attrs: {
'aria-label': this.text.trim()
}
},
this.realText
)
]
let { expand, collapse, toggle } = this
let scope = { expand, collapse, toggle }
let before = this.$scopedSlots.before
? this.$scopedSlots.before(scope)
: this.$slots.before
if (before) {
contents.unshift(...(Array.isArray(before) ? before : [before]))
}
let after = this.$scopedSlots.after
? this.$scopedSlots.after(scope)
: this.$slots.after
if (after) {
contents.push(...(Array.isArray(after) ? after : [after]))
}
let lines = [
h(
'span',
{
style: {
boxShadow: 'transparent 0 0'
},
ref: 'content'
},
contents
)
]
return h(
this.tag,
{
style: {
maxHeight: this.realMaxHeight,
overflow: 'hidden'
}
},
lines
)
}
}
使用:
import VClamp from './components/Clamp.js'
<v-clamp:class="{
demo: true,
hyphens: hyphens1
}":max-lines="lines"autoresize:style="{
width: `${width1}px`
}"
>
{{ zh ? textZh : text }}
<buttonslot="after"slot-scope="{ toggle }"class="toggle btn btn-sm"
@click="toggle"
>
{{ zh ? '切换' : 'Toggle' }}
button>
v-clamp>