Самый надежный способ получить x пикселей значения текста из строки, javascript

У меня есть строка, которая содержит много текста, текста в моем файле JavaScript. У меня также есть элемент, контейнер div #, который стилизован (с использованием отдельного CSS) с потенциально нестандартными line-height, font-size, font-face и, возможно, другими. Он имеет фиксированную высоту и ширину.

Я хотел бы получить максимальный объем текста, который может вписываться в контейнер div # без переполнения из строки. Какой лучший способ сделать это?

Это должно иметь возможность работать с текстом, отформатированным с помощью тегов, например:

<strong>Hello person that is this is long and may take more than a</strong> 
line and so on.

В настоящее время у меня есть плагин JQuery, который работает для простого текста, а следующий код:

// returns the part of the string that cannot fit into the object
$.fn.func = function(str) {
    var height = this.height();

    this.height("auto");
    while(true) {
        if(str == "") {
            this.height(height);
            return str; // the string is empty, we're done
        }

        var r = sfw(str); // r = [word, rest of String] (sfw is a split first word function defined elsewhere
        var w = r[0], s = r[1];

        var old_html = this.html();
        this.html(old_html + " " + w);

        if(this.height() > height)
        {
            this.html(old_html);
            this.height(height);
            return str; // overflow, return to last working version
        }

        str = s;

    }
}

UPDATE:

Данные выглядят следующим образом:

<ol>
  <li>
     <h2>Title</h2>
     <ol>
        <li>Character</li>
        <ol>
          <li>Line one that might go on a long time, SHOULD NOT BE BROKEN</li>
          <li>Line two can be separated from line one, but not from itself</li>
        </ol>
      </ol>
     <ol>
        <li>This can be split from other</li>
        <ol>
          <li>Line one that might go on a long time, SHOULD NOT BE BROKEN</li>
          <li>Line two can be separated from line one, but not from itself</li>
        </ol>
      </ol>
   </li>  <li>
     <h2>Title</h2>
     <ol>
        <li>Character</li>
        <ol>
          <li>Line one that might go on a long time, SHOULD NOT BE BROKEN</li>
          <li>Line two can be separated from line one, but not from itself</li>
        </ol>
      </ol>
     <ol>
        <li>This can be split from other</li>
        <ol>
          <li>Line one that might go on a long time, SHOULD NOT BE BROKEN</li>
          <li>Line two can be separated from line one, but not from itself</li>
        </ol>
      </ol>
   </li>
</ol>

Ответ 1

ну, позвольте мне попытаться это решить;) на самом деле думая о решении, я заметил, что я недостаточно знаю о ваших требованиях, поэтому решил разработать простой код JavaScript и показать результат; после попытки вы можете сказать мне, что неправильно, чтобы я мог исправить/изменить его, разрешить?

Я использовал чистый JavaScript, не jQuery (он может быть переписан, если необходимо). Этот принцип похож на ваш плагин jQuery:

  • мы берем символы один за другим (вместо слов как функция sfw, это можно изменить)
  • Если это часть открывающего тега, браузер не отображает его, поэтому я не обработал его особым образом, просто добавил один на один символ из имени тега и проверил высоту контейнера... не знаю, Плохо. Я имею в виду, когда я пишу container.innerHTML = "My String has a link <a href='#'"; в браузере, я вижу "My String has a link", поэтому тег "незавершенный" не влияет на размер контейнера (по крайней мере во всех браузерах, где я тестировал).
  • проверьте размер контейнера, и если он больше, чем мы ожидаем, тогда предыдущая строка (фактически текущая строка без последнего символа) - это то, что мы ищем
  • теперь нам нужно закрыть все открывающиеся теги, которые не закрыты из-за вырезания

HTML-страницу для проверки:

<html>

  <head>
    <style>
    div {
      font-family: Arial;
      font-size: 20px;
      width: 200px;
      height: 25px;
      overflow: hidden;
    }
    </style>
  </head>

  <body>
     <div id="container"> <strong><i>Strong text with <a href="#">link</a> </i> and </strong> simple text </div>

     <script>
     /**
      * this function crops text inside div element, leaving DOMstructure valid (as much as possible ;).
      * also it makes visible part as "big" as possible, meaning that last visible word will be split 
      * to show its first letters if possible
      *
      * @param container {HTMLDivElement} - container which can also have html elements inside
      * @return {String} - visible part of html inside div element given
      */
     function cropInnerText( container ) {
       var fullText = container.innerHTML; // initial html text inside container 
       var realHeight = container.clientHeight; // remember initial height of the container 
       container.style.height = "auto"; // change height to "auto", now div "fits" its content 

       var i = 0;
       var croppedText = "";
       while(true) {
         // if initial container content is the same that cropped one then there is nothing left to do
         if(croppedText == fullText) { 
           container.style.height = realHeight + "px";
           return croppedText;
         }

         // actually append fullText characters one by one...    
         var nextChar = fullText.charAt( i );
         container.innerHTML = croppedText + nextChar;  

         // ... and check current height, if we still fit size needed
         // if we don't, then we found that visible part of string
         if ( container.clientHeight > realHeight ) {
           // take all opening tags in cropped text 
           var openingTags = croppedText.match( /<[^<>\/]+>/g );
           if ( openingTags != null ) {
             // take all closing tags in cropped text 
             var closingTags = croppedText.match( /<\/[^<>]+>/g ) || [];
             // for each opening tags, which are not closed, in right order...
             for ( var j = openingTags.length - closingTags.length - 1; j > -1; j-- ) {
               var openingTag; 
               if ( openingTags[j].indexOf(' ') > -1 ) {
                 // if there are attributes, then we take only tag name
                 openingTag = openingTags[j].substr(1, openingTags[j].indexOf(' ')-1 ) + '>';
               }
               else {
                 openingTag = openingTags[j].substr(1);
               }
               // ... close opening tag to have valid html
               croppedText += '</' + openingTag;
             }
           }

           // return height of container back ... 
           container.style.height = realHeight + "px";
           // ... as well as its visible content 
           container.innerHTML = croppedText;
           return croppedText;
         }

         i++;
         croppedText += nextChar;
       }

     }

     var container = document.getElementById("container");
     var str = cropInnerText( container );
     console.info( str ); // in this case it prints '<strong><i>Strong text with <a href="#">link</a></i></strong>'
   </script>

</body>

Возможные улучшения/изменения:

  • Я не создаю никаких новых элементов DOM, поэтому я просто повторно использую текущий контейнер (чтобы убедиться, что я учитываю все стили css); таким образом я постоянно меняю свой контент, но после ввода видимого текста вы можете написать fullText обратно в контейнер, если это необходимо (что также не меняю)
  • Обработка исходного текста по слову позволит нам сделать меньше изменений в DOM (мы будем писать слово за словом вместо символа по символу), так что этот путь должен быть быстрее. У вас уже есть функция sfw, поэтому вы можете легко ее изменить.
  • Если у нас есть два слова "our sentence", возможно, что видимый будет только первым ("our"), и "предложение" должно быть разрезано (overflow:hidden будет работать таким образом). В моем случае я добавлю символ по символу, поэтому мой результат может быть "our sent". Опять же, это не сложная часть алгоритма, поэтому на основе вашего кода плагина jQuery вы можете изменить мою работу со словами.

Вопросы, замечания, найденные ошибки приветствуются;) Я тестировал его в IE9, FF3.6, Chrome 9

ОБНОВЛЕНИЕ:. Устранить проблему с помощью <li>, <h1>... например. У меня есть контейнер с контентом:

<div id="container"> <strong><i>Strong text with <ul><li>link</li></ul> </i> and </strong> simple text </div>

В этом случае браузер ведет себя таким образом (строка по строке, что находится в контейнере, и то, что я вижу, показывает в соответствии с алгоритмом):

...
"<strong><i>Strong text with <" -> "<strong><i>Strong text with <"
"<strong><i>Strong text with <u" -> "<strong><i>Strong text with "
"<strong><i>Strong text with <ul" -> "<strong><i>Strong text with <ul></ul>" // well I mean it recognizes ul tag and changes size of container

и результатом алгоритма является строка "<strong><i>Strong text with <u</i></strong>" - с "<u", что не приятно. В этом случае мне нужно обработать, что если мы найдем нашу результирующую строку ("<strong><i>Strong text with <u" в соответствии с алгоритмом), нам нужно удалить последний "незамкнутый" тег ("<u" в нашем случае), поэтому перед закрытием тегов valid html Я добавил следующее:

...
if ( container.clientHeight > realHeight ) {
  /* start of changes */
  var unclosedTags = croppedText.match(/<[\w]*/g);
  var lastUnclosedTag = unclosedTags[ unclosedTags.length - 1 ];
  if ( croppedText.lastIndexOf( lastUnclosedTag ) + lastUnclosedTag.length == croppedText.length ) {
    croppedText = croppedText.substr(0, croppedText.length - lastUnclosedTag.length );
  }
  /* end of changes */
  // take all opening tags in cropped text 
...

возможно, немного ленивая реализация, но ее можно настроить, если она замедляется. Что здесь делается

  • взять все теги без > (в нашем случае он возвращает ["<strong", "<i", "<u"]);
  • взять последний ("<u")
  • если это конец строки croppedText, тогда мы удаляем его

после выполнения, строка результата становится "<strong><i>Strong text with </i></strong>"

UPDATE2 благодарю вас, например, я вижу, что у вас нет только вложенных тегов, но у них также есть "древовидная" структура, я действительно не учитывал это, но это все еще можно исправить;) Сначала я хотел написать свой соответствующий "парсер", но все время я получаю пример, когда я не работаю, поэтому я подумал, что лучше найти уже написанный синтаксический анализатор, и есть одно: Чистый JavaScript HTML Parser. Существует также один шаг:

Хотя эта библиотека не охватывает полная гамма возможных странностей, которые HTML обеспечивает, он обрабатывает много наиболее очевидный материал.

но для вашего примера это работает; эта библиотека не учитывала положение открывающего тега, но

  • мы полагаем, что оригинальная структура html прекрасна (не нарушена);
  • мы закрываем теги в конце результата "string" (так это нормально)

Я думаю, что с этими предположениями эту библиотеку приятно использовать. Тогда функция результата выглядит так:

<script src="http://ejohn.org/files/htmlparser.js"></script>
 <script>
 function cropInnerText( container ) {
   var fullText = container.innerHTML;
   var realHeight = container.clientHeight;
   container.style.height = "auto";

   var i = 0;
   var croppedText = "";
   while(true) {
     if(croppedText == fullText) { 
       container.style.height = realHeight + "px";
       return croppedText;
     }

     var nextChar = fullText.charAt( i );
     container.innerHTML = croppedText + nextChar;  

     if ( container.clientHeight > realHeight ) {
       // we still have to remove unended tag (like "<u" - with no closed bracket)
       var unclosedTags = croppedText.match(/<[\w]*/g);
       var lastUnclosedTag = unclosedTags[ unclosedTags.length - 1 ];
       if ( croppedText.lastIndexOf( lastUnclosedTag ) + lastUnclosedTag.length == croppedText.length ) {
         croppedText = croppedText.substr(0, croppedText.length - lastUnclosedTag.length );
       }

       // this part is now quite simple ;)
       croppedText = HTMLtoXML(croppedText);

       container.style.height = realHeight + "px";
       container.innerHTML = croppedText ;
       return croppedText;
     }

     i++;
     croppedText += nextChar;
   }

 }
 </script>

Ответ 2

Чтобы получить максимально возможную первую строку:

  • Создайте DIV с visibility:hidden; (так что он будет иметь размерность), но поместите его как position:absolute;, чтобы он не нарушил поток вашей страницы.
  • установите стиль своего типа с теми же значениями, что и ваш результат DIV
  • Задайте высоту так же, как и результат DIV, но сохраните width:auto;
  • Добавить текст к нему
  • Продолжайте отрезать текст до тех пор, пока ширина не станет ниже ширины DIV.

В результате вы можете ввести текст.

Отрегулируйте алгоритм, если вам нужно найти количество строк, которые помещаются в контейнер, чтобы сохранить height:auto; и установить фиксированный width.

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

Ответ 3

Чтобы решить эту проблему, вам понадобится дополнительная информация:

  • где я должен "нарезать" текст ввода
  • нарезая его, как мне восстановить две половинки, чтобы я мог набить каждый в DIV?

Что касается вопроса "где нарезать", вам, вероятно, придется вводить уникальные теги привязки <a name="uniq"/> в стратегических точках в вашей строке ввода (скажем... перед каждым открывающим тегом во вводе?). Затем вы можете проверить выложенное положение каждого якоря и найти, где сломать вход.

Найдя наиболее логичную точку для разрыва, вам нужно добавить теги в конце первой половины, чтобы закрыть ее, и добавить теги в начале следующей половины, чтобы открыть ее. Поэтому, когда вы проанализировали свою входную строку, чтобы найти ранее открытые теги, вы сохранили список "стека тегов", когда вы ввели <a/>. Посмотрите на стек тегов, который подходит для этого параллельного элемента, а затем добавьте теги по мере необходимости.

Я могу обнаружить 2 gotchas с этим:

  • вам нужно будет хранить больше информации о каждом перерыве, если теги ввода имеют атрибуты
  • вам может потребоваться обработать некоторые теги как "нерушимые" и сломать ранее <a/> вместо

В конечном счете, мне кажется, вы ожидаете конструкцию столбца HTML5.

Ответ 4

Вы просто пытаетесь отформатировать первую строку в абзаце? Будет ли работать с псевдоселектором CSS : first-line для вашего приложения?

Ответ 5

Вы можете использовать getComputedStyle, чтобы узнать ширину встроенного элемента (то есть он имеет свойство display: inline):

window.getComputedStyle(element, null).getPropertyValue("width");

Если окажется, что ширина этого элемента больше его ширины контейнера, вы будете удалять текст пошагово, пока он не войдет.