Passed
Pull Request — master (#35)
by Wilmer
02:20
created

Tabs::activateFirstVisibleTab()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 10
ccs 8
cts 8
cp 1
crap 5
rs 9.6111
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 = array_merge($this->itemOptions, ['encode' => false]);
84 15
            $this->options = array_merge($this->options, ['encode' => false]);
85 15
            $this->tabContentOptions = array_merge($this->tabContentOptions, ['encode' => false]);
86
        }
87
88 16
        $this->prepareItems($this->items);
89
90 15
        $navWidget = Nav::widget()
91 15
            ->withDropdownClass($this->dropdownClass)
0 ignored issues
show
Bug introduced by
The method withDropdownClass() 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 */ withDropdownClass($this->dropdownClass)
Loading history...
92 15
            ->withItems($this->items)
93 15
            ->withOptions(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 withDropdownClass(string $value): self
110
    {
111 1
        $new = clone $this;
112 1
        $new->dropdownClass = $value;
113
114 1
        return $new;
115
    }
116
117
    /**
118
     * Whether the labels for header items should be HTML-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 withHeaderOptions(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 withItems(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 withItemOptions(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 withNavType(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 withOptions(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 withPanes(array $value): self
238
    {
239 1
        $new = clone $this;
240 1
        $new->panes = $value;
241
242 1
        return $new;
243
    }
244
245
    /**
246
     * Whether to render the `tab-content` container and its content. You may set this property to be false so that you
247
     * can manually render `tab-content` yourself in case your tab contents are complex.
248
     *
249
     * @return $this
250
     */
251 1
    public function withoutRenderTabContent(): self
252
    {
253 1
        $new = clone $this;
254 1
        $new->renderTabContent = false;
255
256 1
        return $new;
257
    }
258
259
    /**
260
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
261
     *
262
     * @param array $value
263
     *
264
     * @return $this
265
     *
266
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
267
     */
268 1
    public function withTabContentOptions(array $value): self
269
    {
270 1
        $new = clone $this;
271 1
        $new->tabContentOptions = $value;
272
273 1
        return $new;
274
    }
275
276
    /**
277
     * Allows you to enable or disable the encoding tags html.
278
     *
279
     * @param bool $value
280
     *
281
     * @return self
282
     */
283 1
    public function withEncodeTags(): self
284
    {
285 1
        $new = clone $this;
286 1
        $new->encodeTags = true;
287
288 1
        return $new;
289
    }
290
291
    /**
292
     * Renders tab panes.
293
     *
294
     * @param array $panes
295
     *
296
     * @throws JsonException
297
     *
298
     * @return string the rendering result.
299
     */
300 15
    private function renderPanes(array $panes): string
301
    {
302 15
        return $this->renderTabContent ? ("\n" . Html::div(implode("\n", $panes), $this->tabContentOptions)) : '';
303
    }
304
305
    /**
306
     * Renders tab items as specified on {@see items}.
307
     *
308
     * @param array $items
309
     * @param string $prefix
310
     *
311
     * @throws JsonException|RuntimeException
312
     */
313 16
    private function prepareItems(array &$items, string $prefix = ''): void
314
    {
315 16
        if (!$this->hasActiveTab()) {
316 15
            $this->activateFirstVisibleTab();
317
        }
318
319 16
        foreach ($items as $n => $item) {
320 15
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
321 15
            $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . $prefix . '-tab' . $n);
322
323
            /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
324 15
            unset($items[$n]['options']['id']);
325
326 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

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