Логотип

Документация по макросам и шаблонам UMI.CMS

Многоуровневое меню

В этом примере рассматривается самое сложное для понимания меню. Однако от топика «Меню в виде списка» пример отличается лишь небольшой логической надстройкой, и понимание принципов, описанных в этой главе, позволит в полной мере оценить преимущества 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 в случае одинакового приоритета выбирается последний встреченный шаблон.