Passed
Pull Request — master (#31)
by Wilmer
02:00
created

Tabs::withoutEncodeLabels()   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 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 6
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 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
 *     ->withAlignment(Tabs::ALIGNMENT_CENTERED)
21
 *     ->withSize(Tabs::SIZE_LARGE)
22
 *     ->withStyle(Tabs::STYLE_BOX)
23
 *     ->withItems([
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 15
    protected function run(): string
88
    {
89 15
        $this->buildOptions();
90
91 15
        return Html::tag('div', "\n" . $this->renderItems() . "\n", $this->options)
92 14
            . $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 10
    public function items(array $value): self
131
    {
132 10
        $new = clone $this;
133 10
        $new->items = $value;
134
135 10
        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
    /**
257
     * Allows you to enable the encoding tags html.
258
     *
259
     * @return self
260
     */
261 1
    public function withEncodeTags(): self
262
    {
263 1
        $new = clone $this;
264 1
        $new->encodeTags = true;
265
266 1
        return $new;
267
    }
268
269 15
    private function buildOptions(): void
270
    {
271 15
        Html::addCssClass($this->options, 'tabs');
272 15
        Html::addCssClass($this->tabsContentOptions, 'tabs-content');
273
274 15
        $this->options['id'] ??= $this->getId() . '-tabs';
275
276 15
        if ($this->size !== '') {
277 1
            Html::addCssClass($this->options, $this->size);
278
        }
279
280 15
        if ($this->alignment !== '') {
281 1
            Html::addCssClass($this->options, $this->alignment);
282
        }
283
284 15
        if ($this->style !== '') {
285 1
            Html::addCssClass($this->options, $this->style);
286
        }
287
288 15
        if ($this->encodeTags === false) {
289 14
            $this->options['encode'] = false;
290 14
            $this->tabsContentOptions['encode'] = false;
291
        }
292 15
    }
293
294
    /**
295
     * @throws JsonException
296
     *
297
     * @return string
298
     */
299 15
    private function renderItems(): string
300
    {
301 15
        $items = '';
302
303 15
        foreach ($this->items as $index => $item) {
304 10
            if (isset($item['visible']) && $item['visible'] === false) {
305 1
                continue;
306
            }
307
308 10
            $items .= "\n" . $this->renderItem($index, $item);
309
        }
310
311 14
        $itemOptions = [];
312
313 14
        if ($this->encodeTags === false) {
314 13
            $itemOptions['encode'] = false;
315
        }
316
317 14
        return Html::tag('ul', $items . "\n", $itemOptions);
318
    }
319
320
    /**
321
     * @param int $index
322
     * @param array $item
323
     *
324
     *@throws InvalidArgumentException|JsonException
325
     *
326
     * @return string
327
     */
328 10
    private function renderItem(int $index, array $item): string
329
    {
330 10
        $id = $this->getId() . '-tabs-c' . $index;
331 10
        $url = ArrayHelper::getValue($item, 'url', '');
332 10
        $icon = ArrayHelper::getValue($item, 'icon', '');
333 10
        $label = ArrayHelper::getValue($item, 'label', '');
334 10
        $encode = ArrayHelper::getValue($item, 'encode', $this->encodeLabels);
335 10
        $options = ArrayHelper::getValue($item, 'options', []);
336 10
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
337 10
        $iconOptions = ArrayHelper::getValue($item, 'iconOptions', []);
338 10
        $content = ArrayHelper::getValue($item, 'content');
339 10
        $contentOptions = ArrayHelper::getValue($item, 'contentOptions', []);
340 10
        $active = $this->isItemActive($item);
341
342 10
        if ($this->encodeTags === false) {
343 9
            $contentOptions['encode'] = false;
344 9
            $iconOptions['encode'] = false;
345 9
            $linkOptions['encode'] = false;
346 9
            $options['encode'] = false;
347
        }
348
349 10
        if ($label === '') {
350 1
            throw new InvalidArgumentException("The 'label' option is required.");
351
        }
352
353 9
        if ($encode === true) {
354 8
            $label = Html::encode($label);
355
        }
356
357 9
        if ($icon !== '') {
358 2
            Html::addCssClass($iconOptions, 'icon is-small');
359 2
            $label = $this->renderIcon($label, $icon, $iconOptions);
360
        }
361
362 9
        if ($url !== '') {
363 3
            $linkOptions['href'] = $url;
364
        }
365
366 9
        if ($active) {
367 3
            Html::addCssClass($options, 'is-active');
368
        }
369
370 9
        if ($content !== null) {
371 3
            if ($url === '') {
372 3
                $linkOptions['href'] = '#' . $id;
373
            }
374
375 3
            $contentOptions['id'] = ArrayHelper::getValue($contentOptions, 'id', $id);
376
377 3
            $this->tabsContent[] = Html::tag('div', $content, $contentOptions);
378
        }
379
380 9
        return Html::tag('li', Html::tag('a', $label, $linkOptions), $options);
381
    }
382
383
    /**
384
     * @param string $label
385
     * @param string $icon
386
     * @param array $iconOptions
387
     *
388
     * @throws JsonException
389
     *
390
     * @return string
391
     */
392 2
    private function renderIcon(string $label, string $icon, array $iconOptions): string
393
    {
394 2
        $rightSide = ArrayHelper::getValue($iconOptions, 'rightSide', false);
395 2
        unset($iconOptions['rightSide']);
396
397
        $elements = [
398 2
            Html::tag('span', Html::tag('i', '', ['class' => $icon, 'aria-hidden' => 'true']), $iconOptions),
399 2
            Html::tag('span', $label),
400
        ];
401
402 2
        if ($rightSide === true) {
403 1
            $elements = array_reverse($elements);
404
        }
405
406 2
        return implode('', $elements);
407
    }
408
409
    /**
410
     * Renders tabs content.
411
     *
412
     * @throws JsonException
413
     *
414
     * @return string
415
     */
416 14
    private function renderTabsContent(): string
417
    {
418 14
        $html = '';
419
420 14
        if (!empty($this->tabsContent)) {
421 3
            $html .= "\n" . Html::tag('div', "\n" . implode("\n", $this->tabsContent) . "\n", $this->tabsContentOptions);
422
        }
423
424 14
        return $html;
425
    }
426
427
    /**
428
     * @param array $item
429
     *
430
     * @return bool
431
     */
432 10
    private function isItemActive(array $item): bool
433
    {
434 10
        if (isset($item['active'])) {
435 2
            return (bool)ArrayHelper::getValue($item, 'active');
436
        }
437
438
        return
439 10
            $this->activateItems
440 10
            && isset($item['url'])
441 10
            && $item['url'] === $this->currentPath;
442
    }
443
}
444