Passed
Push — master ( 1b2ad2...d912e9 )
by Alexander
02:41
created

Dropdown::renderItem()   C

Complexity

Conditions 11
Paths 64

Size

Total Lines 79
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 56
c 0
b 0
f 0
nc 64
nop 1
dl 0
loc 79
ccs 54
cts 54
cp 1
crap 11
rs 6.8133

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\Bootstrap5;
6
7
use JsonException;
8
use RuntimeException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Definitions\Exception\InvalidConfigException;
11
use Yiisoft\Html\Html;
12
use Yiisoft\Html\Tag\Li;
13
14
use function array_key_exists;
15
use function array_merge;
16
use function array_merge_recursive;
17
use function is_string;
18
19
/**
20
 * Dropdown renders a Bootstrap dropdown menu component.
21
 *
22
 * For example,
23
 *
24
 * ```php
25
 * <div class="dropdown">
26
 *     <?php
27
 *         echo Dropdown::widget()
28
 *             ->items([
29
 *                 ['label' => 'DropdownA', 'url' => '/'],
30
 *                 ['label' => 'DropdownB', 'url' => '#'],
31
 *             ]);
32
 *     ?>
33
 * </div>
34
 * ```
35
 */
36
final class Dropdown extends Widget
37
{
38
    private array $items = [];
39
    private bool $encodeLabels = true;
40
    private bool $encodeTags = false;
41
    private array $submenuOptions = [];
42
    private array $options = [];
43
    private array $itemOptions = [];
44
    private array $linkOptions = [];
45
46 31
    protected function run(): string
47
    {
48 31
        if (!isset($this->options['id'])) {
49 30
            $this->options['id'] = "{$this->getId()}-dropdown";
50
        }
51
52
        /** @psalm-suppress InvalidArgument */
53 31
        Html::addCssClass($this->options, ['widget' => 'dropdown-menu']);
54
55 31
        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 self
79
     */
80 31
    public function items(array $value): self
81
    {
82 31
        $new = clone $this;
83 31
        $new->items = $value;
84
85 31
        return $new;
86
    }
87
88
    /**
89
     * When tags Labels HTML should not be encoded.
90
     *
91
     * @return self
92
     */
93 4
    public function withoutEncodeLabels(): self
94
    {
95 4
        $new = clone $this;
96 4
        $new->encodeLabels = false;
97
98 4
        return $new;
99
    }
100
101
    /**
102
     * The HTML attributes for sub-menu container tags.
103
     *
104
     * @param array $value
105
     *
106
     * @return self
107
     */
108 4
    public function submenuOptions(array $value): self
109
    {
110 4
        $new = clone $this;
111 4
        $new->submenuOptions = $value;
112
113 4
        return $new;
114
    }
115
116
    /**
117
     * @param array $value the HTML attributes for the widget container tag. The following special options are
118
     * recognized.
119
     *
120
     * @return self
121
     *
122
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
123
     */
124 19
    public function options(array $value): self
125
    {
126 19
        $new = clone $this;
127 19
        $new->options = $value;
128
129 19
        return $new;
130
    }
131
132
    /**
133
     * Options for each item if not present in self
134
     *
135
     * @param array $options
136
     *
137
     * @return self
138
     */
139 1
    public function itemOptions(array $options): self
140
    {
141 1
        $new = clone $this;
142 1
        $new->itemOptions = $options;
143
144 1
        return $new;
145
    }
146
147
    /**
148
     * Options for each item link if not present in current item
149
     *
150
     * @param array $options
151
     *
152
     * @return self
153
     */
154 1
    public function linkOptions(array $options): self
155
    {
156 1
        $new = clone $this;
157 1
        $new->linkOptions = $options;
158
159 1
        return $new;
160
    }
161
162
    /**
163
     * Renders menu items.
164
     *
165
     * @param array $items the menu items to be rendered
166
     * @param array $options the container HTML attributes
167
     *
168
     * @throws InvalidConfigException|JsonException|RuntimeException if the label option is not specified in one of the
169
     * items.
170
     *
171
     * @return string the rendering result.
172
     */
173 31
    private function renderItems(array $items, array $options = []): string
174
    {
175 31
        $lines = [];
176
177 31
        foreach ($items as $item) {
178 31
            if (is_string($item)) {
179 4
                $item = ['label' => $item, 'encode' => false, 'enclose' => false];
180
            }
181
182 31
            if (isset($item['visible']) && !$item['visible']) {
183 3
                continue;
184
            }
185
186 31
            if (!array_key_exists('label', $item)) {
187 1
                throw new RuntimeException("The 'label' option is required.");
188
            }
189
190 30
            $lines[] = $this->renderItem($item);
191
        }
192
193 30
        $options = array_merge(['aria-expanded' => 'false'], $options);
194
195 30
        return Html::ul()
196 30
            ->items(...$lines)
197 30
            ->attributes($options)
198 30
            ->render();
199
    }
200
201
    /**
202
     * Render current dropdown item
203
     *
204
     * @param array $item
205
     *
206
     * @return Li
207
     */
208 30
    private function renderItem(array $item): Li
209
    {
210 30
        $url = $item['url'] ?? null;
211 30
        $encodeLabel = $item['encode'] ?? $this->encodeLabels;
212 30
        $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
213 30
        $itemOptions = ArrayHelper::getValue($item, 'options', $this->itemOptions);
214 30
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', $this->linkOptions);
215 30
        $active = ArrayHelper::getValue($item, 'active', false);
216 30
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
217 30
        $enclose = ArrayHelper::getValue($item, 'enclose', true);
218
219 30
        if ($url !== null) {
220 19
            Html::addCssClass($linkOptions, ['widget' => 'dropdown-item']);
221
222 19
            if ($disabled) {
223 2
                ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
224 2
                ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
225 2
                Html::addCssClass($linkOptions, ['disable' => 'disabled']);
226 19
            } elseif ($active) {
227 1
                Html::addCssClass($linkOptions, ['active' => 'active']);
228
            }
229
        }
230
231
        /** @psalm-suppress ConflictingReferenceConstraint */
232 30
        if (empty($item['items'])) {
233 30
            if ($url !== null) {
234 19
                $content = Html::a($label, $url, $linkOptions)->encode($this->encodeTags);
235 18
            } elseif ($label === '-') {
236 4
                Html::addCssClass($linkOptions, ['widget' => 'dropdown-divider']);
237 4
                $content = Html::tag('hr', '', $linkOptions);
238 16
            } elseif ($enclose === false) {
239 1
                $content = $label;
240
            } else {
241 15
                Html::addCssClass($linkOptions, ['widget' => 'dropdown-header']);
242 15
                $tag = ArrayHelper::remove($linkOptions, 'tag', 'h6');
243 15
                $content = Html::tag($tag, $label, $linkOptions);
244
            }
245
246 30
            return Li::tag()
247 30
                ->content($content)
248 30
                ->attributes($itemOptions)
249 30
                ->encode(false);
250
        }
251
252 4
        $submenuOptions = $this->submenuOptions;
253
254 4
        if (isset($item['submenuOptions'])) {
255 1
            $submenuOptions = array_merge($submenuOptions, $item['submenuOptions']);
256
        }
257
258 4
        Html::addCssClass($submenuOptions, ['submenu' => 'dropdown-menu']);
259 4
        Html::addCssClass($linkOptions, [
260
            'widget' => 'dropdown-item',
261
            'toggle' => 'dropdown-toggle',
262
        ]);
263
264 4
        $itemOptions = array_merge_recursive(['class' => ['dropdown'], 'aria-expanded' => 'false'], $itemOptions);
265
266 4
        $dropdown = self::widget()
267 4
            ->items($item['items'])
0 ignored issues
show
Bug introduced by
The method items() 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

267
            ->/** @scrutinizer ignore-call */ items($item['items'])
Loading history...
268 4
            ->options($submenuOptions)
269 4
            ->submenuOptions($submenuOptions);
270
271 4
        if ($this->encodeLabels === false) {
272 1
            $dropdown = $dropdown->withoutEncodeLabels();
273
        }
274
275 4
        ArrayHelper::setValue($linkOptions, 'data-bs-toggle', 'dropdown');
276 4
        ArrayHelper::setValue($linkOptions, 'data-bs-auto-close', 'outside');
277 4
        ArrayHelper::setValue($linkOptions, 'aria-haspopup', 'true');
278 4
        ArrayHelper::setValue($linkOptions, 'aria-expanded', 'false');
279 4
        ArrayHelper::setValue($linkOptions, 'role', 'button');
280
281 4
        $toggle = Html::a($label, $url, $linkOptions)->encode($this->encodeTags);
282
283 4
        return Li::tag()
284 4
            ->content($toggle . $dropdown->render())
285 4
            ->attributes($itemOptions)
286 4
            ->encode(false);
287
    }
288
}
289