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

前端 未结 25 2461
栀梦
栀梦 2020-11-22 16:19

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

Is it possible to create an ellipsis on a

with a fixed width and multiple
相关标签:
25条回答
  • 2020-11-22 16:50

    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)

    0 讨论(0)
  • 2020-11-22 16:51

    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>

    0 讨论(0)
  • 2020-11-22 16:52

    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.

    0 讨论(0)
  • 2020-11-22 16:52

    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>
    
    0 讨论(0)
  • 2020-11-22 16:53

    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

    0 讨论(0)
  • 2020-11-22 16:53

    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.

    0 讨论(0)
提交回复
热议问题