Навигация

Если на вашем сайте не две-три страницы, а по крайней мере десяток, то на нем, очевидно, есть и дерево навигации (или карта сайта), имеющее иерархическую структуру. И перед вами неизбежно встает ряд задач:

  • Как отобразить на сайте меню? А ведь их может быть и несколько: главное меню, меню второго уровня и т.п.
  • Как определить какому узлу в навигационном дереве соответсвует текущая страница?
  • Как подсветить активную ссылку при выводе меню?
  • Как вывести "хлебные крошки"?
  • Как редактировать из админки навигационное дерево?

Всем этим занимается стандартный механизм навигации.

Редактируем навигационное дерево

Самый простой способ задать навигационное дерево - напрямую описать его в настройках. В общих чертах этот процес уже был описан в статье "Hello, World!".

Структура навигации в админе представляет собой ассоциативный массив, ключами которого являются тексты ссылок. Значениями массива в простейшем случае могут являются адреса страниц:

 О компании = /about/
 Каталог = /catalog/
 Новости = /news/

В реальности такой записью обойтись не получается, т.к. практически всегда для части ссылок нужно определять не только адреса страниц, но и кучу других параметров. Поэтому предлагается использовать значением не строку-адрес, а ассоциативный массив, в котором адрес имеет ключ "url". Таким образом вышеописанное будет эквивалентно следующему:

 О компании = {
        url = /about/
 }

 Каталог = {
        url = /catalog/
 }

 Новости = {
        url = /news/
 }

Вложенные ссылки описываются параметром "sub" - это массив с таким же синтаксисом, как и корневой массив. Уровень вложенности неограничен:

 О компании = {
        url = /about/
         sub = {
                История = {
                        url = /about/history/
                        sub = {
                                2005 год = /about/history/2005/
                                2006 год = /about/history/2006/
                                2007 год = /about/history/2007/
                                2008 год = /about/history/2008/
                        }
                }
                Руководство = /about/chiefs/
                Контакты = /about/contacts/
        }
 }

 Каталог = {
        url = /catalog/
        sub = {
                Молотки = /catalog/hammers/
                Лопаты = /catalog/shovels/
        }
 }

 Новости = {
        url = /news/
 }

Как видим, в этом примере успешно сочетаются два способа описания ссылок: и как массив, и как строка-адрес. Таким способом мы имеем возможность создавать сколь угодно сложные структуры.

Отображение навигации на сайте

Теперь нам нужно в каком-то шаблоне (обычно - в лейауте) отобразить навигацию. Проще всего это сделать используя вставку NAVIGATION{}, однако этот способ годится в самых элементарных случаях. Давайте рассмотрим более мощные средства для работы с навигацией.

Для доступа к навигации существует метод CMS::navigation(), который возвращает экземпляр класса CMS_Navigation. В этом классе собран весь функционал для работы с навигацией, из которого нас сейчас интересует функция draw:

<div class="navigation"><?= CMS::navigation()->draw() ?></div>

Такая конструкция отобразит главное меню сайта, состоящее из корневых ссылок в виде последовательного набора тегов a.

Как изменить внешний вид набора ссылок

Во-первых, можно воспользоваться средствами CSS. В примере выше мы вывели ссылки не сами по себе, а положили его внутрь специального блока, а значит имеем возможность в широких масштабах управлять стилями ссылок.

Во-вторых, есть возможноть воспользоваться другими шаблонами. По умолчанию для отображения ссылок используется шаблон tao/views/navigation/simple.phtml, но имя шаблона можно передать в функцию draw. Попробуйте, например, вот так:

<div class="navigation"><?= CMS::navigation()->draw('ul') ?></div>

Здесь будет использован шаблон tao/views/navigation/ul.phtml, который отобразит ссылки в виде ненумерованного списка. А вот шаблон ul-tree отобразит всю навигацию в виде многоуровневого дерева.

Все эти шаблоны находятся в каталоге tao/views/navigation, и каждый из них вы можете переопределить, просто скопировав в каталог app/views/navigation и отредактировав по собственному усмотрению. Также вы можете создавать в этом же каталоге и свои шаблоны навигации.

Уровни навигации

Рассмотрим структуру навигации, приведенную в начале данной статьи. Когда мы находимся на главной странице, у нас есть только одно меню, которое можно отобразить: "О компании", "Каталог", "Новости". Но если мы зайдем в "О компании", то у нас сразу же появляется еще один уровень: "История", "Руководство", "Контакты". Это меню обычно рисуют на боковой панели сайта. А если пойдем дальше в "Историю", то получим еще один уровень. И так далее.

Возникает вопрос: как в боковом меню отобразить второй уровень навигации? Вернее, как отобразить - понятно (функцией draw, естественно), а вот как этот второй уровень получить? Он ведь разный для разных страниц, а некоторых страницах его вообще нет.

Прежде всего давайте уясним, что нумерация у нас идет от нуля. Т.е. этот второй уровень на самом деле первый, а первый (который корневой, главный) - нулевой. А получить нужный уровень можно с помощью функции level($n):

 // Нулевой уровень - он же корневой.
 // Такой вызов аналогичен непосредственному обращению к навигации.
 CMS::navigation()->level(0)->draw();

 // Получение уровня, следующего после корневого.
 CMS::navigation()->level(1)->draw();

С объектом, полученный функцией level, можно обращаться точно так же, как и со всей навигацией. В случае, если запрошенный уровень не существует (например, на главной странице кроме корневого уровня ничего нет), функция вернет null, поэтому перед отрисовкой бокового меню следует сначала проверить возвращенное значение.

Текущие (выбранные) ссылки

Что значит "текущая ссылка"? Это значит что мы в данный момент находимся на странице, на которую указывает данная ссылка, либо на одной из вложенных страниц. Например, если мы находимся на странице "2006 год", то не только эта ссылка будет текущей, но и ссылки "О компании" и "История". Т.е. вся цепочка от корня, до той страницы, на которой мы находимся в текущий момент.

Практически всегда требуется выделять текущую ссылку внешним видом. Если вы посмотрите на HTML-код страницы, то увидите, что в стандартных шаблонах текущая ссылка помечается классом current, что позволяет ее выделить, используя средства CSS. Но не всегда все так просто.

Как движок определяет на какой странице мы в данный момент находимся? В самом простейшем варианте - банальным сравнением REQUEST_URI с адресом ссылки. Если совпадает, то эта ссылка считается текущей, как и все ее предки до самого корня. Но вот что делать, например, с разделом "Новости"? Там адреса создаются динамически, например: "/news/<N>/" и т.п. По понятным причинам все эти ссылки мы не можем добавить в навигацию, однако помечать в меню пункт "Новости" (если мы там находимся) нужно. Что делать?

Здесь приходят на помощь регулярные выражения. Давайте добавим в пункт меню параметр "match":

 Новости = {
        url = /news/
        match = {^/news/}
 }

Т.е. мы указали регулярное выражение, которым нужно проверять REQUEST_URI. Если совпадает, то ссылка считается текущей. Обратите внимание, что проверку на полное совпадение это не отменяет. Если адрес страницы не проходит проверку регулярным выражением, но тем не менее равен адресу ссылки, то ссылка, тем не менее, считается текущей.

А вот что делать если определить текущую ссылку по адресу вообще невозможно? Пример: новости на туристическом сайте. Каждая новость привязана к какой-либо стране. Страны - это пункты меню в навигации, и текущую страну (к которой привязана новость) следует выделять в меню. Ни совпадение адреса, ни какие-либо регулярные выражения здесь не помогут. Страна определяется уже при работе контроллера, когда новость загружена, а адреса новостей отличаются лишь цифровым идентификатором.

В этом случае помогут флаги. Давайте такие хитрые пункты меню пометим флагами:

 Гондурас = {
        url = /countries/honduras/
        flag = honduras
 }

 Уругвай = {
        url = /countries/uruguay/
        flag = uruguay
 }

А теперь, если при загрузки новости контроллер определил, что она относится к Гондурасу, то пусть он взведет соответствующий флаг:

 CMS::navigation()->flag('honduras');

И все - ссылка "Гондурас" считается текущей несмотря на то, что ее адрес никак не похож на адрес страницы, на которой мы в данный момент находимся.

Где мы сейчас находимся?

Часто возникает необходимость узнать - в каком месте навигации мы находимся в данный момент. Т.е. какая ссылка в данный момент является выбранной.

Для этого существует две функции. Функция selected_link позволяет получить такую ссылку из списка. Если в списке нет выбранной ссылки, то будет возвращен null.

 $link = CMS::navigation()->selected_link();
 $link = CMS::navigation()->level(1)->selected_link();

Другая функция - current_link является рекурсивной. Найдя в списке выбранную ссылку, она идет на следующей уровень и ищет выбранную ссылку в нем. И так пока есть куда идти.

 $link = CMS::navigation()->current_link();

Если страница, на которой мы в данный момент находимся, есть в навигации, то таким способом мы получим ссылку именно на нее. Если ее нет в навигации, то мы получим либо самую глубоко расположенную в иерархии выбранную ссылку, либо null если текущая страница вообще никак не согласуется с навигацией.

"Хлебные крошки"

Для тех кто не в курсе: "хлебные крошки" это такая навигация, в которой отображается путь к данной странице от корня сайта. Например: "Главная :: О компании :: История :: 2006 год". Список этих ссылок мы можем получить так:

 $route = CMS::navigation()->route();

Соответственно, и отобразить:

 <?= CMS::navigation()->route()->draw('route') ?>

Но конкретно в этом виде данную конструкцию использовать не рекомендуется. Дело в том, что мы вполне можем оказаться на странице, которая не предусмотрена в навигации. В этом случае функция route() вернет нам null. Действительно - если данная страница неизвестна навигации, то и пути к ней составить не представляется возможным. Лучше писать так:

 if ($route = CMS::navigation()->route()) print $route->draw('route');

В данном случае для "хлебных крошек" используется отдельный шаблон, хотя это и не обязательно. В специальном стандарном шаблоне кроме обычных ссылок отражается также ссылка на главную страницу, а сами ссылки отделяются характерным разделителем. Но все это - на усмотрение вебмастера.

Изменение шаблона вывода хлебных крошек

Кроме указанного выше способа вывода крошек существует вставка.

%ROUTE{}

По умолчанию хлебные крошки выводятся в стандартном шаблоне route, где в качестве разделителя используется знак ::. Соответственно во вставке этот шаблон используется по умолчанию. Для внесения каких-либо изменений необходимо переопределить шаблон.

Начиная с версии ядра 2.1.96, повился новый шаблон route-seo. Его особенности:

  • В шаблоне проверяется, не является ли ссылка текущей, если да, то она выводится текстом в теге span.
  • Появилась возможность замены разделителя без переопределения шаблона. Поменять его можно так:
CMS::navigation()->route()->draw('route-seo', array('separator' => '/* здесь нужный разделитель */'))

Фильтрация

Допустим, у нас есть некий перечень разделов, которые присутствует в навигации, но выводить их в главном меню нам не нужно. Это может быть какой-нибудь незначащий элемент (который, тем не менее, должн присутствовать в "хлебных крошках"). Или же меню разделяется на две части, которые нужно выводить по-разному.

Как же нам вывести не все пункты меню, а только избранные? Давайте применим фильтрацию. Прежде всего, в ссылки, которые хотим избрать, добавим какой-нибудь параметр (с произвольным именем и произвольным значением):

 О компании = {
        url = /about/
        topmenu = 1
 }

 Каталог = {
        url = /catalog/
        topmenu = 1
 }

 Дополнительная информация = {
        url = /info/
 }

Теперь отображаем с применением фильтрации:

 <div id="topmenu">
   <?= CMS::navigation()->filter('topmenu')->draw() ?>
 </div>

Применяя filter('topmenu'), мы тем самым рассматриваем не все ссылки, а только те, у которых установлен параметр "topmenu". Допустима и обратная фильтрация. Функция filter('!topmenu') возвратит те пункты, у которых данный параметр не установлен.

Эта функция допускает применение нескольких параметров, которые при этом комбинируются. Например вызов filter('topmenu','!nodraw') возвратит список ссылок, у которых установлен параметр "topmenu",но не установлен параметр "nodraw".

Получение ссылки или списка ссылок по идентификатору

Любую ссылку в навигации можно пометить идентификатором:

Каталог товаров = {
       url = /catalog/
       id = catalog
       sub = {
               ...
       }
}

После этого можно будет программно получить объект WebKit_Navigation_Link, содержащий данную ссылку:

 $link = CMS::navigation()->link_by_id('catalog');

Или же получить объект, содержащий список субссылок данной ссылки:

$linkset = CMS::navigation()->linkset_by_id('catalog');

В последнем случае если у ссылки нет субссылок (параметр sub не задан), то будет возвращен объект, содержащий пустой список.

Добавление ссылок в навигацию

Предположим, что у нас есть каталог товаров со сложной разветвленной структурой. С этой структурой нам нужно обращаться как с навигацией: отображать в виде меню, в "хлебных крошках" и т.п. Структура каталога хранится в БД, редактируется в админе, однако нам совсем не хочется дублировать ее в навигации. Следовательно, нужно реализовать механизм, автоматически добавляющий в навигацию всю структуру каталога.

Для этого прежде всего при помощи функции linkset_by_id получим место, в которое следует добавлять ссылки. А потом воспользуемся функцией add($title,$link). Синтаксис параметров аналогичен тому синтаксису, который мы используем при редактировании навигации в админе. Т.е первый параметр - текст ссылки, второй - либо строка (URL), либо массив.

 $linkset = CMS::navigation()->linkset_by_id('catalog');
 $linkset->add('Молотки','/catalog/hammers/');
 $linkset->add('Лопаты',array(
        'url' => '/catalog/shovels/',
        'id' => 'shovels',
 ));

Добавление ссылок в "Хлебные крошки"

Снова рассмотрим каталог. Допустим, мы находимся на странице с описанием одной конкретной позиции товара. И желаем, чтобы полный путь до нее отражался в "хлебных крошках", причем чтобы там была и ссылка на текущую страницу. Т.е. что-то вроде этого: "Главная :: Каталог :: Раздел :: Подраздел :: Товар".

Раздел и подраздел мы в навигацию добавили. Но добавлять в навигацию товары в каталоге - это уже слишком. Их могут быть тысячи, и навигация разрастется до немыслимых размеров. Но обратите внимание, что при отрисовке меню нам не требуется отображать ссылки на конкретные товары в каталоге. Наличие этих ссылок в навигации нам требуется только для одного: для отрисовки их в "хлебных крошках". Ну так давайте же не будем только ради одного этого добавлять товары в навигацию, а просто при генерации контроллером страницы товара дадим указание механизму навигации: при отрисовке "хлебных крошек" отобразить еще дополнительную ссылку (или не одну, если надо).

 CMS::navigation()->add_to_route('Огромная лопата','/catalog/shovels/huge/');

Синтаксис этой функции аналогичен синтаксису функции add().

17.02.2014
Все статьи