Ellipsis in the middle of a text (Mac style)

Viewed 48379

I need to implement ellipsis ("...") in the middle of a text within a resizable element. Here is what it might look like. So,

"Lorem ipsum dolor sit amet. Ut ornare dignissim ligula sed commodo."

becomes

"Lorem ipsum dolor sit amet ... commodo."

When the element is stretched out to the width of the text, I want the ellipsis to disappear. How can this be done?

16 Answers

In the HTML, put the full value in a custom data-* attribute like

<span data-original="your string here"></span>

Then assign load and resize event listeners to a JavaScript function which will read the original data attribute and place it in the innerHTML of your span tag. Here is an example of the ellipsis function:

function start_and_end(str) {
  if (str.length > 35) {
    return str.substr(0, 20) + '...' + str.substr(str.length-10, str.length);
  }
  return str;
}

Adjust the values, or if possible, make them dynamic, if necessary for different objects. If you have users from different browsers, you can steal a reference width from a text by the same font and size elsewhere in your dom. Then interpolate to an appropriate amount of characters to use.

A tip is also to have an abbr-tag on the ... or who message to make the user be able to get a tooltip with the full string.

<abbr title="simple tool tip">something</abbr>

The following Javascript function will do a middle truncation, like OS X:

function smartTrim(string, maxLength) {
    if (!string) return string;
    if (maxLength < 1) return string;
    if (string.length <= maxLength) return string;
    if (maxLength == 1) return string.substring(0,1) + '...';

    var midpoint = Math.ceil(string.length / 2);
    var toremove = string.length - maxLength;
    var lstrip = Math.ceil(toremove/2);
    var rstrip = toremove - lstrip;
    return string.substring(0, midpoint-lstrip) + '...' 
    + string.substring(midpoint+rstrip);
}       

It will replace characters in the middle with ellipsis. My unit tests show:

var s = '1234567890';
assertEquals(smartTrim(s, -1), '1234567890');
assertEquals(smartTrim(s, 0), '1234567890');
assertEquals(smartTrim(s, 1), '1...');
assertEquals(smartTrim(s, 2), '1...0');
assertEquals(smartTrim(s, 3), '1...90');
assertEquals(smartTrim(s, 4), '12...90');
assertEquals(smartTrim(s, 5), '12...890');
assertEquals(smartTrim(s, 6), '123...890');
assertEquals(smartTrim(s, 7), '123...7890');
assertEquals(smartTrim(s, 8), '1234...7890');
assertEquals(smartTrim(s, 9), '1234...67890');
assertEquals(smartTrim(s, 10), '1234567890');
assertEquals(smartTrim(s, 11), '1234567890');

None of the solutions that I saw here take into account that different characters have different width. My solution takes this into account. Basically, it strips text by one character until it fits, take a look:

const span = document.getElementById("span");
const div = document.getElementById("div");
const originalText = span.textContent;
const textLength = originalText.length;
let part1 = originalText.substr(0, Math.floor(textLength / 2));
let part2 = originalText.substr(Math.floor(textLength / 2));
let trimPart1 = true;
while (span.clientWidth > div.clientWidth) {
  if (trimPart1) {
    part1 = part1.substr(0, part1.length - 1);
  } else {
    part2 = part2.substr(-1 * (part2.length - 1));
  }
  span.textContent = part1 + "..." + part2;
  trimPart1 = !trimPart1;
}
<div id="div" style="overflow: hidden; width: 200px; white-space: nowrap;">
  <span id="span" style="display: inline-block">this is a quite long text that has some words and I want it to be split in half</span>
</div>

https://jsfiddle.net/maxim_mazurok/oujctpz8/56/

Mac's Finder works the same way, it first tries to strip the left part, then the right part. So, it may have WWWW...WWWWW, just as my solution.

It's not the most efficient one though. Perhaps, the same can be achieved using virtual DOM or canvas to better optimize performance.

You can't do that with CSS. The problem is that HTML and CSS are supposed to work in a variety of browsers and fonts and it is almost impossible to calculate the width of a string in a consistent way. This is an idea that might help you. However, you would need to do that a number of times, until you find the string with the appropriate width.

Here's the shortest bit I could find which replaces 3 characters in the middle with ....

function shorten(s, max) {
  return s.length > max ? s.substring(0, (max / 2) - 1) + '...' + s.substring(s.length - (max / 2) + 2, s.length) : s
}

This solution is a mix of the above solutions and puts the last whole word at the end of the shortened text. However in case the last word is longer then a third of the available space it is also shortend from the left. If a dash("-") is found, cut it of there, if not, cut it of anyway.

function truncate(text, textLimit) {
    if (!text) return text;
    if (textLimit < 1) return string;
    if (text.length < textLimit) return text;
    if (textLimit === 1) return text.substring(0,1) + '...';
    /* extract the last word */
    var lastPart = text.slice( string.lastIndexOf(' ')+1 );
    /* if last word is longer then a third of the max available space
       cut it from the left */
    var lastPartLimit = Math.ceil(textLimit / 3);
    if(lastPart.length > lastPartLimit) {
        var truncatedLastPart = lastPart;
        /* Try to find a dash and cut the last word there */
        var lastDashPart = text.slice( text.lastIndexOf('-')+1 );
        if(lastDashPart.length < lastPartLimit){
            truncatedLastPart = lastDashPart;
        }
        /* If the last part is still to long or not available cut it anyway */
        if(truncatedLastPart.length > lastPartLimit) {
            var lastIndex = lastPart.length - lastPartLimit;
            truncatedLastPart = lastPart.substring( lastIndex );
        }
        lastPart = truncatedLastPart;
    }
    var dots = '... ';
    var firsPartLength = textLimit - lastPart.length - dots.length;
    return text.substring(0, firstPartLength) + dots + lastPart;
}

console.log( truncate("New York City", 10) ); // Ne... City (max of 10 characters)
console.log( truncate("New York Kindergarden", 14) ); // Ne...ergarden (max of 14 characters, last word gets cut from the left by a third)
console.log( truncate("New York Kinder-garden", 14) ); // Ne...garden (max of 14 characters, last word gets cut by the dash from the left)

Here's an elegant solution:

function truncateMiddle(word) {
    const tooLongChars = 15; // arbitrary

    if (word.length < tooLongChars) {
        return word;
    }

    const ellipsis = '...';
    const charsOnEitherSide = Math.floor(tooLongChars / 2) - ellipsis.length;

    return word.slice(0, charsOnEitherSide) + ellipsis + word.slice(-charsOnEitherSide);
}

Here's my take on it – a CSS-only solution that sadly only works in Firefox right now:

div {
  width: 20em;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-overflow: " … is also your text.";
}
<div>Here's the long string of letters that accidentally is also your text.</div>

You can specify the second text-overflow declaration directly on the element if you need your text to be dynamic. Browsers that do not support this syntax (Chrome, notably :-) will fall back to plain old ellipsis.

Related