Topmenu.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Theme\Block\Html;
  7. use Magento\Framework\Data\Tree\Node;
  8. use Magento\Framework\Data\Tree\NodeFactory;
  9. use Magento\Framework\Data\TreeFactory;
  10. use Magento\Framework\DataObject\IdentityInterface;
  11. use Magento\Framework\View\Element\Template;
  12. /**
  13. * Html page top menu block
  14. *
  15. * @api
  16. * @since 100.0.2
  17. */
  18. class Topmenu extends Template implements IdentityInterface
  19. {
  20. /**
  21. * Cache identities
  22. *
  23. * @var array
  24. */
  25. protected $identities = [];
  26. /**
  27. * Top menu data tree
  28. *
  29. * @var \Magento\Framework\Data\Tree\Node
  30. */
  31. protected $_menu;
  32. /**
  33. * @var NodeFactory
  34. */
  35. private $nodeFactory;
  36. /**
  37. * @var TreeFactory
  38. */
  39. private $treeFactory;
  40. /**
  41. * @param Template\Context $context
  42. * @param NodeFactory $nodeFactory
  43. * @param TreeFactory $treeFactory
  44. * @param array $data
  45. */
  46. public function __construct(
  47. Template\Context $context,
  48. NodeFactory $nodeFactory,
  49. TreeFactory $treeFactory,
  50. array $data = []
  51. ) {
  52. parent::__construct($context, $data);
  53. $this->nodeFactory = $nodeFactory;
  54. $this->treeFactory = $treeFactory;
  55. }
  56. /**
  57. * Get block cache life time
  58. *
  59. * @return int
  60. * @since 100.1.0
  61. */
  62. protected function getCacheLifetime()
  63. {
  64. return parent::getCacheLifetime() ?: 3600;
  65. }
  66. /**
  67. * Get top menu html
  68. *
  69. * @param string $outermostClass
  70. * @param string $childrenWrapClass
  71. * @param int $limit
  72. * @return string
  73. */
  74. public function getHtml($outermostClass = '', $childrenWrapClass = '', $limit = 0)
  75. {
  76. $this->_eventManager->dispatch(
  77. 'page_block_html_topmenu_gethtml_before',
  78. ['menu' => $this->getMenu(), 'block' => $this, 'request' => $this->getRequest()]
  79. );
  80. $this->getMenu()->setOutermostClass($outermostClass);
  81. $this->getMenu()->setChildrenWrapClass($childrenWrapClass);
  82. $html = $this->_getHtml($this->getMenu(), $childrenWrapClass, $limit);
  83. $transportObject = new \Magento\Framework\DataObject(['html' => $html]);
  84. $this->_eventManager->dispatch(
  85. 'page_block_html_topmenu_gethtml_after',
  86. ['menu' => $this->getMenu(), 'transportObject' => $transportObject]
  87. );
  88. $html = $transportObject->getHtml();
  89. return $html;
  90. }
  91. /**
  92. * Count All Subnavigation Items
  93. *
  94. * @param \Magento\Backend\Model\Menu $items
  95. * @return int
  96. */
  97. protected function _countItems($items)
  98. {
  99. $total = $items->count();
  100. foreach ($items as $item) {
  101. /** @var $item \Magento\Backend\Model\Menu\Item */
  102. if ($item->hasChildren()) {
  103. $total += $this->_countItems($item->getChildren());
  104. }
  105. }
  106. return $total;
  107. }
  108. /**
  109. * Building Array with Column Brake Stops
  110. *
  111. * @param \Magento\Backend\Model\Menu $items
  112. * @param int $limit
  113. * @return array|void
  114. *
  115. * @todo: Add Depth Level limit, and better logic for columns
  116. */
  117. protected function _columnBrake($items, $limit)
  118. {
  119. $total = $this->_countItems($items);
  120. if ($total <= $limit) {
  121. return;
  122. }
  123. $result[] = ['total' => $total, 'max' => (int)ceil($total / ceil($total / $limit))];
  124. $count = 0;
  125. $firstCol = true;
  126. foreach ($items as $item) {
  127. $place = $this->_countItems($item->getChildren()) + 1;
  128. $count += $place;
  129. if ($place >= $limit) {
  130. $colbrake = !$firstCol;
  131. $count = 0;
  132. } elseif ($count >= $limit) {
  133. $colbrake = !$firstCol;
  134. $count = $place;
  135. } else {
  136. $colbrake = false;
  137. }
  138. $result[] = ['place' => $place, 'colbrake' => $colbrake];
  139. $firstCol = false;
  140. }
  141. return $result;
  142. }
  143. /**
  144. * Add sub menu HTML code for current menu item
  145. *
  146. * @param \Magento\Framework\Data\Tree\Node $child
  147. * @param string $childLevel
  148. * @param string $childrenWrapClass
  149. * @param int $limit
  150. * @return string HTML code
  151. */
  152. protected function _addSubMenu($child, $childLevel, $childrenWrapClass, $limit)
  153. {
  154. $html = '';
  155. if (!$child->hasChildren()) {
  156. return $html;
  157. }
  158. $colStops = [];
  159. if ($childLevel == 0 && $limit) {
  160. $colStops = $this->_columnBrake($child->getChildren(), $limit);
  161. }
  162. $html .= '<ul class="level' . $childLevel . ' ' . $childrenWrapClass . '">';
  163. $html .= $this->_getHtml($child, $childrenWrapClass, $limit, $colStops);
  164. $html .= '</ul>';
  165. return $html;
  166. }
  167. /**
  168. * Recursively generates top menu html from data that is specified in $menuTree
  169. *
  170. * @param \Magento\Framework\Data\Tree\Node $menuTree
  171. * @param string $childrenWrapClass
  172. * @param int $limit
  173. * @param array $colBrakes
  174. * @return string
  175. *
  176. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  177. * @SuppressWarnings(PHPMD.NPathComplexity)
  178. */
  179. protected function _getHtml(
  180. \Magento\Framework\Data\Tree\Node $menuTree,
  181. $childrenWrapClass,
  182. $limit,
  183. array $colBrakes = []
  184. ) {
  185. $html = '';
  186. $children = $menuTree->getChildren();
  187. $parentLevel = $menuTree->getLevel();
  188. $childLevel = $parentLevel === null ? 0 : $parentLevel + 1;
  189. $counter = 1;
  190. $itemPosition = 1;
  191. $childrenCount = $children->count();
  192. $parentPositionClass = $menuTree->getPositionClass();
  193. $itemPositionClassPrefix = $parentPositionClass ? $parentPositionClass . '-' : 'nav-';
  194. /** @var \Magento\Framework\Data\Tree\Node $child */
  195. foreach ($children as $child) {
  196. if ($childLevel === 0 && $child->getData('is_parent_active') === false) {
  197. continue;
  198. }
  199. $child->setLevel($childLevel);
  200. $child->setIsFirst($counter == 1);
  201. $child->setIsLast($counter == $childrenCount);
  202. $child->setPositionClass($itemPositionClassPrefix . $counter);
  203. $outermostClassCode = '';
  204. $outermostClass = $menuTree->getOutermostClass();
  205. if ($childLevel == 0 && $outermostClass) {
  206. $outermostClassCode = ' class="' . $outermostClass . '" ';
  207. $currentClass = $child->getClass();
  208. if (empty($currentClass)) {
  209. $child->setClass($outermostClass);
  210. } else {
  211. $child->setClass($currentClass . ' ' . $outermostClass);
  212. }
  213. }
  214. if (is_array($colBrakes) && count($colBrakes) && $colBrakes[$counter]['colbrake']) {
  215. $html .= '</ul></li><li class="column"><ul>';
  216. }
  217. $html .= '<li ' . $this->_getRenderedMenuItemAttributes($child) . '>';
  218. $html .= '<a href="' . $child->getUrl() . '" ' . $outermostClassCode . '><span>' . $this->escapeHtml(
  219. $child->getName()
  220. ) . '</span></a>' . $this->_addSubMenu(
  221. $child,
  222. $childLevel,
  223. $childrenWrapClass,
  224. $limit
  225. ) . '</li>';
  226. $itemPosition++;
  227. $counter++;
  228. }
  229. if (is_array($colBrakes) && count($colBrakes) && $limit) {
  230. $html = '<li class="column"><ul>' . $html . '</ul></li>';
  231. }
  232. return $html;
  233. }
  234. /**
  235. * Generates string with all attributes that should be present in menu item element
  236. *
  237. * @param \Magento\Framework\Data\Tree\Node $item
  238. * @return string
  239. */
  240. protected function _getRenderedMenuItemAttributes(\Magento\Framework\Data\Tree\Node $item)
  241. {
  242. $html = '';
  243. $attributes = $this->_getMenuItemAttributes($item);
  244. foreach ($attributes as $attributeName => $attributeValue) {
  245. $html .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"';
  246. }
  247. return $html;
  248. }
  249. /**
  250. * Returns array of menu item's attributes
  251. *
  252. * @param \Magento\Framework\Data\Tree\Node $item
  253. * @return array
  254. */
  255. protected function _getMenuItemAttributes(\Magento\Framework\Data\Tree\Node $item)
  256. {
  257. $menuItemClasses = $this->_getMenuItemClasses($item);
  258. return ['class' => implode(' ', $menuItemClasses)];
  259. }
  260. /**
  261. * Returns array of menu item's classes
  262. *
  263. * @param \Magento\Framework\Data\Tree\Node $item
  264. * @return array
  265. */
  266. protected function _getMenuItemClasses(\Magento\Framework\Data\Tree\Node $item)
  267. {
  268. $classes = [];
  269. $classes[] = 'level' . $item->getLevel();
  270. $classes[] = $item->getPositionClass();
  271. if ($item->getIsCategory()) {
  272. $classes[] = 'category-item';
  273. }
  274. if ($item->getIsFirst()) {
  275. $classes[] = 'first';
  276. }
  277. if ($item->getIsActive()) {
  278. $classes[] = 'active';
  279. } elseif ($item->getHasActive()) {
  280. $classes[] = 'has-active';
  281. }
  282. if ($item->getIsLast()) {
  283. $classes[] = 'last';
  284. }
  285. if ($item->getClass()) {
  286. $classes[] = $item->getClass();
  287. }
  288. if ($item->hasChildren()) {
  289. $classes[] = 'parent';
  290. }
  291. return $classes;
  292. }
  293. /**
  294. * Add identity
  295. *
  296. * @param string|array $identity
  297. * @return void
  298. */
  299. public function addIdentity($identity)
  300. {
  301. if (!in_array($identity, $this->identities)) {
  302. $this->identities[] = $identity;
  303. }
  304. }
  305. /**
  306. * Get identities
  307. *
  308. * @return array
  309. */
  310. public function getIdentities()
  311. {
  312. return $this->identities;
  313. }
  314. /**
  315. * Get tags array for saving cache
  316. *
  317. * @return array
  318. * @since 100.1.0
  319. */
  320. protected function getCacheTags()
  321. {
  322. return array_merge(parent::getCacheTags(), $this->getIdentities());
  323. }
  324. /**
  325. * Get menu object.
  326. *
  327. * Creates \Magento\Framework\Data\Tree\Node root node object.
  328. * The creation logic was moved from class constructor into separate method.
  329. *
  330. * @return Node
  331. * @since 100.1.0
  332. */
  333. public function getMenu()
  334. {
  335. if (!$this->_menu) {
  336. $this->_menu = $this->nodeFactory->create(
  337. [
  338. 'data' => [],
  339. 'idField' => 'root',
  340. 'tree' => $this->treeFactory->create()
  341. ]
  342. );
  343. }
  344. return $this->_menu;
  345. }
  346. }