You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
12 KiB

  1. <?php
  2. /*
  3. * The MIT License
  4. *
  5. * Copyright 2019 Blobt.
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. namespace iron\widgets;
  26. use Yii;
  27. use yii\base\Widget;
  28. use yii\helpers\ArrayHelper;
  29. use yii\helpers\Html;
  30. use yii\helpers\Url;
  31. /**
  32. * 参照yii\widgets\Menu,根据AdminLTE样式从写的一个小物件
  33. * @author Blobt
  34. * @email 380255922@qq.com
  35. * 使用例子
  36. * <?php
  37. * echo Menu::widget([
  38. * 'items' => [
  39. * ['label' => 'MAIN NAVIGATION', 'is_header' => true],
  40. * ['label' => 'Documentation', 'url' => 'https:www.baidu.com', 'icon' => 'fa-book'],
  41. * ['label' => 'Products', 'url' => ['product/index'], 'items' => [
  42. * ['label' => 'New Arrivals', 'url' => ['product/index', 'tag' => 'new']],
  43. * ['label' => 'Most Popular', 'url' => ['product/index', 'tag' => 'popular']],
  44. * ]],
  45. * ['label' => 'Login', 'url' => ['site/login'], 'visible' => Yii::$app->user->isGuest],
  46. * ]
  47. * ]);
  48. * ?>
  49. *
  50. *
  51. */
  52. class Menu extends Widget
  53. {
  54. /**
  55. * @var array 菜单的item数组。
  56. * 菜单的item同时也是一个数组,结构如下所述:
  57. * lable: string, optional,指定选项的label
  58. * encode:boolean, optional,label是否需要转义
  59. * url: string or array, optional, 生产菜单的路径
  60. * 产生结果和yii的Url::to()方法一致,并且会套用[[linkTemplate]]模板
  61. * visible: boolean, optional, 选项是否可见,默认是true(可见)
  62. * active: boolean or Closure, optional, 是否被选中
  63. * 如果为true,则会在css中添加[[activeCssClass]]
  64. * 当使用Closure时候,匿名函数必须是`function ($item, $hasActiveChild, $isItemActive, $widget)`
  65. * 匿名函数必须返回true或者false
  66. * 如果不是使用匿名函数,就会使用[[isItemActive]]来判断item是否被选中
  67. * items: array, optional, 指定子菜单选项,格式和父菜单是一样的
  68. * template: string, optional,选项的渲染模板
  69. * submenuTemplate: string, optional, 子菜单渲染模板
  70. * options: array, optional, 指定html属性
  71. */
  72. public $items = [];
  73. /**
  74. * @var array html属性,这些指定的属性会加到所有的item中
  75. */
  76. public $itemOptions = [];
  77. /**
  78. * @var string 链接的渲染模板
  79. */
  80. public $linkTemplateWithSub = "<a href=\"{url}\" class=\"nav-link {class}\"><i class=\" nav-icon fas {icon}\"></i><p>{label}<i class=\"right fas fa-angle-left\"></i></p></a>";
  81. public $linkTemplateNoSub = "<a href=\"{url}\" class=\"nav-link {class}\"><i class=\"far {icon} nav-icon\"></i><p>{label}</p></a>";
  82. /**
  83. * @var string label的渲染模板
  84. */
  85. public $labelTemplate = '{label}';
  86. /**
  87. * @var string 子菜单的渲染模板
  88. */
  89. public $submenuTemplate = "\n<ul class=\"nav nav-treeview\">\n{items}\n</ul>\n";
  90. /**
  91. * @var boolean label是否要进行html转义
  92. */
  93. public $encodeLabels = true;
  94. /**
  95. * @var string 被选中的菜单的CSS类
  96. */
  97. public $activeCssClass = 'active';
  98. /**
  99. * @var string 控制二级菜单开合
  100. */
  101. public $menuOpenClass = 'menu-open';
  102. /**
  103. * @var boolean 是否根据路由去判断菜单项是否被选中
  104. */
  105. public $activateItems = true;
  106. /**
  107. * @var boolean 当某一个子菜单的选中时候是否也关联选中父菜单
  108. */
  109. public $activateParents = true;
  110. /**
  111. * @var boolean 如果item的url没有设置时,是否不显示该item
  112. */
  113. public $hideEmptyItems = true;
  114. /**
  115. * @var array 菜单容器标签的属性
  116. */
  117. public $options = [
  118. 'class' => 'nav nav-pills nav-sidebar flex-column',
  119. 'data-widget' => 'treeview',
  120. 'role' => 'menu',
  121. 'data-accordion' => 'false'
  122. ];
  123. /**
  124. * @var string 第一个菜单item的css类
  125. */
  126. public $firstItemCssClass;
  127. /**
  128. * @var string 最后一个菜单item的css类
  129. */
  130. public $lastItemCssClass;
  131. /**
  132. * @var string 路由 ,run的时候会自动获取当前路由
  133. */
  134. public $route;
  135. /**
  136. * @var string $_GET参数
  137. */
  138. public $params;
  139. /**
  140. * @var string 菜单项默认Icon
  141. */
  142. public $defaultIcon = 'fa-circle';
  143. /**
  144. * 渲染菜单
  145. */
  146. public function run()
  147. {
  148. if ($this->route === null && Yii::$app->controller !== null) {
  149. $this->route = Yii::$app->controller->getRoute();
  150. }
  151. if ($this->params === null) {
  152. $this->params = Yii::$app->request->getQueryParams();
  153. }
  154. $items = $this->normalizeItems($this->items, $hasActiveChild);
  155. if (!empty($items)) {
  156. $options = $this->options;
  157. $tag = ArrayHelper::remove($options, 'tag', 'ul');
  158. return Html::tag($tag, $this->renderItems($items), $options);
  159. }
  160. }
  161. /**
  162. * 渲染菜单
  163. * @param array $items
  164. * @return string 渲染结果
  165. */
  166. protected function renderItems($items)
  167. {
  168. $lines = [];
  169. $n = count($items);
  170. foreach ($items as $i => $item) {
  171. /* 获取菜单项的自定义属性 */
  172. $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
  173. $tag = ArrayHelper::remove($options, 'tag', 'li');
  174. $class = ['nav-item'];
  175. if (isset($item['active'])) {
  176. $class[] = $this->activeCssClass;
  177. if (isset($item['items'])) {
  178. $class[] = $this->menuOpenClass;
  179. }
  180. }
  181. if ($i === 0 && $this->firstItemCssClass !== null) {
  182. $class[] = $this->firstItemCssClass;
  183. }
  184. if ($i === $n - 1 && $this->lastItemCssClass !== null) {
  185. $class[] = $this->lastItemCssClass;
  186. }
  187. if (isset($item['items'])) {
  188. $class[] = 'has-treeview';
  189. }
  190. if (isset($item['is_header']) && $item['is_header']) {
  191. $class[] = "header";
  192. }
  193. Html::addCssClass($options, $class);
  194. $menu = $this->renderItem($item);
  195. if (!empty($item['items'])) {
  196. $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
  197. $menu .= strtr($submenuTemplate, [
  198. '{items}' => $this->renderItems($item['items']),
  199. ]);
  200. }
  201. $lines[] = Html::tag($tag, $menu, $options);
  202. }
  203. return implode("\n", $lines);
  204. }
  205. /**
  206. * 渲染菜单项
  207. * @param array $item
  208. * @return string 渲染结果
  209. */
  210. protected function renderItem($item)
  211. {
  212. if (isset($item['url'])) {
  213. if (isset($item['template'])) {
  214. $template = $item['template'];
  215. } else {
  216. $template = isset($item['items']) ? $this->linkTemplateWithSub : $this->linkTemplateNoSub;
  217. }
  218. return strtr($template, [
  219. '{url}' => Html::encode(Url::to($item['url'])),
  220. '{label}' => Html::encode($item['label']),
  221. '{class}' => isset($item['active']) ? 'active' : '',
  222. '{icon}' => Html::encode($item['icon'])
  223. ]);
  224. }
  225. $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
  226. return strtr($template, [
  227. '{label}' => $item['label'],
  228. '{class}' => isset($item['active']) ? 'active' : ''
  229. ]);
  230. }
  231. /**
  232. * 判断菜单项是否被选中
  233. * @param $item array
  234. * @return boolean $item
  235. */
  236. protected function isItemActive($item)
  237. {
  238. if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) {
  239. $route = Yii::getAlias($item['url'][0]);
  240. if ($route[0] !== '/' && Yii::$app->controller) {
  241. $route = Yii::$app->controller->module->getUniqueId() . '/' . $route;
  242. }
  243. $route = ltrim($route, '/');
  244. if ($route != substr($this->route, 0, strrpos($this->route, '/')) && $route != $this->route &&
  245. ltrim(Yii::$app->request->url, '/') !== $route) {
  246. return false;
  247. }
  248. unset($item['url']['#']);
  249. if (count($item['url']) > 1) {
  250. foreach (array_splice($item['url'], 1) as $name => $value) {
  251. if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) {
  252. return false;
  253. }
  254. }
  255. }
  256. return true;
  257. }
  258. return false;
  259. }
  260. /**
  261. * 格式化菜单item
  262. * @param string $item
  263. * @param bool $active
  264. */
  265. protected function normalizeItems($items, &$active)
  266. {
  267. foreach ($items as $i => $item) {
  268. /* 去除visible 为 false的item */
  269. if (isset($item['visible']) && !$item['visible']) {
  270. unset($items[$i]);
  271. continue;
  272. }
  273. /* 添加默认icon */
  274. if (!isset($item['icon'])) {
  275. if (!empty($this->defaultIcon)) {
  276. $items[$i]['icon'] = $this->defaultIcon;
  277. }
  278. }
  279. /* 添加label */
  280. if (!isset($item['label'])) {
  281. $item['label'] = '';
  282. }
  283. /* 转义HTML */
  284. $encodeLabel = isset($item['encode']) ? $item['encode'] : $this->encodeLabels;
  285. if ($encodeLabel) {
  286. $items[$i]['label'] = Html::encode($item['label']);
  287. }
  288. /* 格式化子菜单 item */
  289. $hasActiveChild = false;
  290. if (isset($item['items'])) {
  291. $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
  292. if (empty($items[$i]['items']) && $this->hideEmptyItems) {
  293. unset($items[$i]['items']);
  294. }
  295. }
  296. /* 处理菜单是否被选中 */
  297. if (!isset($item['active'])) {
  298. if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) {
  299. $active = $items[$i]['active'] = true;
  300. }
  301. } elseif ($item['active'] instanceof Closure) {
  302. $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
  303. } elseif ($item['active']) {
  304. $active = true;
  305. }
  306. }
  307. return array_values($items);
  308. }
  309. }