Верстка: отображаем пользовательский контент

    Под пользовательским контентом в этой статье мы понимаем любой текст на странице, автором которого является пользователь. Чаще всего это комментарии, пользовательские записи «на стене» в микроблоге, информация «о себе» в профиле и все в таком роде. Задача не то чтобы очень сложная, но требует внимательности и хорошего понимания основ верстки. В этой статье сделана попытка собрать воедино то наиболее важное, что нужно знать о верстке, включающей пользовательский текст.



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

    • ввести целое предложение там, где ожидалась пара слов,
    • использовать очень длинные слова (чаще всего это url, но может быть и что-нибудь вроде сотни восклицательных знаков),
    • использовать старые браузеры,
    • использовать язык с другим направлением текста,
    • вставлять url и ожидать, что они станут ссылками,
    • и при всем при этом ожидать сохранения форматирования своих сообщений.

    Все это мы должны учитывать и корректно отображать на странице. Для нас это оказалось особенно актуально в процессе работы над конструктором разметок для Легкого клиента 8, включающим в себя набор универсальных веб-контролов. Из этих контролов составляется представление карточки, соответственно, они практически целиком наполняются пользовательским контентом и должны правильно отображаться в любом контексте, в любом сочетании друг с другом.

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

    1. Обрезанием лишнего текста с помощью многоточия,
    2. Переносом на новую строку (здесь часто бывает желательно сохранение оригинального форматирования текста — пробелов и переносов строк).

    Оба случая имеют свои нюансы, однако основной проблемой и там, и там является правильное ограничение ширины блока, содержащего пользовательский текст. Наш план следующий: сначала разберемся с тем, как заставить работать многоточие и как правильно отображать многострочный контент, а потом посмотрим, из-за чего возникают проблемы с ограничением ширины блока и как их эффективно решать.

    Однострочный текст



    Заставить работать многоточие просто, как раз-два-три:

    1. Ограничить ширину блока (см. далее в этой статье);
    2. Задать для этого блока (именно для него) правила:
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    // direction: rtl; ?
    

    3. Элемент, содержащий текст внутри блока (если текст во вложенных элементах), а также все его родители, вплоть до блока, должны иметь display: inline. Помимо текста в блоке могут быть и другие не inline-элементы, важно только, чтобы сам текст и его родители были inline.

    Если многоточие не работает, то алгоритм отладки прост:

    1. В DevTools смотрим ширину текста, и ширину всех его родителей по очереди;
    2. Находим элемент, который должен ограничивать ширину, и, если он «расползся», то заставляем его вернуться в рамки;
    3. Проверяем, что в нем заданы все CSS-свойства, приведенные выше;
    4. Проверяем, что все элементы от блока до текста имеют display: inline.

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


    Для этого нужно программно определить направление и задать соответствующее значение свойства direction в css, либо значение HTML-атрибута dir. В библиотеке twitter RTLtextarea, например, направление текста определяется примерно так:

    // Полностью см. https://github.com/twitter/RTLtextarea/blob/master/src/RTLText.module.js
    function getDirection(plainText) {
        var rtlChar = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg;
        return plainText.match(rtlChar) ? "rtl" : "ltr";
    }

    Логику задания направления текста удобно вынести в директиву в случае использования AngularJS или в stateless компонент в случае с ReactJS.

    Ну и, конечно, если вы решили обрезать текст при помощи многоточия, то важно не забыть добавить тултип, с полным содержанием элемента:


    Либо дать пользователю возможность каким-то еще способом увидеть весь текст.

    Многострочный текст



    Для варианта с переносом строк основную проблему составляют длинные слова и сохранение форматирования. Готовя материал к данной статье, я решил посмотреть, как она решается на передовых сайтах, ориентированных на пользовательский контент. Сюда вошли facebook, twitter, vk, linkedin, livejournal, одноклассники, g+, youtube, яндекс маркет, instagram, системы комментирования hyper comments, disqus, intense debate, cackle, livefyre, ну и конечно же, habr.
    Ресурс Длинные слова Переносы строк Пробелы
    habr (комментарии) overflow: hidden препроцессинг теряются
    facebook (посты и комментарии) word-wrap: break-word препроцессинг теряются
    twitter (твиты) word-wrap: break-word white-space: pre-wrap white-space: pre-wrap
    vk (посты и комментарии) word-wrap: break-word препроцессинг теряются
    linkedin (посты и комментарии) word-wrap: break-word white-space: pre-line теряются
    livejournal (посты) препроцессинг (<wbr>) html html
    livejournal (комментарии) препроцессинг (<wbr>) препроцессинг теряются
    одноклассники (заметки) word-wrap: break-word white-space: pre-wrap white-space: pre-wrap
    одноклассники (комментарии) word-wrap: break-word препроцессинг + white-space: pre-wrap (?) white-space: pre-wrap
    g+ (посты и комментарии) word-wrap: break-word препроцессинг препроцессинг
    youtube word-wrap: break-word white-space: pre-wrap white-space: pre-wrap
    яндекс-маркет (обсуждения) bug (растягивается за пределы) препроцессинг теряются
    яндекс-маркет (отзывы) word-wrap: break-word препроцессинг теряются
    instagram (комментарии) word-wrap: break-word ограничение ввода теряются
    hyper comments word-wrap: break-word препроцессинг теряются
    disqus overflow: hidden препроцессинг теряются
    intense debate overflow: hidden препроцессинг теряются
    cackle word-wrap: break-word white-space: pre-wrap white-space: pre-wrap
    livefyre word-wrap: break-word препроцессинг препроцессинг

    Когда я закончил составлять эту таблицу, у меня было только одно впечатление:


    Как видно, почти везде используется препроцессинг (программная замены переносов строк на тэги <br>, пробелов на &nbsp; и разбитие длинных слов при помощи <wbr>). При этом часто пробелы в пользовательских сообщениях попросту теряются. Это довольно печально, учитывая тот факт, что есть простое и эффективное решение проблемы, состоящее в: a) ограничении ширины блока и б) добавлении всего двух правил:

    word-wrap: break-word;
    white-space: pre-wrap;
    

    Либо, если вы предпочитаете не разрывать длинные слова на несколько строк, а обрезать многоточием:

    overflow: hidden;
    text-overflow: ellipsis;
    white-space: pre-wrap;
    

    Либо, если вы все-таки специально хотите убрать форматирование текста:

    word-wrap: break-word;
    white-space: normal;
    

    Это работает во всех браузерах вплоть до IE8 и красиво решает проблему длинных слов и сохранения форматирования. Тем не менее, почему-то только twitter, одноклассники, youtube и cackle прибегли к этому способу. Маяковский просто переворачивается в гробу, когда, например, пытаешься запостить в vk что-то вроде:

    Для веселия
               планета наша
                           мало оборудована.
    Надо
        вырвать
               радость
                      у грядущих дней.
    В этой жизни
                помереть
                        не трудно.
    Сделать жизнь
                 значительно трудней.
    

    Маяковский — Сергею Есенину (отрывок)

    Искренне надеюсь, что команда vk увидит эту статью и заменит препроцессинг на вот такой кусочек кода в своих стилях (word-wrap: break-word они уже используют):

    .wall_post_text {
        white-space: pre-wrap;
    }
    

    Ну и, конечно, следует не забывать обрабатывать url в пользовательских сообщениях, заменяя их на ссылки. Проблема нетривиальная, но для нее есть много хороших готовых решений.

    Ограничение ширины элемента


    Особенные трудности с тем, чтобы удержать элемент в положенных границах, возникают в случае использования многострочного текста, содержащего длинные слова. Очень часто бывает так, что верстка ведет себя хорошо на нормальном содержимом, но стоит добавить длинное слово — и все расползается как попало:


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

    Борьба с расползанием элементов специфична для каждого вида элемента (block, inline-block и т.д.). Поэтому при отладке таких случаев необходимо пробежаться вверх по родителям проблемного элемента и найти «слабое звено». Рассмотрим каждый из видов элементов отдельно.

    1) display: block;

    Это самый дружественный для верстальщика вариант, т.к. он всегда твердо держится в рамках родителя. Если родитель ограничен, то для такого элемента даже не нужно ничего дополнительно задавать. Если задать width или max-width, то он будет вести себя так, как мы ожидаем.

    2) display: inline-block;

    Такие элементы менее сильны духом и легко могут предаться безобразным вольностям касательно своей ширины. Однако стоит задать для них width или max-width, как все становится на свои места. В общем случае, добавить

    max-width: 100%;
    

    никогда не помешает. Это довольно безопасно и обычно ничего не ломает. Но при этом дает вам гарантию, что inline-block всегда будет в пределах своего родителя.

    3) float

    Когда элемент подвешивается на float, то тут даже block теряет всю свою уверенность и уподобляется слабовольному inline-block. Задание width или max-width возвращает его правоверность, и также max-width: 100% здесь будет удачной идеей. Это вообще настолько хорошее правило, что значение 100% я бы сделал значением по умолчанию для свойства max-width. В конце концов, додумались же сделать по умолчанию min-width: auto в спецификации flexbox, что куда более опасно. Но об этом позже.

    Изменение поведения block при float — все, что нужно знать о float. Остальные элементы и без того требуют ограничения своей ширины, и для них ничего не меняется.

    4) display: table;

    Это, пожалуй, самый из «разболтанных» элементов. Таблица будет упорно растягиваться по содержимому, игнорируя все width и max-width, и ничто не поколеблет ее либеральных взглядов. До тех пор, пока не будет добавлено следующее правило:

    display: table;
    table-layout: fixed;
    

    Оно сразу вразумляет таблицу, и простой width или max-width решают все проблемы с шириной.

    5) display: flex;

    Flexbox неимоверно крут, и позволяет делать простым способом то, что иначе очень трудно, либо вообще невозможно. Однако использовать его для отображения пользовательского контента пока не самая лучшая идея. По крайней мере, если вы намерены корректно обрабатывать длинные слова и поддерживать IE10.

    Если не брать во внимание IE10, то все замечательно. Нужно только знать пару особенностей и правильно настроить flexbox. Контейнер flexbox (там, где задано display: flex) нас в данном случае не интересует, т.к. с ним проблем не возникает. Проблемы возникают с элементами контейнера, и тут нужно соблюсти следующие правила:

    1. Во-первых, shrink-factor (второй параметр свойства flex) должен быть больше нуля, чтобы браузер взялся за сжатие нашего элемента.

    2. Во-вторых, необходимо сбросить в 0 пресловутое свойство min-width. Его значение auto было введено специально для flexbox, и к тому же сделано значением по умолчанию. Лично для меня это всегда было источником непредсказуемого контр интуитивного поведения flexbox, пока я об этом не знал. В частности, длинные слова будут растягивать flex-элементы, вместо того чтобы переносится по break-word (аналогично с white-space: nowrap). И, что особенно печально, об этом нигде не пишут. Ни в одном «полном руководстве» по flexbox ни слова не сказано про эту ловушку. Я в свое время узнал об этом случайно, попав через поиск на багтрекер Firefox, где разработчики, объясняя поведение браузера, сослались на спецификацию (Firefox реализовал эту часть спецификации раньше Chrome, поэтому некоторые считали это багом Firefox, т.к. из-за «min-width: auto» flexbox ведет себя не так, как ожидаешь).

    3. Вроде бы, первых двух правил должно было быть достаточно, но на практике, чтобы сжать длинные слова, браузерам требуется задать также flex-basis (третий параметр свойства flex), отличный от auto. Соответственно при этом важно, чтобы grow-factor (первый параметр свойства flex) был больше нуля, т.к. иначе наш элемент сожмется до значения flex-basis (например, до нуля, если flex-basis: 0).

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

    flex: 1 1 0%;
    min-width: 0;
    

    Обратите внимание, что третье значение в свойстве flex задано с единицами измерения. Это баг в IE11, и без единиц измерения в нем ничего не работает. Кроме того, по некоторым причинам лучше использовать 0% вместо 0px (спасибо monochromer за ссылку). Вместо нуля можно, конечно, задать и что-то другое (кроме auto), а вместо единиц для grow- и shrink-факторов что угодно, кроме 0.

    Однако, в IE10, к сожалению, это не работает — длинные слова беспощадно растягивают элементы, несмотря ни на что. Можно, конечно, задать width или max-width, но в случае с flexbox, как правило, этого сделать нельзя, т.к. ширина элемента неизвестна заранее (либо это уже будет не flexbox). Например, простой макет: иконка и текст справа. Иконка занимает определенную ширину, а текст — все оставшееся пространство контейнера. Если у вас получится заставить этот пример отображаться в IE10 средствами flexbox так же, как в IE11, напишите, пожалуйста, в комментариях. Проверить можно здесь.

    На этом все, если что-то упустил, пишите в комментариях. Приятной вам верстки!

    P.S. отдельная благодарность Вадиму Макееву, руководителю и редактору портала Веб-стандарты за техническое рецензирование статьи.
    ДоксВижн 34,24
    Компания
    Поделиться публикацией
    Комментарии 18
    • +2
      Рискую нарваться на минусы за банальную благодарность, но искреннее вам спасибо за таблицу. Много раз цеплялся глазами за кривые обрезки статей, но все время ленился разобраться с причинами и правильными методами.
      • 0
        Ох, есть ещё один тип головной боли — вертикальное видео называется. Определение и корректное отображение во всех браузерах — костыль на костыле
        • 0
          Автору спасибо за статью.
          • 0
            Коммент к картинке:
            В последнее время все чаще звучит: «А в Firefox проверял? А в Safari?»
            То ли IE лучше стал, то ли к нему просто привыкли, но факт в том, что все реже именно IE является проблемой.
            • 0
              Ох, только сегодня два часа потерял на фикс багов, воспроизводящихся исключительно в Safary. Причем, один из них воспроизводился только на ipad (на iphone все ок), и только в portrait ориентации. Люблю Safary.
              • +2
                Возьмите, Safari!
                • +2
                  Да ну вас! Один раз — опечатка, два раза — повод для шутки.
            • 0
              Лично для меня это всегда было источником непредсказуемого контр интуитивного поведения flexbox, пока я об этом не знал. И, что особенно печально, об этом нигде не пишут. Ни в одном «полном руководстве» по flexbox ни слова не сказано про эту ловушку.

              Ну вот. А сами туда же. «Есть проблема», о её сути – ни слова. Так в чём же проблема с min-width: auto у flex-элементов?

              • +2
                В контексте этой статьи в том, что длинные слова будут растягивать flex-элементы, вместо того чтобы переносится по break-word.

                По спецификации auto «is the smaller of its content size and its specified size.» Минимальная ширина для текста — ширина самого длинного слова, что приводит к таким плачевным последствиям. Однако, если поместить внутрь flex-элемента картинку, или какой-нибудь широкий блок, то получим тоже самое.

                Ну и вообще, даже если не брать крайние случаи, просто нарушается логика, заданная во flex свойствах. Например вот здесь элементы имеют одинаковые basis, shirk- и grow-факторы, и по идее, должны быть одинаковой ширины. Но из-за min-width: auto левый занимает больше пространства. Это я называют «контр-интуитивно».
                • +1

                  Спасибо. Мне кажется, это пояснение стоит добавить в статью. Потому что сейчас из неё непонятно, зачем нужна эта «магия». Вы сами сказали, что напоролись на «баг» случайно, вероятно потому, что в реальной жизни он себя проявляет редко, но метко. Так что стоило бы рассказать, откуда у него ноги растут.

                  • +2
                    Согласен, только трудно поместить все, не нарушив логику повествования. Добавил немного, что относится непосредственно к теме статьи.
                • +1
                  Да, еще как подсказал monochromer, white-space: nowrap делает минмальную ширину текстового содержимого равной ширине текста в одну строчку.
                • 0
                  Во-вторых, необходимо сбросить в 0 пресловутое свойство min-width. Его значение auto было введено специально для flexbox, и к тому же сделано значением по умолчанию. И, что особенно печально, об этом нигде не пишут.

                  На CSS-tricks выходила статья по этому поводу.

                  Обратите внимание, что третье значение в свойстве flex задано с единицами измерения. Это баг в IE11, и без единиц измерения в нем ничего не работает.

                  Филлип Уолтон советовал писать flex-basis в процентах для сокращенной записи: 1 0 0%, так как некоторые css-минимизаторы вырезают, по его словам, px, а % оставляют.
                  • 0
                    Спасибо за ссылки! Я когда задался специально целью найти хоть какое-то упоминание min-width: auto потерпел неудачу: ) Здорово, что хоть кто-то об этом написал.
                    • 0
                      Добавил в статью про проценты.
                    • +1
                      По-моему, эта статья недооценена.
                      • 0
                        Так называемый «shrink-factor (второй параметр свойства flex)» это свойство flex-shrink
                        • 0
                          Рекомендуется использовать свойство flex, вместо задания компонентов по отдельности, т.к. чаще всего эти три свойства (flex-shrink, flex-grow, flex-basis) нужно задавать вместе.

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

                        Самое читаемое