Passed
Pull Request — master (#60)
by Wilmer
12:52
created

Tabs::itemsAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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