Passed
Push — master ( 6d0312...290831 )
by Alexander
04:13 queued 01:47
created

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