Passed
Pull Request — master (#52)
by Wilmer
01:58
created

Dropdown::buttonAttributes()   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 ReflectionException;
9
use Yiisoft\Html\Html;
10
use Yiisoft\Html\Tag\A;
11
use Yiisoft\Html\Tag\Button;
12
use Yiisoft\Html\Tag\CustomTag;
13
use Yiisoft\Html\Tag\Div;
14
use Yiisoft\Html\Tag\Span;
15
use Yiisoft\Widget\Widget;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Yii\Bulma\Widget. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
17
use function array_merge;
18
use function implode;
19
20
/**
21
 * The dropdown component is a container for a dropdown button and a dropdown menu.
22
 *
23
 * @link https://bulma.io/documentation/components/dropdown/
24
 */
25
final class Dropdown extends Widget
26
{
27
    private array $attributes = [];
28
    private string $autoIdPrefix = 'w';
29
    private array $buttonAttributes = [];
30
    private array $buttonIconAttributes = ['class' => 'icon is-small'];
31
    private string $buttonIconCssClass = '';
32
    private string $buttonIconText = '&#8595;';
33
    private string $buttonLabel = 'Clic Me';
34
    private array $buttonLabelAttributes = [];
35
    private string $dividerCssClass = 'dropdown-divider';
36
    private string $dropdownCssClass = 'dropdown';
37
    private string $dropdownContentCssClass = 'dropdown-content';
38
    private string $dropdownItemActiveCssClass = 'is-active';
39
    private string $dropdownItemCssClass = 'dropdown-item';
40
    private string $dropdownItemDisabledStyleCss = 'opacity:.65;pointer-events:none;';
41
    private string $dropdownItemHeaderCssClass = 'dropdown-header';
42
    private string $dropdownMenuCssClass = 'dropdown-menu';
43
    private string $dropdownTriggerCssClass = 'dropdown-trigger';
44
    private bool $encloseByContainer = true;
45
    private array $items = [];
46
    private bool $submenu = false;
47
    private array $submenuAttributes = [];
48
49
    /**
50
     * Returns a new instance with the specified attributes.
51
     *
52
     * @param array $value The HTML attributes for the widget container nav tag.
53
     *
54
     * @return self
55
     *
56
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
57
     */
58 3
    public function attributes(array $value): self
59
    {
60 3
        $new = clone $this;
61 3
        $new->attributes = $value;
62 3
        return $new;
63
    }
64
65
    /**
66
     * Returns a new instance with the specified prefix to the automatically generated widget IDs.
67
     *
68
     * @param string $value The prefix to the automatically generated widget IDs.
69
     *
70
     * @return self
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
     * The HTML attributes for the dropdown button. The following special options are recognized.
81
     *
82
     * @param array $value
83
     *
84
     * @return self
85
     */
86 2
    public function buttonAttributes(array $value): self
87
    {
88 2
        $new = clone $this;
89 2
        $new->buttonAttributes = $value;
90 2
        return $new;
91
    }
92
93
    /**
94
     * The HTML attributes for the dropdown icon button. The following special options are recognized.
95
     *
96
     * @param array $value
97
     *
98
     * @return self
99
     */
100 2
    public function buttonIconAttributes(array $value): self
101
    {
102 2
        $new = clone $this;
103 2
        $new->buttonIconAttributes = $value;
104 2
        return $new;
105
    }
106
107
    /**
108
     * Set icon css class for the dropdown button.
109
     *
110
     * @param string $value
111
     *
112
     * @return self
113
     */
114 2
    public function buttonIconCssClass(string $value): self
115
    {
116 2
        $new = clone $this;
117 2
        $new->buttonIconCssClass = $value;
118 2
        return $new;
119
    }
120
121
    /**
122
     * Set icon text for the dropdown button.
123
     *
124
     * @param string $value
125
     *
126
     * @return self
127
     */
128 3
    public function buttonIconText(string $value): self
129
    {
130 3
        $new = clone $this;
131 3
        $new->buttonIconText = $value;
132 3
        return $new;
133
    }
134
135
    /**
136
     * Set label for the dropdown button.
137
     *
138
     * @param string $value
139
     *
140
     * @return self
141
     */
142 2
    public function buttonLabel(string $value): self
143
    {
144 2
        $new = clone $this;
145 2
        $new->buttonLabel = $value;
146 2
        return $new;
147
    }
148
149
    /**
150
     * The HTML attributes for the dropdown button label. The following special options are recognized.
151
     *
152
     * @param array $value
153
     *
154
     * @return self
155
     */
156 2
    public function buttonLabelAttributes(array $value): self
157
    {
158 2
        $new = clone $this;
159 2
        $new->buttonLabelAttributes = $value;
160 2
        return $new;
161
    }
162
163
    /**
164
     * A horizontal line to separate dropdown items.
165
     *
166
     * @return self
167
     */
168 11
    public function dividerCssClass(string $value): self
169
    {
170 11
        $new = clone $this;
171 11
        $new->dividerCssClass = $value;
172 11
        return $new;
173
    }
174
175 9
    public function dropdownCssClass(string $value): self
176
    {
177 9
        $new = clone $this;
178 9
        $new->dropdownCssClass = $value;
179 9
        return $new;
180
    }
181
182
    /**
183
     * The dropdown box, with a white background and a shadow.
184
     *
185
     * @return self
186
     *
187
     * @link https://bulma.io/documentation/components/dropdown/#dropdown-content
188
     */
189 2
    public function dropdownContentCssClass(string $value): self
190
    {
191 2
        $new = clone $this;
192 2
        $new->dropdownContentCssClass = $value;
193 2
        return $new;
194
    }
195
196 2
    public function dropdownItemActiveCssClass(string $value): self
197
    {
198 2
        $new = clone $this;
199 2
        $new->dropdownItemActiveCssClass = $value;
200 2
        return $new;
201
    }
202
203 11
    public function dropdownItemCssClass(string $value): self
204
    {
205 11
        $new = clone $this;
206 11
        $new->dropdownItemCssClass = $value;
207 11
        return $new;
208
    }
209
210 2
    public function dropdownItemDisabledStyleCss(string $value): self
211
    {
212 2
        $new = clone $this;
213 2
        $new->dropdownItemDisabledStyleCss = $value;
214 2
        return $new;
215
    }
216
217 2
    public function dropdownItemHeaderCssClass(string $value): self
218
    {
219 2
        $new = clone $this;
220 2
        $new->dropdownItemHeaderCssClass = $value;
221 2
        return $new;
222
    }
223
224
    /**
225
     * The toggable menu, hidden by default.
226
     *
227
     * @return self
228
     */
229 2
    public function dropdownMenuCssClass(string $value): self
230
    {
231 2
        $new = clone $this;
232 2
        $new->dropdownMenuCssClass = $value;
233 2
        return $new;
234
    }
235
236
    /**
237
     * The toggable menu, hidden by default.
238
     *
239
     * @return self
240
     */
241 2
    public function dropdownTriggerCssClass(string $value): self
242
    {
243 2
        $new = clone $this;
244 2
        $new->dropdownTriggerCssClass = $value;
245 2
        return $new;
246
    }
247
248
    /**
249
     * Returns a new instance with the specified ID of the widget.
250
     *
251
     * @param string $value The ID of the widget.
252
     *
253
     * @return self
254
     */
255 2
    public function id(string $value): self
256
    {
257 2
        $new = clone $this;
258 2
        $new->attributes['id'] = $value;
259 2
        return $new;
260
    }
261
262
    /**
263
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
264
     * single menu with the following structure:
265
     *
266
     * - label: string, required, the label of the item link.
267
     * - encode: bool, optional, whether to HTML-encode item label.
268
     * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
269
     *   If not set, the item will be treated as a menu header when the item has no sub-menu.
270
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
271
     * - urlAttributes: array, optional, the HTML attributes of the item link.
272
     * - attributes: array, optional, the HTML attributes of the item.
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
     * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
276
     *   merged with {@see submenuOptions}.
277
     *
278
     * To insert divider use `-`.
279
     *
280
     * @param array $value
281
     *
282
     * @return self
283
     */
284 29
    public function items(array $value): self
285
    {
286 29
        $new = clone $this;
287 29
        $new->items = $value;
288 29
        return $new;
289
    }
290
291
    /**
292
     * Set if it is a submenu or subdropdown,
293
     *
294
     * @param bool $value
295
     *
296
     * @return self
297
     */
298 3
    public function submenu(bool $value): self
299
    {
300 3
        $new = clone $this;
301 3
        $new->submenu = $value;
302 3
        return $new;
303
    }
304
305
    /**
306
     * The HTML attributes for sub-menu container tags.
307
     *
308
     * @param array $value
309
     *
310
     * @return self
311
     */
312 3
    public function submenuAttributes(array $value): self
313
    {
314 3
        $new = clone $this;
315 3
        $new->submenuAttributes = $value;
316 3
        return $new;
317
    }
318
319
    /**
320
     * If the widget should be unclosed by container.
321
     *
322
     * @param bool $value
323
     *
324
     * @return self
325
     */
326 10
    public function unClosedByContainer(bool $value = false): self
327
    {
328 10
        $new = clone $this;
329 10
        $new->encloseByContainer = $value;
330 10
        return $new;
331
    }
332
333
    /**
334
     * @throws ReflectionException
335
     */
336 28
    private function renderDropdown(): string
337
    {
338 28
        $attributes = $this->attributes;
339
340
        /** @var string */
341 28
        $id = $attributes['id'] ?? Html::generateId($this->autoIdPrefix) . '-dropdown';
342 28
        unset($attributes['id']);
343
344 28
        if ($this->encloseByContainer) {
345 20
            Html::addCssClass($attributes, $this->dropdownCssClass);
346 20
            $html = Div::tag()
347 20
                ->attributes($attributes)
348 20
                ->content(PHP_EOL . $this->renderDropdownTrigger($id) . PHP_EOL)
349 19
                ->encode(false)
350 19
                ->render();
351
        } else {
352 9
            $html = $this->renderItems();
353
        }
354
355 27
        return $html;
356
    }
357
358
    /**
359
     * Render dropdown button.
360
     *
361
     * @return string the rendering toggle button.
362
     *
363
     * @link https://bulma.io/documentation/components/dropdown/#hoverable-or-toggable
364
     */
365 19
    private function renderDropdownButton(string $id): string
366
    {
367 19
        $buttonAttributes = $this->buttonAttributes;
368
369 19
        Html::addCssClass($buttonAttributes, 'button');
370
371 19
        $buttonAttributes['aria-haspopup'] = 'true';
372 19
        $buttonAttributes['aria-controls'] = $id;
373
374 19
        return Button::tag()
375 19
            ->attributes($buttonAttributes)
376 19
            ->content(
377 19
                $this->renderLabelButton(
378 19
                    $this->buttonLabel,
379 19
                    $this->buttonLabelAttributes,
380 19
                    $this->buttonIconText,
381 19
                    $this->buttonIconCssClass,
382 19
                    $this->buttonIconAttributes,
383
                )
384
            )
385 19
            ->encode(false)
386 19
            ->render() . PHP_EOL;
387
    }
388
389 2
    public function renderDropdownButtonLink(): string
390
    {
391 2
        return A::tag()
392 2
            ->class($this->dropdownItemCssClass)
393 2
            ->content(
394 2
                $this->renderLabelButton(
395 2
                    $this->buttonLabel,
396 2
                    $this->buttonLabelAttributes,
397 2
                    $this->buttonIconText,
398 2
                    $this->buttonIconCssClass,
399 2
                    $this->buttonIconAttributes,
400
                )
401
            )
402 2
            ->encode(false)
403 2
            ->render() . PHP_EOL;
404
    }
405
406
    /**
407
     * @throws ReflectionException
408
     */
409 28
    protected function run(): string
410
    {
411 28
        return $this->renderDropdown();
412
    }
413
414
    /**
415
     * @throws ReflectionException
416
     */
417 20
    private function renderDropdownContent(): string
418
    {
419 20
        return Div::tag()
420 20
            ->class($this->dropdownContentCssClass)
421 20
            ->content(PHP_EOL . $this->renderItems() . PHP_EOL)
422 19
            ->encode(false)
423 19
            ->render();
424
    }
425
426
    /**
427
     * @throws ReflectionException
428
     */
429 20
    private function renderDropdownMenu(string $id): string
430
    {
431 20
        return Div::tag()
432 20
            ->class($this->dropdownMenuCssClass)
433 20
            ->content(PHP_EOL . $this->renderDropdownContent() . PHP_EOL)
434 19
            ->encode(false)
435 19
            ->id($id)
436 19
            ->render();
437
    }
438
439
    /**
440
     * @throws ReflectionException
441
     */
442 20
    private function renderDropdownTrigger(string $id): string
443
    {
444 20
        if ($this->submenu !== true) {
445 19
            $button = $this->renderDropdownButton($id);
446
        } else {
447 2
            $button = $this->renderDropdownButtonLink();
448
        }
449
450 20
        return Div::tag()
451 20
            ->class($this->dropdownTriggerCssClass)
452 20
            ->content(PHP_EOL . $button)
453 20
            ->encode(false)
454 20
            ->render() . PHP_EOL . $this->renderDropdownMenu($id);
455
    }
456
457
    /**
458
     * Renders menu items.
459
     *
460
     * @throws InvalidArgumentException|ReflectionException if the label option is not specified in one of the items.
461
     *
462
     * @return string the rendering result.
463
     */
464 28
    private function renderItems(): string
465
    {
466 28
        $lines = [];
467
468
        /** @var array|string $item */
469 28
        foreach ($this->items as $item) {
470 28
            if ($item === '-') {
471 4
                $lines[] = CustomTag::name('hr')->class($this->dividerCssClass)->render();
472
            } else {
473 28
                if (!isset($item['label']) && $item !== '-') {
474 1
                    throw new InvalidArgumentException('The "label" option is required.');
475
                }
476
477
                /** @var string */
478 27
                $itemLabel = $item['label'] ?? '';
479
480 27
                if (isset($item['encode']) && $item['encode'] === true) {
481 1
                    $itemLabel = Html::encode($itemLabel);
482
                }
483
484
                /** @var array */
485 27
                $items = $item['items'] ?? [];
486
487
                /** @var array */
488 27
                $urlAttributes = $item['urlAttributes'] ?? [];
489
490
                /** @var string */
491 27
                $iconText = $item['iconText'] ?? '';
492
493
                /** @var string */
494 27
                $iconCssClass = $item['iconCssClass'] ?? '';
495
496
                /** @var array */
497 27
                $iconAttributes = $item['iconAttributes'] ?? [];
498
499
                /** @var string */
500 27
                $url = $item['url'] ?? '';
501
502
                /** @var bool */
503 27
                $active = $item['active'] ?? false;
504
505
                /** @var bool */
506 27
                $disabled = $item['disable'] ?? false;
507
508
                /** @var bool */
509 27
                $enclose = $item['enclose'] ?? true;
510
511
                /** @var bool */
512 27
                $submenu = $item['submenu'] ?? false;
513
514 27
                $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes);
515
516 27
                Html::addCssClass($urlAttributes, $this->dropdownItemCssClass);
517
518 27
                if ($disabled) {
519 2
                    Html::addCssStyle($urlAttributes, $this->dropdownItemDisabledStyleCss);
520 26
                } elseif ($active) {
521 5
                    Html::addCssClass($urlAttributes, $this->dropdownItemActiveCssClass);
522
                }
523
524 27
                if ($items === []) {
525 27
                    if ($itemLabel === '-') {
526 1
                        $content = CustomTag::name('hr')->class($this->dividerCssClass)->render();
527 27
                    } elseif ($enclose === false) {
528 1
                        $content = $itemLabel;
529 27
                    } elseif ($url === '') {
530 1
                        $content = CustomTag::name('h6')
531 1
                            ->class($this->dropdownItemHeaderCssClass)
532 1
                            ->content($itemLabel)
533 1
                            ->encode(null)
534 1
                            ->render();
535
                    } else {
536 26
                        $content = A::tag()
537 26
                            ->attributes($urlAttributes)
538 26
                            ->content($itemLabel)
539 26
                            ->encode(false)
540 26
                            ->url($url)
541 26
                            ->render();
542
                    }
543
544 27
                    $lines[] = $content;
545
                } else {
546
                    /** @var array */
547 2
                    $submenuAttributes = $item['submenuAttributes'] ?? [];
548 2
                    $submenuAttributes = array_merge($this->submenuAttributes, $submenuAttributes);
549
550 2
                    $dropdown = self::widget();
551
552
                    $lines[] = $dropdown
553 2
                        ->attributes($this->attributes)
0 ignored issues
show
Bug introduced by
The method attributes() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bulma\Dropdown or Yiisoft\Yii\Bulma\Breadcrumbs or Yiisoft\Yii\Bulma\Nav or Yiisoft\Yii\Bulma\NavBar. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

553
                        ->/** @scrutinizer ignore-call */ attributes($this->attributes)
Loading history...
554 2
                        ->dividerCssClass($this->dividerCssClass)
555 2
                        ->dropdownItemCssClass($this->dropdownItemCssClass)
556 2
                        ->items($items)
557 2
                        ->submenu($submenu)
558 2
                        ->submenuAttributes($submenuAttributes)
559 2
                        ->render();
560
                }
561
            }
562
        }
563
564 27
        return implode(PHP_EOL, $lines);
565
    }
566
567 20
    private function renderLabelButton(
568
        string $label,
569
        array $labelAttributes,
570
        string $iconText,
571
        string $iconCssClass,
572
        array $iconAttributes = []
573
    ): string {
574 20
        $html = '';
575
576 20
        if ($label !== '') {
577 20
            $html = PHP_EOL . Span::tag()
578 20
                ->attributes($labelAttributes)
579 20
                ->content($label)
580 20
                ->encode(false)
581 20
                ->render();
582
        }
583
584 20
        if ($iconText !== '' || $iconCssClass !== '') {
585 20
            $html .= PHP_EOL .
586 20
                Span::tag()
587 20
                    ->attributes($iconAttributes)
588 20
                    ->content(CustomTag::name('i')->class($iconCssClass)->content($iconText)->encode(false)->render())
589 20
                    ->encode(false)
590 20
                    ->render();
591
        }
592
593 20
        return $html . PHP_EOL;
594
    }
595
596 27
    private function renderLabelItem(
597
        string $label,
598
        string $iconText,
599
        string $iconCssClass,
600
        array $iconAttributes = []
601
    ): string {
602 27
        $html = '';
603
604 27
        if ($iconText !== '' || $iconCssClass !== '') {
605 1
            $html = Span::tag()
606 1
                ->attributes($iconAttributes)
607 1
                ->content(CustomTag::name('i')->class($iconCssClass)->content($iconText)->encode(false)->render())
608 1
                ->encode(false)
609 1
                ->render();
610
        }
611
612 27
        if ($label !== '') {
613 27
            $html .= $label;
614
        }
615
616 27
        return $html;
617
    }
618
}
619