Passed
Push — master ( c03bea...c0a5b3 )
by Rustam
01:44
created

Menu::activeCssClass()   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\Widgets;
6
7
use Closure;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Html\Html;
10
use Yiisoft\Widget\Widget;
11
12
/**
13
 * Menu displays a multi-level menu using nested HTML lists.
14
 *
15
 * The main property of Menu is {@see items}, which specifies the possible items in the menu. A menu item can contain
16
 * sub-items which specify the sub-menu under that menu item.
17
 *
18
 * Menu checks the current route and request parameters to toggle certain menu items with active state.
19
 *
20
 * Note that Menu only renders the HTML tags about the menu. It does not do any styling. You are responsible to provide
21
 * CSS styles to make it look like a real menu.
22
 *
23
 * The following example shows how to use Menu:
24
 *
25
 * ```php
26
 * echo Menu::Widget()
27
 *     ->items([
28
 *         ['label' => 'Login', 'url' => 'site/login', 'visible' => true],
29
 *     ]);
30
 * ```
31
 */
32
class Menu extends Widget
33
{
34
    /**
35
     * @var array list of menu items. Each menu item should be an array of the following structure:
36
     *
37
     * - label: string, optional, specifies the menu item label. When {@see encodeLabels} is true, the label will be
38
     *   HTML-encoded. If the label is not specified, an empty string will be used.
39
     * - encode: bool, optional, whether this item`s label should be HTML-encoded. This param will override global
40
     *   {@see encodeLabels} param.
41
     * - url: string or array, optional, specifies the URL of the menu item. When this is set, the actual menu item
42
     *   content will be generated using {@see linkTemplate}; otherwise, {@see labelTemplate} will be used.
43
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
44
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
45
     * - active: bool or Closure, optional, whether this menu item is in active state (currently selected). When
46
     *   using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $Widget)`. Closure
47
     *   must return `true` if item should be marked as `active`, otherwise - `false`. If a menu item is active, its CSS
48
     *   class will be appended with {@see activeCssClass}. If this option is not set, the menu item will be set active
49
     *   automatically when the current request is triggered by `url`. For more details, please refer to
50
     *   {@see isItemActive()}.
51
     * - template: string, optional, the template used to render the content of this menu item. The token `{url}` will
52
     *   be replaced by the URL associated with this menu item, and the token `{label}` will be replaced by the label
53
     *   of the menu item. If this option is not set, {@see linkTemplate} or {@see labelTemplate} will be used instead.
54
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus. The token `{items}` will
55
     *   be replaced with the rendered sub-menu items. If this option is not set, [[submenuTemplate]] will be used
56
     *   instead.
57
     * - options: array, optional, the HTML attributes for the menu container tag.
58
     */
59
    private array $items = [];
60
61
    /**
62
     * @var array list of HTML attributes shared by all menu {@see items}. If any individual menu item specifies its
63
     * `options`, it will be merged with this property before being used to generate the HTML attributes for the menu
64
     * item tag. The following special options are recognized:
65
     *
66
     * - tag: string, defaults to "li", the tag name of the item container tags. Set to false to disable container tag.
67
     *   See also {@see \Yiisoft\Html\Html::tag()}
68
     *
69
     * {@see \Yiisoft\Html\Html::renderTagAttributes() for details on how attributes are being rendered}
70
     */
71
    private array $itemOptions = [];
72
73
    /**
74
     * @var string the template used to render the body of a menu which is a link. In this template, the token `{url}`
75
     * will be replaced with the corresponding link URL; while `{label}` will be replaced with the link text.
76
     *
77
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
78
     */
79
    private string $linkTemplate = '<a href="{url}">{label}</a>';
80
81
    /**
82
     * @var string the template used to render the body of a menu which is NOT a link.
83
     *
84
     * In this template, the token `{label}` will be replaced with the label of the menu item.
85
     *
86
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
87
     */
88
    private string $labelTemplate = '{label}';
89
90
    /**
91
     * @var string the template used to render a list of sub-menus.
92
     *
93
     * In this template, the token `{items}` will be replaced with the rendered sub-menu items.
94
     */
95
    private string $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
96
97
    /**
98
     * @var bool whether the labels for menu items should be HTML-encoded.
99
     */
100
    private bool $encodeLabels = true;
101
102
    /**
103
     * @var string the CSS class to be appended to the active menu item.
104
     */
105
    private string $activeCssClass = 'active';
106
107
    /**
108
     * @var bool whether to automatically activate items according to whether their route setting matches the currently
109
     * requested route.
110
     *
111
     * {@see isItemActive()}
112
     */
113
    private bool $activateItems = true;
114
115
    /**
116
     * @var bool whether to activate parent menu items when one of the corresponding child menu items is active. The
117
     * activated parent menu items will also have its CSS classes appended with {@see activeCssClass}.
118
     */
119
    private bool $activateParents = false;
120
121
    /**
122
     * @var string|null $currentPath Allows you to assign the current path of the url from request controller.
123
     */
124
    private ?string $currentPath = null;
125
126
    /**
127
     * @var bool whether to hide empty menu items. An empty menu item is one whose `url` option is not set and which has
128
     * no visible child menu items.
129
     */
130
    private bool $hideEmptyItems = true;
131
132
    /**
133
     * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
134
     *
135
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
136
     *   See also {@see \Yiisoft\Html\Html::tag()}.
137
     *
138
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
139
     */
140
    private array $options = [];
141
142
    /**
143
     * @var string|null the CSS class that will be assigned to the first item in the main menu or each submenu. Defaults to
144
     * null, meaning no such CSS class will be assigned.
145
     */
146
    private ?string $firstItemCssClass = null;
147
148
    /**
149
     * @var string|null the CSS class that will be assigned to the last item in the main menu or each submenu. Defaults to
150
     * null, meaning no such CSS class will be assigned.
151
     */
152
    private ?string $lastItemCssClass = null;
153
154
    /**
155
     * Renders the menu.
156
     *
157
     * @return string the result of Widget execution to be outputted.
158
     */
159 7
    public function run(): string
160
    {
161 7
        $items = $this->normalizeItems($this->items, $hasActiveChild);
162
163 7
        if (empty($items)) {
164
            return '';
165
        }
166
167 7
        $options = $this->options;
168 7
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
169
170 7
        return Html::tag($tag, $this->renderItems($items), $options);
171
    }
172
173
    /**
174
     * {@see $activateItems}
175
     *
176
     * @param bool $value
177
     *
178
     * @return Menu
179
     */
180
    public function activateItems(bool $value): Menu
181
    {
182
        $this->activateItems = $value;
183
184
        return $this;
185
    }
186
187
    /**
188
     * {@see $activateParents}
189
     *
190
     * @param bool $value
191
     *
192
     * @return Menu
193
     */
194
    public function activateParents(bool $value): Menu
195
    {
196
        $this->activateParents = $value;
197
198
        return $this;
199
    }
200
201
    /**
202
     * {@see $activeCssClass}
203
     *
204
     * @param string $value
205
     *
206
     * @return Menu
207
     */
208 2
    public function activeCssClass(string $value): Menu
209
    {
210 2
        $this->activeCssClass = $value;
211
212 2
        return $this;
213
    }
214
215
    /**
216
     * {@see $currentPath}
217
     *
218
     * @param string $value
219
     *
220
     * @return Menu
221
     */
222
    public function currentPath(string $value): Menu
223
    {
224
        $this->currentPath = $value;
225
226
        return $this;
227
    }
228
229
    /**
230
     * {@see $encodeLabels}
231
     *
232
     * @param bool $value
233
     *
234
     * @return Menu
235
     */
236 5
    public function encodeLabels(bool $value): Menu
237
    {
238 5
        $this->encodeLabels = $value;
239
240 5
        return $this;
241
    }
242
243
    /**
244
     * {@see $firstItemCssClass}
245
     *
246
     * @param string $value
247
     *
248
     * @return Menu
249
     */
250
    public function firstItemCssClass(string $value): Menu
251
    {
252
        $this->firstItemCssClass = $value;
253
254
        return $this;
255
    }
256
257
    /**
258
     * {@see $hideEmptyItems}
259
     *
260
     * @param bool $value
261
     *
262
     * @return Menu
263
     */
264
    public function hideEmptyItems(bool $value): Menu
265
    {
266
        $this->hideEmptyItems = $value;
267
268
        return $this;
269
    }
270
271
    /**
272
     * {@see $items}
273
     *
274
     * @param array $value
275
     *
276
     * @return Menu
277
     */
278 7
    public function items(array $value): Menu
279
    {
280 7
        $this->items = $value;
281
282 7
        return $this;
283
    }
284
285
    /**
286
     * {@see $itemOptions}
287
     *
288
     * @param array $value
289
     *
290
     * @return Menu
291
     */
292 1
    public function itemOptions(array $value): Menu
293
    {
294 1
        $this->itemOptions = $value;
295
296 1
        return $this;
297
    }
298
299
    /**
300
     * {@see $labelTemplate}
301
     *
302
     * @param string $value
303
     *
304
     * @return Menu
305
     */
306 2
    public function labelTemplate(string $value): Menu
307
    {
308 2
        $this->labelTemplate = $value;
309
310 2
        return $this;
311
    }
312
313
    /**
314
     * {@see $lastItemCssClass}
315
     *
316
     * @param string $value
317
     *
318
     * @return Menu
319
     */
320
    public function lastItemCssClass(string $value): Menu
321
    {
322
        $this->lastItemCssClass = $value;
323
324
        return $this;
325
    }
326
327
    /**
328
     * {@see $linkTemplate}
329
     *
330
     * @param string $value
331
     *
332
     * @return Menu
333
     */
334 2
    public function linkTemplate(string $value): Menu
335
    {
336 2
        $this->linkTemplate = $value;
337
338 2
        return $this;
339
    }
340
341
    /**
342
     * {@see $options}
343
     *
344
     * @param array $value
345
     *
346
     * @return Menu
347
     */
348 1
    public function options(array $value): Menu
349
    {
350 1
        $this->options = $value;
351
352 1
        return $this;
353
    }
354
355
    /**
356
     * Recursively renders the menu items (without the container tag).
357
     *
358
     * @param array $items the menu items to be rendered recursively
359
     *
360
     * @return string the rendering result
361
     */
362 7
    protected function renderItems(array $items): string
363
    {
364 7
        $n = count($items);
365 7
        $lines = [];
366
367 7
        foreach ($items as $i => $item) {
368 7
            $options = \array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
369 7
            $tag = ArrayHelper::remove($options, 'tag', 'li');
370 7
            $class = [];
371
372 7
            if ($item['active']) {
373 3
                $class[] = $this->activeCssClass;
374
            }
375
376 7
            if ($i === 0 && $this->firstItemCssClass !== null) {
377
                $class[] = $this->firstItemCssClass;
378
            }
379
380 7
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
381
                $class[] = $this->lastItemCssClass;
382
            }
383
384 7
            Html::addCssClass($options, $class);
385
386 7
            $menu = $this->renderItem($item);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type object; however, parameter $item of Yiisoft\Yii\Widgets\Menu::renderItem() does only seem to accept array, 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

386
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item);
Loading history...
387
388 7
            if (!empty($item['items'])) {
389
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
390
                $menu .= strtr($submenuTemplate, [
391
                    '{items}' => $this->renderItems($item['items']),
392
                ]);
393
            }
394
395 7
            $lines[] = Html::tag($tag, $menu, $options);
396
        }
397
398 7
        return implode("\n", $lines);
399
    }
400
401
    /**
402
     * Renders the content of a menu item.
403
     *
404
     * Note that the container and the sub-menus are not rendered here.
405
     *
406
     * @param array $item the menu item to be rendered. Please refer to {@see items} to see what data might be in the
407
     * item.
408
     *
409
     * @return string the rendering result
410
     */
411 7
    protected function renderItem(array $item): string
412
    {
413 7
        if (isset($item['url'])) {
414 7
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
415
416 7
            return strtr($template, [
417 7
                '{url}'   => Html::encode($item['url']),
418 7
                '{label}' => $item['label'],
419
            ]);
420
        }
421
422 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
423
424 2
        return strtr($template, [
425 2
            '{label}' => $item['label'],
426
        ]);
427
    }
428
429
    /**
430
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
431
     *
432
     * @param array $items  the items to be normalized.
433
     * @param bool|null $active whether there is an active child menu item.
434
     *
435
     * @return array the normalized menu items
436
     */
437 7
    protected function normalizeItems(array $items, ?bool &$active): array
438
    {
439 7
        foreach ($items as $i => $item) {
440 7
            if (isset($item['visible']) && !$item['visible']) {
441
                unset($items[$i]);
442
                continue;
443
            }
444
445 7
            if (!isset($item['label'])) {
446
                $item['label'] = '';
447
            }
448
449 7
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
450 7
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
451 7
            $hasActiveChild = false;
452
453 7
            if (isset($item['items'])) {
454
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
455
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
456
                    unset($items[$i]['items']);
457
                    if (!isset($item['url'])) {
458
                        unset($items[$i]);
459
                        continue;
460
                    }
461
                }
462
            }
463
464 7
            if (!isset($item['active'])) {
465 6
                if (($this->activateParents && $hasActiveChild) || ($this->activateItems && $this->isItemActive($item))) {
466
                    $active = $items[$i]['active'] = true;
467
                } else {
468 6
                    $items[$i]['active'] = false;
469
                }
470 3
            } elseif ($item['active'] instanceof Closure) {
471 1
                $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
472 3
            } elseif ($item['active']) {
473 3
                $active = true;
474
            }
475
        }
476
477 7
        return \array_values($items);
478
    }
479
480
    /**
481
     * Checks whether a menu item is active.
482
     *
483
     * This is done by checking match that specified in the `url` option of the menu item.
484
     *
485
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
486
     *
487
     * @param array $item the menu item to be checked
488
     * @param bool $active returns the result when the item is active
489
     *
490
     * @return bool whether the menu item is active
491
     */
492 7
    protected function isItemActive(array $item, bool $active = false): bool
493
    {
494 7
        if ($this->activateItems && $this->currentPath !== '/' && isset($item['url']) && $item['url'] === $this->currentPath) {
495
            $active = true;
496
        }
497
498 7
        return $active;
499
    }
500
}
501