Passed
Push — master ( 566566...d0f53d )
by Alexander
02:18
created

Tabs::prepareItems()   B

Complexity

Conditions 11
Paths 26

Size

Total Lines 56
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 31
c 2
b 0
f 0
nc 26
nop 2
dl 0
loc 56
ccs 32
cts 32
cp 1
crap 11
rs 7.3166

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\Html\Html;
11
12
use function array_key_exists;
13
use function array_merge;
14
15
/**
16
 * Tabs renders a Tab bootstrap javascript component.
17
 *
18
 * For example:
19
 *
20
 * ```php
21
 * echo Tabs::widget()
22
 *     ->items([
23
 *         [
24
 *             'label' => 'One',
25
 *             'content' => 'Anim pariatur cliche...',
26
 *             'active' => true,
27
 *         ],
28
 *         [
29
 *             'label' => 'Two',
30
 *             'content' => 'Anim pariatur cliche...',
31
 *             'headerOptions' => [...],
32
 *             'options' => ['id' => 'myveryownID'],
33
 *         ],
34
 *         [
35
 *             'label' => 'Example',
36
 *             'url' => 'http://www.example.com',
37
 *         ],
38
 *         [
39
 *             'label' => 'Dropdown',
40
 *             'items' => [
41
 *                  [
42
 *                      'label' => 'DropdownA',
43
 *                      'content' => 'DropdownA, Anim pariatur cliche...',
44
 *                  ],
45
 *                  [
46
 *                      'label' => 'DropdownB',
47
 *                      'content' => 'DropdownB, Anim pariatur cliche...',
48
 *                  ],
49
 *                  [
50
 *                      'label' => 'External Link',
51
 *                      'url' => 'http://www.example.com',
52
 *                  ],
53
 *             ],
54
 *         ],
55
 *     ]);
56
 * ```
57
 */
58
final class Tabs extends Widget
59
{
60
    private array $items = [];
61
    private array $itemOptions = [];
62
    private array $headerOptions = [];
63
    private bool $encodeLabels = true;
64
    private bool $encodeTags = false;
65
    private string $navType = 'nav-tabs';
66
    private bool $renderTabContent = true;
67
    private array $tabContentOptions = [];
68
    private string $dropdownClass = Dropdown::class;
69
    private array $panes = [];
70
    private array $options = [];
71
72 16
    protected function run(): string
73
    {
74 16
        if (!isset($this->options['id'])) {
75 14
            $this->options['id'] = "{$this->getId()}-tabs";
76
        }
77
78
        /** @psalm-suppress InvalidArgument */
79 16
        Html::addCssClass($this->options, ['widget' => 'nav', $this->navType]);
80 16
        Html::addCssClass($this->tabContentOptions, ['tabContentOptions' => 'tab-content']);
81
82 16
        if ($this->encodeTags === false) {
83 15
            $this->itemOptions['encode'] = false;
84 15
            $this->options['encode'] = false;
85 15
            $this->tabContentOptions['encode'] = false;
86
        }
87
88 16
        $this->prepareItems($this->items);
89
90 15
        $navWidget = Nav::widget()
91 15
            ->dropdownClass($this->dropdownClass)
0 ignored issues
show
Bug introduced by
The method dropdownClass() 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\ButtonDropdown or Yiisoft\Yii\Bootstrap5\Tabs. ( Ignorable by Annotation )

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

91
            ->/** @scrutinizer ignore-call */ dropdownClass($this->dropdownClass)
Loading history...
92 15
            ->items($this->items)
93 15
            ->options(ArrayHelper::merge(['role' => 'tablist'], $this->options));
94
95 15
        if ($this->encodeLabels === false) {
96 1
            $navWidget = $navWidget->withoutEncodeLabels();
97
        }
98
99 15
        return $navWidget->render() . $this->renderPanes($this->panes);
100
    }
101
102
    /**
103
     * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
104
     *
105
     * @param string $value
106
     *
107
     * @return $this
108
     */
109 1
    public function dropdownClass(string $value): self
110
    {
111 1
        $new = clone $this;
112 1
        $new->dropdownClass = $value;
113
114 1
        return $new;
115
    }
116
117
    /**
118
     * When tags Labels HTML should not be encoded.
119
     *
120
     * @return $this
121
     */
122 1
    public function withoutEncodeLabels(): self
123
    {
124 1
        $new = clone $this;
125 1
        $new->encodeLabels = false;
126
127 1
        return $new;
128
    }
129
130
    /**
131
     * List of HTML attributes for the header container tags. This will be overwritten by the "headerOptions" set in
132
     * individual {@see items}.
133
     *
134
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
135
     *
136
     * @param array $value
137
     *
138
     * @return $this
139
     */
140 1
    public function headerOptions(array $value): self
141
    {
142 1
        $new = clone $this;
143 1
        $new->headerOptions = $value;
144
145 1
        return $new;
146
    }
147
148
    /**
149
     * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
150
     *
151
     * - label: string, required, the tab header label.
152
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override
153
     *   global `$this->encodeLabels` param.
154
     * - headerOptions: array, optional, the HTML attributes of the tab header.
155
     * - content: string, optional, the content (HTML) of the tab pane.
156
     * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
157
     *   the browser to this URL.
158
     * - options: array, optional, the HTML attributes of the tab pane container.
159
     * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
160
     *   'active' explicitly - the first one will be activated.
161
     * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
162
     * - items: array, optional, can be used instead of `content` to specify a dropdown items
163
     *   configuration array. Each item can hold three extra keys, besides the above ones:
164
     *     * active: bool, optional, whether the item tab header and pane should be visible or not.
165
     *     * content: string, required if `items` is not set. The content (HTML) of the tab pane.
166
     *     * contentOptions: optional, array, the HTML attributes of the tab content container.
167
     *
168
     * @param array $value
169
     *
170
     * @return $this
171
     */
172 15
    public function items(array $value): self
173
    {
174 15
        $new = clone $this;
175 15
        $new->items = $value;
176
177 15
        return $new;
178
    }
179
180
    /**
181
     * List of HTML attributes for the item container tags. This will be overwritten by the "options" set in individual
182
     * {@see items}. The following special options are recognized.
183
     *
184
     * @param array $value
185
     *
186
     * @return $this
187
     *
188
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
189
     */
190 1
    public function itemOptions(array $value): self
191
    {
192 1
        $new = clone $this;
193 1
        $new->itemOptions = $value;
194
195 1
        return $new;
196
    }
197
198
    /**
199
     * Specifies the Bootstrap tab styling.
200
     *
201
     * @param string $value
202
     *
203
     * @return $this
204
     */
205 1
    public function navType(string $value): self
206
    {
207 1
        $new = clone $this;
208 1
        $new->navType = $value;
209
210 1
        return $new;
211
    }
212
213
    /**
214
     * The HTML attributes for the widget container tag. The following special options are recognized.
215
     *
216
     * @param array $value
217
     *
218
     * @return $this
219
     *
220
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
221
     */
222 2
    public function options(array $value): self
223
    {
224 2
        $new = clone $this;
225 2
        $new->options = $value;
226
227 2
        return $new;
228
    }
229
230
    /**
231
     * Tab panes (contents).
232
     *
233
     * @param array $value
234
     *
235
     * @return $this
236
     */
237 1
    public function panes(array $value): self
238
    {
239 1
        $new = clone $this;
240 1
        $new->panes = $value;
241
242 1
        return $new;
243
    }
244
245
    /**
246
     * Manually render `tab-content` yourself in case your tab contents are complex.
247
     *
248
     * @return $this
249
     */
250 1
    public function withoutRenderTabContent(): self
251
    {
252 1
        $new = clone $this;
253 1
        $new->renderTabContent = false;
254
255 1
        return $new;
256
    }
257
258
    /**
259
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
260
     *
261
     * @param array $value
262
     *
263
     * @return $this
264
     *
265
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
266
     */
267 1
    public function tabContentOptions(array $value): self
268
    {
269 1
        $new = clone $this;
270 1
        $new->tabContentOptions = $value;
271
272 1
        return $new;
273
    }
274
275
    /**
276
     * Allows you to enable the encoding tags html.
277
     *
278
     * @return self
279
     */
280 1
    public function encodeTags(): self
281
    {
282 1
        $new = clone $this;
283 1
        $new->encodeTags = true;
284
285 1
        return $new;
286
    }
287
288
    /**
289
     * Renders tab panes.
290
     *
291
     * @param array $panes
292
     *
293
     * @throws JsonException
294
     *
295
     * @return string the rendering result.
296
     */
297 15
    private function renderPanes(array $panes): string
298
    {
299 15
        return $this->renderTabContent ? ("\n" . Html::div(implode("\n", $panes), $this->tabContentOptions)) : '';
300
    }
301
302
    /**
303
     * Renders tab items as specified on {@see items}.
304
     *
305
     * @param array $items
306
     * @param string $prefix
307
     *
308
     * @throws JsonException|RuntimeException
309
     */
310 16
    private function prepareItems(array &$items, string $prefix = ''): void
311
    {
312 16
        if (!$this->hasActiveTab()) {
313 15
            $this->activateFirstVisibleTab();
314
        }
315
316 16
        foreach ($items as $n => $item) {
317 15
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
318 15
            $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . $prefix . '-tab' . $n);
319
320
            /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
321 15
            unset($items[$n]['options']['id']);
322
323 15
            if (!ArrayHelper::remove($item, 'visible', true)) {
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type object; however, parameter $array of Yiisoft\Arrays\ArrayHelper::remove() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

323
            if (!ArrayHelper::remove(/** @scrutinizer ignore-type */ $item, 'visible', true)) {
Loading history...
324 3
                continue;
325
            }
326
327 15
            if (!array_key_exists('label', $item)) {
328 1
                throw new RuntimeException('The "label" option is required.');
329
            }
330
331 14
            $selected = ArrayHelper::getValue($item, 'active', false);
332 14
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
333 14
            $headerOptions = ArrayHelper::getValue($item, 'headerOptions', $this->headerOptions);
334
335 14
            if (isset($item['items'])) {
336 3
                $this->prepareItems($items[$n]['items'], '-dd' . $n);
337 3
                continue;
338
            }
339
340 14
            ArrayHelper::setValue($items[$n], 'options', $headerOptions);
341
342 14
            if (isset($item['url'])) {
343 3
                continue;
344
            }
345
346 14
            ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
347 14
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.bs-toggle', 'tab');
348 14
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
349 14
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
350
351 14
            if (!$disabled) {
352 14
                ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
353
            }
354
355
            /** @psalm-suppress InvalidArgument */
356 14
            Html::addCssClass($options, ['widget' => 'tab-pane']);
357
358 14
            if ($selected) {
359 14
                Html::addCssClass($options, ['active' => 'active']);
360
            }
361
362
            /** @psalm-suppress ConflictingReferenceConstraint */
363 14
            if ($this->renderTabContent) {
364 13
                $tag = ArrayHelper::remove($options, 'tag', 'div');
365 13
                $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options);
366
            }
367
        }
368 15
    }
369
370
    /**
371
     * @return bool if there's active tab defined.
372
     */
373 16
    private function hasActiveTab(): bool
374
    {
375 16
        foreach ($this->items as $item) {
376 15
            if (isset($item['active']) && $item['active'] === true) {
377 4
                return true;
378
            }
379
        }
380
381 15
        return false;
382
    }
383
384
    /**
385
     * Sets the first visible tab as active.
386
     *
387
     * This method activates the first tab that is visible and not explicitly set to inactive (`'active' => false`).
388
     */
389 15
    private function activateFirstVisibleTab(): void
390
    {
391 15
        foreach ($this->items as $i => $item) {
392 14
            $active = ArrayHelper::getValue($item, 'active', null);
393 14
            $visible = ArrayHelper::getValue($item, 'visible', true);
394 14
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
395
396 14
            if ($visible && $active !== false && $disabled !== true) {
397 14
                $this->items[$i]['active'] = true;
398 14
                return;
399
            }
400
        }
401 1
    }
402
}
403