Passed
Pull Request — master (#53)
by
unknown
02:53
created

Menu::renderLabel()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 10
c 0
b 0
f 0
nc 8
nop 5
dl 0
loc 24
ccs 11
cts 11
cp 1
crap 5
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Widgets;
6
7
use InvalidArgumentException;
8
use Stringable;
9
use Yiisoft\Definitions\Exception\CircularReferenceException;
10
use Yiisoft\Definitions\Exception\InvalidConfigException;
11
use Yiisoft\Definitions\Exception\NotInstantiableException;
12
use Yiisoft\Factory\NotFoundException;
13
use Yiisoft\Html\Html;
14
use Yiisoft\Html\Tag\I;
15
use Yiisoft\Html\Tag\Span;
16
use Yiisoft\Widget\Widget;
17
18
use function array_merge;
19
use function count;
20
use function implode;
21
use function strtr;
22
23
/**
24
 * Menu displays a multi-level menu using nested HTML lists.
25
 *
26
 * The {@see Menu::items()} method specifies the possible items in the menu.
27
 * A menu item can contain sub-items which specify the sub-menu under that menu item.
28
 *
29
 * Menu checks the current route and request parameters to toggle certain menu items with active state.
30
 *
31
 * Note that Menu only renders the HTML tags about the menu. It does not do any styling.
32
 * You are responsible to provide CSS styles to make it look like a real menu.
33
 *
34
 * The following example shows how to use Menu:
35
 *
36
 * ```php
37
 * <?= Menu::Widget()
38
 *     ->items([
39
 *         ['label' => 'Login', 'link' => 'site/login', 'visible' => true],
40
 *     ]);
41
 * ?>
42
 * ```
43
 */
44
final class Menu extends Widget
45
{
46
    private array $afterAttributes = [];
47
    private string $afterContent = '';
48
    private string $afterTag = 'span';
49
    private string $activeClass = 'active';
50
    private bool $activateItems = true;
51
    private array $attributes = [];
52
    private array $beforeAttributes = [];
53
    private string $beforeContent = '';
54
    private string $beforeTag = 'span';
55
    private bool $container = true;
56
    private string $currentPath = '';
57
    private string $disabledClass = 'disabled';
58
    private bool $dropdownContainer = true;
59
    private array $dropdownContainerAttributes = [];
60
    private string $dropdownContainerTag = 'li';
61
    private array $dropdownDefinitions = [];
62
    private string $firstItemClass = '';
63
    private array $iconContainerAttributes = [];
64
    private array $items = [];
65
    private bool $itemsContainer = true;
66
    private array $itemsContainerAttributes = [];
67
    private string $itemsTag = 'li';
68
    private string $lastItemClass = '';
69
    private array $linkAttributes = [];
70
    private string $linkClass = '';
71
    private string $linkTag = 'a';
72
    private string $tagName = 'ul';
73
    private string $template = '{items}';
74
75
    /**
76
     * Return new instance with specified active or disable activate items.
77
     *
78
     * @param bool $value The value to be assigned to the activateItems property.
79
     */
80 1
    public function activateItems(bool $value): self
81
    {
82 1
        $new = clone $this;
83 1
        $new->activateItems = $value;
84
85 1
        return $new;
86
    }
87
88
    /**
89
     * Returns a new instance with the specified active CSS class.
90
     *
91
     * @param string $value The CSS class to be appended to the active menu item.
92
     */
93 2
    public function activeClass(string $value): self
94
    {
95 2
        $new = clone $this;
96 2
        $new->activeClass = $value;
97
98 2
        return $new;
99
    }
100
101
    /**
102
     * Returns a new instance with the specified after container attributes.
103
     *
104
     * @param array $values Attribute values indexed by attribute names.
105
     */
106 2
    public function afterAttributes(array $values): self
107
    {
108 2
        $new = clone $this;
109 2
        $new->afterAttributes = $values;
110
111 2
        return $new;
112
    }
113
114
    /**
115
     * Returns a new instance with the specified after container class.
116
     *
117
     * @param string $value The class name.
118
     */
119 2
    public function afterClass(string $value): self
120
    {
121 2
        $new = clone $this;
122 2
        Html::addCssClass($new->afterAttributes, $value);
123
124 2
        return $new;
125
    }
126
127
    /**
128
     * Returns a new instance with the specified after content.
129
     *
130
     * @param string|Stringable $content The content.
131
     */
132 3
    public function afterContent(string|Stringable $content): self
133
    {
134 3
        $new = clone $this;
135 3
        $new->afterContent = (string) $content;
136
137 3
        return $new;
138
    }
139
140
    /**
141
     * Returns a new instance with the specified after container tag.
142
     *
143
     * @param string $value The after container tag.
144
     */
145 3
    public function afterTag(string $value): self
146
    {
147 3
        $new = clone $this;
148 3
        $new->afterTag = $value;
149
150 3
        return $new;
151
    }
152
153
    /**
154
     * Returns a new instance with the HTML attributes. The following special options are recognized.
155
     *
156
     * @param array $values Attribute values indexed by attribute names.
157
     */
158 2
    public function attributes(array $values): self
159
    {
160 2
        $new = clone $this;
161 2
        $new->attributes = $values;
162
163 2
        return $new;
164
    }
165
166
    /**
167
     * Returns a new instance with the specified before container attributes.
168
     *
169
     * @param array $values Attribute values indexed by attribute names.
170
     */
171 2
    public function beforeAttributes(array $values): self
172
    {
173 2
        $new = clone $this;
174 2
        $new->beforeAttributes = $values;
175
176 2
        return $new;
177
    }
178
179
    /**
180
     * Returns a new instance with the specified before container class.
181
     *
182
     * @param string $value The before container class.
183
     */
184 2
    public function beforeClass(string $value): self
185
    {
186 2
        $new = clone $this;
187 2
        Html::addCssClass($new->beforeAttributes, $value);
188
189 2
        return $new;
190
    }
191
192
    /**
193
     * Returns a new instance with the specified before content.
194
     *
195
     * @param string|Stringable $value The content.
196
     */
197 3
    public function beforeContent(string|Stringable $value): self
198
    {
199 3
        $new = clone $this;
200 3
        $new->beforeContent = (string) $value;
201
202 3
        return $new;
203
    }
204
205
    /**
206
     * Returns a new instance with the specified before container tag.
207
     *
208
     * @param string $value The before container tag.
209
     */
210 3
    public function beforeTag(string $value): self
211
    {
212 3
        $new = clone $this;
213 3
        $new->beforeTag = $value;
214
215 3
        return $new;
216
    }
217
218
    /**
219
     * Returns a new instance with the specified the class `menu` widget.
220
     *
221
     * @param string $value The class `menu` widget.
222
     */
223 2
    public function class(string $value): self
224
    {
225 2
        $new = clone $this;
226 2
        Html::addCssClass($new->attributes, $value);
227
228 2
        return $new;
229
    }
230
231
    /**
232
     * Returns a new instance with the specified enable or disable the container widget.
233
     *
234
     * @param bool $value The container widget enable or disable, for default is `true`.
235
     */
236 2
    public function container(bool $value): self
237
    {
238 2
        $new = clone $this;
239 2
        $new->container = $value;
240
241 2
        return $new;
242
    }
243
244
    /**
245
     * Returns a new instance with the specified the current path.
246
     *
247
     * @param string $value The current path.
248
     */
249 7
    public function currentPath(string $value): self
250
    {
251 7
        $new = clone $this;
252 7
        $new->currentPath = $value;
253
254 7
        return $new;
255
    }
256
257
    /**
258
     * Returns a new instance with the specified disabled CSS class.
259
     *
260
     * @param string $value The CSS class to be appended to the disabled menu item.
261
     */
262 2
    public function disabledClass(string $value): self
263
    {
264 2
        $new = clone $this;
265 2
        $new->disabledClass = $value;
266
267 2
        return $new;
268
    }
269
270
    /**
271
     * Returns a new instance with the specified dropdown container class.
272
     *
273
     * @param string $value The dropdown container class.
274
     */
275 2
    public function dropdownContainerClass(string $value): self
276
    {
277 2
        $new = clone $this;
278 2
        Html::addCssClass($new->dropdownContainerAttributes, $value);
279
280 2
        return $new;
281
    }
282
283
    /**
284
     * Returns a new instance with the specified dropdown container tag.
285
     *
286
     * @param string $value The dropdown container tag.
287
     */
288 1
    public function dropdownContainerTag(string $value): self
289
    {
290 1
        $new = clone $this;
291 1
        $new->dropdownContainerTag = $value;
292
293 1
        return $new;
294
    }
295
296
    /**
297
     * Returns a new instance with the specified dropdown definition widget.
298
     *
299
     * @param array $values The dropdown definition widget.
300
     */
301 2
    public function dropdownDefinitions(array $values): self
302
    {
303 2
        $new = clone $this;
304 2
        $new->dropdownDefinitions = $values;
305
306 2
        return $new;
307
    }
308
309
    /**
310
     * Returns a new instance with the specified first item CSS class.
311
     *
312
     * @param string $value The CSS class that will be assigned to the first item in the main menu or each submenu.
313
     */
314 2
    public function firstItemClass(string $value): self
315
    {
316 2
        $new = clone $this;
317 2
        $new->firstItemClass = $value;
318
319 2
        return $new;
320
    }
321
322
    /**
323
     * Returns a new instance with the specified icon container attributes.
324
     *
325
     * @param array $values Attribute values indexed by attribute names.
326
     */
327 2
    public function iconContainerAttributes(array $values): self
328
    {
329 2
        $new = clone $this;
330 2
        $new->iconContainerAttributes = $values;
331
332 2
        return $new;
333
    }
334
335
    /**
336
     * List of items in the nav widget. Each array element represents a single menu item which can be either a string or
337
     * an array with the following structure:
338
     *
339
     * - label: string, required, the nav item label.
340
     * - active: bool, whether the item should be on active state or not.
341
     * - disabled: bool, whether the item should be on disabled state or not. For default `disabled` is false.
342
     * - encodeLabel: bool, whether the label should be HTML encoded or not. For default `encodeLabel` is true.
343
     * - items: array, optional, the item's submenu items. The structure is the same as for `items` option.
344
     * - itemsContainerAttributes: array, optional, the HTML attributes for the item's submenu container.
345
     * - link: string, the item's href. Defaults to "#". For default `link` is "#".
346
     * - linkAttributes: array, the HTML attributes of the item's link. For default `linkAttributes` is `[]`.
347
     * - icon: string, the item's icon. For default is ``.
348
     * - iconAttributes: array, the HTML attributes of the item's icon. For default `iconAttributes` is `[]`.
349
     * - iconClass: string, the item's icon CSS class. For default is ``.
350
     * - liAttributes: array, optional, the HTML attributes of the item container.
351
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
352
     * - dropdown: array, optional, the configuration array for creating dropdown submenu items. The array structure is
353
     *   the same as the parent item configuration array.
354
     *
355
     * If a menu item is a string, it will be rendered directly without HTML encoding.
356
     *
357
     * @param array $values the list of items to be rendered.
358
     */
359 26
    public function items(array $values): self
360
    {
361 26
        $new = clone $this;
362 26
        $new->items = $values;
363
364 26
        return $new;
365
    }
366
367
    /**
368
     * Returns a new instance with the specified if enabled or disabled the items' container.
369
     *
370
     * @param bool $value The items container enable or disable, for default is `true`.
371
     */
372 1
    public function itemsContainer(bool $value): self
373
    {
374 1
        $new = clone $this;
375 1
        $new->itemsContainer = $value;
376
377 1
        return $new;
378
    }
379
380
    /**
381
     * Returns a new instance with the specified items' container attributes.
382
     *
383
     * @param array $values Attribute values indexed by attribute names.
384
     */
385 2
    public function itemsContainerAttributes(array $values): self
386
    {
387 2
        $new = clone $this;
388 2
        $new-> itemsContainerAttributes = $values;
389
390 2
        return $new;
391
    }
392
393
    /**
394
     * Returns a new instance with the specified items' container class.
395
     *
396
     * @param string $value The CSS class that will be assigned to the items' container.
397
     */
398 2
    public function itemsContainerClass(string $value): self
399
    {
400 2
        $new = clone $this;
401 2
        Html::addCssClass($new->itemsContainerAttributes, $value);
402
403 2
        return $new;
404
    }
405
406
    /**
407
     * Returns a new instance with the specified items tag.
408
     *
409
     * @param string $value The tag that will be used to wrap the items.
410
     */
411 2
    public function itemsTag(string $value): self
412
    {
413 2
        $new = clone $this;
414 2
        $new->itemsTag = $value;
415
416 2
        return $new;
417
    }
418
419
    /**
420
     * Returns a new instance with the specified last item CSS class.
421
     *
422
     * @param string $value The CSS class that will be assigned to the last item in the main menu or each submenu.
423
     */
424 2
    public function lastItemClass(string $value): self
425
    {
426 2
        $new = clone $this;
427 2
        $new->lastItemClass = $value;
428
429 2
        return $new;
430
    }
431
432
    /**
433
     * Returns a new instance with the specified link attributes.
434
     *
435
     * @param array $values Attribute values indexed by attribute names.
436
     */
437 2
    public function linkAttributes(array $values): self
438
    {
439 2
        $new = clone $this;
440 2
        $new->linkAttributes = $values;
441
442 2
        return $new;
443
    }
444
445
    /**
446
     * Returns a new instance with the specified link css class.
447
     *
448
     * @param string $value The CSS class that will be assigned to the link.
449
     */
450 2
    public function linkClass(string $value): self
451
    {
452 2
        $new = clone $this;
453 2
        $new->linkClass = $value;
454
455 2
        return $new;
456
    }
457
458
    /**
459
     * Returns a new instance with the specified link tag.
460
     *
461
     * @param string $value The tag that will be used to wrap the link.
462
     */
463 2
    public function linkTag(string $value): self
464
    {
465 2
        $new = clone $this;
466 2
        $new->linkTag = $value;
467
468 2
        return $new;
469
    }
470
471
    /**
472
     * Returns a new instance with the specified tag for rendering the menu.
473
     *
474
     * @param string $value The tag for rendering the menu.
475
     */
476 2
    public function tagName(string $value): self
477
    {
478 2
        $new = clone $this;
479 2
        $new->tagName = $value;
480
481 2
        return $new;
482
    }
483
484
    /**
485
     * Returns a new instance with the specified the template used to render the main menu.
486
     *
487
     * @param string $value The template used to render the main menu. In this template, the token `{items}` will be
488
     * replaced.
489
     */
490 2
    public function template(string $value): self
491
    {
492 2
        $new = clone $this;
493 2
        $new->template = $value;
494
495 2
        return $new;
496
    }
497
498
    /**
499
     * Renders the menu.
500
     *
501
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
502
     *
503
     * @return string The result of Widget execution to be outputted.
504
     */
505 26
    protected function run(): string
506
    {
507 26
        $items = $this->normalizeItems($this->items, $this->currentPath, $this->activateItems);
508
509 26
        if ($items === []) {
510 1
            return '';
511
        }
512
513 25
        return $this->renderMenu($items);
514
    }
515
516
    /**
517
     * Checks whether a menu item is active.
518
     *
519
     * This is done by checking match that specified in the `url` option of the menu item.
520
     *
521
     * @param string $link The link of the menu item.
522
     * @param string $currentPath The current path.
523
     * @param bool $activateItems Whether to activate items having no link.
524
     *
525
     * @return bool Whether the menu item is active.
526
     */
527 25
    private function isItemActive(string $link, string $currentPath, bool $activateItems): bool
528
    {
529 25
        return ($link === $currentPath) && $activateItems;
530
    }
531
532
    /**
533
     * Normalize the given array of items for the menu.
534
     *
535
     * @param array $items The items to be normalized.
536
     * @param string $currentPath The current path.
537
     * @param bool $activateItems Whether to activate items.
538
     * @param bool $active Should The parent be active too.
539
     *
540
     * @return array The normalized array of items.
541
     */
542 26
    public function normalizeItems(
543
        array $items,
544
        string $currentPath = '/',
545
        bool $activateItems = true,
546
        bool &$active = false,
547
    ): array {
548
        /**
549
         * @psalm-var array[] $items
550
         * @psalm-suppress RedundantConditionGivenDocblockType
551
         */
552 26
        foreach ($items as $i => $child) {
553 25
            if (is_array($child)) {
554
                /** @var array */
555 25
                $dropdown = $child['items'] ?? [];
556
557 25
                if ($dropdown !== []) {
558 4
                    $items[$i]['items'] = $this->normalizeItems($dropdown, $currentPath, $activateItems, $active);
559
                } else {
560
                    /** @var string */
561 25
                    $link = $child['link'] ?? '/';
562
                    /** @var bool */
563 25
                    $active = $child['active'] ?? false;
564
565 25
                    if ($active === false) {
566 25
                        $items[$i]['active'] = $this->isItemActive($link, $currentPath, $activateItems);
567
                    }
568
569
                    /** @var bool */
570 25
                    $items[$i]['disabled'] = $child['disabled'] ?? false;
571
                    /** @var bool */
572 25
                    $items[$i]['encodeLabel'] = $child['encodeLabel'] ?? true;
573
                    /** @var string */
574 25
                    $items[$i]['icon'] = $child['icon'] ?? '';
575
                    /** @var array */
576 25
                    $items[$i]['iconAttributes'] = $child['iconAttributes'] ?? [];
577
                    /** @var string */
578 25
                    $items[$i]['iconClass'] = $child['iconClass'] ?? '';
579
                    /** @var array */
580 25
                    $items[$i]['iconContainerAttributes'] = $child['iconContainerAttributes'] ?? [];
581
                    /** @var bool */
582 25
                    $items[$i]['visible'] = $child['visible'] ?? true;
583
                }
584
            }
585
        }
586
587 26
        return $items;
588
    }
589
590 2
    private function renderAfterContent(): string
591
    {
592 2
        if ($this->afterTag === '') {
593 1
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
594
        }
595
596 1
        return PHP_EOL .
597 1
            Html::normalTag($this->afterTag, $this->afterContent, $this->afterAttributes)
598 1
                ->encode(false)
599 1
                ->render();
600
    }
601
602 2
    private function renderBeforeContent(): string
603
    {
604 2
        if ($this->beforeTag === '') {
605 1
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
606
        }
607
608 1
        return Html::normalTag($this->beforeTag, $this->beforeContent, $this->beforeAttributes)
609 1
            ->encode(false)
610 1
            ->render();
611
    }
612
613
    /**
614
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
615
     */
616 4
    private function renderDropdown(array $items): string
617
    {
618 4
        $dropdownDefinitions = $this->dropdownDefinitions;
619
620 4
        if ($dropdownDefinitions === []) {
621 3
            $dropdownDefinitions = [
622
                'container()' => [false],
623
                'dividerClass()' => ['dropdown-divider'],
624
                'toggleAttributes()' => [
625
                    ['aria-expanded' => 'false', 'data-bs-toggle' => 'dropdown', 'role' => 'button'],
626
                ],
627
                'toggleType()' => ['link'],
628
            ];
629
        }
630
631 4
        $dropdown = Dropdown::widget($dropdownDefinitions)->items($items)->render();
0 ignored issues
show
Bug introduced by
The method items() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Widgets\Menu or Yiisoft\Yii\Widgets\Breadcrumbs or Yiisoft\Yii\Widgets\Dropdown. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

631
        $dropdown = Dropdown::widget($dropdownDefinitions)->/** @scrutinizer ignore-call */ items($items)->render();
Loading history...
632
633 4
        if ($this->dropdownContainerTag === '') {
634 1
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
635
        }
636
637 3
        return match ($this->dropdownContainer) {
638 3
            true => Html::normalTag($this->dropdownContainerTag, $dropdown, $this->dropdownContainerAttributes)
639 3
                ->encode(false)
640 3
                ->render(),
641 3
            false => $dropdown,
642
        };
643
    }
644
645
    /**
646
     * Renders the content of a menu item.
647
     *
648
     * Note that the container and the sub-menus are not rendered here.
649
     *
650
     * @param array $item The menu item to be rendered. Please refer to {@see items} to see what data might be in the
651
     * item.
652
     *
653
     * @return string The rendering result.
654
     */
655 24
    private function renderItem(array $item): string
656
    {
657
        /** @var string */
658 24
        $label = $item['label'] ?? '';
659
        /** @var array */
660 24
        $linkAttributes = $item['linkAttributes'] ?? [];
661 24
        $linkAttributes = array_merge($linkAttributes, $this->linkAttributes);
662
663 24
        if ($this->linkClass !== '') {
664 1
            Html::addCssClass($linkAttributes, $this->linkClass);
665
        }
666
667 24
        if ($item['active']) {
668 6
            $linkAttributes['aria-current'] = 'page';
669 6
            Html::addCssClass($linkAttributes, $this->activeClass);
670
        }
671
672 24
        if ($item['disabled']) {
673 12
            Html::addCssClass($linkAttributes, $this->disabledClass);
674
        }
675
676 24
        if ($item['encodeLabel']) {
677 24
            $label = Html::encode($label);
678
        }
679
680 24
        if (isset($item['link']) && is_string($item['link'])) {
681 19
            $linkAttributes['href'] = $item['link'];
682
        }
683
684
        /**
685
         * @var string $item['icon']
686
         * @var string $item['iconClass']
687
         * @var array $item['iconAttributes']
688
         * @var array $item['iconContainerAttributes']
689
         */
690 24
        $label = $this->renderLabel(
691
            $label,
692 24
            $item['icon'],
693 24
            $item['iconClass'],
694 24
            $item['iconAttributes'],
695 24
            $item['iconContainerAttributes'],
696
        );
697
698 24
        if ($this->linkTag === '') {
699 1
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
700
        }
701
702 23
        return match (isset($linkAttributes['href'])) {
703 19
            true => Html::normalTag($this->linkTag, $label, $linkAttributes)->encode(false)->render(),
704 23
            false => $label,
705
        };
706
    }
707
708
    /**
709
     * Recursively renders the menu items (without the container tag).
710
     *
711
     * @param array $items The menu items to be rendered recursively.
712
     *
713
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
714
     */
715 25
    private function renderItems(array $items): string
716
    {
717 25
        $lines = [];
718 25
        $n = count($items);
719
720
        /** @psalm-var array[] $items  */
721 25
        foreach ($items as $i => $item) {
722 25
            if (isset($item['items'])) {
723
                /** @psalm-var array $item['items'] */
724 4
                $lines[] = strtr($this->template, ['{items}' => $this->renderDropdown([$item])]);
725 24
            } elseif ($item['visible']) {
726
                /** @psalm-var array|null $item['itemsContainerAttributes'] */
727 24
                $itemsContainerAttributes = array_merge(
728 24
                    $this->itemsContainerAttributes,
729 24
                    $item['itemsContainerAttributes'] ?? [],
730
                );
731
732 24
                if ($i === 0 && $this->firstItemClass !== '') {
733 1
                    Html::addCssClass($itemsContainerAttributes, $this->firstItemClass);
734
                }
735
736 24
                if ($i === $n - 1 && $this->lastItemClass !== '') {
737 1
                    Html::addCssClass($itemsContainerAttributes, $this->lastItemClass);
738
                }
739
740 24
                $menu = $this->renderItem($item);
741
742 23
                if ($this->itemsTag === '') {
743 1
                    throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
744
                }
745
746 22
                $lines[] = match ($this->itemsContainer) {
747 1
                    false => $menu,
748 21
                    default => strtr(
749 21
                        $this->template,
750
                        [
751 21
                            '{items}' => Html::normalTag($this->itemsTag, $menu, $itemsContainerAttributes)
752 21
                                ->encode(false)
753 21
                                ->render(),
754
                        ],
755
                    ),
756
                };
757
            }
758
        }
759
760 22
        return PHP_EOL . implode(PHP_EOL, $lines);
761
    }
762
763
    /**
764
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
765
     */
766 25
    private function renderMenu(array $items): string
767
    {
768 25
        $afterContent = '';
769 25
        $attributes = $this->attributes;
770 25
        $beforeContent = '';
771
772 25
        $content = $this->renderItems($items) . PHP_EOL;
773
774 22
        if ($this->beforeContent !== '') {
775 2
            $beforeContent = $this->renderBeforeContent() . PHP_EOL;
776
        }
777
778 21
        if ($this->afterContent !== '') {
779 2
            $afterContent = $this->renderAfterContent();
780
        }
781
782 20
        if ($this->tagName === '') {
783 1
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
784
        }
785
786 19
        return match ($this->container) {
787 1
            false => $beforeContent . trim($content) . $afterContent,
788
            default => $beforeContent .
789 18
                Html::normalTag($this->tagName, $content, $attributes)->encode(false) .
790
                $afterContent,
791
        };
792
    }
793
794 24
    private function renderLabel(
795
        string $label,
796
        string $icon,
797
        string $iconClass,
798
        array $iconAttributes = [],
799
        array $iconContainerAttributes = []
800
    ): string {
801 24
        $html = '';
802 24
        $iconContainerAttributes = array_merge($this->iconContainerAttributes, $iconContainerAttributes);
803
804 24
        if ($iconClass !== '') {
805 1
            Html::addCssClass($iconAttributes, $iconClass);
806
        }
807
808 24
        if ($icon !== '' || $iconClass !== '') {
809 1
            $i = I::tag()->addAttributes($iconAttributes)->content($icon)->encode(false)->render();
810 1
            $html = Span::tag()->addAttributes($iconContainerAttributes)->content($i)->encode(false)->render();
811
        }
812
813 24
        if ($label !== '') {
814 24
            $html .= $label;
815
        }
816
817 24
        return $html;
818
    }
819
}
820