Меню с иконками на основе 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 http://www.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 немного подправить.

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>