1С-Битрикс. Пишем человеческий шаблон многоуровнего меню каталога
Не буду писать про боль при интегации дефотного шаблона. Давайте просто напишем свой, с блекджеком и вот этим всем.
В заметке (интересного):
- построение древовидного массива без рекурсии (неограниченной вложенности)
- анонимная рекурсивная функция
- формирование урлов для секций супер-быстрым способом
- поработаем с сущностями инфоблока через ORM D7
Вводные данные
Предположим, что шаблон вашего сайта называется main. Все пути в заметке будут на основе его.
Шаблон меню обзовём template-with-ul-recursive.
Тип меню будет catalog-left-menu
Вызываем компонент bitrix:menu
$APPLICATION->IncludeComponent( "bitrix:menu", "template-with-ul-recursive", array( "ROOT_MENU_TYPE" => "catalog-left-menu", "MENU_CACHE_TYPE" => "N", "MENU_CACHE_TIME" => "3600", "MENU_CACHE_USE_GROUPS" => "N", "MENU_CACHE_GET_VARS" => array( ), "MAX_LEVEL" => "4", "CHILD_MENU_TYPE" => "", "USE_EXT" => "Y", "DELAY" => "N", "ALLOW_MULTI_SELECT" => "Y", "CSS_CLASS_OUTER" => "left-menu", "COMPONENT_TEMPLATE" => "template-with-ul-recursive" ), false );
Из важного:
- MENU_CACHE_TYPE в N - кеширование обязательно выключаем, у нас будет своё
- USE_EXT в Y - пункты меню будем наполнять в .ext файле
- ALLOW_MULTI_SELECT в Y - корректно определит автивности всех вложенностей
Остальные параметры индивидуальны.
CSS-класс для каждого пункта меню
Для секции добавим (пригодится) новое пользовательское свойство с именем UF_CSS_CLASS_MENU и типом строка. Свойство можно добавить на странице редактирования любого раздела нужного инфоблока (вкладка "Доп. поля")
Наполняем пунктами меню
В корне сайта создаём файл с именем .catalog-left-menu.menu_ext.php и содержимым:
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); /** @var array $aMenuLinks */ use Bitrix\Iblock\SectionTable; use Bitrix\Main\Loader; use Bitrix\Iblock\IblockTable; use Bitrix\Main\Data\Cache; use Bitrix\Main\Data\TaggedCache; use Bitrix\Main\Entity; use Bitrix\Main\Application; $catalogIblockId = 1; $aMenuLinksExt = array(); $cache = Cache::createInstance(); $cacheTime = 86400; $cacheId = 'catalog-left-menu-ext'; if ($cache->initCache($cacheTime, $cacheId, '/olegpro/bitrix.menu_ext/catalog-left-menu')) { $aMenuLinksExt = $cache->GetVars(); } elseif ($cache->startDataCache()) { if (Loader::includeModule('iblock')) { $iblockIterator = IblockTable::getList(array( 'select' => array('SECTION_PAGE_URL', 'CODE'), 'filter' => array('=ID' => $catalogIblockId), 'limit' => 1 )); if ($iblock = $iblockIterator->fetch()) { $connection = Application::getConnection(); $entityUtsTableName = sprintf('b_uts_iblock_%s_section', $catalogIblockId); $entityUtsTableNameTableExists = $connection->isTableExists($entityUtsTableName); if ($entityUtsTableNameTableExists) { $entityUts = Entity\Base::compileEntity('utsSectionOlegproLeftCatalogMenu' . randString(4), [ 'VALUE_ID' => ['data_type' => 'integer'], 'UF_CSS_CLASS_MENU' => ['data_type' => 'string'], ], ['table_name' => sprintf('b_uts_iblock_%s_section', $catalogIblockId)] ); } $sectionIteratorParameters = array( 'select' => [ 'CODE', 'NAME', 'ID', 'DEPTH_LEVEL', 'IBLOCK_SECTION_ID', ], 'filter' => [ '=IBLOCK_ID' => $catalogIblockId, '<=DEPTH_LEVEL' => 4, '=ACTIVE' => 'Y', '=GLOBAL_ACTIVE' => 'Y', ], 'order' => [ 'LEFT_MARGIN' => 'ASC', ], 'runtime' => [], ); if ($entityUtsTableNameTableExists && isset($entityUts) && is_object($entityUts)) { $sectionIteratorParameters['select']['UF_CSS_CLASS_MENU'] = 'UF.UF_CSS_CLASS_MENU'; $sectionIteratorParameters['runtime'][] = new Entity\ReferenceField('UF', $entityUts, ['=this.ID' => 'ref.VALUE_ID'] ); } $sectionIterator = SectionTable::getList($sectionIteratorParameters); $sections = []; while ($section = $sectionIterator->fetch()) { $sections[$section['ID']] = $section; } unset($section); foreach ($sections as $section) { $sectionCodes = [ $section['CODE'] ]; $parentId = $section['IBLOCK_SECTION_ID']; while (isset($parentId)) { if (isset($sections[$parentId])) { $sectionCodes[] = $sections[$parentId]['CODE']; $parentId = $sections[$parentId]['IBLOCK_SECTION_ID']; } else { $parentId = null; } } $aMenuLinksExt[] = array( $section['NAME'], str_replace( array( '#SITE_DIR#', '#IBLOCK_CODE#', '#SECTION_CODE_PATH#', ), array( SITE_DIR, $iblock['CODE'], implode('/', array_reverse($sectionCodes)) ), $iblock['SECTION_PAGE_URL'] ), array(), array( 'ID' => $section['ID'], 'DEPTH_LEVEL' => $section['DEPTH_LEVEL'], 'CODE' => $section['CODE'], 'IBLOCK_SECTION_ID' => $section['IBLOCK_SECTION_ID'], 'UF_CSS_CLASS_MENU' => $section['UF_CSS_CLASS_MENU'], ) ); } if (defined('BX_COMP_MANAGED_CACHE')) { $tagCache = new TaggedCache(); $tagCache->startTagCache('/olegpro/bitrix.menu_ext/catalog-left-menu'); $tagCache->registerTag(sprintf('iblock_id_%s', $catalogIblockId)); $tagCache->endTagCache(); } } else { $cache->abortDataCache(); } } $cache->endDataCache($aMenuLinksExt); } $aMenuLinks = array_merge($aMenuLinks, $aMenuLinksExt);
Значение переменной $catalogIblockId меняем на ID инфоблока каталога.
Важно! Урлы секций обрабатываются только для варианта #SECTION_CODE_PATH#
Создаём папку и файлы шаблона
Создадим папку для нашего шаблона local/templates/main/components/bitrix/menu/template-with-ul-recursive
Создаём в этой папке файл result_modifier.php. В нём мы соберём массив нужного нам формата, чтобы потом в шаблоне было удобно выводить само меню.
/** * Created by olegpro.ru. * User: Oleg Maksimenko <oleg.39style@gmail.com> * Date: 26.08.2016. Time: 11:15 */ if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); /** @var array $arResult */ /** @var array $arParams */ /** @var CBitrixComponentTemplate $this */ // [section_id => [all parents ids]] $selectedItems = []; // [section_id => parent_section_id] $mapSectionIds = []; // [section_id => [all parents ids]] $mapSectionParentIds = []; $allSelectedItems = []; foreach ($arResult as $arItem) { if ($arItem['SELECTED'] && isset($arItem['PARAMS']['ID'])) { $selectedItems[$arItem['PARAMS']['ID']] = 1; } if (isset($arItem['PARAMS']['ID']) && array_key_exists('IBLOCK_SECTION_ID', $arItem['PARAMS'])) { $mapSectionIds[$arItem['PARAMS']['ID']] = $arItem['PARAMS']['IBLOCK_SECTION_ID']; } } $selectedDirectItemsId = $selectedItems; foreach ($mapSectionIds as $sectionId => $sectionParentId) { $sectionsParent = []; $parentId = $sectionParentId; while (isset($parentId)) { $sectionsParent[] = $parentId; $parentId = isset($mapSectionIds[$parentId]) ? $mapSectionIds[$parentId] : null; } $mapSectionParentIds[$sectionId] = $sectionsParent; if (isset($selectedItems[$sectionId])) { $selectedItems[$sectionId] = $sectionsParent; $allSelectedItems = array_merge($allSelectedItems, $sectionsParent); } } if (!empty($allSelectedItems)) { $allSelectedItems = array_flip($allSelectedItems); } foreach ($arResult as $key => $arItem) { if ( isset( $arItem['PARAMS']['ID'], $allSelectedItems[$arItem['PARAMS']['ID']] ) ) { $arItem['SELECTED'] = true; $arResult[$key] = $arItem; } } unset($key, $arItem); // Generate hierarchical tree $map = [ 0 => [ 'CHILDREN' => [] ] ]; foreach ($arResult as &$arItem) { $arItem['CHILDREN'] = []; $map[$arItem['PARAMS']['ID']] = &$arItem; } foreach ($arResult as &$arItem) { $map[(int)$arItem['PARAMS']['IBLOCK_SECTION_ID']]['CHILDREN'][] = &$arItem; } $arResultCopy = $arResult; $arResult = [ 'CHILDREN' => $map[0]['CHILDREN'], 'SELECTED_DIRECT_IDS' => $selectedDirectItemsId, ]; $map = null; unset($map);
Создаём в папке local/templates/main/components/bitrix/menu/template-with-ul-recursive файл с параметрами .parameters.php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); use Bitrix\Main\Localization\Loc; Loc::loadMessages(__FILE__); $arTemplateParameters = array( 'CSS_CLASS_OUTER' => array( 'NAME' => Loc::getMessage('MENU_CSS_CLASS_OUTER'), 'TYPE' => 'STRING', 'DEFAULT' => 'menu', ), 'CSS_CLASS_ITEM' => array( 'NAME' => Loc::getMessage('MENU_CSS_CLASS_ITEM'), 'TYPE' => 'STRING', 'DEFAULT' => 'menu', ), 'CSS_CLASS_ITEM_ACTIVE' => array( 'NAME' => Loc::getMessage('MENU_CSS_CLASS_ITEM_ACTIVE'), 'TYPE' => 'STRING', 'DEFAULT' => 'menu', ), );
Создаём в папке local/templates/main/components/bitrix/menu/template-with-ul-recursive/lang/ru/ файл .parameters.php с языковыми фразами параметров
/** * Created by olegpro.ru * User: Oleg Maksimenko <oleg.39style@gmail.com> * Date: 16.11.2015. Time: 15:05 */ $MESS['MENU_CSS_CLASS_OUTER'] = 'CSS класс для списка (ul)'; $MESS['MENU_CSS_CLASS_ITEM'] = 'CSS класс для элемента списка (li)'; $MESS['MENU_CSS_CLASS_ITEM_ACTIVE'] = 'CSS класс для активного элемента списка';
Создаём в папке local/templates/main/components/bitrix/menu/template-with-ul-recursive/ файл template.php с нашим шаблоном
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); /** @var array $arResult */ /** @var array $arParams */ /** @var CBitrixComponentTemplate $this */ $this->setFrameMode(true); if (!empty($arResult)) { $menuRecursive = function ($items, $level = 1) use ($arParams, $arResult, &$menuRecursive) { $items = array_values($items); $countItems = sizeof($items); if (sizeof($items) > 0) { $cssClassOuter = array_filter( array_map('trim', explode(' ', $arParams['CSS_CLASS_OUTER'])) ); $cssClassOuter = array_map( function ($value) use ($level, &$cssClassOuter) { return implode(' ', [$value, sprintf('%s-level%d', $value, $level)]); }, $cssClassOuter ); <ul class=" echo implode(' ', $cssClassOuter) "> foreach ($items as $i => $arItem) { $cssClasses = [ $arParams['CSS_CLASS_ITEM'], ]; if (($i + 1) % 2 == 0) { $cssClasses[] = 'even'; } else { $cssClasses[] = 'odd'; } if ($i == 0) { $cssClasses[] = 'first'; } if ($countItems == $i + 1) { $cssClasses[] = 'last'; } if ($arItem['SELECTED']) { $cssClasses[] = $arParams['CSS_CLASS_ITEM_ACTIVE']; } if (isset($arItem['PARAMS'], $arItem['PARAMS']['CLASS']) && strlen(trim($arItem['PARAMS']['CLASS']))) { $cssClasses[] = trim($arItem['PARAMS']['CLASS']); } if ($arItem['SELECTED'] && isset($arResult['SELECTED_DIRECT_IDS'][$arItem['PARAMS']['ID']])) { <li class=" echo implode(' ', $cssClasses) "> <span> echo $arItem['TEXT'] </span> $menuRecursive($arItem['CHILDREN'], $arItem['PARAMS']['DEPTH_LEVEL'] + 1) </li> } else { <li class=" echo implode(' ', $cssClasses) "> <a href=" echo $arItem['LINK'] "> echo $arItem['TEXT'] </a> $menuRecursive($arItem['CHILDREN'], $arItem['PARAMS']['DEPTH_LEVEL'] + 1) </li> } } </ul> } }; $menuRecursive($arResult['CHILDREN']); }
Вот, собственно, и всё. Надеюсь, вам понравилось Подписывайтесь на канал, ставьте пальцы вверх и т.п. :)
6 комментариев
там вся боль успешно убирается через result_modifier.php https://dev.1c-bitrix.ru/community/webdev/user/25535/blog/hierarchical-menus-and-resultmodifier/
я в этом нагрмождении кода вижу только очередное доказательство ненужности D7 для инфоблоков
Очень интересно. Недавно изучаю битрикс , как и программирования и Ваши статьи дают понимания Битрикса, а так же служат для меня примером, как грамотно писать код. Спасибо за труды.
Сергей, спасибо!
http://prnt.sc/eimes5
Это что за сумашествие? А switch хотя б не судьба использовать?
Чем здесь поможет свич?
Посмотри ещё раз на свой скриншот и покажи свой вариант, чтобы работал точно так же, как у меня.
I agree. whole tape of logic that is hard to read. why it is impossible to take out processing to the class and speaking methods