Test Setup Failed
Pull Request — master (#61)
by Wilmer
02:18
created

Dropdown::itemDisabledStyleCss()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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