Dropdown   C
last analyzed

Complexity

Total Complexity 53

Size/Duplication

Total Lines 601
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 53
eloc 241
c 2
b 1
f 0
dl 0
loc 601
ccs 251
cts 251
cp 1
rs 6.96

32 Methods

Rating   Name   Duplication   Size   Complexity  
A autoIdPrefix() 0 5 1
A renderDropdownContent() 0 7 1
A renderDropdownTrigger() 0 13 2
A renderDropdownMenu() 0 8 1
A buttonAttributes() 0 5 1
A renderDropdown() 0 20 2
A dividerCssClass() 0 5 1
A buttonIconAttributes() 0 5 1
A renderLabelButton() 0 31 4
A renderDropdownButton() 0 22 1
A itemCssClass() 0 5 1
A renderDropdownButtonLink() 0 15 1
A enclosedByContainer() 0 5 1
A id() 0 5 1
A itemDisabledStyleCss() 0 5 1
A attributes() 0 5 1
A buttonIconText() 0 5 1
A render() 0 3 1
A contentCssClass() 0 5 1
A buttonLabelAttributes() 0 5 1
A triggerCssClass() 0 5 1
A buttonLabel() 0 5 1
A renderLabelItem() 0 25 4
A submenuAttributes() 0 5 1
A itemHeaderCssClass() 0 5 1
A submenu() 0 5 1
A cssClass() 0 5 1
A menuCssClass() 0 5 1
C renderItems() 0 102 14
A items() 0 5 1
A buttonIconCssClass() 0 5 1
A itemActiveCssClass() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Dropdown often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Dropdown, and based on these observations, apply Extract Interface, too.

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\Button;
15
use Yiisoft\Html\Tag\CustomTag;
16
use Yiisoft\Html\Tag\Div;
17
use Yiisoft\Html\Tag\Span;
18
use Yiisoft\Widget\Widget;
19
20
use function array_merge;
21
use function implode;
22
use function is_array;
23
24
/**
25
 * The dropdown component is a container for a dropdown button and a dropdown menu.
26
 *
27
 * @link https://bulma.io/documentation/components/dropdown/
28
 */
29
final class Dropdown extends Widget
30
{
31
    private array $attributes = [];
32
    private string $autoIdPrefix = 'w';
33
    private array $buttonAttributes = [];
34
    private array $buttonIconAttributes = ['class' => 'icon is-small'];
35
    private string $buttonIconCssClass = '';
36
    private string $buttonIconText = '&#8595;';
37
    private string $buttonLabel = 'Click Me';
38
    private array $buttonLabelAttributes = [];
39
    private string $cssClass = 'dropdown';
40
    private string $contentCssClass = 'dropdown-content';
41
    private string $dividerCssClass = 'dropdown-divider';
42
    private bool $encloseByContainer = true;
43
    private string $itemActiveCssClass = 'is-active';
44
    private string $itemCssClass = 'dropdown-item';
45
    private string $itemDisabledStyleCss = 'opacity:.65;pointer-events:none;';
46
    private string $itemHeaderCssClass = 'dropdown-header';
47
    private array $items = [];
48
    private string $menuCssClass = 'dropdown-menu';
49
    private bool $submenu = false;
50
    private array $submenuAttributes = [];
51
    private string $triggerCssClass = 'dropdown-trigger';
52
53
    /**
54
     * Returns a new instance with the specified HTML attributes for widget.
55
     *
56
     * @param array $values Attribute values indexed by attribute names.
57
     *
58
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} For details on how attributes are being rendered.
59
     */
60 3
    public function attributes(array $values): self
61
    {
62 3
        $new = clone $this;
63 3
        $new->attributes = $values;
64 3
        return $new;
65
    }
66
67
    /**
68
     * Returns a new instance with the specified prefix to the automatically generated widget IDs.
69
     *
70
     * @param string $value The prefix to the automatically generated widget IDs.
71
     */
72 1
    public function autoIdPrefix(string $value): self
73
    {
74 1
        $new = clone $this;
75 1
        $new->autoIdPrefix = $value;
76 1
        return $new;
77
    }
78
79
    /**
80
     * Returns a new instance with the specified HTML attributes for the dropdown button.
81
     *
82
     * @param array $values Attribute values indexed by attribute names.
83
     */
84 2
    public function buttonAttributes(array $values): self
85
    {
86 2
        $new = clone $this;
87 2
        $new->buttonAttributes = $values;
88 2
        return $new;
89
    }
90
91
    /**
92
     * Returns a new instance with the specified HTML attributes for the dropdown button icon.
93
     *
94
     * @param array $values Attribute values indexed by attribute names.
95
     */
96 2
    public function buttonIconAttributes(array $values): self
97
    {
98 2
        $new = clone $this;
99 2
        $new->buttonIconAttributes = $values;
100 2
        return $new;
101
    }
102
103
    /**
104
     * Returns a new instance with the specified icon CSS class for the dropdown button.
105
     *
106
     * @param string $value The CSS class.
107
     */
108 2
    public function buttonIconCssClass(string $value): self
109
    {
110 2
        $new = clone $this;
111 2
        $new->buttonIconCssClass = $value;
112 2
        return $new;
113
    }
114
115
    /**
116
     * Returns a new instance with the specified icon text for the dropdown button.
117
     *
118
     * @param string $value The text.
119
     */
120 3
    public function buttonIconText(string $value): self
121
    {
122 3
        $new = clone $this;
123 3
        $new->buttonIconText = $value;
124 3
        return $new;
125
    }
126
127
    /**
128
     * Returns a new instance with the specified label for the dropdown button.
129
     *
130
     * @param string $value The label.
131
     */
132 2
    public function buttonLabel(string $value): self
133
    {
134 2
        $new = clone $this;
135 2
        $new->buttonLabel = $value;
136 2
        return $new;
137
    }
138
139
    /**
140
     * Returns a new instance with the specified HTML attributes for the dropdown button label.
141
     *
142
     * @param array $values Attribute values indexed by attribute names.
143
     */
144 2
    public function buttonLabelAttributes(array $values): self
145
    {
146 2
        $new = clone $this;
147 2
        $new->buttonLabelAttributes = $values;
148 2
        return $new;
149
    }
150
151
    /**
152
     * Returns a new instance with the specified CSS class for dropdown content.
153
     *
154
     * @param string $value The CSS class.
155
     *
156
     * @link https://bulma.io/documentation/components/dropdown/#dropdown-content
157
     */
158 2
    public function contentCssClass(string $value): self
159
    {
160 2
        $new = clone $this;
161 2
        $new->contentCssClass = $value;
162 2
        return $new;
163
    }
164
165
    /**
166
     * Returns a new instance with the specified CSS class for the dropdown container.
167
     *
168
     * @param string $value The CSS class.
169
     */
170 9
    public function cssClass(string $value): self
171
    {
172 9
        $new = clone $this;
173 9
        $new->cssClass = $value;
174 9
        return $new;
175
    }
176
177
    /**
178
     * Returns a new instance with the specified CSS class for horizontal line separating dropdown items.
179
     *
180
     * @param string $value The CSS class.
181
     */
182 11
    public function dividerCssClass(string $value): self
183
    {
184 11
        $new = clone $this;
185 11
        $new->dividerCssClass = $value;
186 11
        return $new;
187
    }
188
189
    /**
190
     * Returns a new instance with the specified if the widget should be enclosed by container.
191
     *
192
     * @param bool $value Whether the widget should be enclosed by container. Defaults to true.
193
     */
194 10
    public function enclosedByContainer(bool $value = false): self
195
    {
196 10
        $new = clone $this;
197 10
        $new->encloseByContainer = $value;
198 10
        return $new;
199
    }
200
201
    /**
202
     * Returns a new instance with the specified ID of the widget.
203
     *
204
     * @param string $value The ID of the widget.
205
     */
206 2
    public function id(string $value): self
207
    {
208 2
        $new = clone $this;
209 2
        $new->attributes['id'] = $value;
210 2
        return $new;
211
    }
212
213
    /**
214
     * Returns a new instance with the specified CSS class for active dropdown item.
215
     *
216
     * @param string $value The CSS class.
217
     */
218 2
    public function itemActiveCssClass(string $value): self
219
    {
220 2
        $new = clone $this;
221 2
        $new->itemActiveCssClass = $value;
222 2
        return $new;
223
    }
224
225
    /**
226
     * Returns a new instance with the specified CSS class for dropdown item.
227
     *
228
     * @param string $value The CSS class.
229
     */
230 11
    public function itemCssClass(string $value): self
231
    {
232 11
        $new = clone $this;
233 11
        $new->itemCssClass = $value;
234 11
        return $new;
235
    }
236
237
    /**
238
     * Returns a new instance with the specified style attributes for disabled dropdown item.
239
     *
240
     * @param string $value The CSS class.
241
     */
242 2
    public function itemDisabledStyleCss(string $value): self
243
    {
244 2
        $new = clone $this;
245 2
        $new->itemDisabledStyleCss = $value;
246 2
        return $new;
247
    }
248
249
    /**
250
     * Returns a new instance with the specified CSS class for dropdown item header.
251
     *
252
     * @param string $value The CSS class.
253
     */
254 2
    public function itemHeaderCssClass(string $value): self
255
    {
256 2
        $new = clone $this;
257 2
        $new->itemHeaderCssClass = $value;
258 2
        return $new;
259
    }
260
261
    /**
262
     * Returns a new instance with the specified list of items.
263
     *
264
     * Each array element can be either an HTML string, or an array representing a single menu with the following
265
     * structure:
266
     *
267
     * - label: string, required, the label of the item link.
268
     * - encode: bool, optional, whether to HTML-encode item label.
269
     * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
270
     *   If not set, the item will be treated as a menu header when the item has no sub-menu.
271
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
272
     * - urlAttributes: array, optional, the HTML attributes of the item link.
273
     * - items: array, optional, the submenu items. The structure is the same as this property.
274
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
275
     * - submenuAttributes: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
276
     *   merged with {@see submenuAttributes}.
277
     *
278
     * To insert divider use `-`.
279
     *
280
     * @param array $value The menu items.
281
     */
282 29
    public function items(array $value): self
283
    {
284 29
        $new = clone $this;
285 29
        $new->items = $value;
286 29
        return $new;
287
    }
288
289
    /**
290
     * Returns a new instance with the specified dropdown menu CSS class.
291
     *
292
     * @param string $value The CSS class.
293
     */
294 2
    public function menuCssClass(string $value): self
295
    {
296 2
        $new = clone $this;
297 2
        $new->menuCssClass = $value;
298 2
        return $new;
299
    }
300
301
    /**
302
     * Returns a new instance with the specified dropdown trigger CSS class.
303
     *
304
     * @param string $value The CSS class.
305
     */
306 2
    public function triggerCssClass(string $value): self
307
    {
308 2
        $new = clone $this;
309 2
        $new->triggerCssClass = $value;
310 2
        return $new;
311
    }
312
313
    /**
314
     * Returns a new instance with the specified if it is a submenu or sub-dropdown.
315
     *
316
     * @param bool $value Whether it is a submenu or sub-dropdown. Defaults to false.
317
     */
318 3
    public function submenu(bool $value): self
319
    {
320 3
        $new = clone $this;
321 3
        $new->submenu = $value;
322 3
        return $new;
323
    }
324
325
    /**
326
     * Returns a new instance with the specified HTML attributes for sub-menu container tag.
327
     *
328
     * @param array $values Attribute values indexed by attribute names.
329
     */
330 3
    public function submenuAttributes(array $values): self
331
    {
332 3
        $new = clone $this;
333 3
        $new->submenuAttributes = $values;
334 3
        return $new;
335
    }
336
337
    /**
338
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
339
     */
340 28
    public function render(): string
341
    {
342 28
        return $this->renderDropdown();
343
    }
344
345
    /**
346
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
347
     */
348 28
    private function renderDropdown(): string
349
    {
350 28
        $attributes = $this->attributes;
351
352
        /** @var string */
353 28
        $id = $attributes['id'] ?? (Html::generateId($this->autoIdPrefix) . '-dropdown');
354 28
        unset($attributes['id']);
355
356 28
        if ($this->encloseByContainer) {
357 20
            Html::addCssClass($attributes, $this->cssClass);
358 20
            $html = Div::tag()
359 20
                ->attributes($attributes)
360 20
                ->content(PHP_EOL . $this->renderDropdownTrigger($id) . PHP_EOL)
361 20
                ->encode(false)
362 20
                ->render();
363
        } else {
364 9
            $html = $this->renderItems();
365
        }
366
367 27
        return $html;
368
    }
369
370
    /**
371
     * Render dropdown button.
372
     *
373
     * @return string The rendering result.
374
     *
375
     * @link https://bulma.io/documentation/components/dropdown/#hoverable-or-toggable
376
     */
377 19
    private function renderDropdownButton(string $id): string
378
    {
379 19
        $buttonAttributes = $this->buttonAttributes;
380
381 19
        Html::addCssClass($buttonAttributes, 'button');
382
383 19
        $buttonAttributes['aria-haspopup'] = 'true';
384 19
        $buttonAttributes['aria-controls'] = $id;
385
386 19
        return Button::tag()
387 19
                ->attributes($buttonAttributes)
388 19
                ->content(
389 19
                    $this->renderLabelButton(
390 19
                        $this->buttonLabel,
391 19
                        $this->buttonLabelAttributes,
392 19
                        $this->buttonIconText,
393 19
                        $this->buttonIconCssClass,
394 19
                        $this->buttonIconAttributes,
395 19
                    )
396 19
                )
397 19
                ->encode(false)
398 19
                ->render() . PHP_EOL;
399
    }
400
401 2
    private function renderDropdownButtonLink(): string
402
    {
403 2
        return A::tag()
404 2
                ->class($this->itemCssClass)
405 2
                ->content(
406 2
                    $this->renderLabelButton(
407 2
                        $this->buttonLabel,
408 2
                        $this->buttonLabelAttributes,
409 2
                        $this->buttonIconText,
410 2
                        $this->buttonIconCssClass,
411 2
                        $this->buttonIconAttributes,
412 2
                    )
413 2
                )
414 2
                ->encode(false)
415 2
                ->render() . PHP_EOL;
416
    }
417
418
    /**
419
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
420
     */
421 20
    private function renderDropdownContent(): string
422
    {
423 20
        return Div::tag()
424 20
            ->class($this->contentCssClass)
425 20
            ->content(PHP_EOL . $this->renderItems() . PHP_EOL)
426 20
            ->encode(false)
427 20
            ->render();
428
    }
429
430
    /**
431
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
432
     */
433 20
    private function renderDropdownMenu(string $id): string
434
    {
435 20
        return Div::tag()
436 20
            ->class($this->menuCssClass)
437 20
            ->content(PHP_EOL . $this->renderDropdownContent() . PHP_EOL)
438 20
            ->encode(false)
439 20
            ->id($id)
440 20
            ->render();
441
    }
442
443
    /**
444
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
445
     */
446 20
    private function renderDropdownTrigger(string $id): string
447
    {
448 20
        if (!$this->submenu) {
449 19
            $button = $this->renderDropdownButton($id);
450
        } else {
451 2
            $button = $this->renderDropdownButtonLink();
452
        }
453
454 20
        return Div::tag()
455 20
                ->class($this->triggerCssClass)
456 20
                ->content(PHP_EOL . $button)
457 20
                ->encode(false)
458 20
                ->render() . PHP_EOL . $this->renderDropdownMenu($id);
459
    }
460
461
    /**
462
     * Renders menu items.
463
     *
464
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
465
     *
466
     * @return string the rendering result.
467
     */
468 28
    private function renderItems(): string
469
    {
470 28
        $lines = [];
471
472
        /** @var array|string $item */
473 28
        foreach ($this->items as $item) {
474 28
            if ($item === '-') {
475 4
                $lines[] = CustomTag::name('hr')
476 4
                    ->class($this->dividerCssClass)
477 4
                    ->render();
478
            } else {
479 28
                if (!isset($item['label'])) {
480 1
                    throw new InvalidArgumentException('The "label" option is required.');
481
                }
482
483
                /** @var string */
484 27
                $itemLabel = $item['label'] ?? '';
485
486 27
                if (isset($item['encode']) && $item['encode'] === true) {
487 1
                    $itemLabel = Html::encode($itemLabel);
488
                }
489
490
                /** @var array */
491 27
                $items = $item['items'] ?? [];
492
493
                /** @var array */
494 27
                $urlAttributes = $item['urlAttributes'] ?? [];
495
496
                /** @var string */
497 27
                $iconText = $item['iconText'] ?? '';
498
499
                /** @var string */
500 27
                $iconCssClass = $item['iconCssClass'] ?? '';
501
502
                /** @var array */
503 27
                $iconAttributes = $item['iconAttributes'] ?? [];
504
505
                /** @var string */
506 27
                $url = $item['url'] ?? '';
507
508
                /** @var bool */
509 27
                $active = $item['active'] ?? false;
510
511
                /** @var bool */
512 27
                $disabled = $item['disable'] ?? false;
513
514
                /** @var bool */
515 27
                $enclose = $item['enclose'] ?? true;
516
517
                /** @var bool */
518 27
                $submenu = $item['submenu'] ?? false;
519
520 27
                $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes);
521
522 27
                Html::addCssClass($urlAttributes, $this->itemCssClass);
523
524 27
                if ($disabled) {
525 2
                    Html::addCssStyle($urlAttributes, $this->itemDisabledStyleCss);
526 26
                } elseif ($active) {
527 5
                    Html::addCssClass($urlAttributes, $this->itemActiveCssClass);
528
                }
529
530 27
                if ($items === []) {
531 27
                    if ($itemLabel === '-') {
532 1
                        $content = CustomTag::name('hr')
533 1
                            ->class($this->dividerCssClass)
534 1
                            ->render();
535 27
                    } elseif ($enclose === false) {
536 1
                        $content = $itemLabel;
537 27
                    } elseif ($url === '') {
538 1
                        $content = CustomTag::name('h6')
539 1
                            ->class($this->itemHeaderCssClass)
540 1
                            ->content($itemLabel)
541 1
                            ->encode(null)
542 1
                            ->render();
543
                    } else {
544 26
                        $content = A::tag()
545 26
                            ->attributes($urlAttributes)
546 26
                            ->content($itemLabel)
547 26
                            ->encode(false)
548 26
                            ->url($url)
549 26
                            ->render();
550
                    }
551
552 27
                    $lines[] = $content;
553
                } else {
554 2
                    $submenuAttributes = isset($item['submenuAttributes']) && is_array($item['submenuAttributes'])
555 2
                        ? array_merge($this->submenuAttributes, $item['submenuAttributes']) : $this->submenuAttributes;
556
557 2
                    $lines[] = self::widget()
558 2
                        ->attributes($this->attributes)
559 2
                        ->dividerCssClass($this->dividerCssClass)
560 2
                        ->itemCssClass($this->itemCssClass)
561 2
                        ->items($items)
562 2
                        ->submenu($submenu)
563 2
                        ->submenuAttributes($submenuAttributes)
564 2
                        ->render();
565
                }
566
            }
567
        }
568
569 27
        return implode(PHP_EOL, $lines);
570
    }
571
572 20
    private function renderLabelButton(
573
        string $label,
574
        array $labelAttributes,
575
        string $iconText,
576
        string $iconCssClass,
577
        array $iconAttributes = []
578
    ): string {
579 20
        $html = '';
580
581 20
        if ($label !== '') {
582 20
            $html = PHP_EOL . Span::tag()
583 20
                    ->attributes($labelAttributes)
584 20
                    ->content($label)
585 20
                    ->encode(false)
586 20
                    ->render();
587
        }
588
589 20
        if ($iconText !== '' || $iconCssClass !== '') {
590 20
            $html .= PHP_EOL .
591 20
                Span::tag()
592 20
                    ->attributes($iconAttributes)
593 20
                    ->content(CustomTag::name('i')
594 20
                        ->class($iconCssClass)
595 20
                        ->content($iconText)
596 20
                        ->encode(false)
597 20
                        ->render())
598 20
                    ->encode(false)
599 20
                    ->render();
600
        }
601
602 20
        return $html . PHP_EOL;
603
    }
604
605 27
    private function renderLabelItem(
606
        string $label,
607
        string $iconText,
608
        string $iconCssClass,
609
        array $iconAttributes = []
610
    ): string {
611 27
        $html = '';
612
613 27
        if ($iconText !== '' || $iconCssClass !== '') {
614 1
            $html = Span::tag()
615 1
                ->attributes($iconAttributes)
616 1
                ->content(CustomTag::name('i')
617 1
                    ->class($iconCssClass)
618 1
                    ->content($iconText)
619 1
                    ->encode(false)
620 1
                    ->render())
621 1
                ->encode(false)
622 1
                ->render();
623
        }
624
625 27
        if ($label !== '') {
626 27
            $html .= $label;
627
        }
628
629 27
        return $html;
630
    }
631
}
632