Cross-browser multi-line text overflow with ellips

2018-12-31 19:17发布

问题:

I made an image for this question to make it easier to understand.

Is it possible to create an ellipsis on a <div> with a fixed width and multiple lines?

\"text-overflow\"

I’ve tried some jQuery plugins out here and there, but cannot find the one I’m looking for. Any recommendation? Ideas?

回答1:

Just a quick basic idea.

I was testing with the following markup:

<div id=\"fos\">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nisi ligula, dapibus a volutpat sit amet, mattis et dui. Nunc porttitor accumsan orci id luctus. Phasellus ipsum metus, tincidunt non rhoncus id, dictum a lectus. Nam sed ipsum a lacus sodales eleifend. Vestibulum lorem felis, rhoncus elementum vestibulum eget, dictum ut velit. Nullam venenatis, elit in suscipit imperdiet, orci purus posuere mauris, quis adipiscing ipsum urna ac quam.</p>  
</div>

And CSS:

#fos { width: 300px; height: 190px; overflow: hidden; }
#fos p { padding: 10px; margin: 0; }

Applying this jQuery will accomplish the desired result:

var $p = $(\'#fos p\');
var divh = $(\'#fos\').height();
while ($p.outerHeight() > divh) {
    $p.text(function (index, text) {
        return text.replace(/\\W*\\s(\\S)*$/, \'...\');
    });
}

It repeatedly tries to remove the last word of the text until it reaches the desired size. Because of the overflow: hidden; the process remains invisible and even with JS turned off the result remains \'visually correct\' (without the \"...\" of course).

If you combine this with a sensible truncation on the server-side (that leaves only a small overhead) then it will run quicker :).

Again, this is not a complete solution, just an idea.

UPDATE: Added a jsFiddle Demo.



回答2:

Try the jQuery.dotdotdot plugin.

$(\".ellipsis\").dotdotdot();


回答3:

Javascript libraries for \"line clamping\"

Note that \"line clamping\" is also referred as \"Ellipsis on block of multi-lines\" or \"vertical ellipsis\".


github.com/BeSite/jQuery.dotdotdot

  • Pros: 2.5Kb (minified & gzipped), no big activity on repo but not bad either
  • Cons: jQuery dependency, paid for commercial use (CC-BY-NC-4.0 license)
  • my 2 cents: stackoverflow.com/questions/25187774/read-more-and-read-less-with-dotdotdot-jquery/29118739#29118739
  • helpful stackoverflow.com/questions/19015945/jquery-dotdotdot-expand-truncate-text-onclick
  • helpful gist.github.com/chiragparekh/c7e33dc749ed25544bde

github.com/josephschmitt/Clamp.js

  • Cons: code repo barely active
  • informative reusablebits.com/post/2980974411/clamp-js-v0-2-explanations-and-performance

Here are a few more I did not investigate yet:

  • github.com/ftlabs/ftellipsis
  • github.com/micjamking/Succinct
  • github.com/pvdspek/jquery.autoellipsis and pvdspek.github.io/jquery.autoellipsis
  • github.com/rviscomi/trunk8
  • github.com/dobiatowski/jQuery.FastEllipsis
  • github.com/theproductguy/ThreeDots
  • github.com/tbasse/jquery-truncate
  • github.com/kbwood/more

CSS solutions for line clamping

There are some CSS solutions, but the simplest uses -webkit-line-clamp which has poor browser support. See live demo on jsfiddle.net/AdrienBe/jthu55v7/

Many people went to great efforts in order to make this happen using CSS only. See articles and questions about it:

  • css-tricks.com/line-clampin : 5 stars article on line camplin
  • mobify.com/blog/multiline-ellipsis-in-pure-css : CSS only
  • cssmojo.com/line-clamp_for_non_webkit-based_browsers/ : \"mimic\" -webkit-line-clamp in non webkit browsers
  • With CSS, use "..." for overflowed block of multi-lines
  • Cross-browser multi-line text overflow with ellipsis appended within a width and height fixed `<div>`
  • How to display only the first few lines of a div (clamping)?
  • jquery limit lines in a paragraph and apply three periods to the end
  • Limit text length to n lines using CSS

What I\'d recommend

Keep it simple. Unless you have great amount of time to dedicate to this feature, go for the simplest & tested solution: simple CSS or a well tested javascript library.

Go for something fancy/complex/highly-customized & you will pay the price for this down the road.


What others do

Having a fade out like Airbnb does might be a good solution. It probably is basic CSS coupled with basic jQuery. Actually, it seems very similar to this solution on CSSTricks

\"AirBnb

Oh, and if you look for design inspirations:

  • smashingmagazine.com/2009/07/designing-read-more-and-continue-reading-links/, from 2009 though...
  • Dribbble probably has interesting designs...I could not find a way to gather them though (via search or tags), feel free to share a relevant link


回答4:

There is no such feature in HTML, and this is very frustrating.

I have developed a library to deal with this.

  • Multiline text-overflow: ellipsis
  • Multiline text with technologies that does not support it: SVG, Canvas for example
  • Have exactly the same line breaks in your SVG text, in your HTML rendering, and in your PDF export for example

Check out my site for screenshot, tutorial, and dowload link.



回答5:

Pure JS solution based on bažmegakapa\'s solution, and some cleanup to account for people who try to give a height/max-height that is less than the element\'s lineHeight:

  var truncationEl = document.getElementById(\'truncation-test\');
  function calculateTruncation(el) {
    var text;
    while(el.clientHeight < el.scrollHeight) {
      text = el.innerHTML.trim();
      if(text.split(\' \').length <= 1) {
        break;
      }
      el.innerHTML = text.replace(/\\W*\\s(\\S)*$/, \'...\');
    }
  }

  calculateTruncation(truncationEl);


回答6:

I have a solution which works well but instead an ellipsis it uses a gradient. The advantages are that you don\'t have to do any JavaScript calculations and it works for variable width containers including table cells. It uses a couple of extra divs, but it\'s very easy to implement.

http://salzerdesign.com/blog/?p=453

Edit: Sorry, I did\'t know that the link wasn\'t enough. The solution is to put a div around the text, and style the div to control the overflow. Inside the div put another div with a \"fade\" gradient which can be made by using CSS or an image (for old IE). The gradient goes from transparent to the background color of the table cell and is a bit wider than an ellipsis. If the text is long and overflows, it goes under the \"fade\" div and looks \"faded out\". If the text is short, the fade is invisible so there is no problem. The two containers can be adjusted to let one or multiple lines show by setting the height of the container as a multiple of the text line height. The \"fade\" div can be positioned to only cover the last line.



回答7:

Here is a pure CSS way to accomplish this: http://www.mobify.com/blog/multiline-ellipsis-in-pure-css/

Here is a summary:

\"enter

<html>
<head>
<style>
    html, body, p { margin: 0; padding: 0; font-family: sans-serif;}

    .ellipsis {
        overflow: hidden;
        height: 200px;
        line-height: 25px;
        margin: 20px;
        border: 5px solid #AAA; }

    .ellipsis:before {
        content:\"\";
        float: left;
        width: 5px; height: 200px; }

    .ellipsis > *:first-child {
        float: right;
        width: 100%;
        margin-left: -5px; }        

    .ellipsis:after {
        content: \"\\02026\";  

        box-sizing: content-box;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;

        float: right; position: relative;
        top: -25px; left: 100%; 
        width: 3em; margin-left: -3em;
        padding-right: 5px;

        text-align: right;

        background: -webkit-gradient(linear, left top, right top,
            from(rgba(255, 255, 255, 0)), to(white), color-stop(50%, white));
        background: -moz-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);           
        background: -o-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);
        background: -ms-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);
        background: linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white); }
</style>
</head>
<body>
    <div class=\"ellipsis\">
        <div>
            <p>Call me Ishmael.....</p> 
        </div>
    </div>
</body>
</html>


回答8:

Not an exact answer to the question, but I came across this page when trying to do very similar, but wanting to add a link to \"view more\" rather than just a straightforward ellipsis. This is a jQuery function that will add a \"more\" link to text that is overflowing a container. Personally I\'m using this with Bootstrap, but of course it will work without.

\"Example

To use, put your text in a container as follows:

<div class=\"more-less\">
    <div class=\"more-block\">
        <p>The long text goes in here</p>
    </div>
</div>

When the following jQuery function is added, any of the divs that are larger than the adjustheight value will be truncated and have a \"More\" link added.

$(function(){
    var adjustheight = 60;
    var moreText = \'+ More\';
    var lessText = \'- Less\';
    $(\".more-less .more-block\").each(function(){
        if ($(this).height() > adjustheight){
            $(this).css(\'height\', adjustheight).css(\'overflow\', \'hidden\');
            $(this).parent(\".more-less\").append
                (\'<a style=\"cursor:pointer\" class=\"adjust\">\' + moreText + \'</a>\');
        }
    });
    $(\".adjust\").click(function() {
        if ($(this).prev().css(\'overflow\') == \'hidden\')
        {
            $(this).prev().css(\'height\', \'auto\').css(\'overflow\', \'visible\');
            $(this).text(lessText);
        }
        else {
            $(this).prev().css(\'height\', adjustheight).css(\'overflow\', \'hidden\');
            $(this).text(moreText);
        }
    });
});

Based on this, but updated: http://shakenandstirredweb.com/240/jquery-moreless-text



回答9:

The mentioned dotdotdot jQuery plugin work nice with angular:

(function (angular) {
angular.module(\'app\')
    .directive(\'appEllipsis\', [
        \"$log\", \"$timeout\", function ($log, $timeout) {
            return {
                restrict: \'A\',
                scope: false,
                link: function (scope, element, attrs) {

                    // let the angular data binding run first
                    $timeout(function() {
                        element.dotdotdot({
                            watch: \"window\"
                        });
                    });
                }
            }

        }
    ]);
})(window.angular);

The corresponding markup would be:

<p app-ellipsis>{{ selectedItem.Description }}</p>


回答10:

Here\'s a vanilla JavaScript solution you can use in a pinch:

// @param 1 = element containing text to truncate
// @param 2 = the maximum number of lines to show
function limitLines(el, nLines) {
  var nHeight,
    el2 = el.cloneNode(true);
  // Create clone to determine line height
  el2.style.position = \'absolute\';
  el2.style.top = \'0\';
  el2.style.width = \'10%\';
  el2.style.overflow = \'hidden\';
  el2.style.visibility = \'hidden\';
  el2.style.whiteSpace = \'nowrap\';
  el.parentNode.appendChild(el2);
  nHeight = (el2.clientHeight+2)*nLines; // Add 2 pixels of slack
  // Clean up
  el.parentNode.removeChild(el2);
  el2 = null;
  // Truncate until desired nLines reached
  if (el.clientHeight > nHeight) {
    var i = 0,
      imax = nLines * 35;
    while (el.clientHeight > nHeight) {
      el.innerHTML = el.textContent.slice(0, -2) + \'&hellip;\';
      ++i;
      // Prevent infinite loop in \"print\" media query caused by
      // Bootstrap 3 CSS: a[href]:after { content:\" (\" attr(href) \")\"; }
      if (i===imax) break;
    }
  }
}

limitLines(document.getElementById(\'target\'), 7);
#test {
  width: 320px;
  font-size: 18px;
}
<div id=\"test\">
  <p>Paragraph 1</p>
  <p id=\"target\">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
  <p>Paragraph 3</p>
</div>

You can play around with it in the codepen below. Try changing the font size in the CSS panel and make a minor edit in the HTML panel (e.g., add an extra space somewhere) to update the results. Regardless of the font size, the middle paragraph should always be truncated to the number of lines in the second parameter passed to limitLines().

Codepen: http://codepen.io/thdoan/pen/BoXbEK



回答11:

EDIT: Came across Shave which is JS plugin that does multi line text truncation based on a given max-height really well. It uses binary search to find the optimum break point. Definitely worth investigating.


ORIGINAL ANSWER:

I had to come up with a vanilla JS solution for this problem. In the case that I had worked on, I had to fit a long product name into limited width and over two lines; truncated by ellipsis if needed.

I used answers from various SO posts to cook up something that fit my needs. The strategy is as follows:

  1. Calculate the average character width of the font variant for the desired font size.
  2. Calculate the width of the container
  3. Calculate number of characters which fit on one line in the container
  4. Calculate the number of characters to truncate the string to based on the number of characters that fit on a line and the number of lines the text is supposed to wrap over.
  5. Truncate the input text based on the previous calculation (factoring in for extra characters added by ellipsis) and append \"...\" to the end

Code sample:

/**
 * Helper to get the average width of a character in px
 * NOTE: Ensure this is used only AFTER font files are loaded (after page load)
 * @param {DOM element} parentElement 
 * @param {string} fontSize 
 */
function getAverageCharacterWidth(parentElement, fontSize) {
    var textSample = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()\";
    parentElement = parentElement || document.body;
    fontSize = fontSize || \"1rem\";
    var div = document.createElement(\'div\');
    div.style.width = \"auto\";
    div.style.height = \"auto\";
    div.style.fontSize = fontSize;
    div.style.whiteSpace = \"nowrap\";
    div.style.position = \"absolute\";
    div.innerHTML = textSample;
    parentElement.appendChild(div);

    var pixels = Math.ceil((div.clientWidth + 1) / textSample.length);
    parentElement.removeChild(div);
    return pixels;
}

/**
 * Helper to truncate text to fit into a given width over a specified number of lines
 * @param {string} text Text to truncate
 * @param {string} oneChar Average width of one character in px
 * @param {number} pxWidth Width of the container (adjusted for padding)
 * @param {number} lineCount Number of lines to span over
 * @param {number} pad Adjust this to ensure optimum fit in containers. Use a negative value to Increase length of truncation, positive values to decrease it.
 */
function truncateTextForDisplay(text, oneChar, pxWidth, lineCount, pad) {
    var ellipsisPadding = isNaN(pad) ? 0 : pad;
    var charsPerLine = Math.floor(pxWidth / oneChar);
    var allowedCount = (charsPerLine * (lineCount)) - ellipsisPadding;
    return text.substr(0, allowedCount) + \"...\";
}


//SAMPLE USAGE:
var rawContainer = document.getElementById(\"raw\");
var clipContainer1 = document.getElementById(\"clip-container-1\");
var clipContainer2 = document.getElementById(\"clip-container-2\");

//Get the text to be truncated
var text=rawContainer.innerHTML;

//Find the average width of a character
//Note: Ideally, call getAverageCharacterWidth only once and reuse the value for the same font and font size as this is an expensive DOM operation
var oneChar = getAverageCharacterWidth();

//Get the container width
var pxWidth = clipContainer1.clientWidth;

//Number of lines to span over
var lineCount = 2;

//Truncate without padding
clipContainer1.innerHTML = truncateTextForDisplay(text, oneChar, pxWidth, lineCount);

//Truncate with negative padding value to adjust for particular font and font size
clipContainer2.innerHTML = truncateTextForDisplay(text, oneChar, pxWidth, lineCount,-10);
.container{
  display: inline-block;
  width: 200px;
  overflow: hidden;
  height: auto;
  border: 1px dotted black;
  padding: 10px;
  }
<h4>Untruncated</h4>
<div id=\"raw\" class=\"container\">
This is super long text which needs to be clipped to the correct length with ellipsis spanning over two lines
</div>
<h4>Truncated</h4>
<div id=\"clip-container-1\" class=\"container\">
</div>
<h4>Truncated with Padding Tweak</h4>
<div id=\"clip-container-2\" class=\"container\">
</div>

PS:

  1. If the truncation is to be on only one line, the pure CSS method of using text-overflow: ellipsis is neater
  2. Fonts which don\'t have a fixed width may cause the truncation to happen too early or too late (as different characters have different widths). Using the pad parameter helps mitigate this in some cases but will not be fool proof :)
  3. Will add in links and references to the original posts after I get laptop back (need history)

PPS: Just realised this is very similar to the approach as suggested by @DanMan and @st.never. Checkout the code snippets for an implementation example.



回答12:

Maybe quite late but using SCSS you can declare a function like:

@mixin clamp-text($lines, $line-height) {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: $lines;
  line-height: $line-height;
  max-height: unquote(\'#{$line-height*$lines}em\');

  @-moz-document url-prefix() {
    position: relative;
    height: unquote(\'#{$line-height*$lines}em\');

    &::after {
      content: \'\';
      text-align: right;
      position: absolute;
      bottom: 0;
      right: 0;
      width: 30%;
      height: unquote(\'#{$line-height}em\');
      background: linear-gradient(
        to right,
        rgba(255, 255, 255, 0),
        rgba(255, 255, 255, 1) 50%
      );
    }
  }
}

And use it like:

.foo {
    @include clamp-text(1, 1.4);
}

Which will truncate the text to one line and knowing that it is 1.4 its line-height. The output expected is chrome to render with ... at the end and FF with some cool fade at the end

Firefox

\"enter

Chrome

\"enter



回答13:

You probably can\'t do it (currently?) without a fixed-width font like Courier. With a fixed-width font every letter occupies the same horizontal space, so you could probably count the letters and multiply the result with the current font size in ems or exs. Then you would just have to test how many letters fit on one line, and then break it up.

Alternatively, for non-fixed-with fonts you might be able to create a mapping for all possible characters (like i = 2px, m = 5px) and then do the math. A lot of ugly work though.



回答14:

To expand on @DanMan\'s solution: in the case where variable-width fonts are used, you could use an average font width. This has two problems: 1) a text with too many W\'s would overflow and 2) a text with too many I\'s would be truncated earlier.

Or you could take a worst-case approach and use the width of the letter \"W\" (which I believe is the widest). This removes problem 1 above but intensifies problem 2.

A different approach could be: leave overflow: clip in the div and add an ellipsis section (maybe another div or image) with float: right; position: relative; bottom: 0px; (untested). The trick is to make the image appear above the end of text.

You could also only display the image when you know it\'s going to overflow (say, after about 100 characters)



回答15:

With this code there is no need for an extra wrapper div if the element has it\'s height limited by a max-height style.

// Shorten texts in overflowed paragraphs to emulate Operas text-overflow: -o-ellipsis-lastline
$(\'.ellipsis-lastline\').each(function(i, e) {
    var $e = $(e), original_content = $e.text();
    while (e.scrollHeight > e.clientHeight)
        $e.text($e.text().replace(/\\W*\\w+\\W*$/, \'…\'));
    $e.attr(\'data-original-content\', original_content);
});

Also it saves the original text in a data attribute that can be displayed using only styles, eg. on mouse over:

.ellipsis-lastline {
    max-height: 5em;
}
.ellipsis-lastline:before {
    content: attr(data-original-content);
    position: absolute;
    display: none;
}
.ellipsis-lastline:hover:before {
    display: block;
}


回答16:

Pure JS demo (without jQuery and \'while\' loop)

When I searched solution of multiline ellipsis problem I was surprised that there is no any good one without jQuery. Also there are a few solutions based on \'while\' loop, but I think they are not effective and dangerous due to possibility to get into infinite loop. So I wrote this code:

function ellipsizeTextBox(el) {
  if (el.scrollHeight <= el.offsetHeight) {
    return;
  }

  let wordArray = el.innerHTML.split(\' \');
  const wordsLength = wordArray.length;
  let activeWord;
  let activePhrase;
  let isEllipsed = false;

  for (let i = 0; i < wordsLength; i++) {
    if (el.scrollHeight > el.offsetHeight) {
      activeWord = wordArray.pop();
      el.innerHTML = activePhrase = wordArray.join(\' \');
    } else {
      break;
    }
  }

  let charsArray = activeWord.split(\'\');
  const charsLength = charsArray.length;

  for (let i = 0; i < charsLength; i++) {
    if (el.scrollHeight > el.offsetHeight) {
      charsArray.pop();
      el.innerHTML = activePhrase + \' \' + charsArray.join(\'\')  + \'...\';
      isEllipsed = true;
    } else {
      break;
    }
  }

  if (!isEllipsed) {
    activePhrase = el.innerHTML;

    let phraseArr = activePhrase.split(\'\');
    phraseArr = phraseArr.slice(0, phraseArr.length - 3)
    el.innerHTML = phraseArr.join(\'\') + \'...\';
  }
}

let el = document.getElementById(\'ellipsed\');

ellipsizeTextBox(el);


回答17:

In my scenario I couldn\'t get to work any of the functions mentioned above and I also needed to tell the function how many lines to show regardless of the font-size or container size.

I based my solution on the use of the Canvas.measureText method (whic is an HTML5 feature) as explained here by Domi, so it is not completely cross-browser.

You can see how it works on this fiddle.

This is the code:

var processTexts = function processTexts($dom) {
    var canvas = processTexts .canvas || (processTexts .canvas = document.createElement(\"canvas\"));

    $dom.find(\'.block-with-ellipsis\').each(function (idx, ctrl) {
        var currentLineAdded = false;
        var $this = $(ctrl);

        var font = $this.css(\'font-family\').split(\",\")[0]; //This worked for me so far, but it is not always so easy.
        var fontWeight = $(this).css(\'font-weight\');
        var fontSize = $(this).css(\'font-size\');
        var fullFont = fontWeight + \" \" + fontSize + \" \" + font;
        // re-use canvas object for better performance
        var context = canvas.getContext(\"2d\");
        context.font = fullFont;

        var widthOfContainer = $this.width();
        var text = $.trim(ctrl.innerHTML);
        var words = text.split(\" \");
        var lines = [];
        //Number of lines to span over, this could be calculated/obtained some other way.
        var lineCount = $this.data(\'line-count\');

        var currentLine = words[0];
        var processing = \"\";

        var isProcessing = true;
        var metrics = context.measureText(text);
        var processingWidth = metrics.width;
        if (processingWidth > widthOfContainer) {
            for (var i = 1; i < words.length && isProcessing; i++) {
                currentLineAdded = false;
                processing = currentLine + \" \" + words[i];
                metrics = context.measureText(processing);
                processingWidth = metrics.width;
                if (processingWidth <= widthOfContainer) {
                    currentLine = processing;
                } else {
                    if (lines.length < lineCount - 1) {
                        lines.push(currentLine);
                        currentLine = words[i];
                        currentLineAdded = true;
                    } else {
                        processing = currentLine + \"...\";
                        metrics = context.measureText(processing);
                        processingWidth = metrics.width;
                        if (processingWidth <= widthOfContainer) {
                            currentLine = processing;
                        } else {
                            currentLine = currentLine.slice(0, -3) + \"...\";
                        }
                        lines.push(currentLine);
                        isProcessing = false;
                        currentLineAdded = true;
                    }
                }
            }
            if (!currentLineAdded)
                lines.push(currentLine);
            ctrl.innerHTML = lines.join(\" \");
        }
    });
};

(function () {
    $(document).ready(function () {
        processTexts($(document));
    });
})();

And the HTML to use it would be like this:

<div class=\"block-with-ellipsis\" data-line-count=\"2\">
    VERY LONG TEXT THAT I WANT TO BREAK IN LINES. VERY LONG TEXT THAT I WANT TO BREAK IN LINES.
</div>

The code to get the font-family is rather simple, and in my case works, but for more complex scenarios you may need to use something along these lines.

Also, in my case I am telling the function how many lines to use, but you could calculate how many lines to show according to the container size and font.



回答18:

Here I made another library with faster algorithm. Please check:

https://github.com/i-ahmed-biz/fast-ellipsis

To install using bower:

bower install fast-ellipsis

To install using npm:

bower install fast-ellipsis 

Hope you enjoy!



回答19:

Very simple javascript solution. Divs has to be styled f.e.:

.croppedTexts { 
  max-height: 32px;
  overflow: hidden;
}

And JS:

var list = document.body.getElementsByClassName(\"croppedTexts\");
for (var i = 0; i < list.length; i++) {
  cropTextToFit(list[i]);
}

function cropTextToFit (o) {
  var lastIndex;
  var txt = o.innerHTML;
  if (!o.title) o.title = txt;

  while (o.scrollHeight > o.clientHeight) {
    lastIndex = txt.lastIndexOf(\" \");
    if (lastIndex == -1) return;
    txt = txt.substring(0, lastIndex);
    o.innerHTML = txt + \"…\";
  }
}


回答20:

I have made a version that leaves the html intact. jsfiddle example

jQuery

function shorten_text_to_parent_size(text_elem) {
  textContainerHeight = text_elem.parent().height();


  while (text_elem.outerHeight(true) > textContainerHeight) {
    text_elem.html(function (index, text) {
      return text.replace(/(?!(<[^>]*>))\\W*\\s(\\S)*$/, \'...\');
    });

  }
}

$(\'.ellipsis_multiline\').each(function () {
  shorten_text_to_parent_size($(this))
});

CSS

.ellipsis_multiline_box {
  position: relative;
  overflow-y: hidden;
  text-overflow: ellipsis;
}

jsfiddle example



回答21:

I wrote an angular component that solves the problem. It splits a given text into span elements. After rendering, it removes all overflowing elements and places the ellipsis right after the last visible element.

Usage example:

<app-text-overflow-ellipsis [text]=\"someText\" style=\"max-height: 50px\"></app-text-overflow-ellipsis>

Stackblitz demo: https://stackblitz.com/edit/angular-wfdqtd

The component:

import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ElementRef, HostListener,
  Input,
  OnChanges,
  ViewChild
} from \'@angular/core\';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: \'app-text-overflow-ellipsis\',
  template: `
    <span *ngFor=\"let word of words; let i = index\" [innerHTML]=\"word + (!endsWithHyphen(i) ? \' \' : \'\')\"></span>
    <span #ellipsis [hidden]=\"!showEllipsis && !initializing\" [class.initializing]=\"initializing\" [innerHTML]=\"\'...\' + (initializing ? \'&nbsp;\' : \'\')\"></span>
  `,
  styles: [`
    :host {
      display: block; 
      position: relative;
    }
    .initializing {
      opacity: 0;
    }
  `
  ]
})

export class TextOverflowEllipsisComponent implements OnChanges {
  @Input()
  text: string;

  showEllipsis: boolean;
  initializing: boolean;

  words: string[];

  @ViewChild(\'ellipsis\')
  ellipsisElement: ElementRef;

  constructor(private element: ElementRef, private cdRef: ChangeDetectorRef) {}

  ngOnChanges(){
    this.init();
  }

  @HostListener(\'window:resize\')
  init(){
    // add space after hyphens
    let text = this.text.replace(/-/g, \'- \') ;

    this.words = text.split(\' \');
    this.initializing = true;
    this.showEllipsis = false;
    this.cdRef.detectChanges();

    setTimeout(() => {
      this.initializing = false;
      let containerElement = this.element.nativeElement;
      let containerWidth = containerElement.clientWidth;
      let wordElements = (<HTMLElement[]>Array.from(containerElement.childNodes)).filter((element) =>
        element.getBoundingClientRect && element !== this.ellipsisElement.nativeElement
      );
      let lines = this.getLines(wordElements, containerWidth);
      let indexOfLastLine = lines.length - 1;
      let lineHeight = this.deductLineHeight(lines);
      if (!lineHeight) {
        return;
      }
      let indexOfLastVisibleLine = Math.floor(containerElement.clientHeight / lineHeight) - 1;

      if (indexOfLastVisibleLine < indexOfLastLine) {

        // remove overflowing lines
        for (let i = indexOfLastLine; i > indexOfLastVisibleLine; i--) {
          for (let j = 0; j < lines[i].length; j++) {
            this.words.splice(-1, 1);
          }
        }

        // make ellipsis fit into last visible line
        let lastVisibleLine = lines[indexOfLastVisibleLine];
        let indexOfLastWord = lastVisibleLine.length - 1;
        let lastVisibleLineWidth = lastVisibleLine.map(
          (element) => element.getBoundingClientRect().width
        ).reduce(
          (width, sum) => width + sum, 0
        );
        let ellipsisWidth = this.ellipsisElement.nativeElement.getBoundingClientRect().width;
        for (let i = indexOfLastWord; lastVisibleLineWidth + ellipsisWidth >= containerWidth; i--) {
          let wordWidth = lastVisibleLine[i].getBoundingClientRect().width;
          lastVisibleLineWidth -= wordWidth;
          this.words.splice(-1, 1);
        }


        this.showEllipsis = true;
      }
      this.cdRef.detectChanges();

      // delay is to prevent from font loading issues
    }, 1000);

  }

  deductLineHeight(lines: HTMLElement[][]): number {
    try {
      let rect0 = lines[0][0].getBoundingClientRect();
      let y0 = rect0[\'y\'] || rect0[\'top\'] || 0;
      let rect1 = lines[1][0].getBoundingClientRect();
      let y1 = rect1[\'y\'] || rect1[\'top\'] || 0;
      let lineHeight = y1 - y0;
      if (lineHeight > 0){
        return lineHeight;
      }
    } catch (e) {}

    return null;
  }

  getLines(nodes: HTMLElement[], clientWidth: number): HTMLElement[][] {
    let lines = [];
    let currentLine = [];
    let currentLineWidth = 0;

    nodes.forEach((node) => {
      if (!node.getBoundingClientRect){
        return;
      }

      let nodeWidth = node.getBoundingClientRect().width;
      if (currentLineWidth + nodeWidth > clientWidth){
        lines.push(currentLine);
        currentLine = [];
        currentLineWidth = 0;
      }
      currentLine.push(node);
      currentLineWidth += nodeWidth;
    });
    lines.push(currentLine);

    return lines;
  }

  endsWithHyphen(index: number): boolean {
    let length = this.words[index].length;
    return this.words[index][length - 1] === \'-\' && this.words[index + 1] && this.words[index + 1][0];
  }
}


回答22:

not sure if this is what you\'re looking for, it uses min-height instead of height.

    <div id=\"content\" style=\"min-height:10px;width:190px;background:lightblue;\">
    <?php 
        function truncate($text,$numb) {
            // source: www.kigoobe.com, please keep this if you are using the function
            $text = html_entity_decode($text, ENT_QUOTES);
            if (strlen($text) > $numb) {
                $text = substr($text, 0, $numb);
                $etc = \"...\"; 
                $text = $text.$etc;
            } 
            $text = htmlentities($text, ENT_QUOTES);
            return $text;
        }
        echo truncate(\"this is a multi-lines text block, some lines inside the div, while some outside\", 63);
    ?>
    </div>


回答23:

Very simple func will do.

Directive:

  $scope.truncateAlbumName = function (name) {
    if (name.length > 36) {
      return name.slice(0, 34) + \"..\";
    } else {
      return name;
    }
  };

View:

<#p>{{truncateAlbumName(album.name)}}<#/p>