В этом примере рассматривается самое сложное для понимания меню. Однако от топика «Меню в виде списка» пример отличается лишь небольшой логической надстройкой, и понимание принципов, описанных в этой главе, позволит в полной мере оценить преимущества XSLT в качестве языка шаблонизатора.
Построение подобного меню тесно связано с построением так называемой "карты сайта" — списка страниц, отражающего реальную иерархию страниц на сайте (см. также Карта сайта средствами XSLT-шаблонизатора).
Под многоуровневым меню мы будем понимать меню с вложенными пунктами, которые будут определяться реальной иерархией страниц на сайте.
Таким образом, если в структуре сайта мы видим иерархию:
-
страница 1
-
страница 1.1
-
страница 1.2
-
страница 1.3
-
страница 1.3.1
-
-
-
страница 2
-
страница 3
То в HTML-коде мы ожидаем получить следующее представление (содержимое тега <a> опущено):
<ul>
<li>
<a>страница 1</a>
<ul>
<li>
<a>страница 1.1</a>
</li>
<li>
<a>страница 1.2</a>
</li>
<li>
<a>страница 1.3</a>
<ul>
<li>
<a>страница 1.3</a>
</li>
</ul>
</li>
</ul>
</li>
<li>
<a>страница 2</a>
</li>
<li>
<a>страница 3</a>
</li>
</ul>
Сконцентрируемся на задаче получения такого списка, а вопросы оформления его при помощи CSS уже можно будет решить самостоятельно.
За основу возьмем шаблоны, приведенные в топике «Меню в виде списка».
Замечание
Этот подход не лишен недостатков, которые мы обсудим далее. Однако он иллюстрирует важные моменты работы XSLT-шаблонизатора.
Мы знаем, что шаблоны из топика «Меню в виде списка» выводят первый уровень, указывают ссылки на страницы и отмечают активный пункт меню.
Из задачи следует, что, если у нас есть вложенные подстраницы, то в текущий элемент списка <li></li> необходимо вставить новый элемент <ul></ul>, внутри которого нужно перечислить все подстраницы.
Таким образом исходные шаблоны для отдельного элемента списка первого уровня должен нам представляться следующим образом:
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
</li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<a href="{@link}" class="active">
<xsl:value-of select="@name"/>
</a>
</li>
</xsl:template>
Мы знаем, что для того, чтобы получить список вложенных страниц (отображаемых в меню) для определенной страницы сайта, следует передать макросу %content menu()% еще один параметр — идентификатор страницы. Идентификатор доступен в этих шаблонах как атрибут id
элемента item
(так же как нам доступен атрибут link
, который мы вставляем в ссылку).
Логично было бы потом обработать результаты, которые вернет макрос, и вывести их в нужном нам месте.
Для обработки результатов воспользуемся инструкцией apply-templates
, для получения результатов — функцией document()
, а для построения запроса макроса с параметром — функцией concat()
:
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="document(concat('udata://content/menu/0/1/', @id))/udata"/>
</li>
</xsl:template>
В этот момент должен возникнуть резонный вопрос: следует ли нам назначить этому вызову свой собственный режим, например mode="menu-level2", чтобы обработать результаты макроса отдельно?
Правильный ответ в рамках поставленной задачи: НЕТ. Этот шаблон у нас уже есть.
Для того, чтобы понять, почему это так — рассмотрим уже имеющийся шаблон, обрабатывающий вызов макроса %content menu()%:
<xsl:template match="udata[@module = 'content'][@method = 'menu']">
<ul>
<xsl:apply-templates select="items/item" mode="menu"/>
</ul>
</xsl:template>
Этот шаблон уже и так делает то, что нам нужно: берет содержимое элемента udata
с атрибутами module = 'content'
и method = 'menu'
, вставляет теги <ul></ul>
и отправляет на обработку все элементы item
, результаты помещая между <ul></ul>
.
Еще раз остановимся на этом моменте: когда мы отдаем результат в обработку при помощи инструкции apply-templates
, шаблонизатор ищет подходящий шаблон среди всех доступных шаблонов. Поэтому условие match="udata[@module = 'content'][@method = 'menu']
также сработает и для повторного вызова макроса.
Аналогично, этот шаблон получит все элементы item
при помощи <xsl:apply-templates select="items/item" mode="menu"/>
и отправит их на обработку двум следующим шаблонам (в которые мы выше добавили повторный вызов макроса):
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="document(concat('udata://content/menu/0/1/', @id))/udata[items/item]"/>
</li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<a href="{@link}" class="active">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="document(concat('udata://content/menu/0/1/', @id))/udata[items/item]"/>
</li>
</xsl:template>
Как видно, этих шаблонах мы снова вызываем макрос %content menu()% и снова у нас уже есть шаблон для обработки результатов. Эта процедура будет повторена снова и снова, пока у нас не будут выбраны все страницы с отмеченной опцией "Отображать в меню". Шаблонизатор, таким образом, "развернет" все дерево иерархии страниц, которые должны попасть в меню, и превратит его во вложенные списки.
Уточняющее условие [items/item]
в select
для apply-templates
, позволяет выбирать элемент udata
только в том случае, если у него есть вложенные элементы item
(то есть вложенные подстраницы).
Замечание
Если мы уберем повторный вызов макроса из <xsl:template match="item" mode="menu">
, то меню будет разворачиваться только для активных пунктов.
В итоге, если нас интересует вся иерархия, мы должны получить следующий набор шаблонов:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" method="html" indent="yes"/>
<xsl:template match="/">
<html>
<head></head>
<body>
<div class="menu">
<xsl:apply-templates select="document('udata://content/menu/')/udata"/>
</div>
<div class="content">
</div>
</body>
</html>
</xsl:template>
<xsl:template match="udata[@module = 'content'][@method = 'menu']">
<ul>
<xsl:apply-templates select="items/item" mode="menu"/>
</ul>
</xsl:template>
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="document(concat('udata://content/menu/0/1/', @id))/udata[items/item]"/>
</li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<a href="{@link}" class="active">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="document(concat('udata://content/menu/0/1/', @id))/udata[items/item]"/>
</li>
</xsl:template>
</xsl:stylesheet>
Как нетрудно заметить, вызов макроса осуществляется много раз. И чем более сложная иерархия — тем чаще. Ниже описывается способ, когда достаточно одного вызова макроса %content menu()% для того, чтобы обработать всю иерархию страниц, выводимых в меню.
Рассмотрим ответ макроса %content menu()%, где параметр max_depth
регулирует уровень отображения вложенных пунктов меню. Зададим max_depth = 3
.
Вызов макроса: udatа://content/menu/0/3 (или можно набрать в URL: http://ваш_сайт/udata/content/menu/0/3)
Примерный ответ макроса (пустые строки вставлены для читаемости):
<udata module="content" method="menu" generation-time="0.035795">
<items>
<item id="94" link="/page1/" name="страница 1" xlink:href="upage://94">
<items>
<item id="105" link="/page11/" name="страница 1.1" xlink:href="upage://105">страница 1.1</item>
<item id="106" link="/page12/" name="страница 1.2" xlink:href="upage://106">страница 1.2</item>
<item id="107" link="/page13/" name="страница 1.3" xlink:href="upage://107">
<items>
<item id="115" link="/page131/" name="страница 1.3.1" xlink:href="upage://115">страница 1.3.1</item>
</items>
страница 1.3
</item>
</items>
страница 1
</item>
<item id="95" link="/page2/" name="страница 2" xlink:href="upage://95">страница 2</item>
<item id="96" link="/page3/" name="страница 3" xlink:href="upage://96">страница 3</item>
</items>
</udata>
Задачи у нас те же, что и в предыдущем топике, их можно выразить таким же представлением шаблона:
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
</li>
</xsl:template>
Однако, глядя на ответ макроса, мы можем видеть, что для некоторых элементов item
мы уже имеем доступ ко вложенным страницам — вложенные элементы item
описывают нам подпункты меню.
Тогда, мы можем обработать весь список без дополнительных вызовов и запросов к системе — просто укажем, что надо обработать эти элементы при помощи инструкции apply-templates
.
Однако, нам необходимо, чтобы каждый новый список находился в элементе <ul></ul>
— следовательно для всего списка подстраниц необходимо выбирать элемент items (он содержит весь список).
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
И необходимо описать шаблон именно для него (условие match="items"
). Этот шаблон обрамляет список <ul></ul>
и отправляет (apply-templates
) вложенные элементы item
на обработку.
<xsl:template match="items" mode="menu">
<ul>
<xsl:apply-templates select="item" mode="menu"/>
</ul>
</xsl:template>
Также как в и предыдущем примере с повторным вызовом макроса, для элемента item
уже есть шаблоны. И это шаблоны <xsl:template match="item" mode="menu">
— для неактивного пункта меню, и <xsl:template match="item[@status = 'active']" mode="menu">
— для активного пункта меню.
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name" />
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<a href="{@link}" class="active">
<xsl:value-of select="@name" />
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
И в этих шаблонах теперь стоит запрос на вложенные элементы. В случае их наличия вызов <xsl:apply-templates select="items" mode="menu"/>
снова отправит их к <xsl:template match="items" mode="menu">
, который создаст новые теги списка, и отдаст элементы списка на обработку снова этим шаблонам.
Таким образом эти три шаблона обработают все дерево, отданное макросом %content menu()%.
Замечание
Если мы уберем вызов apply-templates
из <xsl:template match="item" mode="menu">
, то меню будет разворачиваться только для активных пунктов.
Можно заметить, что шаблон, обрабатывающий результаты вызова макроса, также вставляет элемент <ul></ul>
. Можно воспользоваться этим свойством и передать эту задачу шаблону, обрабатывающему элемент items
. Тогда набор шаблонов будет выглядеть следующим образом:
<xsl:template match="udata[@module = 'content'][@method = 'menu']">
<xsl:apply-templates select="items" mode="menu"/>
</xsl:template>
<xsl:template match="items" mode="menu">
<ul>
<xsl:apply-templates select="item" mode="menu"/>
</ul>
</xsl:template>
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/> </li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<a href="{@link}" class="active">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
</xsl:stylesheet>
Если посмотреть внимательно, то можно увидеть, что можно также обойтись без шаблона <xsl:template match="udata[@module = 'content'][@method = 'menu']">
.
Тогда при первом вызове из шаблона дизайна просто достаточно сразу выбрать элемент items
, но обязательно задать ему свой режим mode="menu"
:
<xsl:template match="/">
<html>
<head></head>
<body>
<div class="menu">
<xsl:apply-templates select="document('udata://content/menu/0/3')/udata/items" mode="menu"/>
</div>
<div class="content">
</div>
</body>
</html>
</xsl:template>
Этот вариант получился наиболее лаконичным, быстрым, и использующим всю гибкость языка XSLT:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" method="html" indent="yes"/>
<xsl:template match="/">
<html>
<head></head>
<body>
<div class="menu">
<xsl:apply-templates select="document('udata://content/menu/0/3')/udata/items" mode="menu"/>
</div>
<div class="content">
</div>
</body>
</html>
</xsl:template>
<xsl:template match="items" mode="menu">
<ul>
<xsl:apply-templates select="item" mode="menu"/>
</ul>
</xsl:template>
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<a href="{@link}" class="active">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
</xsl:stylesheet>
Макрос %content menu()% помечает родительский элемент также атрибутом status = 'active'
. Иногда это может быть нежелательно — в таком случае можно воспользоваться еще одним дополнительным уточняющим условием.
Рассмотрим два примера решения для шаблонов с одним вызовом макроса (см. выше).
Добавим шаблон с условием для неактивных пунктов меню:
<xsl:template match="item[.//item[@status = 'active']]" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
Это условие говорит о том, что этот шаблон будет выбран для элемента item, у которого есть дочерние элементы с атрибутом status = 'active'
. Логично предположить, что в этот момент открыта дочерняя страница, для этого элемента.
Таким образом итоговый набор шаблонов должен выглядеть следующим образом:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" method="html" indent="yes"/>
<xsl:template match="/">
<html>
<head></head>
<body>
<div class="menu">
<xsl:apply-templates select="document('udata://content/menu/0/3')/udata/items" mode="menu"/>
</div>
<div class="content">
</div>
</body>
</html>
</xsl:template>
<xsl:template match="items" mode="menu">
<ul>
<xsl:apply-templates select="item" mode="menu"/>
</ul>
</xsl:template>
<xsl:template match="item" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
</li>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<span>
<xsl:value-of select="@name"/>
</span>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
<xsl:template match="item[.//item[@status = 'active']]" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
</xsl:stylesheet>
В этой ситуации мы можем немного оптимизировать код шаблонов, используя то же условие, что и в предыдущем примере:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" method="html" indent="yes"/>
<xsl:template match="/">
<html>
<head></head>
<body>
<div class="menu">
<xsl:apply-templates select="document('udata://content/menu/0/3')/udata/items" mode="menu"/>
</div>
<div class="content">
</div>
</body>
</html>
</xsl:template>
<xsl:template match="items" mode="menu">
<ul>
<xsl:apply-templates select="item" mode="menu"/>
</ul>
</xsl:template>
<xsl:template match="item[@status = 'active']" mode="menu">
<li>
<span>
<xsl:value-of select="@name"/>
</span>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
<xsl:template match="item|item[.//item[@status = 'active']]" mode="menu">
<li>
<a href="{@link}">
<xsl:value-of select="@name"/>
</a>
<xsl:apply-templates select="items" mode="menu"/>
</li>
</xsl:template>
</xsl:stylesheet>
Замечание
Следует иметь в виду, что приоритет у условий item[@status = 'active']
и item[.//item[@status = 'active']]
— одинаковый, поэтому шаблон для неактивных и родительских страниц нужно помещать после шаблона для активных пунктов. По правилам языка XSLT в случае одинакового приоритета выбирается последний встреченный шаблон.