Passed
Push — master ( b7d446...14a5dc )
by Alexander
02:33
created

Tabs::renderItems()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 4
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],
24
 *         ['label' => 'Music', 'icon' => 'fas fa-music'],
25
 *         ['label' => 'Videos', 'icon' => 'fas fa-film'],
26
 *         ['label' => 'Documents', 'icon' => 'far fa-file-alt'],
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
70 11
    private function buildOptions(): void
71
    {
72 11
        Html::addCssClass($this->options, 'tabs');
73
74 11
        $this->options['id'] ??= "{$this->getId()}-tabs";
75
76 11
        if ($this->size !== '') {
77 1
            Html::addCssClass($this->options, $this->size);
78
        }
79
80 11
        if ($this->alignment !== '') {
81 1
            Html::addCssClass($this->options, $this->alignment);
82
        }
83
84 11
        if ($this->style !== '') {
85 1
            Html::addCssClass($this->options, $this->style);
86
        }
87 11
    }
88
89
    /**
90
     * @throws \JsonException
91
     *
92
     * @return string
93
     */
94 11
    protected function run(): string
95
    {
96 11
        $this->buildOptions();
97
98 11
        return Html::tag('div', "\n" . $this->renderItems() . "\n", $this->options);
99
    }
100
101
    /**
102
     * @param array $value
103
     *
104
     * @return self
105
     */
106 1
    public function options(array $value): self
107
    {
108 1
        $new = clone $this;
109 1
        $new->options = $value;
110
111 1
        return $new;
112
    }
113
114
    /**
115
     * List of tabs items.
116
     *
117
     * Each tab item should be an array of the following structure:
118
     *
119
     * - `label`: string, required, the nav item label.
120
     * - `url`: string, optional, the item's URL.
121
     * - `visible`: bool, optional, whether this menu item is visible.
122
     * - `linkOptions`: array, optional, the HTML attributes of the item's link.
123
     * - `options`: array, optional, the HTML attributes of the item container (LI).
124
     * - `active`: bool, optional, whether the item should be on active state or not.
125
     * - `encode`: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for only this item.
126
     * - `icon`: string, the tab item icon.
127
     * - `iconOptions`: array, optional, the HTML attributes of the item's icon.
128
     *     - `rightSide`: bool, position the icon to the right.
129
     *
130
     * @param array $value
131
     *
132
     * @return self
133
     */
134 6
    public function items(array $value): self
135
    {
136 6
        $new = clone $this;
137 6
        $new->items = $value;
138
139 6
        return $new;
140
    }
141
142
    /**
143
     * @param bool $value Whether to automatically activate item its route matches the currently requested route.
144
     *
145
     * @return self
146
     */
147 1
    public function activateItems(bool $value): self
148
    {
149 1
        $new = clone $this;
150 1
        $new->activateItems = $value;
151
152 1
        return $new;
153
    }
154
155
    /**
156
     * @param bool $value Whether the labels for menu items should be HTML-encoded.
157
     *
158
     * @return self
159
     */
160 1
    public function encodeLabels(bool $value): self
161
    {
162 1
        $new = clone $this;
163 1
        $new->encodeLabels = $value;
164
165 1
        return $new;
166
    }
167
168
    /**
169
     * @param string|null $value Allows you to assign the current path of the URL from request controller.
170
     *
171
     * @return self
172
     */
173 2
    public function currentPath(?string $value): self
174
    {
175 2
        $new = clone $this;
176 2
        $new->currentPath = $value;
177
178 2
        return $new;
179
    }
180
181
    /**
182
     * @throws \JsonException
183
     *
184
     * @return string
185
     */
186 11
    private function renderItems(): string
187
    {
188 11
        $items = '';
189 11
        foreach ($this->items as $index => $item) {
190 6
            if (isset($item['visible']) && $item['visible'] === false) {
191 1
                continue;
192
            }
193 6
            $items .= "\n" . $this->renderItem($index, $item);
194
        }
195
196 11
        return Html::tag('ul', $items . "\n");
197
    }
198
199
    /**
200
     * @param int $index
201
     * @param array $item
202
     *
203
     * @throws InvalidArgumentException|\JsonException
204
     *
205
     * @return string
206
     */
207 6
    private function renderItem(int $index, array $item): string
208
    {
209 6
        $url = ArrayHelper::getValue($item, 'url', '');
210 6
        $icon = ArrayHelper::getValue($item, 'icon', '');
211 6
        $label = ArrayHelper::getValue($item, 'label', '');
212 6
        $encode = ArrayHelper::getValue($item, 'encode', $this->encodeLabels);
213 6
        $options = ArrayHelper::getValue($item, 'options', []);
214 6
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
215 6
        $iconOptions = ArrayHelper::getValue($item, 'iconOptions', []);
216
217 6
        $options['id'] = ArrayHelper::getValue($item, 'id', $this->options['id'] . '-' . $index);
218
219 6
        if ($label === '') {
220
            throw new InvalidArgumentException("The 'label' option is required.");
221
        }
222
223 6
        if ($encode === true) {
224 5
            $label = Html::encode($label);
225
        }
226
227 6
        if ($icon !== '') {
228 2
            Html::addCssClass($iconOptions, 'icon is-small');
229 2
            $label = $this->renderIcon($label, $icon, $iconOptions);
230
        }
231
232 6
        if ($this->isItemActive($item)) {
233 2
            Html::addCssClass($options, 'is-active');
234
        }
235
236 6
        if ($url !== '') {
237 3
            $linkOptions['href'] = $url;
238
        }
239
240 6
        return Html::tag('li', Html::tag('a', $label, $linkOptions), $options);
241
    }
242
243
    /**
244
     * @param array $item
245
     *
246
     * @return bool
247
     */
248 6
    private function isItemActive(array $item): bool
249
    {
250 6
        if (isset($item['active'])) {
251 1
            return (bool)ArrayHelper::getValue($item, 'active');
252
        }
253
254
        return
255 6
            $this->activateItems
256 6
            && isset($item['url'])
257 6
            && $item['url'] === $this->currentPath;
258
    }
259
260
    /**
261
     * @param string $value Size of the tabs list.
262
     * @see self::SIZE_ALL
263
     *
264
     * @throws InvalidArgumentException
265
     *
266
     * @return self
267
     */
268 2
    public function size(string $value): self
269
    {
270 2
        if (!in_array($value, self::SIZE_ALL, true)) {
271 1
            $values = implode('", "', self::SIZE_ALL);
272 1
            throw new InvalidArgumentException("Invalid size. Valid values are: \"$values\".");
273
        }
274
275 1
        $new = clone $this;
276 1
        $new->size = $value;
277
278 1
        return $new;
279
    }
280
281
    /**
282
     * @param string $value Alignment the tabs list.
283
     *
284
     * @throws InvalidArgumentException
285
     *
286
     * @return self
287
     */
288 2
    public function alignment(string $value): self
289
    {
290 2
        if (!in_array($value, self::ALIGNMENT_ALL, true)) {
291 1
            $values = implode('", "', self::ALIGNMENT_ALL);
292 1
            throw new InvalidArgumentException("Invalid alignment. Valid values are: \"$values\".");
293
        }
294
295 1
        $new = clone $this;
296 1
        $new->alignment = $value;
297
298 1
        return $new;
299
    }
300
301
    /**
302
     * @param string $value Style of the tabs list.
303
     *
304
     * @throws InvalidArgumentException
305
     *
306
     * @return self
307
     */
308 2
    public function style(string $value): self
309
    {
310 2
        if (!in_array($value, self::STYLE_ALL, true)) {
311 1
            $values = implode('", "', self::STYLE_ALL);
312 1
            throw new InvalidArgumentException("Invalid alignment. Valid values are: \"$values\".");
313
        }
314
315 1
        $new = clone $this;
316 1
        $new->style = $value;
317
318 1
        return $new;
319
    }
320
321
    /**
322
     * @param string $label
323
     * @param string $icon
324
     * @param array $iconOptions
325
     *
326
     * @throws \JsonException
327
     *
328
     * @return string
329
     */
330 2
    private function renderIcon(string $label, string $icon, array $iconOptions): string
331
    {
332 2
        $rightSide = ArrayHelper::getValue($iconOptions, 'rightSide', false);
333 2
        unset($iconOptions['rightSide']);
334
335
        $elements = [
336 2
            Html::tag('span', Html::tag('i', '', ['class' => $icon, 'aria-hidden' => 'true']), $iconOptions),
337 2
            Html::tag('span', $label),
338
        ];
339
340 2
        if ($rightSide === true) {
341 1
            $elements = array_reverse($elements);
342
        }
343
344 2
        return implode('', $elements);
345
    }
346
}
347