Passed
Pull Request — master (#79)
by Wilmer
02:24
created

Dropdown::render()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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