Passed
Pull Request — master (#90)
by
unknown
13:09
created

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

472
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active', false);
Loading history...
473
        }
474
475 28
        return isset($item['url']) && $item['url'] === $this->currentPath && $this->activateItems;
476
    }
477
}
478