Passed
Push — master ( b3c21f...007d67 )
by Alexander
12:51
created

Tabs::linkOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
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
final class Tabs extends Widget
59
{
60
    public const NAV_PILLS = 'nav-pills';
61
62
    private array $items = [];
63
    private bool $encodeTags = false;
64
    private string $navType = 'nav-tabs';
65
    private bool $renderTabContent = true;
66
    private array $tabContentOptions = [];
67
    private array $paneOptions = [];
68
    private array $panes = [];
69
    private Nav $nav;
70
    private array $navDefinitions = [];
71
72 18
    public function getId(?string $suffix = '-tabs'): ?string
73
    {
74 18
        return $this->navDefinitions['options']['id'] ?? parent::getId('-tabs');
75
    }
76
77 18
    protected function beforeRun(): bool
78
    {
79 18
        Html::addCssClass($this->tabContentOptions, ['tabContentOptions' => 'tab-content']);
80
81 18
        $navDefinitions = $this->prepareNavDefinitions();
82 18
        $this->prepareItems($this->items, $navDefinitions['options']['id']);
83 17
        $this->nav = $this->prepareNav($navDefinitions, $this->items);
84
85 17
        return parent::beforeRun();
86
    }
87
88 17
    protected function run(): string
89
    {
90 17
        return $this->nav->render() . $this->renderPanes($this->panes);
91
    }
92
93
    /**
94
     * Set all options for nav widget
95
     *
96
     * @param array $definitions
97
     * @param bool $replace
98
     *
99
     * @return self
100
     */
101 8
    public function navDefinitions(array $definitions, bool $replace = true): self
102
    {
103 8
        $new = clone $this;
104
105 8
        if ($replace) {
106 1
            $new->navDefinitions = $definitions;
107
        } else {
108 7
            $new->navDefinitions = array_merge($new->navDefinitions, $definitions);
109
        }
110
111 8
        return $new;
112
    }
113
114
    /**
115
     * Set allowed option for Nav::widget
116
     *
117
     * @param string $name
118
     * @param mixed $value
119
     *
120
     * @return self
121
     */
122 7
    public function navDefinition(string $name, $value): self
123
    {
124 7
        return $this->navDefinitions([$name => $value], false);
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->navDefinition('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->navDefinition('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->navDefinition('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->navDefinition('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->navDefinition('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
     * Specifies the Bootstrap tab styling.
244
     *
245
     * @param string $value
246
     *
247
     * @return self
248
     */
249 4
    public function navType(string $value): self
250
    {
251 4
        $new = clone $this;
252 4
        $new->navType = $value;
253
254 4
        return $new;
255
    }
256
257
    /**
258
     * The HTML attributes for the widget container tag. The following special options are recognized.
259
     *
260
     * @param array $value
261
     *
262
     * @return self
263
     *
264
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
265
     */
266 2
    public function options(array $value): self
267
    {
268 2
        return $this->navDefinition('options', $value);
269
    }
270
271
    /**
272
     * Tab panes (contents).
273
     *
274
     * @param array $value
275
     *
276
     * @return self
277
     */
278 1
    public function panes(array $value): self
279
    {
280 1
        $new = clone $this;
281 1
        $new->panes = $value;
282
283 1
        return $new;
284
    }
285
286
    /**
287
     * Manually render `tab-content` yourself in case your tab contents are complex.
288
     *
289
     * @return self
290
     */
291 1
    public function withoutRenderTabContent(): self
292
    {
293 1
        $new = clone $this;
294 1
        $new->renderTabContent = false;
295
296 1
        return $new;
297
    }
298
299
    /**
300
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
301
     *
302
     * @param array $value
303
     *
304
     * @return self
305
     *
306
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
307
     */
308 1
    public function tabContentOptions(array $value): self
309
    {
310 1
        $new = clone $this;
311 1
        $new->tabContentOptions = $value;
312
313 1
        return $new;
314
    }
315
316
    /**
317
     * Renders tab panes.
318
     *
319
     * @param array $panes
320
     *
321
     * @throws JsonException
322
     *
323
     * @return string the rendering result.
324
     */
325 17
    private function renderPanes(array $panes): string
326
    {
327 17
        return $this->renderTabContent
328 16
            ? ("\n" . Html::div(implode("\n", $panes), $this->tabContentOptions)->encode($this->encodeTags))
329 17
            : '';
330
    }
331
332
    /**
333
     * Prepare Nav::widget for using
334
     *
335
     * @param array $options
336
     * @param array $items
337
     *
338
     * @return Nav
339
     */
340 17
    private function prepareNav(array $definitions, array $items): Nav
341
    {
342 17
        $widgetDefinitions = [];
343
344 17
        foreach ($definitions as $name => $value) {
345 17
            $widgetDefinitions[$name . '()'] = [$value];
346
        }
347
348 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\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

348
        return Nav::widget($widgetDefinitions)->/** @scrutinizer ignore-call */ items($items);
Loading history...
349
    }
350
351
    /**
352
     * Prepare options to send it to Nav::widget
353
     *
354
     * @return array
355
     */
356 18
    private function prepareNavDefinitions(): array
357
    {
358 18
        $definitions = $this->navDefinitions;
359 18
        $definitions['options']['id'] = $this->getId();
360
361 18
        if (!isset($definitions['options']['role'])) {
362 18
            $definitions['options']['role'] = 'tablist';
363
        }
364
365
        /** @psalm-suppress InvalidArgument */
366 18
        Html::addCssClass($definitions['options'], ['widget' => 'nav', $this->navType]);
367
368 18
        return $definitions;
369
    }
370
371
    /**
372
     * Renders tab items as specified on {@see items}.
373
     *
374
     * @param array $items
375
     * @param string $navId
376
     * @param string $prefix
377
     *
378
     * @throws JsonException|RuntimeException
379
     */
380 18
    private function prepareItems(array &$items, ?string $navId, string $prefix = ''): void
381
    {
382 18
        if (!$this->hasActiveTab()) {
383 17
            $this->activateFirstVisibleTab();
384
        }
385
386 18
        foreach ($items as $n => $item) {
387 17
            $options = array_merge($this->paneOptions, ArrayHelper::remove($item, 'paneOptions', []));
388 17
            $options['id'] = ArrayHelper::getValue($options, 'id', $navId . $prefix . '-tab' . $n);
389
390
            /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
391 17
            unset($items[$n]['options']['id']);
392
393 17
            if (!ArrayHelper::remove($item, 'visible', true)) {
394 3
                continue;
395
            }
396
397 17
            if (!array_key_exists('label', $item)) {
398 1
                throw new RuntimeException('The "label" option is required.');
399
            }
400
401 16
            $selected = ArrayHelper::getValue($item, 'active', false);
402 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
403
404 16
            if (isset($item['items'])) {
405 3
                $this->prepareItems($items[$n]['items'], $navId, '-dd' . $n);
406 3
                continue;
407
            }
408
409 16
            if (isset($item['url'])) {
410 3
                continue;
411
            }
412
413 16
            if (!isset($item['linkOptions'])) {
414 16
                $items[$n]['linkOptions'] = $this->navDefinitions['linkOptions'] ?? [];
415
            }
416
417 16
            ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
418 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.bs-toggle', 'tab');
419 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
420 16
            ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
421
422 16
            if (!$disabled) {
423 16
                ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
424
            }
425
426
            /** @psalm-suppress InvalidArgument */
427 16
            Html::addCssClass($options, ['widget' => 'tab-pane']);
428
429 16
            if ($selected) {
430 16
                Html::addCssClass($options, ['active' => 'active']);
431
            }
432
433
            /** @psalm-suppress ConflictingReferenceConstraint */
434 16
            if ($this->renderTabContent) {
435 15
                $tag = ArrayHelper::remove($options, 'tag', 'div');
436 15
                $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options)->encode($this->encodeTags)->render();
437
            }
438
        }
439 17
    }
440
441
    /**
442
     * @return bool if there's active tab defined.
443
     */
444 18
    private function hasActiveTab(): bool
445
    {
446 18
        foreach ($this->items as $item) {
447 17
            if (isset($item['active']) && $item['active'] === true) {
448 4
                return true;
449
            }
450
        }
451
452 17
        return false;
453
    }
454
455
    /**
456
     * Sets the first visible tab as active.
457
     *
458
     * This method activates the first tab that is visible and not explicitly set to inactive (`'active' => false`).
459
     */
460 17
    private function activateFirstVisibleTab(): void
461
    {
462 17
        foreach ($this->items as $i => $item) {
463 16
            $active = ArrayHelper::getValue($item, 'active', null);
464 16
            $visible = ArrayHelper::getValue($item, 'visible', true);
465 16
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
466
467 16
            if ($visible && $active !== false && $disabled !== true) {
468 16
                $this->items[$i]['active'] = true;
469 16
                return;
470
            }
471
        }
472 1
    }
473
}
474