Passed
Pull Request — master (#31)
by Wilmer
02:00
created

Dropdown::renderItems()   C

Complexity

Conditions 14
Paths 163

Size

Total Lines 80
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 14.0022

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 46
nc 163
nop 2
dl 0
loc 80
ccs 43
cts 44
cp 0.9773
crap 14.0022
rs 5.7416
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bulma;
6
7
use InvalidArgumentException;
8
use JsonException;
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 implode;
15
use function is_array;
16
17
/**
18
 * The dropdown component is a container for a dropdown button and a dropdown menu.
19
 *
20
 * @link https://bulma.io/documentation/components/dropdown/
21
 */
22
final class Dropdown extends Widget
23
{
24
    private string $buttonLabel = '';
25
    private array $buttonLabelOptions = [];
26
    private array $buttonIcon = ['class' => 'fas fa-angle-down', 'aria-hidden' => 'true'];
27
    private array $buttonIconOptions = [];
28
    private string $dividerClass = 'dropdown-divider';
29
    private string $itemsClass = 'dropdown-menu';
30
    private string $itemClass = 'dropdown-item';
31
    private bool $encodeLabels = true;
32
    private bool $encodeTags = false;
33
    private bool $encloseByContainer = true;
34
    private array $items = [];
35
    private array $itemsOptions = [];
36
    private array $options = [];
37
    private array $buttonOptions = [];
38
    private array $linkOptions = ['aria-haspopup' => 'true', 'aria-expanded' => 'false'];
39
    private array $triggerOptions = [];
40
41 19
    protected function run(): string
42
    {
43 19
        $this->buildOptions();
44
45 19
        return $this->buildDropdown();
46
    }
47
48
    /**
49
     * Set label for the dropdown button.
50
     *
51
     * @param string $value
52
     *
53
     * @return self
54
     */
55 7
    public function buttonLabel(string $value): self
56
    {
57 7
        $new = clone $this;
58 7
        $new->buttonLabel = $value;
59 7
        return $new;
60
    }
61
62
    /**
63
     * The HTML attributes for the dropdown button.
64
     *
65
     * @param array $value
66
     *
67
     * @return self
68
     *
69
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
70
     */
71 1
    public function buttonLabelOptions(array $value): self
72
    {
73 1
        $new = clone $this;
74 1
        $new->buttonLabelOptions = $value;
75 1
        return $new;
76
    }
77
78
    /**
79
     * Set CSS class for dropdown divider.
80
     *
81
     * @param $value
82
     *
83
     * @return self
84
     */
85 12
    public function dividerClass(string $value): self
86
    {
87 12
        $new = clone $this;
88 12
        $new->dividerClass = $value;
89 12
        return $new;
90
    }
91
92
    /**
93
     * Set CSS class for dropdown item.
94
     *
95
     * @param $value
96
     *
97
     * @return self
98
     */
99 12
    public function itemClass(string $value): self
100
    {
101 12
        $new = clone $this;
102 12
        $new->itemClass = $value;
103 12
        return $new;
104
    }
105
106
    /**
107
     * Set CSS class for dropdown items container.
108
     *
109
     * @param string $value
110
     *
111
     * @return self
112
     */
113 12
    public function itemsClass(string $value): self
114
    {
115 12
        $new = clone $this;
116 12
        $new->itemsClass = $value;
117 12
        return $new;
118
    }
119
120
    /**
121
     * When tags Labels HTML should not be encoded.
122
     *
123
     * @return $this
124
     */
125 2
    public function withoutEncodeLabels(): self
126
    {
127 2
        $new = clone $this;
128 2
        $new->encodeLabels = false;
129 2
        return $new;
130
    }
131
132
    /**
133
     * Disable enclosed by container dropdown.
134
     *
135
     * @return self
136
     */
137 12
    public function withoutEncloseByContainer(): self
138
    {
139 12
        $new = clone $this;
140 12
        $new->encloseByContainer = false;
141 12
        return $new;
142
    }
143
144
    /**
145
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
146
     * single menu with the following structure:
147
     *
148
     * - label: string, required, the label of the item link.
149
     * - encode: bool, optional, whether to HTML-encode item label.
150
     * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
151
     *   If not set, the item will be treated as a menu header when the item has no sub-menu.
152
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
153
     * - linkOptions: array, optional, the HTML attributes of the item link.
154
     * - itemsOptions: array, optional, the HTML attributes of the item.
155
     * - items: array, optional, the submenu items. The structure is the same as this property.
156
     *
157
     * To insert divider use `-`.
158
     *
159
     * @param array $value
160
     *
161
     * @return self
162
     */
163 19
    public function items(array $value): self
164
    {
165 19
        $new = clone $this;
166 19
        $new->items = $value;
167 19
        return $new;
168
    }
169
170
    /**
171
     * The HTML attributes for the widget container tag.
172
     *
173
     * @param array $value
174
     *
175
     * @return self
176
     *
177
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
178
     */
179 1
    public function options(array $value): self
180
    {
181 1
        $new = clone $this;
182 1
        $new->options = $value;
183 1
        return $new;
184
    }
185
186
    /**
187
     * The HTML attributes for the widget button tag.
188
     *
189
     * @param array $value
190
     *
191
     * @return self
192
     *
193
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
194
     */
195 1
    public function buttonOptions(array $value): self
196
    {
197 1
        $new = clone $this;
198 1
        $new->buttonOptions = $value;
199 1
        return $new;
200
    }
201
202
    /**
203
     * The HTML attributes for the widget items.
204
     *
205
     * @param array $value
206
     *
207
     * @return self
208
     *
209
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
210
     */
211 12
    public function itemsOptions(array $value): self
212
    {
213 12
        $new = clone $this;
214 12
        $new->itemsOptions = $value;
215 12
        return $new;
216
    }
217
218
    /**
219
     * The HTML attributes for the widget container trigger.
220
     *
221
     * @param array $value
222
     *
223
     * @return self
224
     *
225
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
226
     */
227 1
    public function triggerOptions(array $value): self
228
    {
229 1
        $new = clone $this;
230 1
        $new->triggerOptions = $value;
231 1
        return $new;
232
    }
233
234
    /**
235
     * Allows you to enable the encoding tags html.
236
     *
237
     * @return self
238
     */
239
    public function withEncodeTags(): self
240
    {
241
        $new = clone $this;
242
        $new->encodeTags = true;
243
244
        return $new;
245
    }
246
247 19
    private function buildDropdown(): string
248
    {
249 19
        if ($this->encloseByContainer) {
250 7
            $html = Html::beginTag('div', $this->options) . "\n";
251 7
            $html .= $this->buildDropdownTrigger();
252 7
            $html .= $this->renderItems($this->items, $this->itemsOptions) . "\n";
253 7
            $html .= Html::endTag('div');
254
        } else {
255 12
            $html = $this->renderItems($this->items, $this->itemsOptions);
256
        }
257
258 18
        return $html;
259
    }
260
261 7
    private function buildDropdownTrigger(): string
262
    {
263
        return
264 7
            Html::beginTag('div', $this->triggerOptions) . "\n" .
265 7
                Html::beginTag('button', $this->buttonOptions) . "\n" .
266 7
                    Html::tag('span', $this->buttonLabel, $this->buttonLabelOptions) . "\n" .
267 7
                    Html::beginTag('span', $this->buttonIconOptions) . "\n" .
268 7
                        Html::tag('i', '', $this->buttonIcon) . "\n" .
269 7
                    Html::endTag('span') . "\n" .
270 7
                Html::endTag('button') . "\n" .
271 7
            Html::endTag('div') . "\n";
272
    }
273
274 19
    private function buildOptions(): void
275
    {
276 19
        if ($this->encloseByContainer && (!isset($this->options['id']))) {
277 7
            $this->options['id'] = "{$this->getId()}-dropdown";
278 7
            $this->options = $this->addOptions($this->options, 'dropdown');
279 7
            $this->triggerOptions = $this->addOptions($this->triggerOptions, 'dropdown-trigger');
280 7
            $this->buttonOptions = $this->addOptions(
281 7
                array_merge(
282 7
                    $this->buttonOptions,
283 7
                    [ 'aria-haspopup' => 'true', 'aria-controls' => 'dropdown-menu']
284
                ),
285 7
                'button'
286
            );
287
288 7
            $this->buttonIconOptions = $this->addOptions($this->buttonIconOptions, 'icon is-small');
289 12
        } elseif (!isset($this->itemsOptions['id'])) {
290 11
            $this->itemsOptions['id'] = "{$this->getId()}-dropdown";
291
        }
292
293 19
        $this->itemsOptions = $this->addOptions($this->itemsOptions, $this->itemsClass);
294 19
    }
295
296
    /**
297
     * Renders menu items.
298
     *
299
     * @param array $items the menu items to be rendered
300
     * @param array $itemsOptions the container HTML attributes
301
     *
302
     * @throws InvalidArgumentException|JsonException if the label option is not specified in one of the items.
303
     *
304
     * @return string the rendering result.
305
     */
306 19
    private function renderItems(array $items, array $itemsOptions = []): string
307
    {
308 19
        $lines = [];
309
310 19
        foreach ($items as $item) {
311 18
            if ($item === '-') {
312 10
                $lines[] = Html::tag('div', '', ['class' => $this->dividerClass]);
313 10
                continue;
314
            }
315
316 18
            if (!isset($item['label']) && $item !== '-') {
317 1
                throw new InvalidArgumentException('The "label" option is required.');
318
            }
319
320 17
            $this->encodeLabels = $item['encode'] ?? $this->encodeLabels;
321
322 17
            if ($this->encodeLabels) {
323 14
                $label = Html::encode($item['label']);
324
            } else {
325 3
                $label = $item['label'];
326
            }
327
328 17
            $iconOptions = [];
329
330 17
            $icon = $item['icon'] ?? '';
331
332 17
            if (array_key_exists('iconOptions', $item) && is_array($item['iconOptions'])) {
333 1
                $iconOptions = $this->addOptions($iconOptions, 'icon');
334
            }
335
336 17
            $label = $this->renderIcon($label, $icon, $iconOptions);
337
338 17
            $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
339 17
            $active = ArrayHelper::getValue($item, 'active', false);
340 17
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
341
342 17
            if ($this->encodeTags === false) {
343 17
                $linkOptions['encode'] = false;
344
            }
345
346 17
            Html::addCssClass($linkOptions, $this->itemClass);
347
348 17
            if ($disabled) {
349 1
                Html::addCssStyle($linkOptions, 'opacity:.65; pointer-events:none;');
350
            }
351
352
            /** @psalm-suppress ConflictingReferenceConstraint */
353 17
            if ($active) {
354 3
                Html::addCssClass($linkOptions, 'is-active');
355
            }
356
357 17
            $url = $item['url'] ?? null;
358
359 17
            if (empty($item['items'])) {
360 17
                $lines[] = Html::a($label, $url, $linkOptions);
361
            } else {
362 1
                $lines[] = Html::a($label, $url, array_merge($this->linkOptions, $linkOptions));
363
364 1
                $dropdownWidget = self::widget()
365 1
                    ->dividerClass($this->dividerClass)
0 ignored issues
show
Bug introduced by
The method dividerClass() 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. ( Ignorable by Annotation )

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

365
                    ->/** @scrutinizer ignore-call */ dividerClass($this->dividerClass)
Loading history...
366 1
                    ->itemClass($this->itemClass)
367 1
                    ->itemsClass($this->itemsClass)
368 1
                    ->items($item['items']);
369
370 1
                if ($this->encloseByContainer === false) {
371 1
                    $dropdownWidget = $dropdownWidget->withoutEncloseByContainer();
372
                }
373
374 1
                if ($this->encodeLabels === false) {
375
                    $dropdownWidget = $dropdownWidget->withoutEncodeLabels();
376
                }
377
378 1
                $lines[] = $dropdownWidget->render();
379
            }
380
        }
381
382
        return
383 18
            Html::beginTag('div', $itemsOptions) . "\n" .
384 18
                implode("\n", $lines) . "\n" .
385 18
            Html::endTag('div');
386
    }
387
388 17
    private function renderIcon(string $label, string $icon, array $iconOptions): string
389
    {
390 17
        if ($icon !== '') {
391 1
            $label = Html::beginTag('span', $iconOptions) .
392 1
                Html::tag('i', '', ['class' => $icon, 'encode' => false]) .
393 1
                Html::endTag('span') .
394 1
                Html::tag('span', $label, ['encode' => false]);
395
        }
396
397 17
        return $label;
398
    }
399
}
400