Tabs::paneOptions()   A
last analyzed

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 0
Metric Value
cc 1
eloc 3
c 0
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
 *             'options' => [...],
32
 *             'paneOptions' => ['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
 * @psalm-suppress MissingConstructor
59
 */
60
final class Tabs extends Widget
61
{
62
    public const NAV_PILLS = 'nav-pills';
63
64
    private array $items = [];
65
    private bool $encodeTags = false;
66
    private string $navType = 'nav-tabs';
67
    private bool $renderTabContent = true;
68
    private array $tabContentOptions = [];
69
    private array $paneOptions = [];
70
    private array $panes = [];
71
    private Nav $nav;
72
    private array $navDefinitions = [];
73
74 18
    public function getId(?string $suffix = '-tabs'): ?string
75
    {
76 18
        return $this->navDefinitions['options']['id'] ?? parent::getId($suffix);
77
    }
78
79 18
    public function render(): string
80
    {
81 18
        Html::addCssClass($this->tabContentOptions, ['tabContentOptions' => 'tab-content']);
82
83 18
        $navDefinitions = $this->prepareNavDefinitions();
84 18
        $this->prepareItems($this->items, $navDefinitions['options']['id']);
85 17
        $this->nav = $this->prepareNav($navDefinitions, $this->items);
86
87 17
        return $this->nav->render() . $this->renderPanes($this->panes);
88
    }
89
90
    /**
91
     * Set all options for nav widget
92
     */
93 8
    public function navDefinitions(array $definitions, bool $replace = true): self
94
    {
95 8
        $new = clone $this;
96
97 8
        if ($replace) {
98 1
            $new->navDefinitions = $definitions;
99
        } else {
100 7
            $new->navDefinitions = array_merge($new->navDefinitions, $definitions);
101
        }
102
103 8
        return $new;
104
    }
105
106
    /**
107
     * Set allowed option for Nav::widget
108
     */
109 7
    public function navDefinition(string $name, mixed $value): self
110
    {
111 7
        return $this->navDefinitions([$name => $value], false);
112
    }
113
114
    /**
115
     * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
116
     */
117 1
    public function dropdownClass(string $value): self
118
    {
119 1
        return $this->navDefinition('dropdownClass', $value);
120
    }
121
122
    /**
123
     * Base options for nav
124
     */
125
    public function dropdownOptions(array $options): self
126
    {
127
        return $this->navDefinition('dropdownOptions', $options);
128
    }
129
130
    /**
131
     * When tags Labels HTML should not be encoded.
132
     */
133 2
    public function withoutEncodeLabels(): self
134
    {
135 2
        return $this->navDefinition('withoutEncodeLabels', false);
136
    }
137
138
    /**
139
     * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
140
     *
141
     * - label: string, required, the tab header label.
142
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override
143
     *   global `$this->encodeLabels` param.
144
     * - headerOptions: array, optional, the HTML attributes of the tab header.
145
     * - content: string, optional, the content (HTML) of the tab pane.
146
     * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
147
     *   the browser to this URL.
148
     * - options: array, optional, the HTML attributes of the tab pane container.
149
     * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
150
     *   'active' explicitly - the first one will be activated.
151
     * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
152
     * - items: array, optional, can be used instead of `content` to specify a dropdown items
153
     *   configuration array. Each item can hold three extra keys, besides the above ones:
154
     *     * active: bool, optional, whether the item tab header and pane should be visible or not.
155
     *     * content: string, required if `items` is not set. The content (HTML) of the tab pane.
156
     *     * contentOptions: optional, array, the HTML attributes of the tab content container.
157
     */
158 17
    public function items(array $value): self
159
    {
160 17
        $new = clone $this;
161 17
        $new->items = $value;
162
163 17
        return $new;
164
    }
165
166
    /**
167
     * List of HTML attributes for the header container tags. This will be overwritten by the "options" set in
168
     * individual {@see items}.
169
     *
170
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
171
     * {@see Nav::itemOptions()}
172
     */
173 2
    public function itemOptions(array $value): self
174
    {
175 2
        return $this->navDefinition('itemOptions', $value);
176
    }
177
178
    /**
179
     * Options for each item link if not present in current item
180
     *
181
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
182
     * {@see Nav::linkOptions()}
183
     */
184 1
    public function linkOptions(array $value): self
185
    {
186 1
        return $this->navDefinition('linkOptions', $value);
187
    }
188
189
    /**
190
     * List of HTML attributes for the item container tags. This will be overwritten by the "options" set in individual
191
     * {@see items}. The following special options are recognized.
192
     *
193
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
194
     */
195 1
    public function paneOptions(array $options): self
196
    {
197 1
        $new = clone $this;
198 1
        $new->paneOptions = $options;
199
200 1
        return $new;
201
    }
202
203
    /**
204
     * Specifies the Bootstrap tab styling.
205
     */
206 4
    public function navType(string $value): self
207
    {
208 4
        $new = clone $this;
209 4
        $new->navType = $value;
210
211 4
        return $new;
212
    }
213
214
    /**
215
     * The HTML attributes for the widget container tag. The following special options are recognized.
216
     *
217
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
218
     */
219 2
    public function options(array $value): self
220
    {
221 2
        return $this->navDefinition('options', $value);
222
    }
223
224
    /**
225
     * Tab panes (contents).
226
     */
227 1
    public function panes(array $value): self
228
    {
229 1
        $new = clone $this;
230 1
        $new->panes = $value;
231
232 1
        return $new;
233
    }
234
235
    /**
236
     * Manually render `tab-content` yourself in case your tab contents are complex.
237
     */
238 1
    public function withoutRenderTabContent(): self
239
    {
240 1
        $new = clone $this;
241 1
        $new->renderTabContent = false;
242
243 1
        return $new;
244
    }
245
246
    /**
247
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
248
     *
249
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
250
     */
251 1
    public function tabContentOptions(array $value): self
252
    {
253 1
        $new = clone $this;
254 1
        $new->tabContentOptions = $value;
255
256 1
        return $new;
257
    }
258
259
    /**
260
     * Renders tab panes.
261
     *
262
     * @throws JsonException
263
     *
264
     * @return string the rendering result.
265
     */
266 17
    private function renderPanes(array $panes): string
267
    {
268 17
        return $this->renderTabContent
269 16
            ? ("\n" . Html::div(implode("\n", $panes), $this->tabContentOptions)->encode($this->encodeTags))
270 17
            : '';
271
    }
272
273
    /**
274
     * Prepare Nav::widget for using
275
     */
276 17
    private function prepareNav(array $definitions, array $items): Nav
277
    {
278 17
        $widgetDefinitions = [];
279
280 17
        foreach ($definitions as $name => $value) {
281 17
            $widgetDefinitions[$name . '()'] = [$value];
282
        }
283
284 17
        return Nav::widget([], $widgetDefinitions)->items($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\ButtonDropdown 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

284
        return Nav::widget([], $widgetDefinitions)->/** @scrutinizer ignore-call */ items($items);
Loading history...
285
    }
286
287
    /**
288
     * Prepare options to send it to Nav::widget
289
     */
290 18
    private function prepareNavDefinitions(): array
291
    {
292 18
        $definitions = $this->navDefinitions;
293 18
        $definitions['options']['id'] = $this->getId();
294
295 18
        if (!isset($definitions['options']['role'])) {
296 18
            $definitions['options']['role'] = 'tablist';
297
        }
298
299
        /** @psalm-suppress InvalidArgument */
300 18
        Html::addCssClass($definitions['options'], ['widget' => 'nav', $this->navType]);
301
302 18
        return $definitions;
303
    }
304
305
    /**
306
     * Renders tab items as specified on {@see items}.
307
     *
308
     * @throws JsonException|RuntimeException
309
     */
310 18
    private function prepareItems(array &$items, ?string $navId, string $prefix = ''): void
311
    {
312 18
        if (!$this->hasActiveTab()) {
313 17
            $this->activateFirstVisibleTab();
314
        }
315
316 18
        foreach ($items as $n => $item) {
317 17
            $options = array_merge($this->paneOptions, ArrayHelper::remove($item, 'paneOptions', []));
318 17
            $options['id'] = ArrayHelper::getValue($options, 'id', $navId . $prefix . '-tab' . $n);
319
320
            /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
321 17
            unset($items[$n]['options']['id']);
322
323 17
            if (!ArrayHelper::remove($item, 'visible', true)) {
324 3
                continue;
325
            }
326
327 17
            if (!array_key_exists('label', $item)) {
328 1
                throw new RuntimeException('The "label" option is required.');
329
            }
330
331 16
            $selected = ArrayHelper::getValue($item, 'active', false);
332 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
333
334 16
            if (isset($item['items'])) {
335 3
                $this->prepareItems($items[$n]['items'], $navId, '-dd' . $n);
336 3
                continue;
337
            }
338
339 16
            if (isset($item['url'])) {
340 3
                continue;
341
            }
342
343 16
            if (!isset($item['linkOptions'])) {
344 16
                $items[$n]['linkOptions'] = $this->navDefinitions['linkOptions'] ?? [];
345
            }
346
347 16
            ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
348 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.bs-toggle', 'tab');
349 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
350 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
351
352 16
            if (!$disabled) {
353 16
                ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
354
            }
355
356
            /** @psalm-suppress InvalidArgument */
357 16
            Html::addCssClass($options, ['widget' => 'tab-pane']);
358
359 16
            if ($selected) {
360 16
                Html::addCssClass($options, ['active' => 'active']);
361
            }
362
363
            /** @psalm-suppress ConflictingReferenceConstraint */
364 16
            if ($this->renderTabContent) {
365 15
                $tag = ArrayHelper::remove($options, 'tag', 'div');
366 15
                $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options)
367 15
                    ->encode($this->encodeTags)
368 15
                    ->render();
369
            }
370
        }
371
    }
372
373
    /**
374
     * @return bool if there's active tab defined.
375
     */
376 18
    private function hasActiveTab(): bool
377
    {
378 18
        foreach ($this->items as $item) {
379 17
            if (isset($item['active']) && $item['active'] === true) {
380 4
                return true;
381
            }
382
        }
383
384 17
        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 17
    private function activateFirstVisibleTab(): void
393
    {
394 17
        foreach ($this->items as $i => $item) {
395 16
            $active = ArrayHelper::getValue($item, 'active', null);
396 16
            $visible = ArrayHelper::getValue($item, 'visible', true);
397 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
398
399 16
            if ($visible && $active !== false && $disabled !== true) {
400 16
                $this->items[$i]['active'] = true;
401 16
                return;
402
            }
403
        }
404
    }
405
}
406