Passed
Pull Request — master (#78)
by
unknown
05:23 queued 02:14
created

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

481
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active', false);
Loading history...
482
        }
483
484 27
        return isset($item['url'])
485 27
            && $this->currentPath !== '/'
486 27
            && $item['url'] === $this->currentPath
487 27
            && $this->activateItems;
488
    }
489
}
490