Tabs   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 41
eloc 175
c 2
b 1
f 0
dl 0
loc 437
ccs 162
cts 162
cp 1
rs 9.1199

18 Methods

Rating   Name   Duplication   Size   Complexity  
A alignment() 0 10 2
A id() 0 5 1
A render() 0 32 5
A renderIcon() 0 24 2
B renderItem() 0 77 8
A renderItems() 0 31 4
A renderTabsContent() 0 18 2
A itemsAttributes() 0 5 1
A attributes() 0 5 1
A items() 0 5 1
A style() 0 10 2
A encode() 0 5 1
A autoIdPrefix() 0 5 1
A isItemActive() 0 7 5
A tabsContentAttributes() 0 5 1
A size() 0 10 2
A deactivateItems() 0 5 1
A currentPath() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Tabs often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Tabs, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bulma;
6
7
use InvalidArgumentException;
8
use Yiisoft\Html\Html;
9
use Yiisoft\Html\Tag\A;
10
use Yiisoft\Html\Tag\Div;
11
use Yiisoft\Html\Tag\I;
12
use Yiisoft\Html\Tag\Span;
13
use Yiisoft\Widget\Widget;
14
15
use function array_key_exists;
16
use function array_reverse;
17
use function implode;
18
use function in_array;
19
use function is_bool;
20
21
/**
22
 * Simple responsive horizontal navigation tabs, with different styles.
23
 *
24
 * ```php
25
 * echo Tabs::widget()
26
 *     ->alignment(Tabs::ALIGNMENT_CENTERED)
27
 *     ->size(Tabs::SIZE_LARGE)
28
 *     ->style(Tabs::STYLE_BOX)
29
 *     ->items([
30
 *         [
31
 *             'label' => 'Pictures',
32
 *             'icon' => 'fas fa-image',
33
 *             'active' => true,
34
 *             'content' => 'Some text about pictures',
35
 *             'contentAttributes' => [
36
 *                 'class' => 'is-active',
37
 *             ],
38
 *         ],
39
 *         ['label' => 'Music', 'icon' => 'fas fa-music', 'content' => 'Some text about music'],
40
 *         ['label' => 'Videos', 'icon' => 'fas fa-film', 'content' => 'Some text about videos'],
41
 *         ['label' => 'Documents', 'icon' => 'far fa-file-alt', 'content' => 'Some text about documents'],
42
 *     ]);
43
 * ```
44
 *
45
 * @link https://bulma.io/documentation/components/tabs/
46
 */
47
final class Tabs extends Widget
48
{
49
    public const ALIGNMENT_CENTERED = 'is-centered';
50
    public const ALIGNMENT_RIGHT = 'is-right';
51
    private const ALIGNMENT_ALL = [
52
        self::ALIGNMENT_CENTERED,
53
        self::ALIGNMENT_RIGHT,
54
    ];
55
56
    public const SIZE_SMALL = 'is-small';
57
    public const SIZE_MEDIUM = 'is-medium';
58
    public const SIZE_LARGE = 'is-large';
59
    private const SIZE_ALL = [
60
        self::SIZE_SMALL,
61
        self::SIZE_MEDIUM,
62
        self::SIZE_LARGE,
63
    ];
64
65
    public const STYLE_BOX = 'is-boxed';
66
    public const STYLE_TOGGLE = 'is-toggle';
67
    public const STYLE_TOGGLE_ROUNDED = 'is-toggle is-toggle-rounded';
68
    public const STYLE_FULLWIDTH = 'is-fullwidth';
69
    private const STYLE_ALL = [
70
        self::STYLE_BOX,
71
        self::STYLE_TOGGLE,
72
        self::STYLE_TOGGLE_ROUNDED,
73
        self::STYLE_FULLWIDTH,
74
    ];
75
76
    private bool $activateItems = true;
77
    private string $alignment = '';
78
    private array $attributes = [];
79
    private string $autoIdPrefix = 'w';
80
    private ?string $currentPath = null;
81
    private bool $encode = true;
82
    private array $items = [];
83
    private array $itemsAttributes = [];
84
    private string $size = '';
85
    private string $style = '';
86
    private array $tabsContent = [];
87
    private array $tabsContentAttributes = [];
88
89
    /**
90
     * Returns a new instance with the specified alignment the tabs list.
91
     *
92
     * @param string $value The alignment the tabs list. By default, not class is added and the size is considered
93
     * "is-left". Possible values: Tabs::ALIGNMENT_CENTERED, Tabs::ALIGNMENT_RIGHT.
94
     *
95
     * @throws InvalidArgumentException
96
     */
97 3
    public function alignment(string $value): self
98
    {
99 3
        if (!in_array($value, self::ALIGNMENT_ALL, true)) {
100 1
            $values = implode('", "', self::ALIGNMENT_ALL);
101 1
            throw new InvalidArgumentException("Invalid alignment. Valid values are: \"$values\".");
102
        }
103
104 2
        $new = clone $this;
105 2
        $new->alignment = $value;
106 2
        return $new;
107
    }
108
109
    /**
110
     * Returns a new instance with the specified HTML attributes for widget.
111
     *
112
     * @param array $values Attribute values indexed by attribute names.
113
     *
114
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} For details on how attributes are being rendered.
115
     */
116 2
    public function attributes(array $values): self
117
    {
118 2
        $new = clone $this;
119 2
        $new->attributes = $values;
120 2
        return $new;
121
    }
122
123
    /**
124
     * Returns a new instance with the specified prefix to the automatically generated widget IDs.
125
     *
126
     * @param string $value The prefix to the automatically generated widget IDs.
127
     */
128 1
    public function autoIdPrefix(string $value): self
129
    {
130 1
        $new = clone $this;
131 1
        $new->autoIdPrefix = $value;
132 1
        return $new;
133
    }
134
135
    /**
136
     * Returns a new instance with the specified current path.
137
     *
138
     * @param string $value The current path.
139
     */
140 3
    public function currentPath(string $value): self
141
    {
142 3
        $new = clone $this;
143 3
        $new->currentPath = $value;
144 3
        return $new;
145
    }
146
147
    /**
148
     * Returns a new instance with the specified disables active items according to their current path.
149
     */
150 2
    public function deactivateItems(): self
151
    {
152 2
        $new = clone $this;
153 2
        $new->activateItems = false;
154 2
        return $new;
155
    }
156
157
    /**
158
     * Returns a new instance with the specified whether the tags for the tabs are encoded.
159
     *
160
     * @param bool $value Whether to encode the output.
161
     */
162 2
    public function encode(bool $value): self
163
    {
164 2
        $new = clone $this;
165 2
        $new->encode = $value;
166 2
        return $new;
167
    }
168
169
    /**
170
     * Returns a new instance with the specified ID of the widget.
171
     *
172
     * @param string $value The ID of the widget.
173
     */
174 2
    public function id(string $value): self
175
    {
176 2
        $new = clone $this;
177 2
        $new->attributes['id'] = $value;
178 2
        return $new;
179
    }
180
181
    /**
182
     * Returns a new instance with the specified items.
183
     *
184
     * @param array $value List of tabs items. Each tab item should be an array of the following structure:
185
     *
186
     * - `label`: string, required, the nav item label.
187
     * - `url`: string, optional, the item's URL.
188
     * - `visible`: bool, optional, whether this menu item is visible.
189
     * - `urlAttributes`: array, optional, the HTML attributes of the item's link.
190
     * - `attributes`: array, optional, the HTML attributes of the item container (LI).
191
     * - `active`: bool, optional, whether the item should be on active state or not.
192
     * - `encode`: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encode option
193
     *    for only this item.
194
     * - `icon`: string, the tab item icon.
195
     * - `iconAttributes`: array, optional, the HTML attributes of the item's icon.
196
     * - `rightSide`: bool, position the icon to the right.
197
     * - `content`: string, required if `items` is not set. The content (HTML) of the tab.
198
     * - `contentAttributes`: array, array, the HTML attributes of the tab content container.
199
     */
200 11
    public function items(array $value): self
201
    {
202 11
        $new = clone $this;
203 11
        $new->items = $value;
204 11
        return $new;
205
    }
206
207
    /**
208
     * Returns a new instance with the specified attributes of the items.
209
     *
210
     * @param array $value List of HTML attributes for the items.
211
     *
212
     * {@see Html::renderTagAttributes()} For details on how attributes are being rendered.
213
     */
214 1
    public function itemsAttributes(array $value): self
215
    {
216 1
        $new = clone $this;
217 1
        $new->itemsAttributes = $value;
218 1
        return $new;
219
    }
220
221
    /**
222
     * Returns a new instance with the specified size of the tabs list.
223
     *
224
     * @param string $value size class. By default, not class is added and the size is considered "normal".
225
     * Possible values: Tabs::SIZE_SMALL, Tabs::SIZE_MEDIUM, Tabs::SIZE_LARGE.
226
     *
227
     * {@see self::SIZE_ALL}
228
     *
229
     * @throws InvalidArgumentException
230
     */
231 3
    public function size(string $value): self
232
    {
233 3
        if (!in_array($value, self::SIZE_ALL, true)) {
234 1
            $values = implode('", "', self::SIZE_ALL);
235 1
            throw new InvalidArgumentException("Invalid size. Valid values are: \"$values\".");
236
        }
237
238 2
        $new = clone $this;
239 2
        $new->size = $value;
240 2
        return $new;
241
    }
242
243
    /**
244
     * Returns a new instance with the specified style of the tabs list.
245
     *
246
     * @param string $value The style of the tabs list. By default, not class is added and the size is considered
247
     * "normal". Possible values: Tabs::STYLE_BOX, Tabs::STYLE_TOGGLE, Tabs::STYLE_TOGGLE_ROUNDED,
248
     * Tabs::STYLE_FULLWIDTH.
249
     *
250
     * @throws InvalidArgumentException
251
     */
252 3
    public function style(string $value): self
253
    {
254 3
        if (!in_array($value, self::STYLE_ALL, true)) {
255 1
            $values = implode('", "', self::STYLE_ALL);
256 1
            throw new InvalidArgumentException("Invalid alignment. Valid values are: \"$values\".");
257
        }
258
259 2
        $new = clone $this;
260 2
        $new->style = $value;
261 2
        return $new;
262
    }
263
264
    /**
265
     * Returns a new instance with the specified attributes of the tabs content.
266
     *
267
     * @param array $value List of HTML attributes for the `tabs-content` container. This will always contain the CSS
268
     * class `tabs-content`.
269
     *
270
     * {@see Html::renderTagAttributes()} For details on how attributes are being rendered.
271
     */
272 2
    public function tabsContentAttributes(array $value): self
273
    {
274 2
        $new = clone $this;
275 2
        $new->tabsContentAttributes = $value;
276 2
        return $new;
277
    }
278
279 15
    public function render(): string
280
    {
281 15
        $attributes = $this->attributes;
282
283 15
        $id = Html::generateId($this->autoIdPrefix) . '-tabs';
284
285 15
        if (array_key_exists('id', $attributes)) {
286
            /** @var string */
287 1
            $id = $attributes['id'];
288 1
            unset($attributes['id']);
289
        }
290
291 15
        Html::addCssClass($attributes, 'tabs');
292
293 15
        if ($this->size !== '') {
294 1
            Html::addCssClass($attributes, $this->size);
295
        }
296
297 15
        if ($this->alignment !== '') {
298 1
            Html::addCssClass($attributes, $this->alignment);
299
        }
300
301 15
        if ($this->style !== '') {
302 1
            Html::addCssClass($attributes, $this->style);
303
        }
304
305 15
        return Div::tag()
306 15
            ->attributes($attributes)
307 15
            ->content(PHP_EOL . $this->renderItems() . PHP_EOL)
308 15
            ->id($id)
309 15
            ->encode(false)
310 15
            ->render() . $this->renderTabsContent();
311
    }
312
313 15
    private function renderItems(): string
314
    {
315 15
        $items = $this->items;
316 15
        $renderItems = '';
317
318
        /**
319
         * @psalm-var array<
320
         *  int,
321
         *  array{
322
         *    active?: bool,
323
         *    content?: string,
324
         *    contentAttributes?: array,
325
         *    encode?: bool,
326
         *    icon?: string,
327
         *    items?: array,
328
         *    label: string,
329
         *    url: string,
330
         *    visible?: bool
331
         * }> $items
332
         */
333 15
        foreach ($items as $index => $item) {
334 10
            if (isset($item['visible']) && $item['visible'] === false) {
335 2
                continue;
336
            }
337
338 10
            $renderItems .= PHP_EOL . $this->renderItem($index, $item);
339
        }
340
341 14
        return Html::tag('ul', $renderItems . PHP_EOL, $this->itemsAttributes)
342 14
            ->encode(false)
343 14
            ->render();
344
    }
345
346
    /**
347
     * @throws InvalidArgumentException
348
     */
349 10
    private function renderItem(int $index, array $item): string
350
    {
351
        /** @var string */
352 10
        $url = $item['url'] ?? '';
353
354
        /** @var string */
355 10
        $icon = $item['icon'] ?? '';
356
357
        /** @var string */
358 10
        $label = $item['label'] ?? '';
359
360
        /** @var bool */
361 10
        $encode = $item['encode'] ?? $this->encode;
362
363
        /** @var array */
364 10
        $attributes = $item['attributes'] ?? [];
365
366
        /** @var array */
367 10
        $urlAttributes = $item['urlAttributes'] ?? [];
368
369
        /** @var array */
370 10
        $iconAttributes = $item['iconAttributes'] ?? [];
371
372
        /** @var string|null */
373 10
        $content = $item['content'] ?? null;
374
375
        /** @var array */
376 10
        $contentAttributes = $item['contentAttributes'] ?? [];
377 10
        $active = $this->isItemActive($item);
378
379 10
        if ($label === '') {
380 1
            throw new InvalidArgumentException("The 'label' option is required.");
381
        }
382
383 9
        if ($encode === true) {
384 9
            $label = Html::encode($label);
385
        }
386
387 9
        if ($icon !== '') {
388 2
            Html::addCssClass($iconAttributes, 'icon is-small');
389 2
            $label = $this->renderIcon($label, $icon, $iconAttributes);
390
        }
391
392 9
        if ($url !== '') {
393 4
            $urlAttributes['href'] = $url;
394
        }
395
396 9
        if ($active) {
397 4
            Html::addCssClass($attributes, ['active' => 'is-active']);
398
        }
399
400 9
        if ($content !== null) {
401 2
            if ($url === '') {
402 2
                $urlAttributes['href'] = '#' . Html::generateId('l') . '-tabs-c' . $index;
403
            }
404
405
            /** @var string */
406 2
            $contentAttributes['id'] ??= Html::generateId($this->autoIdPrefix) . '-tabs-c' . $index;
407
408 2
            $this->tabsContent[] = Div::tag()
409 2
                ->attributes($contentAttributes)
410 2
                ->content($content)
411 2
                ->encode(false)
412 2
                ->render();
413
        }
414
415 9
        return Html::tag(
416 9
            'li',
417 9
            A::tag()
418 9
                ->attributes($urlAttributes)
419 9
                ->content($label)
420 9
                ->encode(false)
421 9
                ->render(),
422 9
            $attributes
423 9
        )
424 9
            ->encode(false)
425 9
            ->render();
426
    }
427
428 2
    private function renderIcon(string $label, string $icon, array $iconAttributes): string
429
    {
430
        /** @var bool */
431 2
        $rightSide = $iconAttributes['rightSide'] ?? false;
432 2
        unset($iconAttributes['rightSide']);
433
434 2
        $elements = [
435 2
            Span::tag()
436 2
                ->attributes($iconAttributes)
437 2
                ->content(I::tag()
438 2
                    ->attributes(['class' => $icon, 'aria-hidden' => 'true'])
439 2
                    ->render())
440 2
                ->encode(false)
441 2
                ->render(),
442 2
            Span::tag()
443 2
                ->content($label)
444 2
                ->render(),
445 2
        ];
446
447 2
        if ($rightSide === true) {
448 1
            $elements = array_reverse($elements);
449
        }
450
451 2
        return implode('', $elements);
452
    }
453
454
    /**
455
     * Renders tabs content.
456
     */
457 14
    private function renderTabsContent(): string
458
    {
459 14
        $html = '';
460
        /** @psalm-var string[] */
461 14
        $tabsContent = $this->tabsContent;
462 14
        $tabsContentAttributes = $this->tabsContentAttributes;
463
464 14
        Html::addCssClass($tabsContentAttributes, 'tabs-content');
465
466 14
        if (!empty($this->tabsContent)) {
467 2
            $html .= PHP_EOL . Div::tag()
468 2
                ->attributes($tabsContentAttributes)
469 2
                ->content(PHP_EOL . implode(PHP_EOL, $tabsContent) . PHP_EOL)
470 2
                ->encode(false)
471 2
                ->render();
472
        }
473
474 14
        return $html;
475
    }
476
477 10
    private function isItemActive(array $item): bool
478
    {
479 10
        if (isset($item['active'])) {
480 3
            return is_bool($item['active']) && $item['active'];
481
        }
482
483 10
        return $this->activateItems && isset($item['url']) && $item['url'] === $this->currentPath;
484
    }
485
}
486