Кросс-браузерный многострочный переполнение текста с добавлением многоточия в пределах фиксированной ширины и высоты `<div>` - PullRequest
165 голосов
/ 04 августа 2010

Я сделал изображение для этого вопроса, чтобы его было легче понять.

Можно ли создать многоточие на <div> с фиксированной шириной и несколькими строками?

text-overflow

Я попробовал несколько плагинов jQuery тут и там, но не могу найти тот, который ищу.Любая рекомендация?Идеи?

Ответы [ 23 ]

1 голос
/ 31 мая 2017

EDIT: Наткнулся на Shave , который является плагином JS, который действительно хорошо выполняет усечение многострочного текста на основе заданной максимальной высоты. Он использует бинарный поиск, чтобы найти оптимальную точку останова. Определенно стоит исследовать.


ОРИГИНАЛЬНЫЙ ОТВЕТ:

Мне пришлось придумать ванильное решение JS для этой проблемы. В случае, когда я работал, мне приходилось помещать длинное название продукта в ограниченную ширину и в две строки; при необходимости усечено многоточием.

Я использовал ответы из разных постов SO, чтобы приготовить что-то, что соответствует моим потребностям. Стратегия следующая:

  1. Рассчитать среднюю ширину символа варианта шрифта для желаемого размера шрифта.
  2. Рассчитайте ширину контейнера
  3. Рассчитать количество символов, которые помещаются на одной строке в контейнере
  4. Рассчитать количество символов, до которых нужно обрезать строку, на основе количества символов, помещающихся в строку, и количества строк, которые текст должен обернуть.
  5. Усечение входного текста на основе предыдущего вычисления (с учетом дополнительных символов, добавленных многоточием) и добавление "..." в конец

Пример кода:

/**
 * 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. Если усечение должно быть только на одной строке, чистый метод CSS с использованием переполнения текста: ellipsis аккуратнее
  2. Шрифты, которые не имеют фиксированной ширины, могут привести к тому, что усечение произойдет слишком рано или слишком поздно (поскольку разные символы имеют разную ширину). Использование параметра pad помогает смягчить это в некоторых случаях, но не будет надежным:)
  3. Добавит ссылки и ссылки на оригинальные посты после того, как я верну ноутбук (нужна история)

PPS: только что понял, что это очень похоже на подход, предложенный @DanMan и @ st.never. Извлеките фрагменты кода для примера реализации.

1 голос
/ 27 апреля 2018

Может быть, довольно поздно, но используя SCSS, вы можете объявить такую ​​функцию:

@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%
      );
    }
  }
}

И используйте это как:

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

Который будет обрезать текст до одной строки и знать, что это 1.4 его высоты строки. Ожидаемый вывод: Chrome будет отображаться с ... в конце, а FF - с небольшим затуханием в конце

Firefox

enter image description here

Chrome

enter image description here

1 голос
/ 27 марта 2015

Упомянутый плагин dotdotdot jQuery хорошо работает с угловым:

(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);

Соответствующая разметка будет:

<p app-ellipsis>{{ selectedItem.Description }}</p>
1 голос
/ 14 ноября 2016

Чистая демонстрация JS (без jQuery и цикла while)

Когда я искал решение проблемы многострочного эллипса, я был удивлен, что без jQuery нет ничего хорошего. Также есть несколько решений, основанных на цикле while, но я думаю, что они не эффективны и опасны из-за возможности попасть в бесконечный цикл. Поэтому я написал этот код:

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);
0 голосов
/ 23 января 2018

Здесь я сделал еще одну библиотеку с более быстрым алгоритмом. Пожалуйста, проверьте:

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

Для установки с помощью беседки:

bower install fast-ellipsis

Для установки с помощью npm:

npm install fast-ellipsis 

Надеюсь, вам понравится!

0 голосов
/ 14 декабря 2017

В моем сценарии я не мог заставить работать ни одну из функций, упомянутых выше, и мне также нужно было указать функции, сколько строк показать, независимо от размера шрифта или размера контейнера.

Я основал свое решение на использовании метода Canvas.measureText (который является HTML5 функцией), как объяснено здесь Domi , поэтому он не является полностью кросс-браузерным.

Вы можете увидеть, как это работает на этой скрипке .

Это код:

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));
    });
})();

А HTML-код для его использования будет выглядеть так:

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

Код для получения семейства шрифтов довольно прост, и в моем случае он работает, но для более сложных сценариев вам может понадобиться что-то использовать по этим строкам .

Кроме того, в моем случае я сообщаю функции, сколько строк использовать, но вы можете рассчитать, сколько строк показать в соответствии с размером контейнера и шрифтом.

0 голосов
/ 06 октября 2010

Чтобы расширить решение @ DanMan: в случае использования шрифтов переменной ширины вы можете использовать среднюю ширину шрифта. Это имеет две проблемы: 1) текст со слишком большим количеством символов W будет переполнен и 2) текст со слишком большим количеством символов I будет обрезан раньше.

Или вы могли бы выбрать подход наихудшего случая и использовать ширину буквы "W" (которая, я считаю, самая широкая). Это устраняет проблему 1 выше, но усиливает проблему 2.

Другой подход может быть следующим: оставить overflow: clip в div и добавить раздел с многоточием (может быть, другой div или изображение) с float: right; position: relative; bottom: 0px; (не проверено). Хитрость в том, чтобы изображение появилось над концом текста.

Вы также можете отображать изображение только тогда, когда знаете, что оно будет переполнено (скажем, после примерно 100 символов)

0 голосов
/ 14 июня 2012

С этим кодом нет необходимости в дополнительном элементе-обертке, если высота элемента ограничена стилем макс. Высоты.

// 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);
});

Кроме того, он сохраняет исходный текст в атрибуте данных, который можетотображаться с использованием только стилей, например.при наведении мыши на:

.ellipsis-lastline {
    max-height: 5em;
}
.ellipsis-lastline:before {
    content: attr(data-original-content);
    position: absolute;
    display: none;
}
.ellipsis-lastline:hover:before {
    display: block;
}
0 голосов
/ 27 сентября 2010

Вы, вероятно, не можете сделать это (в настоящее время?) Без шрифта фиксированной ширины, как Courier. При использовании шрифта фиксированной ширины каждая буква занимает одно и то же горизонтальное пространство, так что вы, вероятно, можете считать буквы и умножить результат на текущий размер шрифта в ems или exs. Тогда вам просто нужно проверить, сколько букв помещается на одной строке, а затем разбить ее.

В качестве альтернативы, для нефиксированных шрифтов вы можете создать отображение для всех возможных символов (например, i = 2px, m = 5px) и затем выполнить математические вычисления. Хотя много ужасной работы.

0 голосов
/ 16 октября 2018

Я сделал версию, которая оставляет HTML нетронутым. Пример jsfiddle

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...