Passed
Pull Request — master (#28)
by
unknown
07:49
created

Tabs::renderIcon()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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