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

Dropdown::renderDropdown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 14
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 16
rs 9.7998
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Widgets;
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\I;
15
use Yiisoft\Html\Tag\Button;
16
use Yiisoft\Html\Tag\Span;
17
use Yiisoft\Widget\Widget;
18
19
use function array_merge;
20
use function gettype;
21
use function implode;
22
use function is_array;
23
use function is_string;
24
use function str_contains;
25
use function trim;
26
27
final class Dropdown extends Widget
28
{
29
    private string $activeClass = 'active';
30
    private bool $container = true;
31
    private array $containerAttributes = [];
32
    private string $containerClass = '';
33
    private string $containerTag = 'div';
34
    private string $disabledClass = 'disabled';
35
    private array $dividerAttributes = [];
36
    private string $dividerClass = 'dropdown-divider';
37
    private string $dividerTag = 'hr';
38
    private string $headerClass = '';
39
    private string $headerTag = 'span';
40
    private string $id = '';
41
    private string $itemClass = '';
42
    private string $itemTag = 'a';
43
    private bool $itemContainer = true;
44
    private array $itemContainerAttributes = [];
45
    private string $itemContainerTag = 'li';
46
    private array $items = [];
47
    private array $itemsContainerAttributes = [];
48
    private string $itemsContainerTag = 'ul';
49
    private array $splitButtonAttributes = [];
50
    private array $splitButtonSpanAttributes = [];
51
    private array $toggleAttributes = [];
52
    private string $toggleType = 'button';
53
54
    /**
55
     * Returns a new instance with the specified active class.
56
     *
57
     * @param string $value The active class.
58
     */
59
    public function activeClass(string $value): self
60
    {
61
        $new = clone $this;
62
        $new->activeClass = $value;
63
64
        return $new;
65
    }
66
67
    /**
68
     * Returns a new instance with the specified if the container is enabled, or not. Default is true.
69
     *
70
     * @param bool $value The container enabled.
71
     */
72
    public function container(bool $value): self
73
    {
74
        $new = clone $this;
75
        $new->container = $value;
76
77
        return $new;
78
    }
79
80
    /**
81
     * Returns a new instance with the specified container HTML attributes.
82
     *
83
     * @param array $values Attribute values indexed by attribute names.
84
     */
85
    public function containerAttributes(array $values): self
86
    {
87
        $new = clone $this;
88
        $new->containerAttributes = $values;
89
90
        return $new;
91
    }
92
93
    /**
94
     * Returns a new instance with the specified container class.
95
     *
96
     * @param string $value The container class.
97
     */
98
    public function containerClass(string $value): self
99
    {
100
        $new = clone $this;
101
        $new->containerClass = $value;
102
103
        return $new;
104
    }
105
106
    /**
107
     * Returns a new instance with the specified container tag.
108
     *
109
     * @param string $value The container tag.
110
     */
111
    public function containerTag(string $value): self
112
    {
113
        $new = clone $this;
114
        $new->containerTag = $value;
115
        return $new;
116
    }
117
118
    /**
119
     * Returns a new instance with the specified disabled class.
120
     *
121
     * @param string $value The disabled class.
122
     */
123
    public function disabledClass(string $value): self
124
    {
125
        $new = clone $this;
126
        $new->disabledClass = $value;
127
128
        return $new;
129
    }
130
131
    /**
132
     * Returns a new instance with the specified divider HTML attributes.
133
     *
134
     * @param array $values Attribute values indexed by attribute names.
135
     */
136
    public function dividerAttributes(array $values): self
137
    {
138
        $new = clone $this;
139
        $new->dividerAttributes = $values;
140
141
        return $new;
142
    }
143
144
    /**
145
     * Returns a new instance with the specified divider class.
146
     *
147
     * @param string $value The divider class.
148
     */
149
    public function dividerClass(string $value): self
150
    {
151
        $new = clone $this;
152
        $new->dividerClass = $value;
153
154
        return $new;
155
    }
156
157
    /**
158
     * Returns a new instance with the specified divider tag.
159
     *
160
     * @param string $value The divider tag.
161
     */
162
    public function dividerTag(string $value): self
163
    {
164
        $new = clone $this;
165
        $new->dividerTag = $value;
166
167
        return $new;
168
    }
169
170
    /**
171
     * Returns a new instance with the specified header class.
172
     *
173
     * @param string $value The header class.
174
     */
175
    public function headerClass(string $value): self
176
    {
177
        $new = clone $this;
178
        $new->headerClass = $value;
179
180
        return $new;
181
    }
182
183
    /**
184
     * Returns a new instance with the specified header tag.
185
     *
186
     * @param string $value The header tag.
187
     */
188
    public function headerTag(string $value): self
189
    {
190
        $new = clone $this;
191
        $new->headerTag = $value;
192
193
        return $new;
194
    }
195
196
    /**
197
     * Returns a new instance with the specified Widget ID.
198
     *
199
     * @param string $value The id of the widget.
200
     */
201
    public function id(string $value): self
202
    {
203
        $new = clone $this;
204
        $new->id = $value;
205
206
        return $new;
207
    }
208
209
    /**
210
     * Returns a new instance with the specified item class.
211
     *
212
     * @param string $value The item class.
213
     */
214
    public function itemClass(string $value): self
215
    {
216
        $new = clone $this;
217
        $new->itemClass = $value;
218
219
        return $new;
220
    }
221
222
    /**
223
     * Returns a new instance with the specified item container, if false, the item container will not be rendered.
224
     *
225
     * @param bool $value The item container.
226
     */
227
    public function itemContainer(bool $value): self
228
    {
229
        $new = clone $this;
230
        $new->itemContainer = $value;
231
232
        return $new;
233
    }
234
235
    /**
236
     * Returns a new instance with the specified item container HTML attributes.
237
     *
238
     * @param array $values Attribute values indexed by attribute names.
239
     */
240
    public function itemContainerAttributes(array $values): self
241
    {
242
        $new = clone $this;
243
        $new->itemContainerAttributes = $values;
244
245
        return $new;
246
    }
247
248
    /**
249
     * Returns a new instance with the specified item container class.
250
     *
251
     * @param string $value The item container class.
252
     */
253
    public function itemContainerClass(string $value): self
254
    {
255
        $new = clone $this;
256
        Html::addCssClass($new->itemContainerAttributes, $value);
257
258
        return $new;
259
    }
260
261
    /**
262
     * Returns a new instance with the specified item container tag.
263
     *
264
     * @param string $value The item container tag.
265
     */
266
    public function itemContainerTag(string $value): self
267
    {
268
        $new = clone $this;
269
        $new->itemContainerTag = $value;
270
271
        return $new;
272
    }
273
274
    /**
275
     * Returns a new instance with the specified item tag.
276
     *
277
     * @param string $value The item tag.
278
     */
279
    public function itemTag(string $value): self
280
    {
281
        $new = clone $this;
282
        $new->itemTag = $value;
283
284
        return $new;
285
    }
286
287
    /**
288
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
289
     * single menu with the following structure:
290
     *
291
     * - label: string, required, the nav item label.
292
     * - active: bool, whether the item should be on active state or not.
293
     * - disabled: bool, whether the item should be on disabled state or not. For default `disabled` is false.
294
     * - enclose: bool, whether the item should be enclosed by a `<li>` tag or not. For default `enclose` is true.
295
     * - encodeLabel: bool, whether the label should be HTML encoded or not. For default `encodeLabel` is true.
296
     * - headerAttributes: array, HTML attributes to be rendered in the item header.
297
     * - link: string, the item's href. Defaults to "#". For default `link` is "#".
298
     * - linkAttributes: array, the HTML attributes of the item's link. For default `linkAttributes` is `[]`.
299
     * - icon: string, the item's icon. For default `icon` is ``.
300
     * - iconAttributes: array, the HTML attributes of the item's icon. For default `iconAttributes` is `[]`.
301
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
302
     * - items: array, optional, the submenu items. The structure is the same as this property.
303
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
304
     * - itemsContainerAttributes: array, optional, the HTML attributes for tag `<li>`.
305
     *
306
     * To insert dropdown divider use `-`.
307
     *
308
     * @param array $value
309
     */
310
    public function items(array $value): self
311
    {
312
        $new = clone $this;
313
        $new->items = $value;
314
315
        return $new;
316
    }
317
318
    /**
319
     * Returns a new instance with the specified items container HTML attributes.
320
     *
321
     * @param array $values Attribute values indexed by attribute names.
322
     */
323
    public function itemsContainerAttributes(array $values): self
324
    {
325
        $new = clone $this;
326
        $new->itemsContainerAttributes = $values;
327
328
        return $new;
329
    }
330
331
    /**
332
     * Returns a new instance with the specified item container class.
333
     *
334
     * @param string $value The item container class.
335
     */
336
    public function itemsContainerClass(string $value): self
337
    {
338
        $new = clone $this;
339
        Html::addCssClass($new->itemsContainerAttributes, $value);
340
341
        return $new;
342
    }
343
344
    /**
345
     * Returns a new instance with the specified items container tag.
346
     *
347
     * @param string $value The items container tag.
348
     */
349
    public function itemsContainerTag(string $value): self
350
    {
351
        $new = clone $this;
352
        $new->itemsContainerTag = $value;
353
354
        return $new;
355
    }
356
357
    /**
358
     * Returns a new instance with the specified split button attributes.
359
     *
360
     * @param array $values Attribute values indexed by attribute names.
361
     */
362
    public function splitButtonAttributes(array $values): self
363
    {
364
        $new = clone $this;
365
        $new->splitButtonAttributes = $values;
366
367
        return $new;
368
    }
369
370
    /**
371
     * Returns a new instance with the specified split button class.
372
     *
373
     * @param string $value The split button class.
374
     */
375
    public function splitButtonClass(string $value): self
376
    {
377
        $new = clone $this;
378
        Html::addCssClass($new->splitButtonAttributes, $value);
379
380
        return $new;
381
    }
382
383
    /**
384
     * Returns a new instance with the specified split button span class.
385
     *
386
     * @param string $value The split button span class.
387
     */
388
    public function splitButtonSpanClass(string $value): self
389
    {
390
        $new = clone $this;
391
        Html::addCssClass($new->splitButtonSpanAttributes, $value);
392
393
        return $new;
394
    }
395
396
    /**
397
     * Returns a new instance with the specified toggle HTML attributes.
398
     *
399
     * @param array $values Attribute values indexed by attribute names.
400
     */
401
    public function toggleAttributes(array $values): self
402
    {
403
        $new = clone $this;
404
        $new->toggleAttributes = $values;
405
406
        return $new;
407
    }
408
409
    /**
410
     * Returns a new instance with the specified toggle class.
411
     *
412
     * @param string $value The toggle class.
413
     */
414
    public function toggleClass(string $value): self
415
    {
416
        $new = clone $this;
417
        Html::addCssClass($new->toggleAttributes, $value);
418
419
        return $new;
420
    }
421
422
    /**
423
     * Returns a new instance with the specified toggle type, if `button` the toggle will be a button, otherwise a
424
     * `a` tag will be used.
425
     *
426
     * @param string $value The toggle tag.
427
     */
428
    public function toggleType(string $value): self
429
    {
430
        $new = clone $this;
431
        $new->toggleType = $value;
432
433
        return $new;
434
    }
435
436
    /**
437
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
438
     */
439
    protected function run(): string
440
    {
441
        $containerAttributes = $this->containerAttributes;
442
        $items = Helper\Normalize::dropdown($this->items);
443
        $items = $this->renderItems($items) . PHP_EOL;
444
445
        if (trim($items) === '') {
446
            return '';
447
        }
448
449
        if ($this->containerClass !== '') {
450
            Html::addCssClass($containerAttributes, $this->containerClass);
451
        }
452
453
        if ($this->containerTag === '') {
454
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
455
        }
456
457
        return match ($this->container) {
458
            true => Html::normalTag($this->containerTag, $items, $containerAttributes)->encode(false)->render(),
459
            false => $items,
460
        };
461
    }
462
463
    private function renderDivider(): string
464
    {
465
        $dividerAttributes = $this->dividerAttributes;
466
467
        if ($this->dividerClass !== '') {
468
            Html::addCssClass($dividerAttributes, $this->dividerClass);
469
        }
470
471
        if ($this->dividerTag === '') {
472
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
473
        }
474
475
        return $this->renderItemContainer(
476
            Html::tag($this->dividerTag, '', $dividerAttributes)->encode(false)->render(),
477
        );
478
    }
479
480
    /**
481
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
482
     */
483
    private function renderDropdown(array $items): string
484
    {
485
        return self::widget()
486
            ->container(false)
0 ignored issues
show
Bug introduced by
The method container() 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\Dropdown. ( Ignorable by Annotation )

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

486
            ->/** @scrutinizer ignore-call */ container(false)
Loading history...
487
            ->dividerAttributes($this->dividerAttributes)
488
            ->headerClass($this->headerClass)
489
            ->headerTag($this->headerTag)
490
            ->itemClass($this->itemClass)
491
            ->itemContainerAttributes($this->itemContainerAttributes)
492
            ->itemContainerTag($this->itemContainerTag)
493
            ->items($items)
494
            ->itemsContainerAttributes(array_merge($this->itemsContainerAttributes))
495
            ->itemTag($this->itemTag)
496
            ->toggleAttributes($this->toggleAttributes)
497
            ->toggleType($this->toggleType)
498
            ->render();
499
    }
500
501
    private function renderHeader(string $label, array $headerAttributes = []): string
502
    {
503
        if ($this->headerClass !== '') {
504
            Html::addCssClass($headerAttributes, $this->headerClass);
505
        }
506
507
        if ($this->headerTag === '') {
508
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
509
        }
510
511
        return $this->renderItemContainer(
512
            Html::normalTag($this->headerTag, $label, $headerAttributes)->encode(false)->render(),
513
        );
514
    }
515
516
    /**
517
     * @param array $item The item to be rendered.
518
     *
519
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
520
     */
521
    private function renderItem(array $item): string
522
    {
523
        if ($item['visible'] === false) {
524
            return '';
525
        }
526
527
        /** @var bool */
528
        $enclose = $item['enclose'] ?? true;
529
        /** @var array */
530
        $headerAttributes = $item['headerAttributes'] ?? [];
531
        /** @var array */
532
        $items = $item['items'] ?? [];
533
        /** @var array */
534
        $itemContainerAttributes = $item['itemContainerAttributes'] ?? [];
535
536
        /**
537
         * @var string $item['label']
538
         * @var string $item['icon']
539
         * @var string $item['iconClass']
540
         * @var array $item['iconAttributes']
541
         * @var array $item['iconContainerAttributes']
542
         */
543
        $label = $this->renderLabel(
544
            $item['label'],
545
            $item['icon'],
546
            $item['iconClass'],
547
            $item['iconAttributes'],
548
            $item['iconContainerAttributes'],
549
        );
550
551
        /** @var array */
552
        $lines = [];
553
        /** @var string */
554
        $link = $item['link'];
555
        /** @var array */
556
        $linkAttributes = $item['linkAttributes'] ?? [];
557
        /** @var array */
558
        $toggleAttributes = $item['toggleAttributes'] ?? [];
559
560
        if ($this->itemClass !== '') {
561
            Html::addCssClass($linkAttributes, $this->itemClass);
562
        }
563
564
        if ($item['active']) {
565
            $linkAttributes['aria-current'] = 'true';
566
            Html::addCssClass($linkAttributes, [$this->activeClass]);
567
        }
568
569
        if ($item['disabled']) {
570
            Html::addCssClass($linkAttributes, $this->disabledClass);
571
        }
572
573
        if ($items === []) {
574
            $lines[] = $this->renderItemContent(
575
                $label,
576
                $link,
577
                $enclose,
578
                $linkAttributes,
579
                $headerAttributes,
580
                $itemContainerAttributes,
581
            );
582
        } else {
583
            $itemContainer = $this->renderItemsContainer($this->renderDropdown($items));
584
            $toggle = $this->renderToggle($label, $link, $toggleAttributes);
585
            $toggleSplitButton = $this->renderToggleSplitButton($label);
586
587
            if ($this->toggleType === 'split' && !str_contains($this->containerClass, 'dropstart')) {
588
                $lines[] = $toggleSplitButton . PHP_EOL . $toggle . PHP_EOL . $itemContainer;
589
            } elseif ($this->toggleType === 'split' && str_contains($this->containerClass, 'dropstart')) {
590
                $lines[] = $toggle . PHP_EOL . $itemContainer . PHP_EOL . $toggleSplitButton;
591
            } else {
592
                $lines[] = $toggle . PHP_EOL . $itemContainer;
593
            }
594
        }
595
596
        /** @psalm-var string[] $lines */
597
        return implode(PHP_EOL, $lines);
598
    }
599
600
    private function renderItemContainer(string $content, array $itemContainerAttributes = []): string
601
    {
602
        if ($this->itemContainerTag === '') {
603
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
604
        }
605
606
        if ($itemContainerAttributes === []) {
607
            $itemContainerAttributes = $this->itemContainerAttributes;
608
        }
609
610
        return Html::normalTag($this->itemContainerTag, $content, $itemContainerAttributes)
611
            ->encode(false)
612
            ->render();
613
    }
614
615
    private function renderItemsContainer(string $content): string
616
    {
617
        $itemsContainerAttributes = $this->itemsContainerAttributes;
618
619
        if ($this->id !== '') {
620
            $itemsContainerAttributes['aria-labelledby'] = $this->id;
621
        }
622
623
        if ($this->itemsContainerTag === '') {
624
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
625
        }
626
627
        return Html::normalTag($this->itemsContainerTag, $content, $itemsContainerAttributes)
628
            ->encode(false)
629
            ->render();
630
    }
631
632
    /**
633
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
634
     */
635
    private function renderItemContent(
636
        string $label,
637
        string $link,
638
        bool $enclose,
639
        array $linkAttributes = [],
640
        array $headerAttributes = [],
641
        array $itemContainerAttributes = [],
642
    ): string {
643
        return match (true) {
644
            $label === '-' => $this->renderDivider(),
645
            $enclose === false => $label,
646
            $link === '' => $this->renderHeader($label, $headerAttributes),
647
            default => $this->renderItemLink($label, $link, $linkAttributes, $itemContainerAttributes),
648
        };
649
    }
650
651
    /**
652
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
653
     */
654
    private function renderItems(array $items = []): string
655
    {
656
        $lines = [];
657
658
        /** @var array|string $item */
659
        foreach ($items as $item) {
660
            $line = match (gettype($item)) {
661
                'array' => $this->renderItem($item),
662
                'string' => $this->renderDivider(),
663
            };
664
665
            if ($line !== '') {
666
                $lines[] = $line;
667
            }
668
        }
669
670
        return PHP_EOL . implode(PHP_EOL, $lines);
671
    }
672
673
    private function renderLabel(
674
        string $label,
675
        string $icon,
676
        string $iconClass,
677
        array $iconAttributes = [],
678
        array $iconContainerAttributes = []
679
    ): string {
680
        $html = '';
681
682
        if ($iconClass !== '') {
683
            Html::addCssClass($iconAttributes, $iconClass);
684
        }
685
686
        if ($icon !== '' || $iconAttributes !== [] || $iconClass !== '') {
687
            $i = I::tag()->addAttributes($iconAttributes)->content($icon)->encode(false)->render();
688
            $html = Span::tag()->addAttributes($iconContainerAttributes)->content($i)->encode(false)->render();
689
        }
690
691
        if ($label !== '') {
692
            $html .= $label;
693
        }
694
695
        return $html;
696
    }
697
698
    /**
699
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
700
     */
701
    private function renderItemLink(
702
        string $label,
703
        string $link,
704
        array $linkAttributes = [],
705
        array $itemContainerAttributes = []
706
    ): string {
707
        $linkAttributes['href'] = $link;
708
709
        if ($this->itemTag === '') {
710
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
711
        }
712
713
        $linkTag = Html::normalTag($this->itemTag, $label, $linkAttributes)->encode(false)->render();
714
715
        return match ($this->itemContainer) {
716
            true => $this->renderItemContainer($linkTag, $itemContainerAttributes),
717
            default => $linkTag,
718
        };
719
    }
720
721
    private function renderToggle(string $label, string $link, array $toggleAttributes = []): string
722
    {
723
        if ($toggleAttributes === []) {
724
            $toggleAttributes = $this->toggleAttributes;
725
        }
726
727
        if ($this->id !== '') {
728
            $toggleAttributes['id'] = $this->id;
729
        }
730
731
        return match ($this->toggleType) {
732
            'link' => $this->renderToggleLink($label, $link, $toggleAttributes),
733
            'split' => $this->renderToggleSplit($label, $toggleAttributes),
734
            default => $this->renderToggleButton($label, $toggleAttributes),
735
        };
736
    }
737
738
    private function renderToggleButton(string $label, array $toggleAttributes = []): string
739
    {
740
        return Button::tag()->addAttributes($toggleAttributes)->content($label)->type('button')->render();
741
    }
742
743
    private function renderToggleLink(string $label, string $link, array $toggleAttributes = []): string
744
    {
745
        return A::tag()->addAttributes($toggleAttributes)->content($label)->href($link)->render();
746
    }
747
748
    private function renderToggleSplit(string $label, array $toggleAttributes = []): string
749
    {
750
        return Button::tag()
751
            ->addAttributes($toggleAttributes)
752
            ->content(Span::tag()->addAttributes($this->splitButtonSpanAttributes)->content($label))
753
            ->type('button')
754
            ->render();
755
    }
756
757
    private function renderToggleSplitButton(string $label): string
758
    {
759
        return Button::tag()->addAttributes($this->splitButtonAttributes)->content($label)->type('button')->render();
760
    }
761
}
762