Passed
Pull Request — master (#35)
by Wilmer
02:14
created

Dropdown   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 217
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 86
c 3
b 0
f 0
dl 0
loc 217
ccs 83
cts 83
cp 1
rs 10
wmc 23

7 Methods

Rating   Name   Duplication   Size   Complexity  
A withItems() 0 6 1
A withoutEncodeLabels() 0 6 1
A run() 0 14 3
A withOptions() 0 6 1
A withSubmenuOptions() 0 6 1
A withEncodeTags() 0 6 1
C renderItems() 0 92 15
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use JsonException;
8
use RuntimeException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function array_key_exists;
13
use function array_merge;
14
use function array_merge_recursive;
15
use function is_string;
16
17
/**
18
 * Dropdown renders a Bootstrap dropdown menu component.
19
 *
20
 * For example,
21
 *
22
 * ```php
23
 * <div class="dropdown">
24
 *     <?php
25
 *         echo Dropdown::widget()
26
 *             ->withItems([
27
 *                 ['label' => 'DropdownA', 'url' => '/'],
28
 *                 ['label' => 'DropdownB', 'url' => '#'],
29
 *             ]);
30
 *     ?>
31
 * </div>
32
 * ```
33
 */
34
final class Dropdown extends Widget
35
{
36
    private array $items = [];
37
    private bool $encodeLabels = true;
38
    private bool $encodeTags = false;
39
    private array $submenuOptions = [];
40
    private array $options = [];
41
42 25
    public function run(): string
43
    {
44 25
        if (!isset($this->options['id'])) {
45 24
            $this->options['id'] = "{$this->getId()}-dropdown";
46
        }
47
48
        /** @psalm-suppress InvalidArgument */
49 25
        Html::addCssClass($this->options, ['widget' => 'dropdown-menu']);
50
51 25
        if ($this->encodeTags === false) {
52 25
            $this->options = array_merge($this->options, ['itemOptions' => ['encode' =>false], 'encode' => false]);
53
        }
54
55 25
        return $this->renderItems($this->items, $this->options);
56
    }
57
58
    /**
59
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
60
     * single menu with the following structure:
61
     *
62
     * - label: string, required, the label of the item link.
63
     * - encode: bool, optional, whether to HTML-encode item label.
64
     * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
65
     *   If not set, the item will be treated as a menu header when the item has no sub-menu.
66
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
67
     * - linkOptions: array, optional, the HTML attributes of the item link.
68
     * - options: array, optional, the HTML attributes of the item.
69
     * - items: array, optional, the submenu items. The structure is the same as this property.
70
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
71
     * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
72
     *   merged with {@see submenuOptions}.
73
     *
74
     * To insert divider use `-`.
75
     *
76
     * @param array $value
77
     *
78
     * @return $this
79
     */
80 25
    public function withItems(array $value): self
81
    {
82 25
        $new = clone $this;
83 25
        $new->items = $value;
84
85 25
        return $new;
86
    }
87
88
    /**
89
     * Whether the labels for header items should be HTML-encoded.
90
     *
91
     * @param bool $value
92
     *
93
     * @return $this
94
     */
95 23
    public function withoutEncodeLabels(bool $value = false): self
96
    {
97 23
        $new = clone $this;
98 23
        $new->encodeLabels = $value;
99
100 23
        return $new;
101
    }
102
103
    /**
104
     * The HTML attributes for sub-menu container tags.
105
     *
106
     * @param array $value
107
     *
108
     * @return $this
109
     */
110 4
    public function withSubmenuOptions(array $value): self
111
    {
112 4
        $new = clone $this;
113 4
        $new->submenuOptions = $value;
114
115 4
        return $new;
116
    }
117
118
    /**
119
     * @param array $value the HTML attributes for the widget container tag. The following special options are
120
     * recognized.
121
     *
122
     * @return $this
123
     *
124
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
125
     */
126 13
    public function withOptions(array $value): self
127
    {
128 13
        $new = clone $this;
129 13
        $new->options = $value;
130
131 13
        return $new;
132
    }
133
134
    /**
135
     * Allows you to enable or disable the encoding tags html.
136
     *
137
     * @param bool $value
138
     *
139
     * @return self
140
     */
141 1
    public function withEncodeTags(bool $value = true): self
142
    {
143 1
        $new = clone $this;
144 1
        $new->encodeTags = $value;
145
146 1
        return $new;
147
    }
148
149
    /**
150
     * Renders menu items.
151
     *
152
     * @param array $items the menu items to be rendered
153
     * @param array $options the container HTML attributes
154
     *
155
     * @throws JsonException|RuntimeException if the label option is not specified in one of the items.
156
     *
157
     * @return string the rendering result.
158
     */
159 25
    private function renderItems(array $items, array $options = []): string
160
    {
161 25
        $lines = [];
162
163 25
        foreach ($items as $item) {
164 25
            if (is_string($item)) {
165 3
                $item = ['label' => $item, 'encode' => false, 'enclose' => false];
166
            }
167
168 25
            if (isset($item['visible']) && !$item['visible']) {
169 3
                continue;
170
            }
171
172 25
            if (!array_key_exists('label', $item)) {
173 1
                throw new RuntimeException("The 'label' option is required.");
174
            }
175
176 24
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
177 24
            $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
178 24
            $itemOptions = ArrayHelper::getValue($item, 'options', []);
179 24
            $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
180 24
            $active = ArrayHelper::getValue($item, 'active', false);
181 24
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
182 24
            $enclose = ArrayHelper::getValue($item, 'enclose', true);
183
184 24
            Html::addCssClass($linkOptions, ['widget' => 'dropdown-item']);
185
186 24
            if ($this->encodeTags === false) {
187 24
                $linkOptions = array_merge($linkOptions, ['encode' => false]);
188 24
                $itemOptions = array_merge($itemOptions, ['encode' => false]);
189
            }
190
191 24
            if ($disabled) {
192 2
                ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
193 2
                ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
194 2
                Html::addCssClass($linkOptions, ['disable' => 'disabled']);
195 24
            } elseif ($active) {
196 4
                Html::addCssClass($linkOptions, ['active' => 'active']);
197
            }
198
199 24
            $url = $item['url'] ?? null;
200
201 24
            if (empty($item['items'])) {
202 24
                if ($label === '-') {
203 3
                    $content = Html::div('', ['class' => 'dropdown-divider']);
204 24
                } elseif ($enclose === false) {
205 1
                    $content = $label;
206 24
                } elseif ($url === null) {
207 10
                    $content = Html::tag('h6', $label, ['class' => 'dropdown-header']);
208
                } else {
209 18
                    $content = Html::a($label, $url, $linkOptions);
210
                }
211
212 24
                $lines[] = $content;
213
            } else {
214 4
                $submenuOptions = $this->submenuOptions;
215
216 4
                if (isset($item['submenuOptions'])) {
217 1
                    $submenuOptions = array_merge($submenuOptions, $item['submenuOptions']);
218
                }
219
220 4
                Html::addCssClass($submenuOptions, ['submenu' => 'dropdown-menu']);
221 4
                Html::addCssClass($linkOptions, ['toggle' => 'dropdown-toggle']);
222
223 4
                $itemOptions = array_merge_recursive(['class' => ['dropdown'], 'aria-expanded' => 'false'], $itemOptions);
224
225 4
                $lines[] = Html::a($label, $url, array_merge(
226
                    [
227 4
                        'data-bs-toggle' => 'dropdown',
228
                        'aria-haspopup' => 'true',
229
                        'aria-expanded' => 'false',
230
                        'role' => 'button',
231
                    ],
232 4
                    $linkOptions)
233
                )
234
235 4
                . Html::tag(
236 4
                    'ul',
237 4
                    self::widget()
238 4
                        ->withItems($item['items'])
0 ignored issues
show
Bug introduced by
The method withItems() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bootstrap5\Nav or Yiisoft\Yii\Bootstrap5\Carousel or Yiisoft\Yii\Bootstrap5\Dropdown or Yiisoft\Yii\Bootstrap5\Tabs or Yiisoft\Yii\Bootstrap5\Accordion. ( Ignorable by Annotation )

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

238
                        ->/** @scrutinizer ignore-call */ withItems($item['items'])
Loading history...
239 4
                        ->withOptions($submenuOptions)
240 4
                        ->withSubmenuOptions($submenuOptions)
241 4
                        ->withoutEncodeLabels($this->encodeLabels)
242 4
                        ->render(),
243 4
                    $itemOptions,
244
                );
245
            }
246
        }
247
248 24
        $options = array_merge(['aria-expanded' => 'false'], $options);
249
250 24
        return Html::ul($lines, $options);
251
    }
252
}
253