Меню с иконками на основе CMenu в Yii

Велика вероятность, что в новом или старом проекте на Yii появится необходимость изготовления меню с иконками. Это может понадобиться и при вёрстке уже готового шаблона. Рассмотрим несколько решений и сравним их между собой с точки зрения семантики и архитектурной чистоты.
Значки для пунктов меню, например, удобно использовать в панели администрирования (как на рисунке к этой записи).
Стандартное использование виджета CMenu в представлении выглядит так:
<?php $this->widget('zii.widgets.CMenu', array( 'items'=>array( array( 'label'=>'Home', 'url'=>array('site/index') ), // ... array( 'label'=>'Login', 'url'=>array('site/login'), 'visible'=>Yii::app()->user->isGuest ), ), ));
Массив пунктов для items удобно генерировать в модели. В модель Category можно добавить метод getMenuList() (его, кстати, можно взять из поведения DCategoryBehavior) и выводить меню «Категории» простым использованием этого метода:
class Category extends CActiveRecord { // ... public function getMenuList() { $items = array(); $models = $this->findAll(array('order'=>'name ASC'); foreach ($models as $model) { $items[] = array( 'label'=>$model->name, 'url'=>Yii::app()->createUrl('blog/category' array('id'=>$model->id)), ); } return $items; } }
<?php $this->widget('zii.widgets.CMenu', array( 'items'=>Category::model()->getMenuList(); ));
Мы немного отвлеклись на основы. Рассмотрим способы помещения значков в пункты меню.
1. Ручное размещение иконки как фон элемента
Этот способ предполагает размещение иконки фоном в инлайновом стиле элемента, что генерирует код <li style="background-image:url(...)"> для каждого пункта. Он используется, например, здесь.
<?php $this->widget('zii.widgets.CMenu', array( 'items'=>array( array( 'label'=>'Home', 'url'=>array('site/index'), 'linkOptions'=>array('style'=>'background-image:url(/icons/home.gif);') ), // ... ), ));
2. Ручное размещение тега IMG в надписи
Этот способ подразумевает непосредственную вставку самим пользователем тега <img src="..." /> к надписи для передачи полю label. Для поддержки HTML кода в надписях нужно отключить экранирование HTML-сущностей указав 'encodeLabel'=>false:
<?php $this->widget('zii.widgets.CMenu', array( 'encodeLabel'=>false, 'items'=>array( array( 'label'=>'<img src="/icons/home.gif" /> Home', 'url'=>array('site/index') ), // ... ), ));
Так как кодирование отключено, то нам нужно в этом случае самим экранировать символы названия категории в методе getMenuList() вызывая CHtml::encode($model->name):
<?php $this->widget('zii.widgets.CMenu', array( 'encodeLabel'=>false, 'items'=>Category::model()->getMenuList(); ));
class Category extends CActiveRecord { protected $iconPath = 'images/icons', // ... public function getMenuList() { $items = array(); $models = $this->findAll(array('order'=>'name ASC'); foreach ($models as $model) { $image = CHtml::image($model->getIconUrl()); $items[] = array( 'label'=>$image . ' ' . CHtml::encode($model->name), 'url'=>Yii::app()->createUrl('blog/category' array('id'=>$model->id)), ); } return $items; } public function getIconUrl() { return Yii::app()->request->baseUrl . '/' . $this->iconPath . '/' . $this->icon; } }
Оценка решений
Оба рассморенных способа немного «костыльные», так как неуниверсальные и несемантические. Несемантические – так как заставяют нас заботиться о представлении пунктов и генерировать HTML/CSS код. Неуниверсальны и неполиморфны – потому что не позволяют прозрачно использовать один и тот же метод getMenuList() в разных по типу меню.
Нам же может потребоваться выводить эти же категории как выпадающие подпункты без иконок в главном меню в шапке сайта
<?php $this->widget('zii.widgets.CMenu', array( 'items'=>array( array( 'label'=>'Home', 'url'=>array('site/index'), ), array( 'label'=>'Blog', 'url'=>array('blog/index'), 'items'=>Category::model()->getMenuList(); ), array( 'label'=>'Contacts', 'url'=>array('site/contacts'), ), // ... ), ));
и одновременно как меню разделов с иконками в сайдбаре:
<?php $this->widget('zii.widgets.CMenu', array( 'items'=>Category::model()->getMenuList(); ));
Для каждого типа меню тогда придётся делать свой метод вроде getSimpleMenuList(), getIconMenuList() и т.п.
Следовательно, оба этих подхода нарушают архитектурную чистоту кода, так как поставщик данных, по-хорошему, не должен производить никаких стилистических манипуляций с данными.
3. Семантическое решение
С семантической точки зрения более правильно генерацию HTML-разметки или CSS-стилей производить в самом виджете на основе переданных ему от модели данных. Для этого нужно создать свой модернизированный виджет меню для каждого конкретного случая. Отнаследуемся от стандартного класса меню и перепишем в своём метод генерации пункта:
Yii::import('zii.widgets.CMenu'); /** * @author ElisDN <mail@elisdn.ru> * @link https://elisdn.ru */ class DIconMenu extends CMenu { public $iconsPath = '/'; protected function renderMenuItem($item) { $icon = !empty($item['icon']) ? CHtml::image($this->iconsPath . $item['icon'], $item['label']) : ''; $options = isset($item['linkOptions']) ? $item['linkOptions'] : array(); if(isset($item['url'])) { if ($this->linkLabelWrapper !== null) $label = '<' . $this->linkLabelWrapper . '>' . $item['label'] . '</' . $this->linkLabelWrapper . '>'; else $label = $item['label']; return $icon . CHtml::link($label, $item['url'], $options); } else return $icon . CHtml::tag('span', $options, $item['label']); } }
Здесь мы воспользовались вариантом добавления тега <img>. При желании можно подсмотреть реализацию варианта с background-image:url(...) в компоненте EIconizedMenu.
Теперь где это необходимо (например, в главном меню или в боковой колонке сайта) использовать это меню вместо стандартного:
<?php $this->widget('DIconMenu', array( 'iconPath'=>Yii::app()->request->baseUrl . '/icons/', 'items'=>array( array( 'label'=>'Home', 'url'=>array('site/index'), 'icon'=>'nome.png' ), ), ));
Мы добавили новый параметр icon для каждого пункта и параметр iconPath для указания пути. Модернизированная модель Category теперь может выглядеть так
class Category extends CActiveRecord { public $iconsPath = '/'; // ... public function getMenuList() { $items = array(); $models = $this->findAll(array('order'=>'name ASC'); foreach ($models as $model) { $items[] = array( 'label'=>$model->name, 'icon'=>$model->getIconUrl(), 'url'=>$model->getUrl(), ); } return $items; } public function getIconUrl() { return Yii::app()->request->baseUrl . '/' . $this->iconPath . '/' . $this->icon; } private $_url; public function getUrl() { if ($this->_url === null) $this->_url = Yii::app()->createUrl('blog/category' array('id'=>$model->id)); return $this->_url; } }
То есть этот метод ничем не отличается от первоначального стандартного варианта, за исключением добавленного поля иконки. Один и тот же getMenuList() можно полиморфно использовать для DIconMenu, CMenu и любых других виджетов меню.
Этот виджет сформирует меню с иконками вида
<ul> <li><img src="/icons/home.png" alt="Home" /><a href="/">Home</a></li> </ul>
Но вы можете при желании написать свой класс, который как в EIconizedMenu будет добавлять стили к пунктам вроде
<ul> <li><a href="/" style="background-image:url('/icons/home.png');" alt="Home" />Home</a></li> </ul>
На этом всё. Не бойтесь модернизировать готовые чужие классы наследуясь от них и переопределяя некоторые их методы. Это намного удобнее, чем каждый раз придумывать что-то сверхъестественное или чем изменять системные файлы.
Марат ДолотовХорошая статья! Только вот у меня в моделе $this->createUrl не сработал, заменил его просто на array. Спасибо!
Дмитрий ЕлисеевДа, точно. Должно быть Yii::app()->createUrl(...) или array(...).
ЕвгенийОтличная статья. Такие мелочи помогают сделать хороший продукт.
АлексейСпасибо Вам! Пишите еще про Yii = )
klayА вместо
<ul> <li> <a href="#">Item</a> </li> <li> <a href="#">Item</a> <ul> ....... </ul> </li> </ul>можно использовать разметку с div? так чтобы вложенность тоже строилась с использованием div
Дмитрий ЕлисеевТолько если сами переопределите методы renderMenu и renderMenuRecursive в своём наследнике класса CMenu. А какой смысл в div?
klayЯ сделал вот так http://vpaste.net/ZcHTo
Ну и использую вот так:
<?php Yii::app()->controller->widget('XMenu', array( 'items' => $items, // в классе виджета подготовил 'activeCssClass' => 'current', 'itemTagName' => 'div', 'containerTagName' => 'div', 'submenuHtmlOptions' => array( 'class' => 'navigation-list' ) )); ?>Насколько это уродливо? :)
klayСмысла наверное особого нет, просто верстальщик так сверстал http://i.imgur.com/DLnH0fK.png
Дмитрий ЕлисеевЛучше наоборот div на ul заменить и CSS немного подправить.