Passed
Pull Request — master (#69)
by
unknown
11:03
created

Tabs::headerOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
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 bool $encodeTags = false;
62
    private string $navType = 'nav-tabs';
63
    private bool $renderTabContent = true;
64
    private array $tabContentOptions = [];
65
    private array $paneOptions = [];
66
    private array $panes = [];
67
    private array $navOptions = [];
68
69 18
    protected function run(): string
70
    {
71 18
        $navOptions = $this->navOptions;
72
73 18
        if (!isset($navOptions['options()'][0]['id'])) {
74 16
            $navOptions['options()'][0]['id'] = "{$this->getId()}-tabs";
75
        }
76
77 18
        if (!isset($navOptions['options()'][0]['role'])) {
78 18
            $navOptions['options()'][0]['role'] = 'tablist';
79
        }
80
81
        /** @psalm-suppress InvalidArgument */
82 18
        Html::addCssClass($navOptions['options()'][0], ['widget' => 'nav', $this->navType]);
83 18
        Html::addCssClass($this->tabContentOptions, ['tabContentOptions' => 'tab-content']);
84
85 18
        $this->prepareItems($this->items, $navOptions['options()'][0]['id']);
86 17
        $navWidget = Nav::widget($navOptions)->items($this->items);
0 ignored issues
show
Bug introduced by
The method items() 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\Carousel or Yiisoft\Yii\Bootstrap5\Dropdown or Yiisoft\Yii\Bootstrap5\Tabs or Yiisoft\Yii\Bootstrap5\Accordion. ( Ignorable by Annotation )

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

86
        $navWidget = Nav::widget($navOptions)->/** @scrutinizer ignore-call */ items($this->items);
Loading history...
87
88 17
        return $navWidget->render() . $this->renderPanes($this->panes);
89
    }
90
91
    /**
92
     * Set all options for nav widget
93
     *
94
     * @param array $options
95
     * @param bool $replace
96
     *
97
     * @return self
98
     */
99 1
    public function navOptions(array $options): self
100
    {
101 1
        $new = clone $this;
102 1
        $new->navOptions = [];
103
104 1
        foreach ($options as $name => $value) {
105 1
            $new = $new->navOption($name, $value);
106
        }
107
108 1
        return $new;
109
    }
110
111
    /**
112
     * Set allowed Nav::widget option
113
     *
114
     * @param string $name
115
     * @param mixed $value
116
     *
117
     * @return self
118
     */
119 8
    public function navOption(string $name, $value): self
120
    {
121 8
        $new = clone $this;
122 8
        $new->navOptions[$name . '()'] = [$value];
123
124 8
        return $new;
125
    }
126
127
    /**
128
     * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
129
     *
130
     * @param string $value
131
     *
132
     * @return self
133
     */
134 1
    public function dropdownClass(string $value): self
135
    {
136 1
        return $this->navOption('dropdownClass', $value);
137
    }
138
139
    /**
140
     * Base options for nav
141
     *
142
     * @param array $options
143
     *
144
     * @return self
145
     */
146
    public function dropdownOptions(array $options): self
147
    {
148
        return $this->navOption('dropdownOptions', $options);
149
    }
150
151
    /**
152
     * When tags Labels HTML should not be encoded.
153
     *
154
     * @return self
155
     */
156 2
    public function withoutEncodeLabels(): self
157
    {
158 2
        return $this->navOption('withoutEncodeLabels', false);
159
    }
160
161
    /**
162
     * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
163
     *
164
     * - label: string, required, the tab header label.
165
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override
166
     *   global `$this->encodeLabels` param.
167
     * - headerOptions: array, optional, the HTML attributes of the tab header.
168
     * - content: string, optional, the content (HTML) of the tab pane.
169
     * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
170
     *   the browser to this URL.
171
     * - options: array, optional, the HTML attributes of the tab pane container.
172
     * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
173
     *   'active' explicitly - the first one will be activated.
174
     * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
175
     * - items: array, optional, can be used instead of `content` to specify a dropdown items
176
     *   configuration array. Each item can hold three extra keys, besides the above ones:
177
     *     * active: bool, optional, whether the item tab header and pane should be visible or not.
178
     *     * content: string, required if `items` is not set. The content (HTML) of the tab pane.
179
     *     * contentOptions: optional, array, the HTML attributes of the tab content container.
180
     *
181
     * @param array $value
182
     *
183
     * @return self
184
     */
185 17
    public function items(array $value): self
186
    {
187 17
        $new = clone $this;
188 17
        $new->items = $value;
189
190 17
        return $new;
191
    }
192
193
    /**
194
     * List of HTML attributes for the header container tags. This will be overwritten by the "options" set in
195
     * individual {@see items}.
196
     *
197
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
198
     * {@see Nav::itemOptions()}
199
     *
200
     * @param array $value
201
     *
202
     * @return self
203
     */
204 2
    public function itemOptions(array $value): self
205
    {
206 2
        return $this->navOption('itemOptions', $value);
207
    }
208
209
    /**
210
     * Options for each item link if not present in current item
211
     *
212
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
213
     * {@see Nav::linkOptions()}
214
     *
215
     * @param array $options
216
     *
217
     * @return self
218
     */
219 1
    public function linkOptions(array $value): self
220
    {
221 1
        return $this->navOption('linkOptions', $value);
222
    }
223
224
    /**
225
     * List of HTML attributes for the item container tags. This will be overwritten by the "options" set in individual
226
     * {@see items}. The following special options are recognized.
227
     *
228
     * @param array $value
229
     *
230
     * @return self
231
     *
232
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
233
     */
234 1
    public function paneOptions(array $options): self
235
    {
236 1
        $new = clone $this;
237 1
        $new->paneOptions = $options;
238
239 1
        return $new;
240
    }
241
242
243
    /**
244
     * Specifies the Bootstrap tab styling.
245
     *
246
     * @param string $value
247
     *
248
     * @return self
249
     */
250 4
    public function navType(string $value): self
251
    {
252 4
        $new = clone $this;
253 4
        $new->navType = $value;
254
255 4
        return $new;
256
    }
257
258
    /**
259
     * The HTML attributes for the widget container tag. The following special options are recognized.
260
     *
261
     * @param array $value
262
     *
263
     * @return self
264
     *
265
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
266
     */
267 2
    public function options(array $value): self
268
    {
269 2
        return $this->navOption('options', $value);
270
    }
271
272
    /**
273
     * Tab panes (contents).
274
     *
275
     * @param array $value
276
     *
277
     * @return self
278
     */
279 1
    public function panes(array $value): self
280
    {
281 1
        $new = clone $this;
282 1
        $new->panes = $value;
283
284 1
        return $new;
285
    }
286
287
    /**
288
     * Manually render `tab-content` yourself in case your tab contents are complex.
289
     *
290
     * @return self
291
     */
292 1
    public function withoutRenderTabContent(): self
293
    {
294 1
        $new = clone $this;
295 1
        $new->renderTabContent = false;
296
297 1
        return $new;
298
    }
299
300
    /**
301
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
302
     *
303
     * @param array $value
304
     *
305
     * @return self
306
     *
307
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
308
     */
309 1
    public function tabContentOptions(array $value): self
310
    {
311 1
        $new = clone $this;
312 1
        $new->tabContentOptions = $value;
313
314 1
        return $new;
315
    }
316
317
    /**
318
     * Renders tab panes.
319
     *
320
     * @param array $panes
321
     *
322
     * @throws JsonException
323
     *
324
     * @return string the rendering result.
325
     */
326 17
    private function renderPanes(array $panes): string
327
    {
328 17
        return $this->renderTabContent
329 16
            ? ("\n" . Html::div(implode("\n", $panes), $this->tabContentOptions)->encode($this->encodeTags))
330 17
            : '';
331
    }
332
333
    /**
334
     * Renders tab items as specified on {@see items}.
335
     *
336
     * @param array $items
337
     * @param string $prefix
338
     *
339
     * @throws JsonException|RuntimeException
340
     */
341 18
    private function prepareItems(array &$items, string $navId, string $prefix = ''): void
342
    {
343 18
        if (!$this->hasActiveTab()) {
344 17
            $this->activateFirstVisibleTab();
345
        }
346
347 18
        foreach ($items as $n => $item) {
348 17
            $options = array_merge($this->paneOptions, ArrayHelper::remove($item, 'paneOptions', []));
349 17
            $options['id'] = ArrayHelper::getValue($options, 'id', $navId . $prefix . '-tab' . $n);
350
351
            /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
352 17
            unset($items[$n]['options']['id']);
353
354 17
            if (!ArrayHelper::remove($item, 'visible', true)) {
355 3
                continue;
356
            }
357
358 17
            if (!array_key_exists('label', $item)) {
359 1
                throw new RuntimeException('The "label" option is required.');
360
            }
361
362 16
            $selected = ArrayHelper::getValue($item, 'active', false);
363 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
364
365 16
            if (isset($item['items'])) {
366 3
                $this->prepareItems($items[$n]['items'], $navId, '-dd' . $n);
367 3
                continue;
368
            }
369
370 16
            if (isset($item['url'])) {
371 3
                continue;
372
            }
373
374 16
            $items[$n]['linkOptions'] = $this->navOptions['linkOptions()'][0] ?? [];
375
376 16
            ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
377 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.bs-toggle', 'tab');
378 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
379 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
380
381 16
            if (!$disabled) {
382 16
                ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
383
            }
384
385
            /** @psalm-suppress InvalidArgument */
386 16
            Html::addCssClass($options, ['widget' => 'tab-pane']);
387
388 16
            if ($selected) {
389 16
                Html::addCssClass($options, ['active' => 'active']);
390
            }
391
392
            /** @psalm-suppress ConflictingReferenceConstraint */
393 16
            if ($this->renderTabContent) {
394 15
                $tag = ArrayHelper::remove($options, 'tag', 'div');
395 15
                $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options)->encode($this->encodeTags)->render();
396
            }
397
        }
398 17
    }
399
400
    /**
401
     * @return bool if there's active tab defined.
402
     */
403 18
    private function hasActiveTab(): bool
404
    {
405 18
        foreach ($this->items as $item) {
406 17
            if (isset($item['active']) && $item['active'] === true) {
407 4
                return true;
408
            }
409
        }
410
411 17
        return false;
412
    }
413
414
    /**
415
     * Sets the first visible tab as active.
416
     *
417
     * This method activates the first tab that is visible and not explicitly set to inactive (`'active' => false`).
418
     */
419 17
    private function activateFirstVisibleTab(): void
420
    {
421 17
        foreach ($this->items as $i => $item) {
422 16
            $active = ArrayHelper::getValue($item, 'active', null);
423 16
            $visible = ArrayHelper::getValue($item, 'visible', true);
424 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
425
426 16
            if ($visible && $active !== false && $disabled !== true) {
427 16
                $this->items[$i]['active'] = true;
428 16
                return;
429
            }
430
        }
431 1
    }
432
}
433