Меню с иконками на основе 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 = )
А вместо
можно использовать разметку с div? так чтобы вложенность тоже строилась с использованием div
Только если сами переопределите методы renderMenu и renderMenuRecursive в своём наследнике класса CMenu. А какой смысл в div?
Я сделал вот так http://vpaste.net/ZcHTo
Ну и использую вот так:
Насколько это уродливо? :)
Смысла наверное особого нет, просто верстальщик так сверстал http://i.imgur.com/DLnH0fK.png
Лучше наоборот div на ul заменить и CSS немного подправить.