Пара тапок » readmore http://paratapok.ru Блог о веб-разработке Sun, 18 Dec 2022 12:14:48 +0000 ru-RU hourly 1 https://wordpress.org/?v=4.3.34 Автоматическое обрезание длинного текста в спойлер на jQuery http://paratapok.ru/frontend/2541_avtomaticheskoe-obrezanie-dlinnogo-teksta-v-spojler-na-jquery/ http://paratapok.ru/frontend/2541_avtomaticheskoe-obrezanie-dlinnogo-teksta-v-spojler-na-jquery/#comments Sat, 16 Jan 2016 20:42:48 +0000 http://paratapok.ru/?p=2541 Читать далее →]]> На днях возникла задача реализовать своеобразный спойлер на jquery. Суть сводится к тому, чтобы если в блок выводится слишком длинный текст, например, превышающий 2000 символов, тогда текст должен обрезаться, а в конец вставляться многоточие. К тому же после блока необходимо выводить ссылку «Читать далее», которая будет раскрывать текст полностью. Следовало также не забывать и про функционал обратного сворачивания блока в формат анонса.

Задача достаточно стандартная, однако поиск подходящего jquery-плагина не увенчался успехом, поскольку во всех плагинах, которые удалось найти, ограничения задаются не количеством символов, а высотой блока. То есть указывается высота контейнера, и плагин обрезает текст, выходящий за его пределы. В интернете можно найти весьма удобные варианты таких плагинов, например, Readmore.js и dotdotdot. Причем последний даже может отслеживать изменение размера окна и автоматически обновлять результат.

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

Пример работы плагина readmore

Можно выделить несколько преимуществ данного подхода.

  1. Можно быть уверенным, что в блоке будет отображаться объем текста, несущий смысловую нагрузку даже в свернутом состоянии.
  2. Не требуются лишние обработчики на изменение размера окна, которые бы постоянно выполняли проверку того, сколько текста влезает в блок.

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

Итак, алгоритм задачи достаточно прост:

  1. Вырезаем требуемое количество символов – формируем анонс.
  2. Дополняем текст анонса многоточием и добавляем html-обвязку в виде ссылки «Читать далее».
  3. Навешиваем обработчик на ссылку, которая будет менять текст в блоке на анонс и полный в зависимости от состояния.

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

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

// формирование анонса
person.cutBrief = function() {
    var tmp,
        i = 0,                          // счетчик циклов
        j = 0,                          // счетчик циклов
        html = data.html,               // html блока
        htmlLength = html.length,       // количество символов html блока
        count = 0,                      // счетчик текстовых символов
        countFlag = true,               // текущий символ не является html-разметкой
        endCharsLen = ENDCHARS.length,  // размер массива символов, указывающих на окончание слова
        end = htmlLength,               // позиция конца анонса при поиске
        resultLimit = data.limit.total - data.limit.delta,    // требуемое количество символов
        tagName,                        // название тега
        tagStack = [];                  // стек тегов, которые необходимо закрыть в конце анонса

    if (data.count > data.limit.total) {
        // формируем анонс

        for (; i < htmlLength; i++) {
            // если открывается тег
            if (html[i] === '<') {
                countFlag = false;

                // символ не последний
                if (i < htmlLength - 1) {
                    // тег является закрывающим
                    if (html[i+1] === '/') {
                        tmp = html.indexOf('>', i+1);
                        if (tmp > 0) {
                            // верный формат закрытия тега
                            tagName = html.substr(i+2, tmp-i-2);
                            // обнаруженный тег должен иметь закрывающую часть ?
                            if ($.inArray(tagName, TAGDIC) >= 0) {
                                tagStack.pop();
                            }
                        }
                    } else {
                        // тег является открывающим
                        // следующий символ - любая латинская буква ?
                        if (/\w/gi.test(html[i+1])) {
                            // получение имени тега и опредение его на необходимость закрытия
                            tmp = html.indexOf('>', i+1);
                            if (tmp > 0) {
                                tagName = html.substr(i+1, tmp-i-1);
                                // тег должен иметь закрывающую часть
                                if ($.inArray(tagName, TAGDIC) >= 0) {
                                    tagStack.push(tagName);
                                }
                            } else {
                                // не является тегом
                                countFlag = true;
                            }
                        } else {
                            // не является тегом
                            countFlag = true;
                        }
                    }
                }
            }

            // инкрементим счетчик текстовых символов
            if (countFlag) {
                count++;
            }

            // если закрывается тег
            if (html[i] === '>') {
                countFlag = true;
            }

            // дошли до конца требуемого размера анонса
            if (count >= resultLimit) {
                // текущий символ не является концом слова
                if ($.inArray(html[i], ENDCHARS) < 0) {
                    // символ не последний
                    if (i < htmlLength - 1) {
                        // следующий символ тоже не конец слова
                        if ($.inArray(html[i+1], ENDCHARS) < 0) {
                            // ищем первое вхождение каждого символа из набора и выбираем ближайший
                            for (; j < endCharsLen; j++) {
                                tmp = html.indexOf(ENDCHARS[j], i+1);
                                if ((tmp > 0) && (tmp < end)) {
                                    end = tmp;
                                }
                            };
                            i = end;
                        }
                    }
                } else {
                    // слово закончилось целиком
                    count--;
                }
                break;
            }
        };

        // вырезаем кусок html
        data.brief = html.substr(0, i);

        // добавляем точки
        data.brief += opt.ellipsis;

        // закрываем открытые теги
        for (i = tagStack.length - 1; i >= 0; i--) {
            data.brief += '</' + tagStack[i] + '>';
        };
    } else {
        // не обрезаем
        data.brief = html;
    }
};

Применяется плагин стандартно:

$('.b-block--first').readmore();

В плагине предусмотрены следующие параметры:

  • ellipsis {string} – символы, которые будут выводиться в конце анонса;
  • textOpen {string} – текст ссылки в свернутом состоянии;
  • textClose {string} – текст ссылки в развернутом состоянии;
  • callback {function} – функция, исполняющаяся после раскрытия/закрытия блока;
  • brief {integer} – максимальное количество символов анонса, уменьшенное на величину addition;
  • addition {integer} – минимальное количество символов раскрываемой части текста;
  • smoothly {integer} – время плавного раскрытия/закрытия блока в миллисекундах.

Следует заметить, что callback-функция срабатывает только после окончания анимации текста, которая выполняется на jquery с помощью метода animate, и принимает два входных параметра: ссылку блок и текущее состояние.

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

$('.b-block--second').readmore({
    ellipsis: '[...]',
    textOpen: 'Открыть',
    textClose: 'Закрыть',
    callback: function(self, state) {
        state
            ? self.css('background', '#e74c3c')
            : self.css('background', '#3498db');
    },
    brief: 500,
    addition: 100
});

CSS в примере по минимуму, можно и вовсе без него обойтись (код приведен на SCSS):

.b-readmore {
    padding: 15px 0 0 0;
    &__link {
        color: #000;
        text-decoration: underline;
        &:hover,
        &:focus,
        &:active {
            color: #000;
            text-decoration: none;
        }
    }
    &__open {
        display: inline-block;
    }
    &__close {
        display: none;
    }
    &--opened & {
        &__open {
            display: none;
        }
        &__close {
            display: inline-block;
        }
    }
}

Оцениваем и комментируем, как лучше сделать спойлер для сайта по задаваемому количеству символов.

]]>
http://paratapok.ru/frontend/2541_avtomaticheskoe-obrezanie-dlinnogo-teksta-v-spojler-na-jquery/feed/ 6