Passed
Push — master ( e80b4e...741461 )
by Alexander
02:19
created

Dropdown::renderItems()   C

Complexity

Conditions 14
Paths 163

Size

Total Lines 80
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 14

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 46
nc 163
nop 2
dl 0
loc 80
ccs 44
cts 44
cp 1
crap 14
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 $encodeLinks = 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 18
    protected function run(): string
42
    {
43 18
        $this->buildOptions();
44
45 18
        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 11
    public function dividerClass(string $value): self
86
    {
87 11
        $new = clone $this;
88 11
        $new->dividerClass = $value;
89 11
        return $new;
90
    }
91
92
    /**
93
     * Set CSS class for dropdown item.
94
     *
95
     * @param $value
96
     *
97
     * @return self
98
     */
99 11
    public function itemClass(string $value): self
100
    {
101 11
        $new = clone $this;
102 11
        $new->itemClass = $value;
103 11
        return $new;
104
    }
105
106
    /**
107
     * Set CSS class for dropdown items container.
108
     *
109
     * @param string $value
110
     *
111
     * @return self
112
     */
113 11
    public function itemsClass(string $value): self
114
    {
115 11
        $new = clone $this;
116 11
        $new->itemsClass = $value;
117 11
        return $new;
118
    }
119
120
    /**
121
     * Disable encoding for labels.
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 11
    public function withoutEncloseByContainer(): self
138
    {
139 11
        $new = clone $this;
140 11
        $new->encloseByContainer = false;
141 11
        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 18
    public function items(array $value): self
164
    {
165 18
        $new = clone $this;
166 18
        $new->items = $value;
167 18
        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 11
    public function itemsOptions(array $value): self
212
    {
213 11
        $new = clone $this;
214 11
        $new->itemsOptions = $value;
215 11
        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 18
    private function buildDropdown(): string
235
    {
236 18
        if ($this->encloseByContainer) {
237 7
            $html = Html::beginTag('div', $this->options) . "\n";
238 7
            $html .= $this->buildDropdownTrigger();
239 7
            $html .= $this->renderItems($this->items, $this->itemsOptions) . "\n";
240 7
            $html .= Html::endTag('div');
241
        } else {
242 11
            $html = $this->renderItems($this->items, $this->itemsOptions);
243
        }
244
245 17
        return $html;
246
    }
247
248 7
    private function buildDropdownTrigger(): string
249
    {
250
        return
251 7
            Html::beginTag('div', $this->triggerOptions) . "\n" .
252 7
                Html::beginTag('button', $this->buttonOptions) . "\n" .
253 7
                    Html::tag('span', $this->buttonLabel, $this->buttonLabelOptions) . "\n" .
254 7
                    Html::beginTag('span', $this->buttonIconOptions) . "\n" .
255 7
                        Html::tag('i', '', $this->buttonIcon) . "\n" .
256 7
                    Html::endTag('span') . "\n" .
257 7
                Html::endTag('button') . "\n" .
258 7
            Html::endTag('div') . "\n";
259
    }
260
261 18
    private function buildOptions(): void
262
    {
263 18
        if ($this->encloseByContainer && (!isset($this->options['id']))) {
264 7
            $this->options['id'] = "{$this->getId()}-dropdown";
265 7
            $this->options = $this->addOptions($this->options, 'dropdown');
266 7
            $this->triggerOptions = $this->addOptions($this->triggerOptions, 'dropdown-trigger');
267 7
            $this->buttonOptions = $this->addOptions(
268 7
                array_merge(
269 7
                    $this->buttonOptions,
270 7
                    [ 'aria-haspopup' => 'true', 'aria-controls' => 'dropdown-menu']
271
                ),
272 7
                'button'
273
            );
274
275 7
            $this->buttonIconOptions = $this->addOptions($this->buttonIconOptions, 'icon is-small');
276 11
        } elseif (!isset($this->itemsOptions['id'])) {
277 10
            $this->itemsOptions['id'] = "{$this->getId()}-dropdown";
278
        }
279
280 18
        $this->itemsOptions = $this->addOptions($this->itemsOptions, $this->itemsClass);
281 18
    }
282
283
    /**
284
     * Renders menu items.
285
     *
286
     * @param array $items the menu items to be rendered
287
     * @param array $itemsOptions the container HTML attributes
288
     *
289
     * @throws InvalidArgumentException|JsonException if the label option is not specified in one of the items.
290
     *
291
     * @return string the rendering result.
292
     */
293 18
    private function renderItems(array $items, array $itemsOptions = []): string
294
    {
295 18
        $lines = [];
296
297 18
        foreach ($items as $item) {
298 17
            if ($item === '-') {
299 10
                $lines[] = Html::tag('div', '', ['class' => $this->dividerClass]);
300 10
                continue;
301
            }
302
303 17
            if (!isset($item['label']) && $item !== '-') {
304 1
                throw new InvalidArgumentException('The "label" option is required.');
305
            }
306
307 16
            $this->encodeLabels = $item['encode'] ?? $this->encodeLabels;
308
309 16
            if ($this->encodeLabels) {
310 13
                $label = Html::encode($item['label']);
311
            } else {
312 3
                $label = $item['label'];
313
            }
314
315 16
            $iconOptions = [];
316
317 16
            $icon = $item['icon'] ?? '';
318
319 16
            if (array_key_exists('iconOptions', $item) && is_array($item['iconOptions'])) {
320 1
                $iconOptions = $this->addOptions($iconOptions, 'icon');
321
            }
322
323 16
            $label = $this->renderIcon($label, $icon, $iconOptions);
324
325 16
            $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
326 16
            $active = ArrayHelper::getValue($item, 'active', false);
327 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
328
329 16
            if ($this->encodeLinks === false) {
330 16
                $linkOptions['encode'] = false;
331
            }
332
333 16
            Html::addCssClass($linkOptions, $this->itemClass);
334
335 16
            if ($disabled) {
336 1
                Html::addCssStyle($linkOptions, 'opacity:.65; pointer-events:none;');
337
            }
338
339
            /** @psalm-suppress ConflictingReferenceConstraint */
340 16
            if ($active) {
341 3
                Html::addCssClass($linkOptions, ['active' => 'is-active']);
342
            }
343
344 16
            $url = $item['url'] ?? null;
345
346 16
            if (empty($item['items'])) {
347 16
                $lines[] = Html::a($label, $url, $linkOptions);
348
            } else {
349 1
                $lines[] = Html::a($label, $url, array_merge($this->linkOptions, $linkOptions));
350
351 1
                $dropdownWidget = self::widget()
352 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

352
                    ->/** @scrutinizer ignore-call */ dividerClass($this->dividerClass)
Loading history...
353 1
                    ->itemClass($this->itemClass)
354 1
                    ->itemsClass($this->itemsClass)
355 1
                    ->items($item['items']);
356
357 1
                if ($this->encloseByContainer === false) {
358 1
                    $dropdownWidget = $dropdownWidget->withoutEncloseByContainer();
359
                }
360
361 1
                if ($this->encodeLabels === false) {
362 1
                    $dropdownWidget = $dropdownWidget->withoutEncodeLabels();
363
                }
364
365 1
                $lines[] = $dropdownWidget->render();
366
            }
367
        }
368
369
        return
370 17
            Html::beginTag('div', $itemsOptions) . "\n" .
371 17
                implode("\n", $lines) . "\n" .
372 17
            Html::endTag('div');
373
    }
374
375 16
    private function renderIcon(string $label, string $icon, array $iconOptions): string
376
    {
377 16
        if ($icon !== '') {
378 1
            $label = Html::beginTag('span', $iconOptions) .
379 1
                Html::tag('i', '', ['class' => $icon, 'encode' => false]) .
380 1
                Html::endTag('span') .
381 1
                Html::tag('span', $label, ['encode' => false]);
382
        }
383
384 16
        return $label;
385
    }
386
}
387