Multiple lines of text exceeding the specified number of lines hides the excess and displays “… View all “is a common requirement, the Internet has also been achieved similar functions, but still want to write their own look, so write a Vue component, this article briefly introduces the implementation of ideas.
This component supports NPM installation:
Component address: github.com/Lushenggang…
Wintc.top/Laboratory /…
I. Requirement description
A text of variable length can be displayed in a maximum of N lines (for example, 3 lines). More than n lines will display “expand” or “view all” buttons at the end of the last line, click the button to expand and display all content, or jump to another page to display all content.
The expected effects are as follows:
Two, the implementation principle
Pure CSS is difficult to perfectly implement this function, so we have to use JS to implement, implementation ideas are generally similar, are to determine whether the content exceeds the specified number of lines, if so, cut the first X characters of the string, and then “… “View all” is spliced together, where x is the intercept length, which needs to be dynamically calculated.
To achieve this through the above scheme, there are several problems to be solved:
-
- How to determine if text exceeds the specified line number
- How to calculate the string interception length
- Dynamic responses, including responses to page layout changes, string changes, and specified line number changes
Let’s look at these questions in detail.
1. How to determine if a paragraph exceeds the specified number of lines?
First, solve a small problem: how to calculate the height of a given number of rows? The first thing that comes to mind is to use the Rows property of the Textarea, specify the number of rows, and then calculate how high the Textarea will hold up. Another method, which I haven’t tried but should work, is to multiply the computed value of the row height by the number of rows to get the height of the specified number of rows.
Having solved the problem of specifying the height of a line, it is easy to calculate whether a paragraph exceeds the specified number of lines. We can take a textarea with the specified number of lines out of the document flow using absolute positioning, place it below the text, and then compare it to the bottom of the textarea by the bottom of the text container. If the bottom of the text container is further down, the specified number of lines is exceeded. This can be done by using the getBoundingClientRect interface to get the location and size information of the two containers, and then comparing the bottom property in the location information.
You can design the DOM structure like this:
<div class="ellipsis-container"> <div class="textarea-container"> <textarea rows="3" readonly tabindex="-1"></textarea> </div> {{showContent}} <-- showContent represents the truncated part of the string -->... See more </div>Copy the code
Then use CSS to control the Textarea so that it is out of the document stream and cannot be seen, mouse events are triggered, etc. (readonly and tabIndex attributes in the Textarea tag are necessary) :
Ellipsis -container text-align left position relative line-height 1.5 padding 0! important .textarea-container position absolute left 0 right 0 pointer-events none opacity 0 z-index -1 textarea vertical-align middle padding 0 resize none overflow hidden font-size inherit line-height inherit outline none border noneCopy the code
2. How to calculate the truncated length of string x — Bilateral Approximation (Dichotomy idea)
As long as it is possible to determine whether a piece of text exceeds the specified number of lines, we can dynamically try to truncate the string until we find an appropriate truncation length x. This length is sufficient to truncate the string from position x with the first half of the string + “… “View all” and other text just does not exceed the specified number of lines N, but the interception of one more word will exceed N lines. The most intuitive idea is to iterate directly, so that x starts from 0 and grows to the total length of the displayed text. For each x value, we calculate once whether the text exceeds N lines. If the value does not exceed N lines, we continue to iterate, and if the value exceeds x -1, we get the appropriate length and break out of the loop. Of course, you can also have x traversed from the total length of the text decreasing.
The biggest problem here, however, is browser backflow and redraw. Because every time we intercept a string, the browser has to rerender it to see if it’s more than N lines, which triggers a browser redraw or reflow, which happens every time we loop. For normal requirements, assuming N is 3, it is likely that each calculation will result in more than 50 redraws or backflows, which still consumes a lot of performance, possibly tens or even hundreds of milliseconds. This calculation process should be completed in a task (commonly known as “macro task”), otherwise there will be a flashing “exception” in the calculation process, so it can be said that the calculation process is blocked, so the total calculation time must be controlled to a very low, that is, to reduce the number of calculations.
Consider using “bilateral approximation” (or “dichotomy”) to find the appropriate intercept length x, greatly reducing the number of attempts. For the first time, the text length is taken as the truncation length, and the calculation is stopped if the length exceeds N lines. If the length exceeds N lines, binary search continues between 1/2 length and text length; if the length exceeds 0 to 1/2 text length. Until the difference between the start value and the end value of the search interval is 1, the start value is the desired. See the complete code below for details.
3. Monitor page changes
For a Vue project, the string, line number, and so on passed into the component may change at any time. You can watch these properties change and recalculate the length once. On the other hand, the page layout may change due to the addition or deletion of other page elements or style changes, which will affect the width of the text container. In this case, the truncated length should also be recalculated.
To monitor changes in the width of the text container, we can consider using ResizeObserver to monitor, but this interface is not compatible enough (all versions of IE do not support it), so we choose an NPM library, Element-resize-Detector, to monitor (very useful 👍).
Three, code implementation
The complete code implementation is as follows:
<template> <div class="ellipsis-container"> <div class="textarea-container" ref="shadow"> <textarea :rows="rows" readonly tabindex="-1"></textarea> </div> {{ showContent }} <slot name="ellipsis" v-if="(textLength < content.length) || btnShow"> {{ ellipsisText }} <span class="ellipsis-btn" @click="clickBtn">{{ btnText }}</span> </slot> </div> </template> <script> import resizeObserver from 'element-resize-detector' const observer = resizeObserver() export Default {props: {content: {type: String, default: ''}, btnText: {type: String, default: ''}, ellipsisText: { type: String, default: '... ' }, rows: { type: Number, default: 6 }, btnShow: { type: Boolean, default: false }, }, data () { return { textLength: 0, beforeRefresh: null } }, computed: { showContent () { const length = this.beforeRefresh ? this.content.length : this.textLength return this.content.substr(0, this.textLength) }, WatchData () {return [this.content, this.btntext, this.ellipext, this.rows, this.btnShow] } }, watch: { watchData: { immediate: // Add () {// add () {// Add () {// Add () {// Add () {// Add () {// Add () {// () => this.refresh()) }, beforeDestroy () { observer.uninstall(this.$refs.shadow) }, methods: {refresh () {// Calculate the interception length, This.beforerefresh && this.beForerefresh () let stopLoop = false this.beForerefresh = () => stopLoop = true this.textLength = this.content.length const checkLoop = (start, end) => { if (stopLoop || start + 1 >= end) return const rect = this.$el.getBoundingClientRect() const shadowRect = this.$refs.shadow.getBoundingClientRect() const overflow = rect.bottom > shadowRect.bottom overflow ? (end = this.textLength) : (start = this.textLength) this.textLength = Math.floor((start + end) / 2) this.$nextTick(() => checkLoop(start, end)) } this.$nextTick(() => checkLoop(0, this.textLength)) }, $emit('click-btn', event)},}} </script>Copy the code
The refresh function is used in the code implementation to calculate the intercept length and is called when the text content, the rows properties, and so on change, or the text container size changes. Each Refresh call asynchronously recursively calls checkLoop multiple times, and refresh may be called again, with the new refresh call ending the previous checkLoop call.
Four, other
1. Support HTML string considerations
The current implementation does not support HTML text, and if it did, the problem would be much more complicated. Parsing and truncating HTML strings is not as simple as text strings. It is possible to use the browser’s Range API to locate the truncation position. Range’s insertNode and setStart interfaces can be used to “… “View all” is inserted into the specified location, and if the insertion location is just right, you can use the range.clonecontents () interface to retrieve the relevant content of the HTML string. In theory, it is possible, but the details and processing efficiency will be known in practice.
2. Reduce the impact of browser backflow
In the above implementation, each capture requires the browser to rerender the DOM, or redraw. The effect of redrawing is relatively small, but if the number of lines cut from the string changes, it will cause the height of the text container to change, which will cause the browser to backflow. The text container is in the document flow, and backflow will affect the entire document.
To solve this problem, you can use an element detached from the document flow to render and judge the string after dynamic truncation, with a layout similar to the textarea described above. Because it is not in the document flow, the influence of backflow is reduced to the element itself. After obtaining the truncation length, truncate the text and render it to the actual text container. This article is intended as an example of a simple overview of the principle, without this treatment. For those interested in the details, you can check out the Github repository code.