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

Dropdown::renderToggleLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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