Test Setup Failed
Pull Request — master (#61)
by Wilmer
02:18
created

Tabs::options()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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