Passed
Push — master ( 03413c...d73cb5 )
by Alexander
02:40
created

Tabs::getId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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