Test Failed
Pull Request — master (#35)
by Wilmer
03:25 queued 01:10
created

Nav::items()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
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
 *        ->withCurrentPath($currentPath)
55
 *        ->withItems($menuItems)
56
 *        ->withOptions([
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 $encodeTags = false;
112
    private bool $activateItems = true;
113
    private bool $activateParents = false;
114
    private ?string $currentPath = null;
115
    private string $dropdownClass = Dropdown::class;
116
    private array $options = [];
117
118
    public function run(): string
119 19
    {
120
        if (!isset($this->options['id'])) {
121 19
            $this->options['id'] = "{$this->getId()}-nav";
122 9
        }
123
124
        /** @psalm-suppress InvalidArgument */
125
        Html::addCssClass($this->options, ['widget' => 'nav']);
126 19
127
        if ($this->encodeTags === false) {
128 19
            $this->options = array_merge($this->options, ['encode' => false]);
129
        }
130
131
        return $this->renderItems();
132
    }
133
134
    /**
135
     * List of items in the nav widget. Each array element represents a single  menu item which can be either a string
136
     * or an array with the following structure:
137
     *
138 19
     * - label: string, required, the nav item label.
139
     * - url: optional, the item's URL. Defaults to "#".
140 19
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
141
     * - linkOptions: array, optional, the HTML attributes of the item's link.
142 19
     * - options: array, optional, the HTML attributes of the item container (LI).
143 19
     * - active: bool, optional, whether the item should be on active state or not.
144 5
     * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
145
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
146
     *   representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus.
147 19
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
148
     *   only this item.
149
     *
150 19
     * If a menu item is a string, it will be rendered directly without HTML encoding.
151
     *
152
     * @param array $value
153
     *
154
     * @return $this
155
     */
156
    public function withItems(array $value): self
157
    {
158
        $new = clone $this;
159
        $new->items = $value;
160
161
        return $new;
162 19
    }
163
164 19
    /**
165
     * Whether the nav items labels should be HTML-encoded.
166
     *
167
     * @param bool $value
168 19
     *
169
     * @return $this
170
     */
171
    public function withoutEncodeLabels(bool $value = false): self
172 19
    {
173 19
        $new = clone $this;
174 19
        $new->encodeLabels = $value;
175 19
176 19
        return $new;
177 19
    }
178 19
179 19
    /**
180
     * Whether to automatically activate items according to whether their currentPath matches the currently requested.
181 19
     *
182 18
     * @param bool $value
183
     *
184 10
     * @return $this
185
     *
186 10
     * {@see isItemActive}
187 10
     */
188
    public function withActivateItems(bool $value): self
189 10
    {
190 10
        $new = clone $this;
191 10
        $new->activateItems = $value;
192
193
        return $new;
194
    }
195 19
196 19
    /**
197
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
198 19
     *
199 2
     * @param bool $value
200 2
     *
201 2
     * @return $this
202 19
     */
203 12
    public function withActivateParents(bool $value): self
204
    {
205
        $new = clone $this;
206 19
        $new->activateParents = $value;
207
208
        return $new;
209
    }
210
211
    /**
212
     * Allows you to assign the current path of the url from request controller.
213
     *
214
     * @param string|null $value
215
     *
216
     * @return $this
217
     */
218
    public function withCurrentPath(?string $value): self
219
    {
220 10
        $new = clone $this;
221
        $new->currentPath = $value;
222 10
223
        return $new;
224 10
    }
225 10
226 10
    /**
227 10
     * Name of a class to use for rendering dropdowns within this widget. Defaults to {@see Dropdown}.
228 10
     *
229 10
     * @param string $value
230
     *
231
     * @return $this
232
     */
233
    public function withDropdownClass(string $value): self
234
    {
235
        $new = clone $this;
236
        $new->dropdownClass = $value;
237
238
        return $new;
239
    }
240
241
    /**
242 10
     * The HTML attributes for the widget container tag. The following special options are recognized.
243
     *
244 10
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
245 10
     *
246 1
     * @param array $value
247
     *
248
     * @return $this
249 10
     */
250 3
    public function withOptions(array $value): self
251 3
    {
252 1
        $new = clone $this;
253
        $new->options = $value;
254
255
        return $new;
256 10
    }
257 1
258 1
    /**
259
     * Allows you to enable or disable the encoding tags html.
260 1
     *
261 1
     * @param bool $value
262 1
     *
263 1
     * @return self
264
     */
265
    public function withEncodeTags(bool $value = true): self
266
    {
267
        $new = clone $this;
268 10
        $new->encodeTags = $value;
269
270
        return $new;
271
    }
272
273
    /**
274
     * Renders widget items.
275
     *
276
     * @throws JsonException|RuntimeException
277
     *
278
     * @return string
279
     */
280
    private function renderItems(): string
281
    {
282
        $items = [];
283 19
284
        foreach ($this->items as $i => $item) {
285 19
            if (isset($item['visible']) && !$item['visible']) {
286 15
                continue;
287
            }
288
289 18
            $items[] = $this->renderItem($item);
290
        }
291
292
        return Html::tag('ul', implode("\n", $items), $this->options);
293
    }
294
295
    /**
296
     * Renders a widget's item.
297
     *
298
     * @param array|string $item the item to render.
299
     *
300
     * @throws JsonException|RuntimeException
301
     *
302
     * @return string the rendering result.
303
     */
304
    private function renderItem($item): string
305
    {
306
        if (is_string($item)) {
307
            return $item;
308
        }
309
310
        if (!isset($item['label'])) {
311
            throw new RuntimeException("The 'label' option is required.");
312
        }
313
314 19
        $encodeLabel = $item['encode'] ?? $this->encodeLabels;
315
        $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
316 19
        $options = ArrayHelper::getValue($item, 'options', []);
317
        $items = ArrayHelper::getValue($item, 'items');
318 19
        $url = ArrayHelper::getValue($item, 'url', '#');
319
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
320
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
321
        $active = $this->isItemActive($item);
322
323
        if (empty($items)) {
324
            $items = '';
325
        } else {
326
            $linkOptions['data-bs-toggle'] = 'dropdown';
327
328 10
            Html::addCssClass($options, ['widget' => 'dropdown']);
329
            Html::addCssClass($linkOptions, ['widget' => 'dropdown-toggle']);
330 10
331
            if (is_array($items)) {
332 10
                $items = $this->isChildActive($items, $active);
333
                $items = $this->renderDropdown($items, $item);
334
            }
335
        }
336
337
        Html::addCssClass($options, 'nav-item');
338
        Html::addCssClass($linkOptions, 'nav-link');
339
340
        if ($disabled) {
341
            ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
342
            ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
343
            Html::addCssClass($linkOptions, 'disabled');
344
        } elseif ($this->activateItems && $active) {
345
            Html::addCssClass($linkOptions, ['active' => 'active']);
346
        }
347
348
        if ($this->encodeTags === false) {
349
            $linkOptions = array_merge($linkOptions, ['encode' => false]);
350
            $options = array_merge($options, ['encode' => false]);
351 2
        }
352
353 2
        return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options);
354
    }
355 2
356
    /**
357
     * Renders the given items as a dropdown.
358
     *
359
     * This method is called to create sub-menus.
360
     *
361
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
362
     * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
363
     * array.
364
     *
365 1
     * @return string the rendering result.
366
     */
367 1
    private function renderDropdown(array $items, array $parentItem): string
368
    {
369 1
        $dropdownClass = $this->dropdownClass;
370
371
        return $dropdownClass::widget()
372
            ->withoutEncodeLabels($this->encodeLabels)
373
            ->withItems($items)
374
            ->withOptions(ArrayHelper::getValue($parentItem, 'dropdownOptions', []))
375
            ->render();
376
    }
377
378
    /**
379 2
     * Check to see if a child item is active optionally activating the parent.
380
     *
381 2
     * @param array $items
382
     * @param bool $active should the parent be active too
383 2
     *
384
     * @return array
385
     *
386
     * {@see items}
387
     */
388
    private function isChildActive(array $items, bool &$active): array
389
    {
390
        foreach ($items as $i => $child) {
391
            if ($this->isItemActive($child)) {
392
                ArrayHelper::setValue($items[$i], 'active', true);
393
                if ($this->activateParents) {
394
                    $active = true;
395
                }
396
            }
397
398
            if (is_array($child) && ($childItems = ArrayHelper::getValue($child, 'items')) && is_array($childItems)) {
399
                $activeParent = false;
400
                $items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
401
402
                if ($activeParent) {
403
                    $items[$i]['options'] ??= [];
404
                    Html::addCssClass($items[$i]['options'], ['active' => 'active']);
405
                    $active = true;
406
                }
407
            }
408
        }
409
410 10
        return $items;
411
    }
412 10
413
    /**
414 10
     * Checks whether a menu item is active.
415
     *
416
     * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
417
     * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
418
     * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
419
     * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
420
     *
421
     * @param array|string $item the menu item to be checked
422
     *
423
     * @return bool whether the menu item is active
424
     */
425
    private function isItemActive($item): bool
426 11
    {
427
        if (isset($item['active'])) {
428 11
            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

428
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active', false);
Loading history...
429
        }
430 11
431
        return isset($item['url']) && $this->currentPath !== '/' && $item['url'] === $this->currentPath && $this->activateItems;
432
    }
433
}
434