Cross-browser multi-line text overflow with ellipsis appended within a fixed width and height
Asked Answered
B

26

193

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?

Backboard answered 4/8, 2010 at 10:11 Comment(4)
See #537314 (and #4453485)Puny
and #3923239 for css-only solutionHong
related article css-tricks.com/line-clampinAdamok
For anyone looking for this in mid 2016 the short answer is: NO it is not possible in elegant, cross browser, CSS only way. The solution often given as closest to complete (codepen.io/romanrudenko/pen/ymHFh) is so Goldbergian it makes all your body hurt, and still quite ugly.Tish
S
97

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.

Sugared answered 7/10, 2010 at 10:59 Comment(10)
great solution @bazmegakapa... but I have some issues trying to adapt it to my case. I have various li and inside on of each one there is a .block and a .block h2 and I need to apply this to the h2 inside .block but I couldn't get it to work. Is it any diferent if there are more than one .block h2?Pseudonymous
@w0rldart Could you create a jsFiddle showing the way you are trying to do it?Sugared
#9089066Pseudonymous
Doesn't work in WebKit when we tested, but dotdotdot (see Matt's answer) seems to work fine.Antitoxin
@BenjaminOakes Never said it is a ready to use solution. It works fine in the jsFiddle Demo. I never went any further with it, it was just an idea.Sugared
In my case it was leaving just 2 lines of text when there should have been 3. Apparently my container was smaller than line height*3 by a few pixels. Easy fix is to simply add a few pixels to divhSeersucker
I had a bad experience of infinite loop with this script because the text contained only one very long word, so the replace regexp never matched. To avoid this, add this code just after the while line: if(!$p.text().match(/\W*\s(\S)*$/)) break;Lignify
@LukasLT Same here, if the container has some top/bottom padding, you have to add this to divh.Dipole
While not likely to be a issue in this case, updating the DOM and checking layout repeatedly is a bad idea because it could cause a slow down. To mitigate this, you could something similar to a binary search: test to see if the text block already fits, otherwise split the text into words or characters and define your bounds (lower=1 word/chars, upper=all words/chars), while ((upper-lower)>1) {let middle=((lower+upper)/2)|0 /*|0 is quick floor*/; if (test(words.slice(0,middle)+'...')) {lower=middle;} else {upper=middle;}}. As @Lignify found, you'll also want to check for one giant word.Dive
This solution is great. In my case, I need to keep track of the original text so I can truncate the full text value responsively. So when the page loads, I store the original text in a variable, and before I run this logic I make sure to 'refresh' the element with the original text value. Add debounce, and it works wonderfully.Mccowan
E
68

Try the jQuery.dotdotdot plugin.

$(".ellipsis").dotdotdot();
Elemi answered 13/9, 2011 at 16:56 Comment(7)
How would you pronounce that? dot-dot-dot-dot?Latif
Really sucks to use > 600 lines of js to solve a problem that should be solved by cssRollerskate
I have tried it, and it is working fine. Should be the accepted answerViva
@JethroLarson I agree (CSS should provide this feature). This js library is only 2.5Kb once minified & Gzipped though. So this looks fine to me as a work around for now.Adamok
Caveat: make sure your element is visible before calling .dotdotdot() it uses jQuery's innerHeight internally which gets weird if the element is hidden (or not added to the DOM yet).Zechariah
Works but make sure to use window.loaded event and not $(document).ready(), as fonts and other external resources might impact the layout of your HTML. If dotdotdot executes before theses resources are loaded, text will not be truncated at the correct position.Pelkey
This is a commercial tool, it costs $5 for a single site and $35 for multiple sites. Licensing would be a pain. I thought it was free and immediately integrateable, not so!Perseverance
A
39

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


github.com/josephschmitt/Clamp.js


Here are a few more I did not investigate yet:


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:


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 "read more" solution

Oh, and if you look for design inspirations:

Adamok answered 3/3, 2016 at 14:9 Comment(0)
T
23

You can use -webkit-line-clamp property with div.

-webkit-line-clamp: <integer> which means set the maximum number of lines before truncating the content and then displays an ellipsis (…) at the end of the last line.

div {
  width: 205px;
  height: 40px;
  background-color: gainsboro;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  
  /* <integer> values */
  -webkit-line-clamp: 2;
}
<div>This is a multi-lines text block, some lines inside the div, while some outside</div>
Tigges answered 16/10, 2019 at 15:12 Comment(4)
Not sure why somebody has downvoted this answer. The browser support as of March 2020 is pretty decent - 95% caniuse.com/#search=line-clampGahan
This approach has a couple of drawbacks which makes using of this feature not effective: - this won't work with a single long word, e.g. "thisisoneverylonglongword"; - you can still see the truncated text if the line-height is not multiple of the container heightHeal
Note that the ellipsis won't appear on Safari, and as of Safari 15, -webkit-line-clamp appears to be broken, so truncation will only occur via the height value, which may cut mid-line. Try adding line-break: after-white-space to address this.Cypress
Perfect answer.Dimissory
G
10

Found this short CSS-only solution in Adrien Be's answer:

.line-clamp {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical; 
  overflow: hidden; 
}

As of March 2020 browser support is 95.3%, not being supported in IE and Opera Mini. Works on Chrome, Safari, Firefox and Edge.

Gahan answered 17/3, 2020 at 13:37 Comment(0)
C
4

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);
Copier answered 30/3, 2013 at 2:30 Comment(2)
This is very uneffective code. By the way using of "while" look is potential bug with infinite loop.Idleman
I second @Idleman , if the text is a long one, which is quite a possibility when the length of text is unknown.Selfinterest
P
4

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.

Popliteal answered 22/5, 2013 at 21:14 Comment(3)
Please share the important parts of your solution, on SO link-only answers are not allowed.Sugared
the brilliant aspect of this is the text itself is not truncated, so if the user copy-pastes the table the entire content appears.Caterinacatering
A very nice concept. It is also mentioned in this article ("fade out" way) I believe css-tricks.com/line-clampinAdamok
C
4

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

Here is a summary:

enter image description here

<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>
Caniff answered 27/5, 2014 at 18:22 Comment(0)
D
2

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

Diminuendo answered 1/12, 2015 at 10:52 Comment(0)
I
2

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);
Idleman answered 14/11, 2016 at 8:31 Comment(0)
P
2

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.

Procurance answered 31/5, 2017 at 9:46 Comment(0)
M
2

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 + "…";
  }
}
Moxley answered 3/10, 2018 at 8:49 Comment(0)
W
1

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 more screenshot

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

Weariful answered 5/9, 2014 at 13:30 Comment(1)
<sigh> I thought someone might downvote this, presumably because it's not an exact answer to the question. Nonetheless, I hope someone will find it useful, as I couldn't this information anywhere else and this is where I ended after a search.Weariful
P
1

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>
Philippa answered 27/3, 2015 at 18:53 Comment(0)
A
1

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 image description here

Chrome

enter image description here

Alyse answered 27/4, 2018 at 20:13 Comment(0)
P
0

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.

Planetesimal answered 27/9, 2010 at 10:1 Comment(0)
M
0

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)

Mcbroom answered 6/10, 2010 at 19:36 Comment(1)
What is overflow: clip? And what would you expect that CSS with float to do?Sugared
A
0

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;
}
Apical answered 14/6, 2012 at 14:46 Comment(1)
That's often infinite loop.Lawrence
B
0

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.

Barbate answered 14/12, 2017 at 16:7 Comment(0)
K
0

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:

npm install fast-ellipsis 

Hope you enjoy!

Kristofer answered 23/1, 2018 at 2:50 Comment(0)
S
0

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

Seineetmarne answered 16/10, 2018 at 12:27 Comment(0)
M
0

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];
  }
}
Miraculous answered 13/12, 2018 at 0:53 Comment(0)
T
0

Just add these CSS for your class or html tag,

.YOUR_CLASS_NAME {
     width: 300px;
     display: -webkit-box;
     -webkit-box-orient: vertical;
     -webkit-line-clamp: 3;
     overflow: hidden;
  }

if still does not work, refer this doc: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp

Trudi answered 16/6, 2022 at 6:8 Comment(0)
D
0

CSS line clamping is the simplest solution to the problem of multiline truncation.

Line clamping is supported by all major browsers, however it is slightly broken in Webkit. The bottom edge of the box should always be aligned with the bottom of the last line of text. Webkit, when eliding, instead aligns the bottom of the box with the elided line's "text bottom", not its real bottom. This discrepancy becomes very noticeable for large line-height values.

Run the following snippet in Safari to see the problem.

div {
    border: 10px solid black;
    line-height: 2;

    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
<div>Vel mollis dignissim duis ligula nisl tincidunt eget aliquet eget efficitur quis justo integer pellentesque lacus. Viverra libero viverra finibus. Non id diam lorem ipsum dolor sit amet. Consectetur adipiscing elit sed ac eleifend lorem et placerat. Ante vestibulum. Congue augue vel turpis aliquet. Urna a varius urna convallis sed integer dapibus lobortis enim non venenatis orci varius ut fusce porttitor eros vel. Mollis dignissim duis ligula nisl tincidunt eget aliquet eget efficitur.</div>

Fortunately, the element's height can easily be corrected with a bit of JavaScript.

if (navigator.vendor === "Apple Computer, Inc.") {
    const {lineHeight} = window.getComputedStyle(element);
    if (lineHeight.endsWith("px")) {
        const line_height_px = parseFloat(lineHeight);
        element.style.height = line_height_px * Math.ceil(
            element.clientHeight / line_height_px
        ) + "px";
     }
}
Donar answered 20/7, 2022 at 3:33 Comment(0)
L
-2

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>
Lecythus answered 7/8, 2010 at 22:14 Comment(1)
The problem is the number 63 in your codes, if the number is known then everything become easy, just a truncate function does this job, as your codes. However, how to know the number? In other words, how to know where the text will be line-breaked? If this question can be answered, then the problem can be simply resolved in logic of "1, calculate the number; 2, truncate"Backboard
D
-3

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>
Destruct answered 25/3, 2013 at 2:5 Comment(1)
As we already discussed under other answers, the problem in your code is the number 36. Other than making your code specific to a certain container width, it is also not accurate: with not fixed-width fonts, there can be big differences between letters. See iiiiiiiiii vs MMMMMMMMMM (with the current font not that visible though :D).Sugared

© 2022 - 2024 — McMap. All rights reserved.