Passed
Pull Request — master (#82)
by Wilmer
02:47
created

Nav::activeClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
crap 2.0185
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use JsonException;
8
use RuntimeException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function is_array;
13
use function is_string;
14
15
/**
16
 * Nav renders a nav HTML component.
17
 *
18
 * For example:
19
 *
20
 * ```php
21
 *    $menuItems = [
22
 *        [
23
 *            'label' => 'About',
24
 *            'url' => '/about',
25
 *        ],
26
 *        [
27
 *            'label' => 'Contact',
28
 *            'url' => '/contact',
29
 *        ],
30
 *        [
31
 *            'label' => 'Login',
32
 *            'url' => '/login',
33
 *            'visible' => $user->getId() === null,
34
 *        ],
35
 *        [
36
 *            'label' => 'Logout' . ' ' . '(' . $user->getUsername() . ')',
37
 *            'url' => '/logout',
38
 *            'visible' => $user->getId() !== null,
39
 *        ],
40
 *    ];
41
 *
42
 *    echo Nav::widget()
43
 *        ->currentPath($currentPath)
44
 *        ->items($menuItems)
45
 *        ->options([
46
 *            'class' => 'navbar-nav float-right ml-auto'
47
 *        ]);
48
 *
49
 * Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 3.
50
 * Note: $currentPath it must be injected from each controller to the main controller.
51
 *
52
 * SiteController.php
53
 *
54
 * ```php
55
 *
56
 *    public function index(ServerRequestInterface $request): ResponseInterface
57
 *    {
58
 *        $response = $this->responseFactory->createResponse();
59
 *        $currentPath = $request->getUri()->getPath();
60
 *        $output = $this->render('index', ['currentPath' => $currentPath]);
61
 *        $response->getBody()->write($output);
62
 *
63
 *        return $response;
64
 *    }
65
 * ```
66
 *
67
 * Controller.php
68
 *
69
 * ```php
70
 *    private function renderContent($content, array $parameters = []): string
71
 *    {
72
 *        $user = $this->user->getIdentity();
73
 *        $layout = $this->findLayoutFile($this->layout);
74
 *
75
 *        if ($layout !== null) {
76
 *            return $this->view->renderFile(
77
 *                $layout,
78
 *                    [
79
 *                        'aliases' => $this->aliases,
80
 *                        'content' => $content,
81
 *                        'user' => $user,
82
 *                        'params' => $this->params,
83
 *                        'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
84
 *                    ],
85
 *                $this
86
 *            );
87
 *        }
88
 *
89
 *        return $content;
90
 *    }
91
 * ```
92
 *
93
 * {@see http://getbootstrap.com/components/#dropdowns}
94
 * {@see http://getbootstrap.com/components/#nav}
95
 */
96
final class Nav extends Widget
97
{
98
    private array $items = [];
99
    private bool $encodeLabels = true;
100
    private bool $encodeTags = false;
101
    private bool $activateItems = true;
102
    private bool $activateParents = false;
103
    private ?string $activeClass = null;
104
    private string $currentPath = '';
105
    private string $dropdownClass = Dropdown::class;
106
    private array $options = [];
107
    private array $itemOptions = [];
108
    private array $linkOptions = [];
109
    private array $dropdownOptions = [];
110
111 36
    protected function run(): string
112
    {
113 36
        if (!isset($this->options['id'])) {
114 19
            $this->options['id'] = "{$this->getId()}-nav";
115
        }
116
117 36
        Html::addCssClass($this->options, ['widget' => 'nav']);
118
119 36
        return $this->renderItems();
120
    }
121
122
    /**
123
     * List of items in the nav widget. Each array element represents a single  menu item which can be either a string
124
     * or an array with the following structure:
125
     *
126
     * - label: string, required, the nav item label.
127
     * - url: optional, the item's URL. Defaults to "#".
128
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
129
     * - linkOptions: array, optional, the HTML attributes of the item's link.
130
     * - options: array, optional, the HTML attributes of the item container (LI).
131
     * - active: bool, optional, whether the item should be on active state or not.
132
     * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
133
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
134
     *   representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus.
135
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
136
     *   only this item.
137
     *
138
     * If a menu item is a string, it will be rendered directly without HTML encoding.
139
     *
140
     * @param array $value
141
     *
142
     * @return self
143
     */
144 36
    public function items(array $value): self
145
    {
146 36
        $new = clone $this;
147 36
        $new->items = $value;
148
149 36
        return $new;
150
    }
151
152
    /**
153
     * When tags Labels HTML should not be encoded.
154
     *
155
     * @return self
156
     */
157 4
    public function withoutEncodeLabels(): self
158
    {
159 4
        $new = clone $this;
160 4
        $new->encodeLabels = false;
161
162 4
        return $new;
163
    }
164
165
    /**
166
     * Disable activate items according to whether their currentPath.
167
     *
168
     * @return self
169
     *
170
     * {@see isItemActive}
171
     */
172 2
    public function withoutActivateItems(): self
173
    {
174 2
        $new = clone $this;
175 2
        $new->activateItems = false;
176
177 2
        return $new;
178
    }
179
180
    /**
181
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
182
     *
183
     * @return self
184
     */
185 1
    public function activateParents(): self
186
    {
187 1
        $new = clone $this;
188 1
        $new->activateParents = true;
189
190 1
        return $new;
191
    }
192
193
    /**
194
     * Additional CSS class for active item. Like "bg-success", "bg-primary" etc
195
     *
196
     * @param string|null $className
197
     *
198
     * @return self
199
     */
200 1
    public function activeClass(?string $className): self
201
    {
202 1
        if ($this->activeClass === $className) {
203
            return $this;
204
        }
205
206 1
        $new = clone $this;
207 1
        $new->activeClass = $className;
208
209 1
        return $new;
210
    }
211
212
    /**
213
     * Allows you to assign the current path of the url from request controller.
214
     *
215
     * @param string $value
216
     *
217
     * @return self
218
     */
219 3
    public function currentPath(string $value): self
220
    {
221 3
        $new = clone $this;
222 3
        $new->currentPath = $value;
223
224 3
        return $new;
225
    }
226
227
    /**
228
     * Name of a class to use for rendering dropdowns within this widget. Defaults to {@see Dropdown}.
229
     *
230
     * @param string $value
231
     *
232
     * @return self
233
     */
234 2
    public function dropdownClass(string $value): self
235
    {
236 2
        $new = clone $this;
237 2
        $new->dropdownClass = $value;
238
239 2
        return $new;
240
    }
241
242
    /**
243
     * Options for dropdownClass if not present in current item
244
     *
245
     * {@see Nav::renderDropdown()} for details on how this options will be used
246
     *
247
     * @param array $options
248
     *
249
     * @return self
250
     */
251 1
    public function dropdownOptions(array $options): self
252
    {
253 1
        $new = clone $this;
254 1
        $new->dropdownOptions = $options;
255
256 1
        return $new;
257
    }
258
259
    /**
260
     * The HTML attributes for the widget container tag. The following special options are recognized.
261
     *
262
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
263
     *
264
     * @param array $value
265
     *
266
     * @return self
267
     */
268 19
    public function options(array $value): self
269
    {
270 19
        $new = clone $this;
271 19
        $new->options = $value;
272
273 19
        return $new;
274
    }
275
276
    /**
277
     * Options for each item if not present in self
278
     *
279
     * @param array $options
280
     *
281
     * @return self
282
     */
283 4
    public function itemOptions(array $options): self
284
    {
285 4
        $new = clone $this;
286 4
        $new->itemOptions = $options;
287
288 4
        return $new;
289
    }
290
291
    /**
292
     * Options for each item link if not present in current item
293
     *
294
     * @param array $options
295
     *
296
     * @return self
297
     */
298 3
    public function linkOptions(array $options): self
299
    {
300 3
        $new = clone $this;
301 3
        $new->linkOptions = $options;
302
303 3
        return $new;
304
    }
305
306
    /**
307
     * Renders widget items.
308
     *
309
     * @throws JsonException|RuntimeException
310
     *
311
     * @return string
312
     */
313 36
    private function renderItems(): string
314
    {
315 36
        $items = [];
316
317 36
        foreach ($this->items as $i => $item) {
318 35
            if (isset($item['visible']) && !$item['visible']) {
319 5
                continue;
320
            }
321
322 35
            $items[] = $this->renderItem($item);
323
        }
324
325 35
        return Html::tag('ul', implode("\n", $items), $this->options)
326 35
            ->encode($this->encodeTags)
327 35
            ->render();
328
    }
329
330
    /**
331
     * Renders a widget's item.
332
     *
333
     * @param array|string $item the item to render.
334
     *
335
     * @throws JsonException|RuntimeException
336
     *
337
     * @return string the rendering result.
338
     */
339 35
    private function renderItem($item): string
340
    {
341 35
        if (is_string($item)) {
342 2
            return $item;
343
        }
344
345 35
        if (!isset($item['label'])) {
346 1
            throw new RuntimeException("The 'label' option is required.");
347
        }
348
349 34
        $encodeLabel = $item['encode'] ?? $this->encodeLabels;
350 34
        $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
351 34
        $options = ArrayHelper::getValue($item, 'options', $this->itemOptions);
352 34
        $items = ArrayHelper::getValue($item, 'items');
353 34
        $url = ArrayHelper::getValue($item, 'url', '#');
354 34
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', $this->linkOptions);
355 34
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
356 34
        $active = $this->isItemActive($item);
357
358 34
        if (empty($items)) {
359 28
            $items = '';
360
        } else {
361 15
            $linkOptions['data-bs-toggle'] = 'dropdown';
362
363 15
            Html::addCssClass($options, ['widget' => 'dropdown']);
364 15
            Html::addCssClass($linkOptions, ['widget' => 'dropdown-toggle']);
365
366 15
            if (is_array($items)) {
367 15
                $items = $this->isChildActive($items, $active);
368 15
                $items = $this->renderDropdown($items, $item);
369
            }
370
        }
371
372 34
        Html::addCssClass($options, ['nav' => 'nav-item']);
373 34
        Html::addCssClass($linkOptions, ['linkOptions' => 'nav-link']);
374
375 34
        if ($disabled) {
376 3
            $linkOptions['tabindex'] = '-1';
377 3
            $linkOptions['aria-disabled'] = 'true';
378 3
            Html::addCssClass($linkOptions, ['disabled' => 'disabled']);
379 34
        } elseif ($this->activateItems && $active) {
380 20
            Html::addCssClass($linkOptions, ['active' => rtrim('active ' . $this->activeClass)]);
381
        }
382
383 34
        return Html::tag(
384
            'li',
385 34
            Html::a($label, $url, $linkOptions)->encode($this->encodeTags) . $items,
386
            $options
387 34
        )->encode($this->encodeTags)->render();
388
    }
389
390
    /**
391
     * Renders the given items as a dropdown.
392
     *
393
     * This method is called to create sub-menus.
394
     *
395
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
396
     * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
397
     * array.
398
     *
399
     * @return string the rendering result.
400
     */
401 15
    private function renderDropdown(array $items, array $parentItem): string
402
    {
403 15
        $dropdownClass = $this->dropdownClass;
404
405 15
        $dropdown = $dropdownClass::widget()
406 15
            ->items($items)
407 15
            ->options(ArrayHelper::getValue($parentItem, 'dropdownOptions', $this->dropdownOptions));
408
409 15
        if ($this->encodeLabels === false) {
410 1
            $dropdown->withoutEncodeLabels();
411
        }
412
413 15
        return $dropdown->render();
414
    }
415
416
    /**
417
     * Check to see if a child item is active optionally activating the parent.
418
     *
419
     * @param array $items
420
     * @param bool $active should the parent be active too
421
     *
422
     * @return array
423
     *
424
     * {@see items}
425
     */
426 15
    private function isChildActive(array $items, bool &$active): array
427
    {
428 15
        foreach ($items as $i => $child) {
429 15
            if ($this->isItemActive($child)) {
430 3
                ArrayHelper::setValue($items[$i], 'active', true);
431 3
                if ($this->activateParents) {
432 1
                    $active = true;
433
                }
434
            }
435
436 15
            if (is_array($child) && ($childItems = ArrayHelper::getValue($child, 'items')) && is_array($childItems)) {
437 1
                $activeParent = false;
438 1
                $items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
439
440 1
                if ($activeParent) {
441 1
                    $items[$i]['linkOptions'] ??= [];
442 1
                    Html::addCssClass($items[$i]['linkOptions'], ['active' => 'active']);
443 1
                    $active = true;
444
                }
445
            }
446
        }
447
448 15
        return $items;
449
    }
450
451
    /**
452
     * Checks whether a menu item is active.
453
     *
454
     * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
455
     * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
456
     * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
457
     * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
458
     *
459
     * @param array|string $item the menu item to be checked
460
     *
461
     * @return bool whether the menu item is active
462
     */
463 34
    private function isItemActive($item): bool
464
    {
465 34
        if (isset($item['active'])) {
466 23
            return ArrayHelper::getValue($item, 'active', false);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type string; however, parameter $array of Yiisoft\Arrays\ArrayHelper::getValue() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

466
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active', false);
Loading history...
467
        }
468
469 27
        return isset($item['url']) && $item['url'] === $this->currentPath && $this->activateItems;
470
    }
471
}
472