Время от времени всплывает умирающая технология XSLT (в просторечии ЧЫДЕ) и задаёт непростые вопросы. Как, например, взять максимум от 2 чисел в выражении или как организовать цикл. Соединением многих таких вопросов служит пагинатор — вывод навигации по нескольким страницам и, по возможности, удобный. На Javascript есть много примеров простых и удобных пагинаторов. Но если страницы с сервера выдаются в XML, то возникает крамольная мысль: почему бы всё оформление страниц, включая пагинатор, не сделать на статике, в XSLT? Ничего, что в эту статику можно включить JS и сделать всё проще. Кошерный подход лёгких путей не ищет.
Плюсы и минусы XSLT
Когда школьникам Бандиагары показали, в скольких строчках можно организовать цикл на XSLT, они реагировали примерно так:
То же было и со специалистами: habrastorage.org/storage2/451/896/6da/4518966da9d6f146434889bcfadb3ee8.jpg.
Вместо одной строчки на JS нужно писать десяток строчек рекурсии с рядом особенностей. Пусть это минус. Тем не менее, задача эта выполнима, значит, когда-то где-то всплывёт такое решение.
В Сети очень ценятся ответы, похожие на вопросы о том, как сложить 2 числа: "Как комментировать XSLT так, чтобы комменты из HTML не удалялись". Значит, при наличии минимальных знаний репутация на SO вам будет обеспечена.
За годы существования XSLT многие браузеры, кроме очень старых и простых, наработали умение обрабатывать XML+XSLT. Этим можно пользоваться, перекладывая работу с сервера на клиентов (браузеры), пусть даже лишними сотнями строк трудно понимаемого декларативного кода.
Часто декларативность — это хорошо. Набор правил, как в CSS, легче понимается и используется. Но начинать писать рекурсивные процедуры на декларативном языке с родовыми проблемами синтаксиса — это занятие для гиков и людей, попавших в безысходную ситуацию. Поэтому целей у статьи две — почитать и поиграться на досуге — для гиков, а взять работающий пример и настроить его под себя — для людей.
На Хабре подобная тема поднималась здесь: habrahabr.ru/post/138740/ (реализация пагинатора).
Что нужно от пагинатора
От XML он получает только номер текущей страницы и (возможно) номер последней страницы списка. Всё остальное настраивается в статике пагинатора в *.xsl. «Остального» немного, как будет видно из постановки. Всего лишь число ссылок вокруг ссылки на текущую страницу. Но потом добавилось расширение — вывод следующих страниц через десятки или другой интервал. Это показалось хорошей иллюстрацией возможностей пагинатора.
1) выводить текущую страницу (со ссылкой, если требуется иногда обновление её самой или без ссылки);
2) выводить несколько ссылок соседних страниц вокруг (до и после текущей);
3) первую и последнюю (крайние) страницы, если они не попали в «соседние»;
4) троеточие, если между соседними и крайней есть непоказанные страницы;
5) опционально — ссылки на троеточиях, чтобы перейти примерно на середину непоказываемого промежутка страниц;
6) если часть ссылок не выведена, потому что встретились края, добавить невыведенное количество ссылок с другой стороны ссылки текущей страницы. Другими словами — показывать, если есть, что показывать в пределах заданного количества ссылок. Например, показываем 5 ссылок «до» и 5 «после», но при просмотре третьей страницы отображается 2 ссылки «до». Значит, показать 8 ссылок «после», если такие найдутся (не выйдут за пределы максимального числа страниц).
(Это требование выполнено частично — выводятся лишние ссылки справа, когда номер страницы — возле первой и выводится только половина списка, если номер страницы близок к максимальному. Это связано с тем, что потребовало бы большой переделки логики и усложнения выражений, а цели такой строго не стояло.)
7) наконец, двойное использование функции пагинатора — вывод страниц через десятки (или пятёрки, всё настраивается) вслед за первыми. Может быть полезно, если надо быстро перейти вглубь очень большого списка, на десятки страниц, а находимся обычно на первых страницах. Если указана концевая страница, список десятков не выводится.
Окунёмся в дао XSLT
Чтобы рассказ оказался полезным, построим его в виде обучения приёмам программирования на этом декларативном языке. Будем строить пагинатор, от простых моделей до всё более сложных.
За основу построения возьмём некоторый файл логов, которые часто встречаются у веб-администраторов и которые приходится просматривать. Чтобы просматривание было удобным, а затраты на программирование — небольшие, выдаём логи постранично в XML, а всё оформление возлагается на клиентские технологии, включая клиентский XSLT.
Как упоминалось, пагинатор естественнее делать на процедурном языке. Но и XSLT справляется с этой задачей, выполняет всё требования постановки. В интернете разбросано множество примеров реализации и даже один встретился на Хабре. Но примеры без пояснений правил построения приводят к тому, что реализацию приходится делать самостоятельно, начиная с основ. Данный пример — попытка дать пример законченного и функционального пагинатора, для которого есть надежда, что подключение будет простым, а управление им — задокументированным.
Пока записей в нашем логе порядка 500, самый простой способ пагинации — просто вывести 10 ссылок на странице и вручную записать им номера страниц на HTML, вида:
<a href="page.xml?page=2"/>2</a>
Если их немного больше 500 или глубже записи смотрятся редко, достаточно приписать формочку ввода номера страницы. Тоже выход. Это не потребует углубления в XSLT и делается в xsl-файле на общих основаниях.
<?xml version="1.0"?> <!DOCTYPE html> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <html> <head><title>Someone Log</title> <meta http-equiv="x-ua-compatible" content="IE=8"/> <style> body{ ..../* стили для страницы, скрипты, в которых заменены некоторые символы на сущности */ } </style> <script type="text/javascript"> ... </script> </head> <body> <!-- выводим таблицу с данными, ячейку за ячейкой, ничего интересного --> <table class="tb1" id="tb1"> <tr> <th>ip + <span class="n">#</span></th> <th>path</th> <th>browser</th> <th>accType</th> <th>fileName</th> <th>settings</th> <th>date</th> </tr> <xsl:for-each select="/ha/actions/action"> <tr class="account-{accountType} {fileName}"> <td class="help leftJust" title2="{@id}"> <div class="full"> <span><xsl:value-of select="@id"/></span> </div> <div class="brief"><xsl:value-of select="ip"/></div> </td> <td class="leftJust"><a href="http://habrahabr.ru{path}" target="_blank"> <xsl:value-of select="path"/> </a></td> <td class="help UA" title2="{agent}" align="center"> <div class="full"> <div class="fullRel"> <span><xsl:value-of select="agent"/></span> </div> </div> <div class="brief"><xsl:value-of select="browser"/></div> </td> <td><xsl:value-of select="accountType"/></td> <td class="fileName {fileName}"><xsl:value-of select="fileName"/></td> <td> <span class="{settings/property/@value}"> <xsl:value-of select="settings/property/@name"/> </span> </td> <xsl:variable name="dt" select="date"/> <td><span title="{substring($dt,1,10)}"> <xsl:value-of select="substring($dt,12,10)"/> </span></td> </tr> </xsl:for-each> </table> <div class="pagination"> <!-- начался блок пагинации --> <!-- В первом приближении хватает просто HTML: --> <span class=""> <a href="page.xml?page=1"/>1</a> </span> <span class=""> <a href="page.xml?page=2"/>2</a> </span> <span class=""> <a href="page.xml?page=3"/>3</a> </span> ...<!-- и так - 10 ссылок --> </body> </html>
Первая же незадача — затруднительно даже написать класс текущей страницы, чтобы как-то её выделить или дезактивировать. Нет проблем, есть JS для этого. Но в планах — написание ссылок на XSLT. Поэтому нехотя, но посмотрим, как пишут этот странный цикл на 10 строчек и организуем для начала вывод списка ссылок на XSLT.
Создаём рекурсивную функцию. Из тела шаблона вызывают шаблон-функцию. Все папраметры для неё надо передавать — это независимые пространства имён, поэтому нельзя, как в обычных языках, определить глобальные области видимости.
<xsl:template match="/"> ... <xsl:comment>====== в тексте страницы, вместо 10 ссылок ======</xsl:comment> <xsl:call-template name="paginate"> <xsl:with-param name="nLinks" select="10"/> <xsl:with-param name="p" select="/ha/page"/> <xsl:with-param name="url" select="$url"/> </xsl:call-template> </div></body></html> </xsl:template> <xsl:comment>====== функция-цикл - исполнение с концевой рекурсией ======</xsl:comment> <xsl:template name="paginate"> <xsl:param name="i" select="1"/> <xsl:comment>параметр (переменная) цикла</xsl:comment> <xsl:param name="nLinks"/> <xsl:param name="p"/> <xsl:param name="url"/> <xsl:if test="$i <= $nLinks"> <span class="{concat('active', number($i = $p)) }"> <a href="{concat($url, $i)}"> <xsl:value-of select="$i"/> </a> </span> <xsl:call-template name="paginate"> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="nLinks" select="$nLinks"/> <xsl:with-param name="p" select="$p"/> <xsl:with-param name="url" select="$url"/> </xsl:call-template> </xsl:if> </xsl:template>
Ура, мы сделали цикл! 5 строчек вызова и 15 строчек функции сделали своё дело — мы можем отмечать текущую ссылку и не писать 30 строчек HTML! Это — достижение, первый шаг к покорению пагинации. И ничего, что на JS мы обошлись бы 5 и читалось бы лучше. Главное — привыкнуть, а дальше будет просветление.
Для укорочения кода на штук 8 строчек сделан трюк — для задания класса не записан блок choose-when-otherwise, а дописывается 1 или 0 к слову «active», таким образом, «active1» = класс ссылки текущей страницы.
На этом участке видны особенности языка: параметры, заданные по умолчанию, можно не задавать при вызове; в рекурсии обязательно перечисление всех нужных параметров. select="$i + 1" — ключевое место, благодаря которому двигается цикл, а test="$i <= $nLinks — место, благодаря которому он прекращается.
< — необходимость писать так некоторые символы (<, >, &, /) по особенностям языка.
Если число страниц переменное и задаётся числом в элементе , достаточно написать
<xsl:with-param name="nLinks" select="/ha/pageLast"/>
Симметричные ссылки «до» и «после»
Следующая задача: вывести ограниченное число ссылок, половина которых будет идти до ссылки текущей страницы, а вторая половина — после. Используем переменные для тех выражений, которые многократно повторяются. Прокручиваем цикл по интервалу, но не выводим ссылки для номеров, меньших 1. Для чётного количества ссылок считаем, что ссылок «до» будет на 1 больше (скорее всего, это число всегда будет задано нечётным, но протестировать надо для всех случаев).
Для реализации понадобился дополнительный параметр «to», в котором будет храниться максимальный номер страницы и передаваться по рекурсии.
<div class="pagination"> Страницы: <xsl:variable name="url">http://37.230.115.43/actions/last.xml?page=</xsl:variable> <xsl:variable name="p" select="/ha/page"/> <xsl:comment>текущая страница</xsl:comment> <xsl:variable name="nL" select="9"/> <xsl:comment>сколько ссылок в пагинаторе</xsl:comment> <xsl:call-template name="paginate"> <xsl:with-param name="i" select="$p"/> <xsl:with-param name="nLinks" select="$nL"/> <xsl:with-param name="url" select="$u"/> </xsl:call-template> </div></body></html> </xsl:template> <xsl:template name="paginate"> <xsl:param name="i" select="1"/> <xsl:param name="nLinks"/> <xsl:param name="url"/> <xsl:param name="to" select="$i + $nLinks"/> <xsl:variable name="n2" select="floor($nLinks div 2)"/> <xsl:if test="$i < $to"> <xsl:if test="$i - $n2 >= 1"> <span class="{concat('active', number($i = $to - ceiling($nLinks div 2))) }"> <a href="{concat($url, $i - $n2)}"> <xsl:value-of select="$i - $n2"/> </a> </span> </xsl:if> <xsl:call-template name="paginate"> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="url" select="$url"/> <xsl:with-param name="nLinks" select="$nLinks"/> <xsl:with-param name="to" select="$to"/> </xsl:call-template> </xsl:if> </xsl:template>
Не хватает 2 вещей: ссылки на первую страницу, когда нужно, и вывода полного числа ссылок, а не половинного, когда находимся на первой странице. Добавляем перед вызовом функции проверки, когда надо выводить ссылку «1», а когда — троеточие, означающее пропуск части ссылок страниц.
В функцию добавляем счётчик, который считает, сколько в реальности добавлено ссылок, чтобы остановить цикл по достижению $nLinks, а не как сейчас, по количеству $nLinks.
Решение со счётчиком — простое. Этим решением закладывается пара логических бомб, решать которые придётся позже.
1) цикл может никогда не закончиться; ну, это просто, введём ещё контрольный счётчик с числом, скажем, 50, на всякий случай; хм, уже 2 счётчика. Решение не такое красивое, как казалось;
2) начало страниц легко просчитаем, а вот вблизи конца списка страниц — понадобится предугадывать, сколько номеров зайдёт за край допустимого и не будет показано. Но не всё сразу.
Так отмечается (условно) первая страница.
<xsl:variable name="pn2" select="$p - floor($nL div 2)"/> <xsl:if test="$pn2 > 1"> <span class=""> <a href="{concat($url, 1)}">1</a> <xsl:if test="$pn2 > 2"> <a class="ellip" title="{floor(($pn2 +1) div 2)}" href="{concat($url, floor(($pn2 +1) div 2) )}">...</a> </xsl:if> </span> </xsl:if>
На ссылке на троеточии выведена примерно срединная ссылка непоказанного интервала, указываемая в подсказке. Например, показ ссылок начинается с 60-й страницы — 30-я или 29-я будет создана на троеточии. Ссылка без показа числа — лаконичнее, полезнее и совершенно не требует дополнительного места. Троеточия не выводятся, если ссылки показываются, начиная со второй.
Защиты
От разработчика, который начнёт что-то менять в параметрах и случайно задаст, например, минус миллион — ограничиваем число рекурсий, введя параметр stop, равный 50. С ним пагинатор не совершит более 50 итераций.
Пагинация через интервал (пункт 7)
Когда каркас написан, остальные «фичи» добавляются легко (конечно, если разработчик уже в курсе технологий). Чтобы это продемонстрировать, в конечный пагинатор добавим возможность вывода ссылок с интервалом в несколько страниц. иногда это нужно для навигации, иногда — для счёта не страниц, а записей на страницах. Это будет немного нецелевое использование пагинатора, потому что он настроен на вывод ссылок «до и после», а для вывода через интервал это проявится. Но вместо того, чтобы писать новый пагинатор или корректировать этот на нецелевое использование, проще правильно подобрать начальный параметр его, а именно — прибавить floor($n2 div 2). С этой оговоркой и с добавленным параметром step пагинатор начинает работать.
Продолжение следует, но если читатель пожелает посмотреть и использовать готовый пагинатор, он лежит по адресу spmbt.kodingen.com/wk/37.20.115.43.xml. Адреса и ссылки на странице лога изменены, совпадения случайны. Переключатель по страницам деактивирован, поскольку это — статический пример, всегда находящийся на 9-й странице. Но вверху видим пагинатор, построенный через spmbt.kodingen.com/wk/37.20.115.43.xsl. Строки, относящиеся непосредственно к пагинатору:
<?xml version="1.0"?> <!DOCTYPE html> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <html> ... <div class="pagination"> Страницы: <xsl:variable name="url">#page=</xsl:variable> <xsl:variable name="p" select="/ha/page"/> <xsl:comment>текущая страница</xsl:comment> <xsl:variable name="nL" select="11"/> <xsl:comment>сколько ссылок в пагинаторе</xsl:comment> <xsl:variable name="pLast" select="/ha/pageLast"/> <xsl:comment>последняя (если есть; а если нет, то здесь будет пустая строка)</xsl:comment> <xsl:variable name="pn2" select="$p - floor($nL div 2)"/> <xsl:if test="$pn2 > 1"> <span class=""> <a href="{concat($url, 1)}">1</a> <xsl:if test="$pn2 > 2"> <a class="ellip" title="{floor(($pn2 +1) div 2)}" href="{concat($url, floor(($pn2 +1) div 2) )}">...</a> </xsl:if> </span> </xsl:if> <xsl:call-template name="paginate"> <xsl:with-param name="i" select="$p"/> <xsl:with-param name="nLinks" select="$nL"/> <xsl:with-param name="pLast" select="$pLast"/> <xsl:with-param name="url" select="$url"/> </xsl:call-template> <xsl:if test="string-length($pLast) =0"> <xsl:variable name="nL2" select="5"/> <xsl:variable name="step" select="10"/> <xsl:call-template name="paginate"> <xsl:with-param name="i" select="floor(($p + $nL + $step +1) div $step) * $step + floor($nL2 div 2)"/> <xsl:with-param name="nLinks" select="$nL2"/> <xsl:with-param name="pLast" select="$pLast"/> <xsl:with-param name="step" select="$step"/> <xsl:with-param name="url" select="$url"/> <xsl:with-param name="class" select="'gaps'"/> </xsl:call-template> </xsl:if> <xsl:variable name="pp2" select="$p + floor(($nL -1) div 2)"/> <xsl:if test="$pp2 < $pLast"> <span class=""> <xsl:if test="$pp2 < $pLast -1"> <a class="ellip" title="{$pLast - floor(($pLast - $pp2) div 2)}" href="{concat($url, $pLast - floor(($pLast - $pp2) div 2) )}">...</a> </xsl:if> <a href="{concat($url, $pLast)}"><xsl:value-of select="$pLast"/></a> </span> </xsl:if> </div> </body> </html> </xsl:template> <xsl:template name="paginate"> <xsl:param name="i" select="1"/> <xsl:param name="nLinks"/> <xsl:param name="pLast"/> <xsl:param name="step" select="1"/> <xsl:param name="to" select="$i + $nLinks"/> <xsl:param name="url"/> <xsl:param name="class"/> <xsl:param name="count" select="1"/> <xsl:param name="stop" select="50"/> <xsl:variable name="n2" select="floor($nLinks div 2)"/> <xsl:if test="($i < $to or $count <= $nLinks) and $stop > 0"> <xsl:if test="$i - $n2 >= 1 and $i - $n2 <= $pLast or $i - $n2 >= 1 and string-length($pLast) =0"> <span class="{concat($class,' active', number($i = $to - ceiling($nLinks div 2)))}"> <a href="{concat($url, $i - $n2)}"> <xsl:value-of select="$i - $n2"/> </a> </span> </xsl:if> <xsl:call-template name="paginate"> <xsl:with-param name="i" select="$i + $step"/> <xsl:with-param name="to" select="$to"/> <xsl:with-param name="nLinks" select="$nLinks"/> <xsl:with-param name="pLast" select="$pLast"/> <xsl:with-param name="step" select="$step"/> <xsl:with-param name="url" select="$url"/> <xsl:with-param name="class" select="$class"/> <xsl:with-param name="count" select="$count + number($i - $n2 >= 1 and $i - $n2 <= $pLast or $i - $n2 >= 1 and string-length($pLast) =0)"/> <xsl:with-param name="stop" select="$stop - 1"/> </xsl:call-template> </xsl:if> </xsl:template> </xsl:stylesheet>
. На основной код ушло 85 строк — это хороший результат, при том, что выполняется 2 цикла — по страницам и по десяткам страниц, задействованы троеточия со срединными ссылками. Поддерживается в IE8+ и остальных современных браузерах.
ссылка на оригинал статьи http://habrahabr.ru/post/174977/
Добавить комментарий