О сайтах и не о сайтах

Теперь и в tg!

tg

Переехал с твиттера на t.me/tulvit_blog, если что.

Drupal: вывод ноды как разворачивающегося тизера

Продолжаю публиковать небольшие how-to постики по Друпалу. Пишу обычно только о тех тасках, которые на первый взгляд просты и делаются за полчаса-час от силы, но по факту на них уходит иногда и не один день. Связано это чаще всего с поиском оптимального решения, некорректной работой API и подводными камнями.

Итак, таска. Есть страница с нодой, /node/%node_id%, ну или алиас какой. По умолчанию на этой странице выводится вся нода целиком. А надо только тизер с кнопкой свернуть/развернуть.

Раскрыть текст

Свернуть текст

Казалось бы, хватит пары строчек на JavaScript и всё. Ан нет.

Допустим, у нас на руках есть только полный текст, без тизера. Как этот тизер формировать? Есть два варианта.

  • ЯСом обрезать текст до некоторой длины. Но здесь встает вопрос в парсинге.

     

    Текст может содержать разные хтмл теги, и все соответственно "поедет" если мы в тизере оставим что-то незакрытым. Сей вариант много где применяется, но в тех примерах, что смотрел, в исходном коде текста использовались в качестве тегов форматирования только

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

    Хотя, понятное дело, это все больше оправдания. При должном желании любой парсер написать не проблема (как сделано, например, на ютубе, для раздела description под каждым видео, там у них даже списки парсятся, если не влезают все li'шечки, то показываются только верхние, все остальные уходят в display: none). Вопрос больше стоял в целесообразности. Здесь я посчитал, что решение на голом ЯС нецелесообразно.

  • Тизер не формировать, просто div с текстом ужимать по высоте до нужного значения с установкой для него overflow: hidden.

     

    Вполне себе вариант, за исключением одного - текст в таком случае может обрезаться в том числе и прямо посередине букв. Этот вариант тоже много где используется, в том числе на Стиме. С проблемой обрезанных букв борются просто, вниз текста накладывают картинку с градиентом, от прозрачного до белого, чтобы текст своего рода "угасал". Но по мне это смотрится как-то аляповато. Т. е. для развлекательного сайта еще сойдет, но для чего-то серьезного - уже нет.

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

Использовать будем
hook_node_view()
, т. е. хук на событие, которое выполняется до непосредственного рендеринга контента.

function mymodule_node_view($node, $view_mode, $langcode) {

switch ($node->type) {

case 'content_type':

$teaser = field_view_field('node', $node, 'body', 'teaser');

$teaser = render($teaser);

$full = field_view_field('node', $node, 'body', 'full');

$full = render($full);

if (mb_strlen($full) > mb_strlen($teaser)) {

$node->content['teaser'] = array(

'#markup' => '

' . $teaser . '

',

'#weight' => -10,

);

$node->content['toggle'] = array(

'#markup' => '

Развернуть текст
Свернуть текст

',

'#weight' => 11,

);

}

break;

}

}

Небольшие пояснения по коду.

Тизер мы получаем через вызов
field_view_field()
. Почему именно так? Потому что работает. Вариантов получения тизера - масса. И есть куда более логичные, исходя из API Drupal'а. Вот только беда в том, что все они в состоянии "баг на баге и багом погоняет". Висят себе ветки обсуждений на друпал.орге, раз в полгодика обновляются, но никто ничего делать не хочет. А самому хакать ядро себе дороже. Сильно меня Друпал расстроил в этом плане, как начинаешь использовать уже не шибко востребованные функции API, так сразу оказывается, что дела обстоят очень плачевно, мягко говоря.

Помимо текста тизера берем еще и текст всей ноды целиком. Если нода длиннее тизера, то выводим тизер, если они одинаковые по размеру (т. е. когда нода настолько короткая, что тизером служит весь текст полностью), то не выводим.

Дальше идет интересный момент с выводом кнопок управления, этих самых свернуть/развернуть. Казалось бы, надо использовать только один div и текст в нем менять ЯС-ом (с "развернуть" на "свернуть" и обратно). Однако в ряде броузеров есть проблемы с отслеживаем событий по перестройке ДОМа, а конкретно в том же Хроме под Линукс. Проблема заключается в том, что после того, как мы кликнули на кнопку и еще никуда не успели переместить курсор, и эта кнопка куда-то "уехала" из-под курсора (т. е. текст раскрылся или закрылся), то она все равно сохраняет состояние hover, а курсор соответственно так и остается в состоянии pointer. Не так чтобы и глобальная проблема, но таки проблема. И вот сей "хак" с двумя кнопками заместо одной помогает этого избежать. На том же ютубе, кстати, тоже используются два дива, а не один (SHOW MORE/SHOW LESS - это две кнопки, одна из которых находится в состоянии
display: none
).

Дальше идет CSS:

.toggle-button {

width: 100%;

height: 34px;

line-height: 34px;

font-weight: bold;

letter-spacing: 2px;

text-align: center;

color: #717171;

background: #F4F4F4;

border-top: 1px solid #B5B5B5;

text-transform: uppercase;

}

.toggle-button:hover {

color: #2F2F2F;

background: #E0E0E0;

cursor: pointer;

}

#teaser .field-name-body {

display: block;

}

.hide {

display: none;

}

Здесь тоже интересен один момент. Ну, не только здесь, но и далее в коде ЯС. Кнопкам скрыть/раскрыть мы, понятное дело, может назначить нужные нам айдишники и классы. Обернуть тизер в див с нужным id тоже можем. Но вот body выводится в стандартном враппере со стандартными классами, причем эти же классы используются в том числе и во враппере тизера. Возникает вопрос, как "достучаться" до дива с full body средствами CSS? Изначально я использовал оператор CSS "+", он всем хорош, кроме одного - старые версии броузеров его не поддерживают. Поэтому стал использовать другой костыль, а именно применение стиля определенному классу с последующим переопределением этого стиля для нужных блоков. Звучит совсем непонятно, если более наглядно, то у нас на руках вот такая структура:

 

TEASER
FULL BODY

 

И нам надо назначить диву с FULL BODY определенное свойство, скажем
display: none
, не затронув при этом див с тизером (чтобы у него осталось
display: block
). Решается на раз-два через оператор "+", но хочется поддержки и старых браузеров. Поэтому вот такой костыль:

.field-name-body {

display: none;

}

#teaser .field-name-body {

display: block;

}


Ну и в обратную сторону (тизер скрыть, полный текст показать) по аналогии.

И буквально несколько строчек на JavaScript:

(function ($) {

$(document).ready(function () {

if ($('#toggle').length) {

$('.field-name-body').css('display', 'none');

$('#teaser .field-name-body').css('display', 'block');

}

$('#toggle').click(function() {

if ($('#toggle').hasClass('toggled')) {

$('#toggle').removeClass('toggled');

$('.field-name-body').css('display', 'none');

$('#teaser .field-name-body').css('display', 'block');

$('.show').css('display', 'block');

$('.hide').css('display', 'none');

window.scrollTo(0, 0);

}

else {

$('#toggle').addClass('toggled');

$('.field-name-body').css('display', 'block');

$('#teaser .field-name-body').css('display', 'none');

$('.show').css('display', 'none');

$('.hide').css('display', 'block');

}

});

});

})(jQuery);

Вначале идет проверочка на наличие тизера, и если он имеется, то скрываем полный текст. Ну а дальше вешаем обработчик на событие click. В качестве флага я использовал не переменную, а вешал/снимал класс toggled, так вроде бы правильней.

Здесь важны два момента. Первый - не забыть вставить
window.scrollTo(0, 0);
после сворачивания текста, т. к. в противном случае если текст длинный, то после его сворачивания пользователь останется висеть где-то в середине страницы, хотя по логике его должно перекинуть на самый вверх. И второе - подключаем js файл не через *.info файл нашего модуля, а с помощью
drupal_add_js()
. Если подключим через *.info, то скрипт будет выполняться абсолютно на всех страницах нашего сайта, чего нам явно не нужно.