Hemlock
Hemlock

Reputation: 6208

How to have a text-overflow ellipsis at the beginning of a left-to-right element

I have a list of paths (for lack of a better word, maybe bread crumb trails describes them better). Some of the values are too long to display in their parent so I'm using text-overflow: ellipsis. The problem is that the important information is on the right, so I'd like the ellipsis to appear on the left. Something like this this ascii art:

----------------------------
|first > second > third    |
|...second > third > fourth|
|...fifth > sixth > seventh|
----------------------------

Notice that the first row is short enough so it remains left aligned, but the other two are too long so the ellipsis appears on the left hand side.

I'd prefer a CSS only solution, but JS is fine if it can't be avoided. It's ok if the solution only works in Firefox and Chrome.

EDIT: At this point I'm looking for a work around for the bugs in Chrome that prevent it from rendering properly when a document is mixed RTL and LTR. That was all I really needed from the outset, I just didn't realize it.

Upvotes: 87

Views: 50802

Answers (10)

mbaer3000
mbaer3000

Reputation: 493

Using a slightly more complex markup (using the bdi-tag and an extra span for the ellipsis), we can solve the problem fully in CSS, no JS required at all -- cross browser (IE, FF, Chrome) and including keeping punctuation marks to the right:

/* Notes about text-overflow-ellipsis-left() and its child elements as defined here: 1. It needs a fixed height, otherwise the ellipsis hack doesn't work. 2. Also, it needs a background color for the ellipsis:after property other than inherit. 3. Finally, in order to get end-sentence punctuation marks to stay at the right, a <bdi> tag needs to be nested inside the inner text element. */
 .item-content-text-overflow-ellipsis-left {
     background-color: inherit;
     overflow: hidden;
     direction: rtl;
     height: 38px;
     text-align: left;
     line-height: 38px;
}
 .item-content-text-overflow-ellipsis-left__text {
     white-space: nowrap;
     text-align: left;
     min-width: 95% 
    /* fallback */
    ;
     min-width: calc(100% - 1px);
     display: inline-block;
     vertical-align: top;
     height: 39px;
}
 .item-content-text-overflow-ellipsis-left__ellipsis {
     width: 1px 
    /* needed for Firefox, lest it won't line-break */
    ;
     height: 39px;
     display: inline-block;
     position: relative;
}
 .item-content-text-overflow-ellipsis-left__ellipsis:after {
     content: "…";
     width: auto;
     background-color: #fff;
     margin-right: 6px;
     box-shadow: 2px 0 4px #fff;
     position: absolute;
     padding-right: 2px;
     bottom: 39px;
     left: 0;
     height: 39px;
     min-width: 1px;
}
 
Decrease width to see the ellipsis to the left ...
<br><br>
<div
    class="item-content-text-overflow-ellipsis-left">
    <span class="item-content-text-overflow-ellipsis-left__text"><!--
    --><bdi>
        Some text to be potentially truncated!
      </bdi><!--
    --></span><!--
    --><span class="item-content-text-overflow-ellipsis-left__ellipsis"></span>
</div>

Granted, this is something of a hack, involving pseudo-element goodness. However, our team has been using this code in production and we haven't had any issues whatsoever.

The only caveats are: The height of the line needs to be fixed and the background color needs to be known explicitly (inherit won't work).

Upvotes: 0

Hemlock
Hemlock

Reputation: 6208

I finally had to crack in JavaScript:

<html>
    <head>
        <style>
            #container {
                width: 200px;
                border: 1px solid blue;
            }

            #container div {
                width: 100%;
                overflow: hidden;
                white-space: nowrap;
            }
        </style>
        <script>
            function trimRows() {

                var rows = document.getElementById('container').childNodes;
                for (var i=0, row; row = rows[i]; i++) {
                    if (row.scrollWidth > row.offsetWidth) {
                        var textNode = row.firstChild;
                        var value = '...' + textNode.nodeValue;
                        do {
                            value = '...' + value.substr(4);
                            textNode.nodeValue = value;

                        } while (row.scrollWidth > row.offsetWidth);
                    }
                }
            }
        </script>
    </head>
    <body onload='trimRows();'>
    <div id="container" >
        <div>first > second > third</div>
        <div>second > third > fourth > fifth > sixth</div>
        <div>fifth > sixth > seventh > eighth > ninth</div>​
    </div>
    </body>

</html>

JSFiddle

Upvotes: 14

DJDaveMark
DJDaveMark

Reputation: 2855

Double Flip Solution

Cross browser solution using only CSS. It needs to have the text reversed (by the server or using JavaScript), then via CSS flips each character to it's mirror image, then flips the whole text to it's mirror image. A visualisation of the steps would be:

  • eman.elif/gnol/yrev/a/ot/htap/ (backwards text)
  • eman.elif/gnol/yrev/... (add ellipsis)
  • .../very/long/file.name (whole text flipped round)

p {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transform: rotateY(180deg);
}
span {
  display: inline-block;
  transform: rotateY(180deg);
}
<p><span>e</span><span>m</span><span>a</span><span>n</span><span>.</span><span>e</span><span>l</span><span>i</span><span>f</span><span>/</span><span>g</span><span>n</span><span>o</span><span>l</span><span>/</span><span>y</span><span>r</span><span>e</span><span>v</span><span>/</span><span>a</span><span>/</span><span>o</span><span>t</span><span>/</span><span>h</span><span>t</span><span>a</span><span>p</span><span>/</span></p>

The following solutions use rtl while solving the problem with misinterpreted preceding or trailing weak or neutral BiDi characters such as /, \, ~, ., etc. (basically any punctuation or special characters).

CSS Solution

Use a combination of:

p {
  direction: rtl;
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap; /* or pre (e.g. preserve multiple spaces) */
}
span {
  direction: ltr;
  unicode-bidi: bidi-override; /* or isolate, isolate-override, embed */
}
<p><span>/path/to/a/very/long/file.name</span></p>

<bdo> Solution

Another possibility uses the <bdo> Bidirectional Text Override element:

p {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap; /* or pre (e.g. preserve multiple spaces) */
}
<bdo dir="rtl">
  <p>
    <bdo dir="ltr">/path/to/a/very/long/file.name</bdo>
  </p>
</bdo>

Upvotes: 7

yunzen
yunzen

Reputation: 33439

It's a little buggy, but maybe a point in the right direction

http://jsfiddle.net/HerrSerker/ZfbaD/50/

$('.container')
    .animate({'width': 450}, 4000)
    .animate({'width': 100}, 4000)
    .animate({'width': 170}, 4000)
.container {  
  white-space: nowrap;                   
  overflow: hidden;              /* "overflow" value must be different from "visible" */   
  text-overflow: ellipsis;  
    width:170px;
    border:1px solid #999;
    direction:rtl;
}  
.container .part {
  direction:ltr;

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
    <span class="part">second</span> 
    <span class="part">&gt;</span> 
    <span class="part">third</span> 
    <span class="part">&gt;</span> 
    <span class="part">fourth</span> 
    <span class="part">&gt;</span> 
    <span class="part">fifth</span> 
    <span class="part">&gt;</span> 
    <span class="part">sixth</span>
</div>

Upvotes: 7

j08691
j08691

Reputation: 207983

How about something like this jsFiddle? It uses the direction, text-align, and text-overflow to get the ellipsis on the left. According to MDN, there may be the possibility of specifying the ellipsis on the left in the future with the left-overflow-type value however it's considered to still be experimental.

p {
  white-space: nowrap;
  overflow: hidden;
  /* "overflow" value must be different from "visible" */
  text-overflow: ellipsis;
  width: 170px;
  border: 1px solid #999;
  direction: rtl;
  text-align: left;
}
<p>first > second > third<br /> second > third > fourth > fifth > sixth<br /> fifth > sixth > seventh > eighth > ninth</p>​

Upvotes: 118

ElChiniNet
ElChiniNet

Reputation: 2902

If you don't care the indexing of those texts, you could use this method (it reverses the text lines):

If you have in your texts other HTML elements besides <br> you need to make some arrangements to use this method.

HTML code:

<p>first > second > third<br/>
second > third > fourth <br>
fifth > sixth > seventh</p>

CSS code:

p{
  overflow: hidden;
  text-overflow: ellipsis;
  unicode-bidi: bidi-override;
  direction: rtl;
  text-align: left;
  white-space: nowrap;
  width: 140px;
}

JavaScript code

[].forEach.call(document.getElementsByTagName("p"), function(item) {

  var str = item.innerText;

  //Change the operators
  str = str.replace(/[<>]/g, function(char){ return ({"<" : ">", ">" : "<"})[char] });

  //Get lines
  var lines = str.split(/\n/);

  //Reverse the lines
  lines = lines.map(function(l){ return l.split("").reverse().join("") }); 

  //Join the lines
  str = lines.join("<br>");

  item.innerHTML = str;

});

jsfiddle

Upvotes: 1

Graham Dixon
Graham Dixon

Reputation: 11

Using @Hemlocks, @Brian Mortenson and @Jimbo's solutions, I've built a jQuery plugin to solve this problem.

I've also added support to return the initial value using .html() rather than having it return the current innerHTML. Hopefully it will be useful to someone...

(function($) {

$.trimLeft = function(element, options) {

    var trim = this;

    var $element = $(element), // reference to the jQuery version of DOM element
         element = element;    // reference to the actual DOM element

    var initialText = element.innerHTML;

    trim.init = function() {
        overrideNodeMethod("html", function(){ return initialText; });
        trimContents(element, element);
        return trim;
    };

    trim.reset = function(){
        element.innerHTML = initialText;
        return trim;
    };

    //Overide .html() to return initialText.
    var overrideNodeMethod = function(methodName, action) {
        var originalVal = $.fn[methodName];
        var thisNode = $element;
        $.fn[methodName] = function() {
            if (this[0]==thisNode[0]) {
                return action.apply(this, arguments);
            } else {
                return originalVal.apply(this, arguments);
            }
        };
    };

    var trimContents = function(row, node){
        while (row.scrollWidth > row.offsetWidth) {
            var childNode = node.firstChild;
            if (!childNode)
                return true;            
            if (childNode.nodeType == document.TEXT_NODE){
                trimText(row, node, childNode);
            }
            else {
                var empty = trimContents(row, childNode);
                if (empty){
                    node.removeChild(childNode);
                }
            }
        };
    };

    var trimText = function(row, node, textNode){
        var value = '\u2026' + textNode.nodeValue;
        do {
            value = '\u2026' + value.substr(4);
            textNode.nodeValue = value;
            if (value == '\u2026'){
                node.removeChild(textNode);
                return;
            }
        }
        while (row.scrollWidth > row.offsetWidth);
    };

    trim.init();

};

$.fn.trimLeft = (function(options){
  var othat = this;

  var single = function(that){
      if (undefined == $(that).data('trim')) {
          var trim = new $.trimLeft(that, options);
          $(that).data('trim', trim);
          $(window).resize(function(){
              $(that).each(function(){
                    trim.reset().init();
              });
          });
       }   
   };

   var multiple = function(){
        $(othat).each(function() {
            single(this);
        });
    };

    if($(othat).length>1)
        multiple(othat);            
    else
        single(othat);

    //-----------        
    return this;
});


})(jQuery);

Initiate using:

//Call on elements with overflow: hidden and white-space: nowrap 
$('#container>div').trimLeft();
//Returns the original innerHTML
console.log($('#test').html());

fiddle

Upvotes: 1

YGT
YGT

Reputation: 121

Why not just using direction:rtl;

Upvotes: 8

MasterNone
MasterNone

Reputation: 11

I put some JavaScript together to regex out three items and add the ellipsis in where necessary. This does not explicitly look at how much text will fit in the box but if the box is fixed this may not be an issue.

<style>
p {  
    white-space: nowrap;                     
    overflow: hidden;
    text-overflow: ellipsis; 
    width:170px;
    border:1px solid #999;
    direction:rtl;
    text-align:left;
} 
</style>

<p>first &gt; second &gt; third<br />
second &gt; third &gt; fourth &gt; fifth &gt; sixth<br />
fifth &lt; sixth &lt; seventh &lt; eighth &lt; ninth</p>

<script>
    var text = $( 'p' ).text(),
        split = text.split( '\n' ),
        finalStr = '';
    for( i in split ){
        finalStr = finalStr.length > 0 ? finalStr + '<br />' : finalStr;
        var match = /(\w+\s?(<|>)?\s?){3}$/.exec( split[i] );
        finalStr = finalStr + ( split[i].length > match[0].length ? '...' : '' ) + match[0];
    }
    $( 'p' ).empty().html( finalStr );
</script>

Upvotes: 0

ScottS
ScottS

Reputation: 72271

Based on your edit:

At this point I'm looking for a work around for the bugs in Chrome that prevent it from rendering properly when a document is mixed RTL and LTR. That was all I really needed from the outset, I just didn't realize it.

Have you looked into the unicode-bidi css property (see Sitepoint or W3C)? I actually just learned about this myself on another recent post. My guess is you would want to use the embed value for those pieces going the opposite direction to the main site. So in j08691's answer where it is direction: rtl add unicode-bidi: embed to the CSS. This should solve "mixed RTL and LTR" issues you are having.

Upvotes: 0

Related Questions