Tabs::prepareItems()   B
last analyzed

Complexity

Conditions 11
Paths 26

Size

Total Lines 54
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 11.0036

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 54
ccs 31
cts 32
cp 0.9688
crap 11.0036
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\Bootstrap4;
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 implode;
15
16
/**
17
 * Tabs renders a Tab bootstrap javascript component.
18
 *
19
 * For example:
20
 *
21
 * ```php
22
 * echo Tabs::widget()
23
 *     ->items([
24
 *         [
25
 *             'label' => 'One',
26
 *             'content' => 'Anim pariatur cliche...',
27
 *             'active' => true,
28
 *         ],
29
 *         [
30
 *             'label' => 'Two',
31
 *             'content' => 'Anim pariatur cliche...',
32
 *             'headerOptions' => [...],
33
 *             'options' => ['id' => 'myveryownID'],
34
 *         ],
35
 *         [
36
 *             'label' => 'Example',
37
 *             'url' => 'http://www.example.com',
38
 *         ],
39
 *         [
40
 *             'label' => 'Dropdown',
41
 *             'items' => [
42
 *                  [
43
 *                      'label' => 'DropdownA',
44
 *                      'content' => 'DropdownA, Anim pariatur cliche...',
45
 *                  ],
46
 *                  [
47
 *                      'label' => 'DropdownB',
48
 *                      'content' => 'DropdownB, Anim pariatur cliche...',
49
 *                  ],
50
 *                  [
51
 *                      'label' => 'External Link',
52
 *                      'url' => 'http://www.example.com',
53
 *                  ],
54
 *             ],
55
 *         ],
56
 *     ]);
57
 * ```
58
 */
59
class Tabs extends Widget
60
{
61
    private array $panes = [];
62
    private array $items = [];
63
    private array $itemOptions = [];
64
    private array $headerOptions = [];
65
    private array $linkOptions = [];
66
    private bool $encodeLabels = true;
67
    private string $navType = 'nav-tabs';
68
    private bool $renderTabContent = true;
69
    private array $tabContentOptions = [];
70
    private string $dropdownClass = Dropdown::class;
71
    private array $options = [];
72
73 10
    protected function run(): string
74
    {
75 10
        if (!isset($this->options['id'])) {
76 8
            $this->options['id'] = "{$this->getId()}-tabs";
77
        }
78
79 10
        Html::addCssClass($this->options, ['widget' => 'nav', $this->navType]);
80 10
        Html::addCssClass($this->tabContentOptions, 'tab-content');
81
82 10
        $this->registerPlugin('tab', $this->options);
83 10
        $this->prepareItems($this->items);
84
85 10
        return Nav::widget()
86 10
                ->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\Bootstrap4\Tabs or Yiisoft\Yii\Bootstrap4\Nav or Yiisoft\Yii\Bootstrap4\ButtonDropdown. ( Ignorable by Annotation )

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

86
                ->/** @scrutinizer ignore-call */ dropdownClass($this->dropdownClass)
Loading history...
87 10
                ->options(ArrayHelper::merge(['role' => 'tablist'], $this->options))
88 10
                ->items($this->items)
89 10
                ->encodeLabels($this->encodeLabels)
90 10
                ->render()
91 10
                . $this->renderPanes($this->panes);
92
    }
93
94
    /**
95
     * Renders tab items as specified on {@see items}.
96
     *
97
     * @param array $items
98
     * @param string $prefix
99
     *
100
     * @throws JsonException|RuntimeException
101
     */
102 10
    protected function prepareItems(array &$items, string $prefix = ''): void
103
    {
104 10
        if (!$this->hasActiveTab()) {
105 9
            $this->activateFirstVisibleTab();
106
        }
107
108 10
        foreach ($items as $n => $item) {
109 10
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
110 10
            $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . $prefix . '-tab' . $n);
111
112
            /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
113 10
            unset($items[$n]['options']['id']);
114
115 10
            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

115
            if (!ArrayHelper::remove(/** @scrutinizer ignore-type */ $item, 'visible', true)) {
Loading history...
116 3
                continue;
117
            }
118
119 10
            if (!array_key_exists('label', $item)) {
120
                throw new RuntimeException('The "label" option is required.');
121
            }
122
123 10
            $selected = ArrayHelper::getValue($item, 'active', false);
124 10
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
125 10
            $headerOptions = ArrayHelper::getValue($item, 'headerOptions', $this->headerOptions);
126
127 10
            if (isset($item['items'])) {
128 3
                $this->prepareItems($items[$n]['items'], '-dd' . $n);
129 3
                continue;
130
            }
131
132 10
            ArrayHelper::setValue($items[$n], 'options', $headerOptions);
133
134 10
            if (isset($item['url'])) {
135 3
                continue;
136
            }
137
138 10
            ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
139 10
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.toggle', 'tab');
140 10
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
141 10
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
142
143 10
            if (!$disabled) {
144 10
                ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
145
            }
146
147 10
            Html::addCssClass($options, ['widget' => 'tab-pane']);
148
149 10
            if ($selected) {
150 10
                Html::addCssClass($options, 'active');
151
            }
152
153 10
            if ($this->renderTabContent) {
154 10
                $tag = ArrayHelper::remove($options, 'tag', 'div');
155 10
                $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options);
156
            }
157
        }
158 10
    }
159
160
    /**
161
     * @return bool if there's active tab defined.
162
     */
163 10
    protected function hasActiveTab(): bool
164
    {
165 10
        foreach ($this->items as $item) {
166 10
            if (isset($item['active']) && $item['active'] === true) {
167 4
                return true;
168
            }
169
        }
170
171 9
        return false;
172
    }
173
174
    /**
175
     * Sets the first visible tab as active.
176
     *
177
     * This method activates the first tab that is visible and not explicitly set to inactive (`'active' => false`).
178
     */
179 9
    protected function activateFirstVisibleTab(): void
180
    {
181 9
        foreach ($this->items as $i => $item) {
182 9
            $active = ArrayHelper::getValue($item, 'active', null);
183 9
            $visible = ArrayHelper::getValue($item, 'visible', true);
184 9
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
185
186 9
            if ($visible && $active !== false && ($disabled !== true)) {
187 9
                $this->items[$i]['active'] = true;
188 9
                return;
189
            }
190
        }
191
    }
192
193
    /**
194
     * Renders tab panes.
195
     *
196
     * @param array $panes
197
     *
198
     * @throws JsonException
199
     *
200
     * @return string the rendering result.
201
     */
202 10
    public function renderPanes(array $panes): string
203
    {
204 10
        return $this->renderTabContent ? ("\n" . Html::tag('div', implode("\n", $panes), $this->tabContentOptions)) : '';
205
    }
206
207
    /**
208
     * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
209
     *
210
     * @param string $value
211
     *
212
     * @return $this
213
     */
214
    public function dropdownClass(string $value): self
215
    {
216
        $this->dropdownClass = $value;
217
218
        return $this;
219
    }
220
221
    /**
222
     * Whether the labels for header items should be HTML-encoded.
223
     *
224
     * @param bool $value
225
     *
226
     * @return $this
227
     */
228 1
    public function encodeLabels(bool $value): self
229
    {
230 1
        $this->encodeLabels = $value;
231
232 1
        return $this;
233
    }
234
235
    /**
236
     * List of HTML attributes for the header container tags. This will be overwritten by the "headerOptions" set in
237
     * individual {@see items}.
238
     *
239
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
240
     *
241
     * @param array $value
242
     *
243
     * @return $this
244
     */
245
    public function headerOptions(array $value): self
246
    {
247
        $this->headerOptions = $value;
248
249
        return $this;
250
    }
251
252
    /**
253
     * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
254
     *
255
     * - label: string, required, the tab header label.
256
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override
257
     *   global `$this->encodeLabels` param.
258
     * - headerOptions: array, optional, the HTML attributes of the tab header.
259
     * - linkOptions: array, optional, the HTML attributes of the tab header link tags.
260
     * - content: string, optional, the content (HTML) of the tab pane.
261
     * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
262
     *   the browser to this URL.
263
     * - options: array, optional, the HTML attributes of the tab pane container.
264
     * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
265
     *   'active' explicitly - the first one will be activated.
266
     * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
267
     * - items: array, optional, can be used instead of `content` to specify a dropdown items
268
     *   configuration array. Each item can hold three extra keys, besides the above ones:
269
     *     * active: bool, optional, whether the item tab header and pane should be visible or not.
270
     *     * content: string, required if `items` is not set. The content (HTML) of the tab pane.
271
     *     * contentOptions: optional, array, the HTML attributes of the tab content container.
272
     *
273
     * @param array $value
274
     *
275
     * @return $this
276
     */
277 10
    public function items(array $value): self
278
    {
279 10
        $this->items = $value;
280
281 10
        return $this;
282
    }
283
284
    /**
285
     * List of HTML attributes for the item container tags. This will be overwritten by the "options" set in individual
286
     * {@see items}. The following special options are recognized.
287
     *
288
     * @param array $value
289
     *
290
     * @return $this
291
     *
292
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
293
     */
294 1
    public function itemOptions(array $value): self
295
    {
296 1
        $this->itemOptions = $value;
297
298 1
        return $this;
299
    }
300
301
    /**
302
     * List of HTML attributes for the tab header link tags. This will be overwritten by the "linkOptions" set in
303
     * individual {@see items}.
304
     *
305
     * @param array $value
306
     *
307
     * @return $this
308
     *
309
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
310
     */
311
    public function linkOptions(array $value): self
312
    {
313
        $this->linkOptions = $value;
314
315
        return $this;
316
    }
317
318
    /**
319
     * Specifies the Bootstrap tab styling.
320
     *
321
     * @param string $value
322
     *
323
     * @return $this
324
     */
325
    public function navType(string $value): self
326
    {
327
        $this->navType = $value;
328
329
        return $this;
330
    }
331
332
    /**
333
     * The HTML attributes for the widget container tag. The following special options are recognized.
334
     *
335
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
336
     *
337
     * @param array $value
338
     *
339
     * @return $this
340
     */
341 2
    public function options(array $value): self
342
    {
343 2
        $this->options = $value;
344
345 2
        return $this;
346
    }
347
348
    /**
349
     * Tab panes (contents).
350
     *
351
     * @param array $value
352
     *
353
     * @return $this
354
     */
355
    public function panes(array $value): self
356
    {
357
        $this->panes = $value;
358
359
        return $this;
360
    }
361
362
    /**
363
     * Whether to render the `tab-content` container and its content. You may set this property to be false so that you
364
     * can manually render `tab-content` yourself in case your tab contents are complex.
365
     *
366
     * @param bool $value
367
     *
368
     * @return $this
369
     */
370 1
    public function renderTabContent(bool $value): self
371
    {
372 1
        $this->renderTabContent = $value;
373
374 1
        return $this;
375
    }
376
377
    /**
378
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
379
     *
380
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
381
     *
382
     * @param array $value
383
     *
384
     * @return $this
385
     */
386 1
    public function tabContentOptions(array $value): self
387
    {
388 1
        $this->tabContentOptions = $value;
389
390 1
        return $this;
391
    }
392
}
393