Passed
Pull Request — master (#70)
by
unknown
02:35 queued 16s
created

Tabs::navOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
crap 2.0185
rs 10
c 0
b 0
f 0
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 $navOptions = [];
71
72 18
    protected function beforeRun(): bool
73
    {
74 18
        Html::addCssClass($this->tabContentOptions, ['tabContentOptions' => 'tab-content']);
75
76 18
        $navOptions = $this->prepareNavOptions();
77 18
        $this->prepareItems($this->items, $navOptions['options']['id']);
78 17
        $this->nav = $this->prepareNav($navOptions, $this->items);
79
80 17
        return parent::beforeRun();
81
    }
82
83 17
    protected function run(): string
84
    {
85 17
        return $this->nav->render() . $this->renderPanes($this->panes);
86
    }
87
88
    /**
89
     * Set all options for nav widget
90
     *
91
     * @param array $options
92
     * @param bool $replace
93
     *
94
     * @return self
95
     */
96 1
    public function navOptions(array $options, bool $replace = true): self
97
    {
98 1
        $new = clone $this;
99
100 1
        if ($replace) {
101 1
            $new->navOptions = $options;
102
        } else {
103
            $new->navOptions = array_merge($new->navOptions, $options);
104
        }
105
106 1
        return $new;
107
    }
108
109
    /**
110
     * Set allowed option for Nav::widget
111
     *
112
     * @param string $name
113
     * @param mixed $value
114
     *
115
     * @return self
116
     */
117 7
    public function navOption(string $name, $value): self
118
    {
119 7
        $new = clone $this;
120 7
        $new->navOptions[$name] = $value;
121
122 7
        return $new;
123
    }
124
125
    /**
126
     * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
127
     *
128
     * @param string $value
129
     *
130
     * @return self
131
     */
132 1
    public function dropdownClass(string $value): self
133
    {
134 1
        return $this->navOption('dropdownClass', $value);
135
    }
136
137
    /**
138
     * Base options for nav
139
     *
140
     * @param array $options
141
     *
142
     * @return self
143
     */
144
    public function dropdownOptions(array $options): self
145
    {
146
        return $this->navOption('dropdownOptions', $options);
147
    }
148
149
    /**
150
     * When tags Labels HTML should not be encoded.
151
     *
152
     * @return self
153
     */
154 2
    public function withoutEncodeLabels(): self
155
    {
156 2
        return $this->navOption('withoutEncodeLabels', false);
157
    }
158
159
    /**
160
     * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
161
     *
162
     * - label: string, required, the tab header label.
163
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override
164
     *   global `$this->encodeLabels` param.
165
     * - headerOptions: array, optional, the HTML attributes of the tab header.
166
     * - content: string, optional, the content (HTML) of the tab pane.
167
     * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
168
     *   the browser to this URL.
169
     * - options: array, optional, the HTML attributes of the tab pane container.
170
     * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
171
     *   'active' explicitly - the first one will be activated.
172
     * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
173
     * - items: array, optional, can be used instead of `content` to specify a dropdown items
174
     *   configuration array. Each item can hold three extra keys, besides the above ones:
175
     *     * active: bool, optional, whether the item tab header and pane should be visible or not.
176
     *     * content: string, required if `items` is not set. The content (HTML) of the tab pane.
177
     *     * contentOptions: optional, array, the HTML attributes of the tab content container.
178
     *
179
     * @param array $value
180
     *
181
     * @return self
182
     */
183 17
    public function items(array $value): self
184
    {
185 17
        $new = clone $this;
186 17
        $new->items = $value;
187
188 17
        return $new;
189
    }
190
191
    /**
192
     * List of HTML attributes for the header container tags. This will be overwritten by the "options" set in
193
     * individual {@see items}.
194
     *
195
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
196
     * {@see Nav::itemOptions()}
197
     *
198
     * @param array $value
199
     *
200
     * @return self
201
     */
202 2
    public function itemOptions(array $value): self
203
    {
204 2
        return $this->navOption('itemOptions', $value);
205
    }
206
207
    /**
208
     * Options for each item link if not present in current item
209
     *
210
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
211
     * {@see Nav::linkOptions()}
212
     *
213
     * @param array $options
214
     *
215
     * @return self
216
     */
217 1
    public function linkOptions(array $value): self
218
    {
219 1
        return $this->navOption('linkOptions', $value);
220
    }
221
222
    /**
223
     * List of HTML attributes for the item container tags. This will be overwritten by the "options" set in individual
224
     * {@see items}. The following special options are recognized.
225
     *
226
     * @param array $value
227
     *
228
     * @return self
229
     *
230
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
231
     */
232 1
    public function paneOptions(array $options): self
233
    {
234 1
        $new = clone $this;
235 1
        $new->paneOptions = $options;
236
237 1
        return $new;
238
    }
239
240
    /**
241
     * Specifies the Bootstrap tab styling.
242
     *
243
     * @param string $value
244
     *
245
     * @return self
246
     */
247 4
    public function navType(string $value): self
248
    {
249 4
        $new = clone $this;
250 4
        $new->navType = $value;
251
252 4
        return $new;
253
    }
254
255
    /**
256
     * The HTML attributes for the widget container tag. The following special options are recognized.
257
     *
258
     * @param array $value
259
     *
260
     * @return self
261
     *
262
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
263
     */
264 2
    public function options(array $value): self
265
    {
266 2
        return $this->navOption('options', $value);
267
    }
268
269
    /**
270
     * Tab panes (contents).
271
     *
272
     * @param array $value
273
     *
274
     * @return self
275
     */
276 1
    public function panes(array $value): self
277
    {
278 1
        $new = clone $this;
279 1
        $new->panes = $value;
280
281 1
        return $new;
282
    }
283
284
    /**
285
     * Manually render `tab-content` yourself in case your tab contents are complex.
286
     *
287
     * @return self
288
     */
289 1
    public function withoutRenderTabContent(): self
290
    {
291 1
        $new = clone $this;
292 1
        $new->renderTabContent = false;
293
294 1
        return $new;
295
    }
296
297
    /**
298
     * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
299
     *
300
     * @param array $value
301
     *
302
     * @return self
303
     *
304
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
305
     */
306 1
    public function tabContentOptions(array $value): self
307
    {
308 1
        $new = clone $this;
309 1
        $new->tabContentOptions = $value;
310
311 1
        return $new;
312
    }
313
314
    /**
315
     * Renders tab panes.
316
     *
317
     * @param array $panes
318
     *
319
     * @throws JsonException
320
     *
321
     * @return string the rendering result.
322
     */
323 17
    private function renderPanes(array $panes): string
324
    {
325 17
        return $this->renderTabContent
326 16
            ? ("\n" . Html::div(implode("\n", $panes), $this->tabContentOptions)->encode($this->encodeTags))
327 17
            : '';
328
    }
329
330
    /**
331
     * Prepare Nav::widget for using
332
     *
333
     * @param array $options
334
     * @param array $items
335
     *
336
     * @return Nav
337
     */
338 17
    private function prepareNav(array $options, array $items): Nav
339
    {
340 17
        $definitions = [];
341
342 17
        foreach ($options as $name => $value) {
343 17
            $definitions[$name . '()'] = [$value];
344
        }
345
346 17
        return Nav::widget($definitions)->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

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