Автоматическое обрезание длинного текста в спойлер на jQuery
На днях возникла задача реализовать своеобразный спойлер на jquery. Суть сводится к тому, чтобы если в блок выводится слишком длинный текст, например, превышающий 2000 символов, тогда текст должен обрезаться, а в конец вставляться многоточие. К тому же после блока необходимо выводить ссылку «Читать далее», которая будет раскрывать текст полностью. Следовало также не забывать и про функционал обратного сворачивания блока в формат анонса.
Задача достаточно стандартная, однако поиск подходящего jquery-плагина не увенчался успехом, поскольку во всех плагинах, которые удалось найти, ограничения задаются не количеством символов, а высотой блока. То есть указывается высота контейнера, и плагин обрезает текст, выходящий за его пределы. В интернете можно найти весьма удобные варианты таких плагинов, например, Readmore.js и dotdotdot. Причем последний даже может отслеживать изменение размера окна и автоматически обновлять результат.
Однако проблема была в том, что сайт адаптивный и блок принимает разную ширину в зависимости от ширины окна браузера. В итоге могла получиться ситуация, когда в блоке окажется совсем мало символов. Конечно же, можно по какому-то алгоритму менять высоту блока при ресайзе окна, однако было принято решение не делать какую-то надстройку над каким-то готовым решением, а написать свой небольшой плагин, который будет выполнять обрезку текста на jquery исходя из заданного количества символов.
Можно выделить несколько преимуществ данного подхода.
- Можно быть уверенным, что в блоке будет отображаться объем текста, несущий смысловую нагрузку даже в свернутом состоянии.
- Не требуются лишние обработчики на изменение размера окна, которые бы постоянно выполняли проверку того, сколько текста влезает в блок.
При реализации также необходимо было учесть две вещи, влияющих на красоту результата. Во-первых, нужно чтобы текст обрезался по целому слову. Во-вторых, избежать ситуации «раскрывать слишком мало» – может произойти в случае, когда общее количество символов в блоке чуть больше заданного значения, по которому следует производить обрезку, например, на 50-100 символов. Если параметры не будет указаны, то модуль будет использовать дефолтные значения.
Итак, алгоритм задачи достаточно прост:
- Вырезаем требуемое количество символов – формируем анонс.
- Дополняем текст анонса многоточием и добавляем html-обвязку в виде ссылки «Читать далее».
- Навешиваем обработчик на ссылку, которая будет менять текст в блоке на анонс и полный в зависимости от состояния.
Самая трудоемкая часть реализации скрипта 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;
}
}
}
Оцениваем и комментируем, как лучше сделать спойлер для сайта по задаваемому количеству символов.
Отличный плагин!
Подскажите, как добавить скролл вверх к textOpen при закрытии?
Необходимо воспользоваться callback-функцией, в которой в случае перехода блока в закрытое состояние следует определять позицию и скроллить к ней страницу. Пример кода ниже:
var initFlag = false;
$(‘.b-block—second’).readmore({
ellipsis: ‘[…]’,
textOpen: ‘Открыть’,
textClose: ‘Закрыть’,
callback: function(self, state) {
var top;
if (initFlag) {
if (state) return;
// вычисляем позицию элемента, к которому необходимо проскроллить страницу
top = self.offset().top;
$(‘html, body’).animate({scrollTop: top}, 500);
} else {
// помечаем, что инициализация блока уже прошла
initFlag = true;
}
},
brief: 500,
addition: 100
});
Не годиться. Вырезанный текст не индексируется. Нужно чтобы он просто скрывался под display: none;
Сорри, у вас в примере все индексируется. Это у меня не получается. Дело в SCSS. Нельзя ли то же самое в обычном css воспроизвести?
В примере стили есть и на SCSS, и на CSS.
хороший плагин, но возник вопрос, при раскрытии спойлера (плавное) кнопка «открыть список» остается без изменений и только после повторного нажатие меняет своё название на «закрыть список». в чем может быть проблема? До этого было изменение:
$(‘.b-block—third’).readmore({
brief: 500,