Test Failed
Pull Request — master (#35)
by Wilmer
03:52 queued 01:19
created

Tabs::withItemOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 2
cts 3
cp 0.6667
crap 1.037
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 10
    public function run(): string
73
    {
74 10
        if (!isset($this->options['id'])) {
75 8
            $this->options['id'] = "{$this->getId()}-tabs";
76
        }
77
78
        /** @psalm-suppress InvalidArgument */
79 10
        Html::addCssClass($this->options, ['widget' => 'nav', $this->navType]);
80 10
        Html::addCssClass($this->tabContentOptions, 'tab-content');
81
82 10
        if ($this->encodeTags === false) {
83 10
            $this->itemOptions = array_merge($this->itemOptions, ['encode' => false]);
84
            $this->options = array_merge($this->options, ['encode' => false]);
85 10
            $this->tabContentOptions = array_merge($this->tabContentOptions, ['encode' => false]);
86 10
        }
87 10
88 10
        $this->prepareItems($this->items);
89 10
90 10
        return Nav::widget()
91 10
                ->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
                ->withItems($this->items)
93
                ->withOptions(ArrayHelper::merge(['role' => 'tablist'], $this->options))
94
                ->withoutEncodeLabels($this->encodeLabels)
95
                ->render()
96
                . $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 10
     * @param string $value
103
     *
104 10
     * @return $this
105 9
     */
106
    public function withDropdownClass(string $value): self
107
    {
108 10
        $new = clone $this;
109 10
        $new->dropdownClass = $value;
110 10
111
        return $new;
112
    }
113 10
114
    /**
115 10
     * Whether the labels for header items should be HTML-encoded.
116 3
     *
117
     * @param bool $value
118
     *
119 10
     * @return $this
120
     */
121
    public function withoutEncodeLabels(bool $value = false): self
122
    {
123 10
        $new = clone $this;
124 10
        $new->encodeLabels = $value;
125 10
126
        return $new;
127 10
    }
128 3
129 3
    /**
130
     * List of HTML attributes for the header container tags. This will be overwritten by the "headerOptions" set in
131
     * individual {@see items}.
132 10
     *
133
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
134 10
     *
135 3
     * @param array $value
136
     *
137
     * @return $this
138 10
     */
139 10
    public function withHeaderOptions(array $value): self
140 10
    {
141 10
        $new = clone $this;
142
        $new->headerOptions = $value;
143 10
144 10
        return $new;
145
    }
146
147
    /**
148 10
     * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
149
     *
150 10
     * - label: string, required, the tab header label.
151 10
     * - 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 10
     * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
156 10
     *   the browser to this URL.
157 10
     * - 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 10
     * - 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 10
     *     * contentOptions: optional, array, the HTML attributes of the tab content container.
166
     *
167 10
     * @param array $value
168 10
     *
169 4
     * @return $this
170
     */
171
    public function withItems(array $value): self
172
    {
173 9
        $new = clone $this;
174
        $new->items = $value;
175
176
        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 9
     * {@see items}. The following special options are recognized.
182
     *
183 9
     * @param array $value
184 9
     *
185 9
     * @return $this
186 9
     *
187
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
188 9
     */
189 9
    public function withItemOptions(array $value): self
190 9
    {
191
        $new = clone $this;
192
        $new->itemOptions = $value;
193
194
        return $new;
195
    }
196
197
    /**
198
     * Specifies the Bootstrap tab styling.
199
     *
200
     * @param string $value
201
     *
202
     * @return $this
203
     */
204 10
    public function withNavType(string $value): self
205
    {
206 10
        $new = clone $this;
207
        $new->navType = $value;
208
209
        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
    public function withOptions(array $value): self
222
    {
223
        $new = clone $this;
224
        $new->options = $value;
225
226
        return $new;
227
    }
228
229
    /**
230 1
     * Tab panes (contents).
231
     *
232 1
     * @param array $value
233
     *
234 1
     * @return $this
235
     */
236
    public function withPanes(array $value): self
237
    {
238
        $new = clone $this;
239
        $new->panes = $value;
240
241
        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
    public function withRenderTabContent(bool $value): self
253
    {
254
        $new = clone $this;
255
        $new->renderTabContent = $value;
256
257
        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
    public function withTabContentOptions(array $value): self
270
    {
271
        $new = clone $this;
272
        $new->tabContentOptions = $value;
273
274
        return $new;
275
    }
276
277
    /**
278
     * Allows you to enable or disable the encoding tags html.
279 10
     *
280
     * @param bool $value
281 10
     *
282
     * @return self
283 10
     */
284
    public function withEncodeTags(bool $value = true): self
285
    {
286
        $new = clone $this;
287
        $new->encodeTags = $value;
288
289
        return $new;
290
    }
291
292
    /**
293
     * Renders tab panes.
294
     *
295
     * @param array $panes
296 1
     *
297
     * @throws JsonException
298 1
     *
299
     * @return string the rendering result.
300 1
     */
301
    private function renderPanes(array $panes): string
302
    {
303
        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
    private function prepareItems(array &$items, string $prefix = ''): void
315
    {
316
        if (!$this->hasActiveTab()) {
317
            $this->activateFirstVisibleTab();
318
        }
319
320
        foreach ($items as $n => $item) {
321
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
322
            $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
            unset($items[$n]['options']['id']);
326
327
            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
                continue;
329
            }
330
331
            if (!array_key_exists('label', $item)) {
332
                throw new RuntimeException('The "label" option is required.');
333
            }
334
335
            $selected = ArrayHelper::getValue($item, 'active', false);
336
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
337
            $headerOptions = ArrayHelper::getValue($item, 'headerOptions', $this->headerOptions);
338
339
            if (isset($item['items'])) {
340
                $this->prepareItems($items[$n]['items'], '-dd' . $n);
341
                continue;
342
            }
343 2
344
            ArrayHelper::setValue($items[$n], 'options', $headerOptions);
345 2
346
            if (isset($item['url'])) {
347 2
                continue;
348
            }
349
350
            ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
351
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.bs-toggle', 'tab');
352
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
353
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
354
355
            if (!$disabled) {
356
                ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
357
            }
358
359
            /** @psalm-suppress InvalidArgument */
360
            Html::addCssClass($options, ['widget' => 'tab-pane']);
361
362
            if ($selected) {
363
                Html::addCssClass($options, 'active');
364
            }
365
366
            /** @psalm-suppress ConflictingReferenceConstraint */
367
            if ($this->renderTabContent) {
368
                $tag = ArrayHelper::remove($options, 'tag', 'div');
369
                $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options);
370
            }
371
        }
372 1
    }
373
374 1
    /**
375
     * @return bool if there's active tab defined.
376 1
     */
377
    private function hasActiveTab(): bool
378
    {
379
        foreach ($this->items as $item) {
380
            if (isset($item['active']) && $item['active'] === true) {
381
                return true;
382
            }
383
        }
384
385
        return false;
386
    }
387
388 1
    /**
389
     * Sets the first visible tab as active.
390 1
     *
391
     * This method activates the first tab that is visible and not explicitly set to inactive (`'active' => false`).
392 1
     */
393
    private function activateFirstVisibleTab(): void
394
    {
395
        foreach ($this->items as $i => $item) {
396
            $active = ArrayHelper::getValue($item, 'active', null);
397
            $visible = ArrayHelper::getValue($item, 'visible', true);
398
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
399
400
            if ($visible && $active !== false && ($disabled !== true)) {
401
                $this->items[$i]['active'] = true;
402
                return;
403
            }
404
        }
405
    }
406
}
407