1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Yii\Bulma; |
||
6 | |||
7 | use InvalidArgumentException; |
||
8 | use Yiisoft\Definitions\Exception\CircularReferenceException; |
||
9 | use Yiisoft\Definitions\Exception\InvalidConfigException; |
||
10 | use Yiisoft\Definitions\Exception\NotInstantiableException; |
||
11 | use Yiisoft\Factory\NotFoundException; |
||
12 | use Yiisoft\Html\Html; |
||
13 | use Yiisoft\Html\Tag\A; |
||
14 | use Yiisoft\Html\Tag\CustomTag; |
||
15 | use Yiisoft\Html\Tag\Div; |
||
16 | use Yiisoft\Html\Tag\Span; |
||
17 | use Yiisoft\Widget\Widget; |
||
18 | |||
19 | use function implode; |
||
20 | use function is_array; |
||
21 | use function is_string; |
||
22 | |||
23 | /** |
||
24 | * Nav renders a nav HTML component. |
||
25 | * |
||
26 | * @link https://bulma.io/documentation/components/navbar/#basic-navbar |
||
27 | */ |
||
28 | final class Nav extends Widget |
||
29 | { |
||
30 | private bool $activateItems = true; |
||
31 | private bool $activateParents = false; |
||
32 | private array $attributes = []; |
||
33 | private string $currentPath = ''; |
||
34 | private string $dropdownCssClass = 'navbar-dropdown'; |
||
35 | private string $endCssClass = 'navbar-end'; |
||
36 | private bool $enclosedByStartMenu = false; |
||
37 | private bool $enclosedByEndMenu = false; |
||
38 | private string $hasDropdownCssClass = 'has-dropdown'; |
||
39 | private string $isHoverableCssClass = 'is-hoverable'; |
||
40 | private string $itemCssClass = 'navbar-item'; |
||
41 | private array $items = []; |
||
42 | private string $linkCssClass = 'navbar-link'; |
||
43 | private string $menuCssClass = 'navbar-menu'; |
||
44 | private string $startCssClass = 'navbar-start'; |
||
45 | |||
46 | /** |
||
47 | * Returns a new instance with the specified HTML attributes for widget. |
||
48 | * |
||
49 | * @param array $values Attribute values indexed by attribute names. |
||
50 | * |
||
51 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} For details on how attributes are being rendered. |
||
52 | */ |
||
53 | 1 | public function attributes(array $values): self |
|
54 | { |
||
55 | 1 | $new = clone $this; |
|
56 | 1 | $new->attributes = $values; |
|
57 | 1 | return $new; |
|
58 | } |
||
59 | |||
60 | /** |
||
61 | * Returns a new instance with the specified whether to activate parent menu items when one of the corresponding |
||
62 | * child menu items is active. |
||
63 | */ |
||
64 | 2 | public function activateParents(): self |
|
65 | { |
||
66 | 2 | $new = clone $this; |
|
67 | 2 | $new->activateParents = true; |
|
68 | 2 | return $new; |
|
69 | } |
||
70 | |||
71 | /** |
||
72 | * Returns a new instance with the specified allows you to assign the current path of the url from request |
||
73 | * controller. |
||
74 | * |
||
75 | * @param string $value The current path. |
||
76 | */ |
||
77 | 3 | public function currentPath(string $value): self |
|
78 | { |
||
79 | 3 | $new = clone $this; |
|
80 | 3 | $new->currentPath = $value; |
|
81 | 3 | return $new; |
|
82 | } |
||
83 | |||
84 | /** |
||
85 | * Returns a new instance with the specified align the menu items to the right. |
||
86 | * |
||
87 | * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end |
||
88 | */ |
||
89 | 2 | public function enclosedByEndMenu(): self |
|
90 | { |
||
91 | 2 | $new = clone $this; |
|
92 | 2 | $new->enclosedByEndMenu = true; |
|
93 | 2 | return $new; |
|
94 | } |
||
95 | |||
96 | /** |
||
97 | * Returns a new instance with the specified align the menu items to left. |
||
98 | * |
||
99 | * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end |
||
100 | */ |
||
101 | 2 | public function enclosedByStartMenu(): self |
|
102 | { |
||
103 | 2 | $new = clone $this; |
|
104 | 2 | $new->enclosedByStartMenu = true; |
|
105 | 2 | return $new; |
|
106 | } |
||
107 | |||
108 | /** |
||
109 | * Returns a new instance with the specified items. |
||
110 | * |
||
111 | * Each array element represents a single menu item which can be either a string |
||
112 | * or an array with the following structure: |
||
113 | * |
||
114 | * - label: string, required, the nav item label. |
||
115 | * - url: optional, the item's URL. Defaults to "#". |
||
116 | * - urlAttributes: optional, the attributes to be rendered in the item's URL. |
||
117 | * - visible: bool, optional, whether this menu item is visible. Defaults to true. |
||
118 | * - linkAttributes: array, optional, the HTML attributes of the item's link. |
||
119 | * - active: bool, optional, whether the item should be on active state or not. |
||
120 | * - disable: bool, optional, whether the item should be disabled. |
||
121 | * - dropdownAttributes: array, optional, the HTML options that will be passed to the {@see Dropdown} widget. |
||
122 | * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string |
||
123 | * representing the dropdown menu. |
||
124 | * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for |
||
125 | * only this item. |
||
126 | * - iconAttributes: array, optional, the HTML attributes of the item's icon. |
||
127 | * - iconCssClass: string, optional, the icon CSS class. |
||
128 | * - iconText: string, optional, the icon text. |
||
129 | * |
||
130 | * If a menu item is a string, it will be rendered directly without HTML encoding. |
||
131 | * |
||
132 | * @param array $value The menu items. |
||
133 | */ |
||
134 | 17 | public function items(array $value): self |
|
135 | { |
||
136 | 17 | $new = clone $this; |
|
137 | 17 | $new->items = $value; |
|
138 | 17 | return $new; |
|
139 | } |
||
140 | |||
141 | /** |
||
142 | * Returns a new instance with the specified disable activate items according to whether their currentPath. |
||
143 | * |
||
144 | * {@see isItemActive} |
||
145 | */ |
||
146 | 3 | public function withoutActivateItems(): self |
|
147 | { |
||
148 | 3 | $new = clone $this; |
|
149 | 3 | $new->activateItems = false; |
|
150 | 3 | return $new; |
|
151 | } |
||
152 | |||
153 | /** |
||
154 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
155 | */ |
||
156 | 16 | public function render(): string |
|
157 | { |
||
158 | 16 | return $this->renderNav(); |
|
159 | } |
||
160 | |||
161 | /** |
||
162 | * Renders the given items as a dropdown. |
||
163 | * |
||
164 | * This method is called to create sub-menus. |
||
165 | * |
||
166 | * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure. |
||
167 | * |
||
168 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
169 | * |
||
170 | * @return string the rendering result. |
||
171 | * |
||
172 | * @link https://bulma.io/documentation/components/navbar/#dropdown-menu |
||
173 | */ |
||
174 | 8 | private function renderDropdown(array $items): string |
|
175 | { |
||
176 | 8 | return Dropdown::widget() |
|
177 | 8 | ->cssClass('navbar-dropdown') |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
178 | 8 | ->dividerCssClass('navbar-divider') |
|
179 | 8 | ->enclosedByContainer() |
|
180 | 8 | ->itemCssClass('navbar-item') |
|
181 | 8 | ->items($items) |
|
182 | 8 | ->render() . PHP_EOL; |
|
183 | } |
||
184 | |||
185 | /** |
||
186 | * Check to see if a child item is active optionally activating the parent. |
||
187 | * |
||
188 | * @param array $items {@see items} |
||
189 | * @param bool $active Should the parent be active too. |
||
190 | * |
||
191 | * {@see items} |
||
192 | */ |
||
193 | 8 | private function isChildActive(array $items, bool &$active = false): array |
|
194 | { |
||
195 | /** |
||
196 | * @psalm-var array< |
||
197 | * string, |
||
198 | * array{ |
||
199 | * active?: bool, |
||
200 | * disable?: bool, |
||
201 | * encode?: bool, |
||
202 | * icon?: string, |
||
203 | * iconAttributes?: array, |
||
204 | * items?: array, |
||
205 | * label: string, |
||
206 | * url: string, |
||
207 | * visible?: bool |
||
208 | * }|string> $items |
||
209 | */ |
||
210 | 8 | foreach ($items as $i => $child) { |
|
211 | 8 | $url = $child['url'] ?? '#'; |
|
212 | 8 | $active = $child['active'] ?? false; |
|
213 | |||
214 | 8 | if ($active === false && is_array($child)) { |
|
215 | 8 | $child['active'] = $this->isItemActive($url, $this->currentPath, $this->activateItems); |
|
216 | } |
||
217 | |||
218 | 8 | if ($this->activateParents) { |
|
219 | 1 | $active = true; |
|
220 | } |
||
221 | |||
222 | 8 | $childItems = $child['items'] ?? []; |
|
223 | |||
224 | 8 | if ($childItems !== [] && is_array($items[$i])) { |
|
225 | 1 | $items[$i]['items'] = $this->isChildActive($childItems); |
|
226 | |||
227 | 1 | if ($active) { |
|
228 | 1 | $items[$i]['attributes'] = ['active' => true]; |
|
229 | 1 | $active = true; |
|
230 | } |
||
231 | } |
||
232 | } |
||
233 | |||
234 | 8 | return $items; |
|
235 | } |
||
236 | |||
237 | /** |
||
238 | * Checks whether a menu item is active. |
||
239 | * |
||
240 | * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When |
||
241 | * the `url` option of a menu item is specified in terms of an array, its first element is treated as the |
||
242 | * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath |
||
243 | * and parameters match {@see currentPath}, respectively, will a menu item be considered active. |
||
244 | * |
||
245 | * @param string $url The menu item's URL. |
||
246 | * @param string $currentPath The currentPath. |
||
247 | * @param bool $activateItems Whether to activate the parent menu items when the currentPath matches. |
||
248 | * |
||
249 | * @return bool whether the menu item is active |
||
250 | */ |
||
251 | 15 | private function isItemActive(string $url, string $currentPath, bool $activateItems): bool |
|
252 | { |
||
253 | 15 | return ($currentPath !== '/') && ($url === $currentPath) && $activateItems; |
|
254 | } |
||
255 | |||
256 | 15 | private function renderLabelItem( |
|
257 | string $label, |
||
258 | string $iconText, |
||
259 | string $iconCssClass, |
||
260 | array $iconAttributes = [] |
||
261 | ): string { |
||
262 | 15 | $html = ''; |
|
263 | |||
264 | 15 | if ($iconText !== '' || $iconCssClass !== '') { |
|
265 | 1 | $html = Span::tag() |
|
266 | 1 | ->addAttributes($iconAttributes) |
|
267 | 1 | ->content(CustomTag::name('i') |
|
268 | 1 | ->addClass($iconCssClass) |
|
269 | 1 | ->content($iconText) |
|
270 | 1 | ->encode(false) |
|
271 | 1 | ->render()) |
|
272 | 1 | ->encode(false) |
|
273 | 1 | ->render(); |
|
274 | } |
||
275 | |||
276 | 15 | if ($label !== '') { |
|
277 | 15 | $html .= $label; |
|
278 | } |
||
279 | |||
280 | 15 | return $html; |
|
281 | } |
||
282 | |||
283 | /** |
||
284 | * Renders a widget's item. |
||
285 | * |
||
286 | * @param array $item the item to render. |
||
287 | * |
||
288 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
289 | * |
||
290 | * @return string the rendering result. |
||
291 | */ |
||
292 | 16 | private function renderItem(array $item): string |
|
293 | { |
||
294 | 16 | $html = ''; |
|
295 | |||
296 | 16 | if (!isset($item['label'])) { |
|
297 | 1 | throw new InvalidArgumentException('The "label" option is required.'); |
|
298 | } |
||
299 | |||
300 | /** @var string */ |
||
301 | 15 | $itemLabel = $item['label'] ?? ''; |
|
302 | |||
303 | 15 | if (isset($item['encode']) && $item['encode'] === true) { |
|
304 | 1 | $itemLabel = Html::encode($itemLabel); |
|
305 | } |
||
306 | |||
307 | /** @var array */ |
||
308 | 15 | $items = $item['items'] ?? []; |
|
309 | |||
310 | /** @var string */ |
||
311 | 15 | $url = $item['url'] ?? '#'; |
|
312 | |||
313 | /** @var array */ |
||
314 | 15 | $urlAttributes = $item['urlAttributes'] ?? []; |
|
315 | |||
316 | /** @var array */ |
||
317 | 15 | $dropdownAttributes = $item['dropdownAttributes'] ?? []; |
|
318 | |||
319 | /** @var string */ |
||
320 | 15 | $iconText = $item['iconText'] ?? ''; |
|
321 | |||
322 | /** @var string */ |
||
323 | 15 | $iconCssClass = $item['iconCssClass'] ?? ''; |
|
324 | |||
325 | /** @var array */ |
||
326 | 15 | $iconAttributes = $item['iconAttributes'] ?? []; |
|
327 | |||
328 | /** @var bool */ |
||
329 | 15 | $active = $item['active'] ?? $this->isItemActive($url, $this->currentPath, $this->activateItems); |
|
330 | |||
331 | /** @var bool */ |
||
332 | 15 | $disabled = $item['disabled'] ?? false; |
|
333 | |||
334 | 15 | $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes); |
|
335 | |||
336 | 15 | if ($disabled) { |
|
337 | 1 | Html::addCssStyle($urlAttributes, 'opacity:.65; pointer-events:none;'); |
|
338 | } |
||
339 | |||
340 | 15 | if ($this->activateItems && $active) { |
|
341 | 1 | Html::addCssClass($urlAttributes, ['active' => 'is-active']); |
|
342 | } |
||
343 | |||
344 | 15 | if ($items !== []) { |
|
345 | 8 | $attributes = $this->attributes; |
|
346 | 8 | Html::addCssClass( |
|
347 | 8 | $attributes, |
|
348 | 8 | [$this->itemCssClass, $this->hasDropdownCssClass, $this->isHoverableCssClass] |
|
349 | 8 | ); |
|
350 | 8 | Html::addCssClass($urlAttributes, $this->linkCssClass); |
|
351 | 8 | Html::addCssClass($dropdownAttributes, $this->dropdownCssClass); |
|
352 | |||
353 | 8 | $items = $this->isChildActive($items, $active); |
|
354 | 8 | $dropdown = PHP_EOL . $this->renderDropdown($items); |
|
355 | 8 | $a = A::tag() |
|
356 | 8 | ->attributes($urlAttributes) |
|
357 | 8 | ->content($itemLabel) |
|
358 | 8 | ->encode(false) |
|
359 | 8 | ->url($url) |
|
360 | 8 | ->render(); |
|
361 | 8 | $div = Div::tag() |
|
362 | 8 | ->attributes($dropdownAttributes) |
|
363 | 8 | ->content($dropdown) |
|
364 | 8 | ->encode(false) |
|
365 | 8 | ->render(); |
|
366 | 8 | $html = Div::tag() |
|
367 | 8 | ->attributes($attributes) |
|
368 | 8 | ->content(PHP_EOL . $a . PHP_EOL . $div . PHP_EOL) |
|
369 | 8 | ->encode(false) |
|
370 | 8 | ->render(); |
|
371 | } |
||
372 | |||
373 | 15 | if ($html === '') { |
|
374 | 11 | Html::addCssClass($urlAttributes, 'navbar-item'); |
|
375 | 11 | $html = A::tag() |
|
376 | 11 | ->attributes($urlAttributes) |
|
377 | 11 | ->content($itemLabel) |
|
378 | 11 | ->url($url) |
|
379 | 11 | ->encode(false) |
|
380 | 11 | ->render(); |
|
381 | } |
||
382 | |||
383 | 15 | return $html; |
|
384 | } |
||
385 | |||
386 | /** |
||
387 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
388 | */ |
||
389 | 16 | private function renderNav(): string |
|
390 | { |
||
391 | 16 | $items = []; |
|
392 | |||
393 | /** @var array|string $item */ |
||
394 | 16 | foreach ($this->items as $item) { |
|
395 | 16 | $visible = !isset($item['visible']) || $item['visible']; |
|
396 | |||
397 | 16 | if ($visible) { |
|
398 | 16 | $items[] = is_string($item) ? $item : $this->renderItem($item); |
|
399 | } |
||
400 | } |
||
401 | |||
402 | 15 | $links = PHP_EOL . implode(PHP_EOL, $items) . PHP_EOL; |
|
403 | |||
404 | 15 | if ($this->enclosedByStartMenu) { |
|
405 | 1 | $links = PHP_EOL . Div::tag() |
|
406 | 1 | ->class($this->startCssClass) |
|
407 | 1 | ->content($links) |
|
408 | 1 | ->encode(false) |
|
409 | 1 | ->render() . |
|
410 | 1 | PHP_EOL; |
|
411 | } |
||
412 | |||
413 | 15 | if ($this->enclosedByEndMenu) { |
|
414 | 1 | $links = PHP_EOL . Div::tag() |
|
415 | 1 | ->class($this->endCssClass) |
|
416 | 1 | ->content($links) |
|
417 | 1 | ->encode(false) |
|
418 | 1 | ->render() . |
|
419 | 1 | PHP_EOL; |
|
420 | } |
||
421 | |||
422 | 15 | return $this->items !== [] |
|
423 | 15 | ? Div::tag() |
|
424 | 15 | ->class($this->menuCssClass) |
|
425 | 15 | ->content($links) |
|
426 | 15 | ->encode(false) |
|
427 | 15 | ->render() |
|
428 | 15 | : ''; |
|
429 | } |
||
430 | } |
||
431 |