Passed
Push — master ( e80b4e...741461 )
by Alexander
02:19
created

Tabs   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 143
dl 0
loc 388
ccs 126
cts 126
cp 1
rs 9.2
c 2
b 1
f 0
wmc 40

16 Methods

Rating   Name   Duplication   Size   Complexity  
A withoutActivateItems() 0 6 1
A run() 0 6 1
A withoutEncodeLabels() 0 6 1
A alignment() 0 11 2
A options() 0 6 1
A items() 0 6 1
A style() 0 11 2
A tabsContentOptions() 0 6 1
A size() 0 11 2
A currentPath() 0 6 1
A buildOptions() 0 22 5
A renderIcon() 0 15 2
B renderItem() 0 53 9
A renderItems() 0 19 5
A renderTabsContent() 0 9 2
A isItemActive() 0 10 4

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