Passed
Pull Request — master (#61)
by Wilmer
03:03 queued 47s
created

Tabs::encode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
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 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
     *
90
     * @param string $value The alignment the tabs list. By default, not class is added and the size is considered
91
     * "is-left". Possible values: Tabs::ALIGNMENT_CENTERED, Tabs::ALIGNMENT_RIGHT.
92
     *
93
     * @throws InvalidArgumentException
94
     *
95
     * @return self
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
     * 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
     */
118 2
    public function attributes(array $values): self
119
    {
120 2
        $new = clone $this;
121 2
        $new->attributes = $values;
122 2
        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
     * @return self
131
     */
132 1
    public function autoIdPrefix(string $value): self
133
    {
134 1
        $new = clone $this;
135 1
        $new->autoIdPrefix = $value;
136 1
        return $new;
137
    }
138
139
    /**
140
     * Returns a new instance with the specified current path.
141
     *
142
     * @param string $value The current path.
143
     *
144
     * @return self
145
     */
146 3
    public function currentPath(string $value): self
147
    {
148 3
        $new = clone $this;
149 3
        $new->currentPath = $value;
150 3
        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 2
    public function deactivateItems(): self
159
    {
160 2
        $new = clone $this;
161 2
        $new->activateItems = false;
162 2
        return $new;
163
    }
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 2
    public function encode(bool $value): self
173
    {
174 2
        $new = clone $this;
175 2
        $new->encode = $value;
176 2
        return $new;
177
    }
178
179
    /**
180
     * Returns a new instance with the specified ID of the widget.
181
     *
182
     * @param string $value The ID of the widget.
183
     *
184
     * @return self
185
     */
186 2
    public function id(string $value): self
187
    {
188 2
        $new = clone $this;
189 2
        $new->attributes['id'] = $value;
190 2
        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
     * - `url`: string, optional, the item's URL.
200
     * - `visible`: bool, optional, whether this menu item is visible.
201
     * - `urlAttributes`: array, optional, the HTML attributes of the item's link.
202
     * - `attributes`: array, optional, the HTML attributes of the item container (LI).
203
     * - `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
     * - `icon`: string, the tab item icon.
207
     * - `iconAttributes`: array, optional, the HTML attributes of the item's icon.
208
     * - `rightSide`: bool, position the icon to the right.
209
     * - `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 11
    public function items(array $value): self
215
    {
216 11
        $new = clone $this;
217 11
        $new->items = $value;
218 11
        return $new;
219
    }
220
221
    /**
222
     * Returns a new instance with the specified attributes of the items.
223
     *
224
     * @param array $value List of HTML attributes for the items.
225
     *
226
     * {@see Html::renderTagAttributes()} For details on how attributes are being rendered.
227
     *
228
     * @return self
229
     */
230 1
    public function itemsAttributes(array $value): self
231
    {
232 1
        $new = clone $this;
233 1
        $new->itemsAttributes = $value;
234 1
        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
     *
245
     * @throws InvalidArgumentException
246
     *
247
     * @return self
248
     */
249 3
    public function size(string $value): self
250
    {
251 3
        if (!in_array($value, self::SIZE_ALL, true)) {
252 1
            $values = implode('", "', self::SIZE_ALL);
253 1
            throw new InvalidArgumentException("Invalid size. Valid values are: \"$values\".");
254
        }
255
256 2
        $new = clone $this;
257 2
        $new->size = $value;
258 2
        return $new;
259
    }
260
261
    /**
262
     * 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
     * "normal". Possible values: Tabs::STYLE_BOX, Tabs::STYLE_TOGGLE, Tabs::STYLE_TOGGLE_ROUNDED,
266
     * Tabs::STYLE_FULLWIDTH.
267
     *
268
     * @throws InvalidArgumentException
269
     *
270
     * @return self
271
     */
272 3
    public function style(string $value): self
273
    {
274 3
        if (!in_array($value, self::STYLE_ALL, true)) {
275 1
            $values = implode('", "', self::STYLE_ALL);
276 1
            throw new InvalidArgumentException("Invalid alignment. Valid values are: \"$values\".");
277
        }
278
279 2
        $new = clone $this;
280 2
        $new->style = $value;
281 2
        return $new;
282
    }
283
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
     * {@see Html::renderTagAttributes()} For details on how attributes are being rendered.
291
     *
292
     * @return self
293
     */
294 2
    public function tabsContentAttributes(array $value): self
295
    {
296 2
        $new = clone $this;
297 2
        $new->tabsContentAttributes = $value;
298 2
        return $new;
299
    }
300
301 15
    protected function run(): string
302
    {
303 15
        $attributes = $this->attributes;
304
305 15
        $id = Html::generateId($this->autoIdPrefix) . '-tabs';
306
307 15
        if (array_key_exists('id', $attributes)) {
308
            /** @var string */
309 1
            $id = $attributes['id'];
310 1
            unset($attributes['id']);
311
        }
312
313 15
        Html::addCssClass($attributes, 'tabs');
314
315 15
        if ($this->size !== '') {
316 1
            Html::addCssClass($attributes, $this->size);
317
        }
318
319 15
        if ($this->alignment !== '') {
320 1
            Html::addCssClass($attributes, $this->alignment);
321
        }
322
323 15
        if ($this->style !== '') {
324 1
            Html::addCssClass($attributes, $this->style);
325
        }
326
327 15
        return Div::tag()
328 15
            ->attributes($attributes)
329 15
            ->content(PHP_EOL . $this->renderItems() . PHP_EOL)
330 14
            ->id($id)
331 14
            ->encode(false)
332 14
            ->render() . $this->renderTabsContent();
333
    }
334
335 15
    private function renderItems(): string
336
    {
337 15
        $items = $this->items;
338 15
        $renderItems = '';
339
340
        /**
341
         * @psalm-var array<
342
         *  int,
343
         *  array{
344
         *    active?: bool,
345
         *    content?: string,
346
         *    contentAttributes?: array,
347
         *    encode?: bool,
348
         *    icon?: string,
349
         *    items?: array,
350
         *    label: string,
351
         *    url: string,
352
         *    visible?: bool
353
         * }> $items
354
         */
355 15
        foreach ($items as $index => $item) {
356 10
            if (isset($item['visible']) && $item['visible'] === false) {
357 2
                continue;
358
            }
359
360 10
            $renderItems .= PHP_EOL . $this->renderItem($index, $item);
361
        }
362
363 14
        return Html::tag('ul', $renderItems . PHP_EOL, $this->itemsAttributes)->encode(false)->render();
364
    }
365
366
    /**
367
     * @param int $index
368
     * @param array $item
369
     *
370
     * @throws InvalidArgumentException
371
     *
372
     * @return string
373
     */
374 10
    private function renderItem(int $index, array $item): string
375
    {
376
        /** @var string */
377 10
        $url = $item['url'] ?? '';
378
379
        /** @var string */
380 10
        $icon = $item['icon'] ?? '';
381
382
        /** @var string */
383 10
        $label = $item['label'] ?? '';
384
385
        /** @var bool */
386 10
        $encode = $item['encode'] ?? $this->encode;
387
388
        /** @var array */
389 10
        $attributes = $item['attributes'] ?? [];
390
391
        /** @var array */
392 10
        $urlAttributes = $item['urlAttributes'] ?? [];
393
394
        /** @var array */
395 10
        $iconAttributes = $item['iconAttributes'] ?? [];
396
397
        /** @var string|null */
398 10
        $content = $item['content'] ?? null;
399
400
        /** @var array */
401 10
        $contentAttributes = $item['contentAttributes'] ?? [];
402 10
        $active = $this->isItemActive($item);
403
404 10
        if ($label === '') {
405 1
            throw new InvalidArgumentException("The 'label' option is required.");
406
        }
407
408 9
        if ($encode === true) {
409 9
            $label = Html::encode($label);
410
        }
411
412 9
        if ($icon !== '') {
413 2
            Html::addCssClass($iconAttributes, 'icon is-small');
414 2
            $label = $this->renderIcon($label, $icon, $iconAttributes);
415
        }
416
417 9
        if ($url !== '') {
418 4
            $urlAttributes['href'] = $url;
419
        }
420
421 9
        if ($active) {
422 4
            Html::addCssClass($attributes, ['active' => 'is-active']);
423
        }
424
425 9
        if ($content !== null) {
426 2
            if ($url === '') {
427 2
                $urlAttributes['href'] = '#' . Html::generateId('l') . '-tabs-c' . $index;
428
            }
429
430
            /** @var string */
431 2
            $contentAttributes['id'] ??= Html::generateId($this->autoIdPrefix) . '-tabs-c' . $index;
432
433 2
            $this->tabsContent[] = Div::tag()
434 2
                ->attributes($contentAttributes)
435 2
                ->content($content)
436 2
                ->encode(false)
437 2
                ->render();
438
        }
439
440 9
        return Html::tag(
441
            'li',
442 9
            A::tag()->attributes($urlAttributes)->content($label)->encode(false)->render(),
443
            $attributes
444 9
        )->encode(false)->render();
445
    }
446
447
    /**
448
     * @param string $label
449
     * @param string $icon
450
     * @param array $iconAttributes
451
     *
452
     * @return string
453
     */
454 2
    private function renderIcon(string $label, string $icon, array $iconAttributes): string
455
    {
456
        /** @var bool */
457 2
        $rightSide = $iconAttributes['rightSide'] ?? false;
458 2
        unset($iconAttributes['rightSide']);
459
460 2
        $elements = [
461 2
            Span::tag()
462 2
                ->attributes($iconAttributes)
463 2
                ->content(I::tag()->attributes(['class' => $icon, 'aria-hidden' => 'true'])->render())
464 2
                ->encode(false)
465 2
                ->render(),
466 2
            Span::tag()->content($label)->render(),
467
        ];
468
469 2
        if ($rightSide === true) {
470 1
            $elements = array_reverse($elements);
471
        }
472
473 2
        return implode('', $elements);
474
    }
475
476
    /**
477
     * Renders tabs content.
478
     *
479
     * @return string
480
     */
481 14
    private function renderTabsContent(): string
482
    {
483 14
        $html = '';
484
        /** @psalm-var string[] */
485 14
        $tabsContent = $this->tabsContent;
486 14
        $tabsContentAttributes = $this->tabsContentAttributes;
487
488 14
        Html::addCssClass($tabsContentAttributes, 'tabs-content');
489
490 14
        if (!empty($this->tabsContent)) {
491 2
            $html .= PHP_EOL . Div::tag()
492 2
                ->attributes($tabsContentAttributes)
493 2
                ->content(PHP_EOL . implode(PHP_EOL, $tabsContent) . PHP_EOL)
494 2
                ->encode(false)
495 2
                ->render();
496
        }
497
498 14
        return $html;
499
    }
500
501
    /**
502
     * @param array $item
503
     *
504
     * @return bool
505
     */
506 10
    private function isItemActive(array $item): bool
507
    {
508 10
        if (isset($item['active'])) {
509 3
            return is_bool($item['active']) && $item['active'];
510
        }
511
512 10
        return $this->activateItems && isset($item['url']) && $item['url'] === $this->currentPath;
513
    }
514
}
515