Passed
Pull Request — master (#71)
by
unknown
02:05
created

Tabs::dropdownOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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