📜 ⬆️ ⬇️

Paginator (page navigation) on XSLT

From time to time, the dying XSLT technology pops up and asks difficult questions. How, for example, to take a maximum of 2 numbers in an expression or how to organize a cycle. A combination of many such questions is the paginator — displaying navigation on several pages and, if possible, convenient. There are many examples of simple and convenient paginators on Javascript. But if the pages from the server are rendered in XML, then a seditious thought arises: why not complete the design of the pages, including paginator, on static, in XSLT? There is nothing that you can include JS in this static and make everything easier. The kosher approach is not looking for easy ways.

Pros and cons of XSLT


Instead of one line on JS, you need to write a dozen lines of recursion with a number of features. Let it be a minus. Nevertheless, this task is doable, which means that such a solution will pop up somewhere somewhere.

Answers that are similar to questions on how to add 2 numbers are very much appreciated on the web: " How to comment on XSLT so that HTML comments are not deleted ." So, if you have a minimum of knowledge, your reputation on SO will be ensured.

Over the years, XSLT, many browsers, in addition to the very old and simple, have gained the ability to handle XML + XSLT. This can be used by shifting the work from the server to clients (browsers), even if it is extra hundreds of lines of difficult to understand declarative code.
')
Often, declarativeness is good. A set of rules, as in CSS, is easier to understand and use. But starting to write recursive procedures in declarative language with generic syntax problems is an exercise for geeks and people who are in a hopeless situation. Therefore, the article has two goals - to read and play at leisure - for geeks, and take a working example and customize it for yourself - for people.

On Habré, a similar topic was raised here: habrahabr.ru/post/138740 (implementation of paginator).

What you need from paginator


From XML, it receives only the number of the current page and (possibly) the number of the last page of the list. Everything else is configured in the statics of paginator in * .xsl. "Rest" a bit, as will be seen from the statement. Only the number of links around the link to the current page. But then an extension was added - the output of the following pages in dozens or another interval. This seemed to be a good illustration of the capabilities of the paginator.

1) display the current page (with a link if it is sometimes necessary to update it with or without a link);
2) display several links of adjacent pages around (before and after the current one);
3) the first and last (extreme) pages, if they did not fall into the "neighboring";
4) ellipsis, if there are pages not shown between the neighboring and extreme ones;
5) optional - links to ellipsis, to go approximately to the middle of the unplayable page span;
6) if some of the links are not displayed, because there are edges, add an unquantified number of links on the other side of the link of the current page. In other words - show, if there is, what to show within the specified number of links. For example, we show 5 “before” and 5 “after” links, but when viewing the third page, 2 “before” links are displayed. So, show 8 links “after”, if there are such (do not go beyond the maximum number of pages).

(This requirement is partially fulfilled - extra links are displayed on the right when the page number is near the first one and only half of the list is displayed if the page number is close to the maximum. This is due to the fact that it would require a large reworking of logic and complication of expressions, and such a goal is not strictly stood.)

7) finally, the double use of the paginator function - the output of pages through dozens (or fives, everything is configured) after the first. It may be useful if you need to quickly go deep into a very large list, into dozens of pages, and usually we are on the front pages. If an end page is specified, the list of tens is not displayed.

Dive into tao xslt


To make the story useful, we will build it in the form of teaching programming techniques in this declarative language. We will build a paginator, from simple models to more and more complex ones.

For the basis of the construction we take some log files that are often found by web administrators and which have to be viewed. To make browsing convenient, and programming costs are small, we output logs page by page in XML, and all design is imposed on client technologies, including client XSLT.

As mentioned, paginator is more natural to do in a procedural language. But XSLT is coping with this task, fulfills all the requirements of the statement. There are many examples of implementation scattered on the Internet and even one met on Habré. But examples without explanation of the rules of construction lead to the fact that the implementation must be done independently, starting with the basics. This example is an attempt to give an example of a complete and functional paginator, for which there is a hope that the connection will be simple and its management documented.

While the records in our log are about 500, the easiest way to pagination is to simply output 10 links on a page and manually write the page numbers to them in HTML, like:
<a href="page.xml?page=2"/>2</a> 


If there are a little more than 500 or deeper records look rare, it is enough to assign a page number entry form. Same way out. This does not require indentation in XSLT and is done in the xsl file on a general basis.

 <?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> 


The first bad luck is that it is difficult to even write the class of the current page in order to somehow select or deactivate it. No problem, there is a JS for this. But the plans - writing links to XSLT. Therefore, reluctantly, but let's see how this strange cycle is written for 10 lines and organize to start displaying a list of links to XSLT.

Create a recursive function. The template function is called from the template body. All paprameters must be passed to it - these are independent namespaces, so it is impossible, as in ordinary languages, to define global scopes.

 <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> 


Hooray, we did a cycle! 5 lines of a call and 15 lines of a function have done their job - we can mark the current link and not write 30 lines of HTML! This is an achievement, the first step to conquering pagination. And nothing, that on JS we would cost 5 and read better. The main thing is to get used, and then there will be enlightenment.

To shorten the code on pieces of 8 lines, a trick was done - to set the class, the choose-when-otherwise block was not recorded, but 1 or 0 was added to the word “active”, thus, “active1” = class of the link of the current page.

The features of the language are visible on this site: the default parameters can be omitted when calling; in recursion, it is necessary to list all the necessary parameters. select = "$ i + 1" is the key place due to which the cycle moves, and test = "$ i <= $ nLinks - the place due to which it stops.

& lt; - the need to write some characters (<,>, &, /) in terms of language features.

If the number of pages is variable and is given by the number in the element, it suffices to write
 <xsl:with-param name="nLinks" select="/ha/pageLast"/> 


Symmetric links "before" and "after"


The next task: to display a limited number of links, half of which will go to the link of the current page, and the second half after. We use variables for those expressions that are repeated many times. We cycle the cycle by interval, but do not display links for numbers less than 1. For an even number of links, we assume that there are 1 more links (before this number is likely to be odd, but we need to test it for all cases).

For the implementation, an additional parameter “to” is needed, in which the maximum page number will be stored and transmitted by recursion.

 <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> 


Two things are missing: links to the first page, when needed, and outputting the full number of links, not half when we are on the first page. We add before calling the checking function, when it is necessary to display the link “1”, and when - the ellipsis, meaning the omission of a part of the links of the pages.

We add a counter to the function, which counts how many links are actually added to stop the cycle of reaching $ nLinks, and not like now, by the number of $ nLinks.

The solution with a counter is simple. This decision lays a pair of logical bombs, which will have to be solved later.
1) the cycle may never end; well, it's simple, let's introduce another control counter with a number, say, 50, just in case; hmm, already 2 counters. The decision is not as beautiful as it seemed;
2) the beginning of the pages is easy to calculate, but near the end of the list of pages - it will be necessary to predict how many numbers will go beyond the margin of permissible and will not be shown. But not all at once.

So marked (conditionally) the first page.

 <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> 


On the link on the ellipse, there is an approximate middle link of not shown interval, indicated in the tooltip. For example, the display of links starts from the 60th page - the 30th or 29th will be created in three places. A link without showing a number is laconic, more useful and does not require additional space at all. Dots are not displayed if links are shown starting from the second.

Protection


From a developer who starts something to change in the parameters and randomly sets, for example, minus a million, we limit the number of recursions by entering the stop parameter equal to 50. With it, the paginator will not perform more than 50 iterations.

Interval pagination (clause 7)


When the framework is written, the rest of the “features” are added easily (of course, if the developer is already aware of the technology). To demonstrate this, add the ability to display links with an interval of several pages to the final paginator. Sometimes it is necessary for navigation, sometimes - for counting not pages, but records on pages. This will be a little inappropriate use of the paginator, because it is configured to display links “before and after”, and for output through an interval this will manifest. But instead of writing a new paginator or correcting this for inappropriate use, it is easier to correctly select the initial parameter of it, namely, add a floor ($ n2 div 2). With this reservation and with the added step parameter, the paginator starts working.

To be continued , but if the reader wishes to see and use the ready-made paginator, he lies at spmbt.imtqy.com/spmbt/wk/37.20.115.43.xml . Addresses and links on the log page are changed, coincidences are random. The page selector is deactivated, as this is a static example, always located on page 9. But at the top we see a paginator built through spmbt.imtqy.com/spmbt/wk/37.20.115.43.xsl . Lines directly related to the paginator:
XSLT code
 <?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> 

. The main code took 85 lines - this is a good result, despite the fact that 2 cycles are carried out - on pages and on dozens of pages, there are three points with median links. Supported in IE8 + and other modern browsers.

Source: https://habr.com/ru/post/174977/


All Articles