Пагинатор (постраничная навигация) на XSLT

в 12:25, , рубрики: xslt, браузеры, Веб-разработка, пагинатор, метки:

Время от времени всплывает умирающая технология XSLT и задаёт непростые вопросы. Как, например, взять максимум от 2 чисел в выражении или как организовать цикл. Соединением многих таких вопросов служит пагинатор — вывод навигации по нескольким страницам и, по возможности, удобный. На Javascript есть много примеров простых и удобных пагинаторов. Но если страницы с сервера выдаются в XML, то возникает крамольная мысль: почему бы всё оформление страниц, включая пагинатор, не сделать на статике, в XSLT? Ничего, что в эту статику можно включить JS и сделать всё проще. Кошерный подход лёгких путей не ищет.

Плюсы и минусы XSLT

Вместо одной строчки на 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 — место, благодаря которому он прекращается.

&lt; — необходимость писать так некоторые символы (<, >, &, /) по особенностям языка.

Если число страниц переменное и задаётся числом в элементе , достаточно написать

<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. Строки, относящиеся непосредственно к пагинатору:

код XSLT

<?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+ и остальных современных браузерах.

Автор: spmbt

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js