You've already forked vue-clamp
257 lines
5.4 KiB
JavaScript
257 lines
5.4 KiB
JavaScript
import { addListener, removeListener } from 'resize-detector'
|
|
|
|
export default {
|
|
name: 'vue-clamp',
|
|
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
|
|
}
|
|
const { 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)
|
|
}
|
|
},
|
|
isClamped: {
|
|
handler (val) {
|
|
this.$nextTick(() => this.$emit('clampchange', val))
|
|
},
|
|
immediate: true
|
|
}
|
|
},
|
|
mounted () {
|
|
this.init()
|
|
|
|
this.$watch(vm => [vm.maxLines, vm.maxHeight, vm.ellipsis, vm.isClamped].join(), this.update)
|
|
this.$watch(vm => [vm.tag, vm.text, vm.autoresize].join(), this.init)
|
|
},
|
|
updated () {
|
|
this.text = this.getText()
|
|
this.applyChange()
|
|
},
|
|
beforeDestroy () {
|
|
this.cleanUp()
|
|
},
|
|
methods: {
|
|
init () {
|
|
const contents = this.$slots.default
|
|
if (!contents) {
|
|
return
|
|
}
|
|
|
|
this.offset = this.text.length
|
|
|
|
this.cleanUp()
|
|
|
|
if (this.autoresize) {
|
|
addListener(this.$el, this.update)
|
|
this.unregisterResizeCallback = () => {
|
|
removeListener(this.$el, this.update)
|
|
}
|
|
}
|
|
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
|
|
},
|
|
getLines () {
|
|
return this.$refs.content.getClientRects().length
|
|
},
|
|
isOverflow () {
|
|
if (!this.maxLines && !this.maxHeight) {
|
|
return false
|
|
}
|
|
|
|
if (this.maxLines) {
|
|
if (this.getLines() > this.maxLines) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if (this.maxHeight) {
|
|
if (this.$el.scrollHeight > this.$el.offsetHeight) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
getText () {
|
|
// Look for the first non-empty text node
|
|
const [content] = (this.$slots.default || []).filter(
|
|
node => !node.tag && !node.isComment
|
|
)
|
|
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.getLines() < 2) &&
|
|
this.offset < this.text.length
|
|
) {
|
|
this.moveEdge(1)
|
|
}
|
|
},
|
|
clamp () {
|
|
while (this.isOverflow() && this.getLines() > 1 && this.offset > 0) {
|
|
this.moveEdge(-1)
|
|
}
|
|
},
|
|
search (...range) {
|
|
const [from = 0, to = this.offset] = range
|
|
if (to - from <= 3) {
|
|
this.stepToFit()
|
|
return
|
|
}
|
|
const 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) {
|
|
const contents = [
|
|
h(
|
|
'span',
|
|
{
|
|
ref: 'text',
|
|
attrs: {
|
|
'aria-label': this.text.trim()
|
|
}
|
|
},
|
|
this.realText
|
|
)
|
|
]
|
|
|
|
const { expand, collapse, toggle } = this
|
|
const scope = {
|
|
expand,
|
|
collapse,
|
|
toggle,
|
|
clamped: this.isClamped,
|
|
expanded: this.localExpanded
|
|
}
|
|
const before = this.$scopedSlots.before
|
|
? this.$scopedSlots.before(scope)
|
|
: this.$slots.before
|
|
if (before) {
|
|
contents.unshift(...(Array.isArray(before) ? before : [before]))
|
|
}
|
|
const after = this.$scopedSlots.after
|
|
? this.$scopedSlots.after(scope)
|
|
: this.$slots.after
|
|
if (after) {
|
|
contents.push(...(Array.isArray(after) ? after : [after]))
|
|
}
|
|
const lines = [
|
|
h(
|
|
'span',
|
|
{
|
|
style: {
|
|
boxShadow: 'transparent 0 0'
|
|
},
|
|
ref: 'content'
|
|
},
|
|
contents
|
|
)
|
|
]
|
|
return h(
|
|
this.tag,
|
|
{
|
|
style: {
|
|
maxHeight: this.realMaxHeight,
|
|
overflow: 'hidden'
|
|
}
|
|
},
|
|
lines
|
|
)
|
|
}
|
|
}
|