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

Tabs::hasActiveTab()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

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

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