Passed
Branch master (fbd372)
by Wilmer
02:37
created

Dropdown::submenuAttributes()   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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
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
     * @link https://bulma.io/documentation/components/dropdown/#dropdown-content
174
     */
175 2
    public function contentCssClass(string $value): self
176
    {
177 2
        $new = clone $this;
178 2
        $new->contentCssClass = $value;
179 2
        return $new;
180
    }
181
182
    /**
183
     * Set CSS class for the dropdown container.
184
     *
185
     * @param string $value The CSS class.
186
     *
187
     * @return self
188
     */
189 9
    public function cssClass(string $value): self
190
    {
191 9
        $new = clone $this;
192 9
        $new->cssClass = $value;
193 9
        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 11
    public function dividerCssClass(string $value): self
204
    {
205 11
        $new = clone $this;
206 11
        $new->dividerCssClass = $value;
207 11
        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 10
    public function enclosedByContainer(bool $value = false): self
218
    {
219 10
        $new = clone $this;
220 10
        $new->encloseByContainer = $value;
221 10
        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 2
    public function id(string $value): self
232
    {
233 2
        $new = clone $this;
234 2
        $new->attributes['id'] = $value;
235 2
        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 11
    public function itemCssClass(string $value): self
260
    {
261 11
        $new = clone $this;
262 11
        $new->itemCssClass = $value;
263 11
        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
     *   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
     * - urlAttributes: array, optional, the HTML attributes of the item link.
304
     * - attributes: array, optional, the HTML attributes of the item.
305
     * - items: array, optional, the submenu items. The structure is the same as this property.
306
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
307
     * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
308
     *   merged with {@see submenuOptions}.
309
     *
310
     * To insert divider use `-`.
311
     *
312
     * @param array $value The menu items.
313
     *
314
     * @return self
315
     */
316 29
    public function items(array $value): self
317
    {
318 29
        $new = clone $this;
319 29
        $new->items = $value;
320 29
        return $new;
321
    }
322
323
    /**
324
     * Set Dropdown menu CSS class.
325
     *
326
     * @param string $value The CSS class.
327
     *
328
     * @return self
329
     */
330 2
    public function menuCssClass(string $value): self
331
    {
332 2
        $new = clone $this;
333 2
        $new->menuCssClass = $value;
334 2
        return $new;
335
    }
336
337
    /**
338
     * Set Dropdown trigger CSS class.
339
     *
340
     * @param string $value The CSS class.
341
     *
342
     * @return self
343
     */
344 2
    public function triggerCssClass(string $value): self
345
    {
346 2
        $new = clone $this;
347 2
        $new->triggerCssClass = $value;
348 2
        return $new;
349
    }
350
351
    /**
352
     * Set if it is a submenu or sub-dropdown.
353
     *
354
     * @param bool $value Whether it is a submenu or sub-dropdown. Defaults to false.
355
     *
356
     * @return self
357
     */
358 3
    public function submenu(bool $value): self
359
    {
360 3
        $new = clone $this;
361 3
        $new->submenu = $value;
362 3
        return $new;
363
    }
364
365
    /**
366
     * The HTML attributes for sub-menu container tag.
367
     *
368
     * @param array $values Attribute values indexed by attribute names.
369
     *
370
     * @return self
371
     */
372 3
    public function submenuAttributes(array $values): self
373
    {
374 3
        $new = clone $this;
375 3
        $new->submenuAttributes = $values;
376 3
        return $new;
377
    }
378
379
    /**
380
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
381
     */
382 28
    protected function run(): string
383
    {
384 28
        return $this->renderDropdown();
385
    }
386
387
    /**
388
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
389
     */
390 28
    private function renderDropdown(): string
391
    {
392 28
        $attributes = $this->attributes;
393
394
        /** @var string */
395 28
        $id = $attributes['id'] ?? (Html::generateId($this->autoIdPrefix) . '-dropdown');
396 28
        unset($attributes['id']);
397
398 28
        if ($this->encloseByContainer) {
399 20
            Html::addCssClass($attributes, $this->cssClass);
400 20
            $html = Div::tag()
401 20
                ->attributes($attributes)
402 20
                ->content(PHP_EOL . $this->renderDropdownTrigger($id) . PHP_EOL)
403 19
                ->encode(false)
404 19
                ->render();
405
        } else {
406 9
            $html = $this->renderItems();
407
        }
408
409 27
        return $html;
410
    }
411
412
    /**
413
     * Render dropdown button.
414
     *
415
     * @return string The rendering result.
416
     *
417
     * @link https://bulma.io/documentation/components/dropdown/#hoverable-or-toggable
418
     */
419 19
    private function renderDropdownButton(string $id): string
420
    {
421 19
        $buttonAttributes = $this->buttonAttributes;
422
423 19
        Html::addCssClass($buttonAttributes, 'button');
424
425 19
        $buttonAttributes['aria-haspopup'] = 'true';
426 19
        $buttonAttributes['aria-controls'] = $id;
427
428 19
        return Button::tag()
429 19
            ->attributes($buttonAttributes)
430 19
            ->content(
431 19
                $this->renderLabelButton(
432 19
                    $this->buttonLabel,
433 19
                    $this->buttonLabelAttributes,
434 19
                    $this->buttonIconText,
435 19
                    $this->buttonIconCssClass,
436 19
                    $this->buttonIconAttributes,
437
                )
438
            )
439 19
            ->encode(false)
440 19
            ->render() . PHP_EOL;
441
    }
442
443 2
    private function renderDropdownButtonLink(): string
444
    {
445 2
        return A::tag()
446 2
            ->class($this->itemCssClass)
447 2
            ->content(
448 2
                $this->renderLabelButton(
449 2
                    $this->buttonLabel,
450 2
                    $this->buttonLabelAttributes,
451 2
                    $this->buttonIconText,
452 2
                    $this->buttonIconCssClass,
453 2
                    $this->buttonIconAttributes,
454
                )
455
            )
456 2
            ->encode(false)
457 2
            ->render() . PHP_EOL;
458
    }
459
460
    /**
461
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
462
     */
463 20
    private function renderDropdownContent(): string
464
    {
465 20
        return Div::tag()
466 20
            ->class($this->contentCssClass)
467 20
            ->content(PHP_EOL . $this->renderItems() . PHP_EOL)
468 19
            ->encode(false)
469 19
            ->render();
470
    }
471
472
    /**
473
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
474
     */
475 20
    private function renderDropdownMenu(string $id): string
476
    {
477 20
        return Div::tag()
478 20
            ->class($this->menuCssClass)
479 20
            ->content(PHP_EOL . $this->renderDropdownContent() . PHP_EOL)
480 19
            ->encode(false)
481 19
            ->id($id)
482 19
            ->render();
483
    }
484
485
    /**
486
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
487
     */
488 20
    private function renderDropdownTrigger(string $id): string
489
    {
490 20
        if (!$this->submenu) {
491 19
            $button = $this->renderDropdownButton($id);
492
        } else {
493 2
            $button = $this->renderDropdownButtonLink();
494
        }
495
496 20
        return Div::tag()
497 20
            ->class($this->triggerCssClass)
498 20
            ->content(PHP_EOL . $button)
499 20
            ->encode(false)
500 20
            ->render() . PHP_EOL . $this->renderDropdownMenu($id);
501
    }
502
503
    /**
504
     * Renders menu items.
505
     *
506
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
507
     *
508
     * @return string the rendering result.
509
     */
510 28
    private function renderItems(): string
511
    {
512 28
        $lines = [];
513
514
        /** @var array|string $item */
515 28
        foreach ($this->items as $item) {
516 28
            if ($item === '-') {
517 4
                $lines[] = CustomTag::name('hr')->class($this->dividerCssClass)->render();
518
            } else {
519 28
                if (!isset($item['label'])) {
520 1
                    throw new InvalidArgumentException('The "label" option is required.');
521
                }
522
523
                /** @var string */
524 27
                $itemLabel = $item['label'] ?? '';
525
526 27
                if (isset($item['encode']) && $item['encode'] === true) {
527 1
                    $itemLabel = Html::encode($itemLabel);
528
                }
529
530
                /** @var array */
531 27
                $items = $item['items'] ?? [];
532
533
                /** @var array */
534 27
                $urlAttributes = $item['urlAttributes'] ?? [];
535
536
                /** @var string */
537 27
                $iconText = $item['iconText'] ?? '';
538
539
                /** @var string */
540 27
                $iconCssClass = $item['iconCssClass'] ?? '';
541
542
                /** @var array */
543 27
                $iconAttributes = $item['iconAttributes'] ?? [];
544
545
                /** @var string */
546 27
                $url = $item['url'] ?? '';
547
548
                /** @var bool */
549 27
                $active = $item['active'] ?? false;
550
551
                /** @var bool */
552 27
                $disabled = $item['disable'] ?? false;
553
554
                /** @var bool */
555 27
                $enclose = $item['enclose'] ?? true;
556
557
                /** @var bool */
558 27
                $submenu = $item['submenu'] ?? false;
559
560 27
                $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes);
561
562 27
                Html::addCssClass($urlAttributes, $this->itemCssClass);
563
564 27
                if ($disabled) {
565 2
                    Html::addCssStyle($urlAttributes, $this->itemDisabledStyleCss);
566 26
                } elseif ($active) {
567 5
                    Html::addCssClass($urlAttributes, $this->itemActiveCssClass);
568
                }
569
570 27
                if ($items === []) {
571 27
                    if ($itemLabel === '-') {
572 1
                        $content = CustomTag::name('hr')->class($this->dividerCssClass)->render();
573 27
                    } elseif ($enclose === false) {
574 1
                        $content = $itemLabel;
575 27
                    } elseif ($url === '') {
576 1
                        $content = CustomTag::name('h6')
577 1
                            ->class($this->itemHeaderCssClass)
578 1
                            ->content($itemLabel)
579 1
                            ->encode(null)
580 1
                            ->render();
581
                    } else {
582 26
                        $content = A::tag()
583 26
                            ->attributes($urlAttributes)
584 26
                            ->content($itemLabel)
585 26
                            ->encode(false)
586 26
                            ->url($url)
587 26
                            ->render();
588
                    }
589
590 27
                    $lines[] = $content;
591
                } else {
592 2
                    $submenuAttributes = isset($item['submenuAttributes']) && is_array($item['submenuAttributes'])
593 2
                        ? array_merge($this->submenuAttributes, $item['submenuAttributes']) : $this->submenuAttributes;
594
595 2
                    $lines[] = self::widget()
596 2
                        ->attributes($this->attributes)
597 2
                        ->dividerCssClass($this->dividerCssClass)
598 2
                        ->itemCssClass($this->itemCssClass)
599 2
                        ->items($items)
600 2
                        ->submenu($submenu)
601 2
                        ->submenuAttributes($submenuAttributes)
602 2
                        ->render();
603
                }
604
            }
605
        }
606
607 27
        return implode(PHP_EOL, $lines);
608
    }
609
610 20
    private function renderLabelButton(
611
        string $label,
612
        array $labelAttributes,
613
        string $iconText,
614
        string $iconCssClass,
615
        array $iconAttributes = []
616
    ): string {
617 20
        $html = '';
618
619 20
        if ($label !== '') {
620 20
            $html = PHP_EOL . Span::tag()
621 20
                ->attributes($labelAttributes)
622 20
                ->content($label)
623 20
                ->encode(false)
624 20
                ->render();
625
        }
626
627 20
        if ($iconText !== '' || $iconCssClass !== '') {
628 20
            $html .= PHP_EOL .
629 20
                Span::tag()
630 20
                    ->attributes($iconAttributes)
631 20
                    ->content(CustomTag::name('i')->class($iconCssClass)->content($iconText)->encode(false)->render())
632 20
                    ->encode(false)
633 20
                    ->render();
634
        }
635
636 20
        return $html . PHP_EOL;
637
    }
638
639 27
    private function renderLabelItem(
640
        string $label,
641
        string $iconText,
642
        string $iconCssClass,
643
        array $iconAttributes = []
644
    ): string {
645 27
        $html = '';
646
647 27
        if ($iconText !== '' || $iconCssClass !== '') {
648 1
            $html = Span::tag()
649 1
                ->attributes($iconAttributes)
650 1
                ->content(CustomTag::name('i')->class($iconCssClass)->content($iconText)->encode(false)->render())
651 1
                ->encode(false)
652 1
                ->render();
653
        }
654
655 27
        if ($label !== '') {
656 27
            $html .= $label;
657
        }
658
659 27
        return $html;
660
    }
661
}
662