Completed
Pull Request — master (#52)
by Alexander
03:31
created

Menu::isItemActive()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 5.3906

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 9.6111
c 0
b 0
f 0
cc 5
nc 2
nop 2
crap 5.3906
1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\Widget;
5
6
use Closure;
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Html\Html;
9
10
/**
11
 * Menu displays a multi-level menu using nested HTML lists.
12
 *
13
 * The main property of Menu is {@see items}, which specifies the possible items in the menu. A menu item can contain
14
 * sub-items which specify the sub-menu under that menu item.
15
 *
16
 * Menu checks the current route and request parameters to toggle certain menu items with active state.
17
 *
18
 * Note that Menu only renders the HTML tags about the menu. It does not do any styling. You are responsible to provide
19
 * CSS styles to make it look like a real menu.
20
 *
21
 * The following example shows how to use Menu:
22
 *
23
 * ```php
24
 * echo Menu::Widget()
25
 *     ->items([
26
 *         ['label' => 'Login', 'url' => 'site/login', 'visible' => true],
27
 *     ]);
28
 * ```
29
 *
30
 * @method static Menu widget()
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: boolean, 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: boolean, 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: boolean 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 $items = [];
60
61
    /**
62
     * @var array list of HTML attributes shared by all menu [[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
64
     *            the menu 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 $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
76
     *             text. This property will be overridden by the `template` option set in individual menu items via
77
     *             {@see items}.
78
     */
79
    private $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
     *             In this template, the token `{label}` will be replaced with the label of the menu item.
84
     *             This property will be overridden by the `template` option set in individual menu items via
85
     *             {@see items}.
86
     */
87
    private $labelTemplate = '{label}';
88
89
    /**
90
     * @var string the template used to render a list of sub-menus.
91
     *             In this template, the token `{items}` will be replaced with the rendered sub-menu items.
92
     */
93
    private $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
94
95
    /**
96
     * @var bool whether the labels for menu items should be HTML-encoded.
97
     */
98
    private $encodeLabels = true;
99
100
    /**
101
     * @var string the CSS class to be appended to the active menu item.
102
     */
103
    private $activeCssClass = 'active';
104
105
    /**
106
     * @var bool whether to automatically activate items according to whether their route setting matches the currently
107
     *           requested route.
108
     *
109
     * {@see isItemActive()}
110
     */
111
    private $activateItems = true;
112
113
    /**
114
     * @var bool whether to activate parent menu items when one of the corresponding child menu items is active. The
115
     *           activated parent menu items will also have its CSS classes appended with {@see activeCssClass}.
116
     */
117
    private $activateParents = false;
118
119
    /**
120
     * @var string $currentPath Allows you to assign the current path of the url from request controller.
121
     */
122
    private $currentPath;
123
124
    /**
125
     * @var bool whether to hide empty menu items. An empty menu item is one whose `url` option is not set and which has
126
     *           no visible child menu items.
127
     */
128
    private $hideEmptyItems = true;
129
130
    /**
131
     * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
132
     *
133
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
134
     *   See also {@see \Yiisoft\Html\Html::tag()}.
135
     *
136
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
137
     */
138
    private $options = [];
139
140
    /**
141
     * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. Defaults to
142
     *             null, meaning no such CSS class will be assigned.
143
     */
144
    private $firstItemCssClass;
145
146
    /**
147
     * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. Defaults to
148
     *             null, meaning no such CSS class will be assigned.
149
     */
150
    private $lastItemCssClass;
151
152
    /**
153
     * Renders the menu.
154
     *
155
     * @return string the result of Widget execution to be outputted.
156
     */
157 7
    public function getContent(): string
158
    {
159 7
        $items = $this->normalizeItems($this->items, $hasActiveChild);
160
161 7
        if (empty($items)) {
162
            return '';
163
        }
164
165 7
        $options = $this->options;
166 7
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
167
168 7
        return Html::tag($tag, $this->renderItems($items), $options);
169
    }
170
171
    /**
172
     * {@see activateItems}
173
     *
174
     * @param boolean $value
175
     *
176
     * @return $this
177
     */
178
    public function activateItems(bool $value): self
179
    {
180
        $this->activateItems = $value;
181
182
        return $this;
183
    }
184
185
    /**
186
     * {@see activateParents}
187
     *
188
     * @param boolean $value
189
     *
190
     * @return $this
191
     */
192
    public function activateParents(bool $value): self
193
    {
194
        $this->activateParents = $value;
195
196
        return $this;
197
    }
198
199
    /**
200
     * {@see activeCssClass}
201
     *
202
     * @param string $value
203
     *
204
     * @return $this
205
     */
206 2
    public function activeCssClass(string $value): self
207
    {
208 2
        $this->activeCssClass = $value;
209
210 2
        return $this;
211
    }
212
213
    /**
214
     * {@see currentPath}
215
     *
216
     * @param string $value
217
     *
218
     * @return $this
219
     */
220
    public function currentPath(string $value): self
221
    {
222
        $this->currentPath = $value;
223
224
        return $this;
225
    }
226
227
    /**
228
     * {@see encodeLabels}
229
     *
230
     * @param boolean $value
231
     *
232
     * @return $this
233
     */
234 5
    public function encodeLabels(bool $value): self
235
    {
236 5
        $this->encodeLabels = $value;
237
238 5
        return $this;
239
    }
240
241
    /**
242
     * {@see firstItemCssClass}
243
     *
244
     * @param string $value
245
     *
246
     * @return $this
247
     */
248
    public function firstItemCssClass(string $value): self
249
    {
250
        $this->firstItemCssClass = $value;
251
252
        return $this;
253
    }
254
255
    /**
256
     * {@see hideEmptyItems}
257
     *
258
     * @param boolean $value
259
     *
260
     * @return $this
261
     */
262
    public function hideEmptyItems(bool $value): self
263
    {
264
        $this->hideEmptyItems = $value;
265
266
        return $this;
267
    }
268
269
    /**
270
     * {@see items}
271
     *
272
     * @param array $value
273
     *
274
     * @return $this
275
     */
276 7
    public function items(array $value): self
277
    {
278 7
        $this->items = $value;
279
280 7
        return $this;
281
    }
282
283
    /**
284
     * {@see itemOptions}
285
     *
286
     * @param array $value
287
     *
288
     * @return $this
289
     */
290 1
    public function itemOptions(array $value): self
291
    {
292 1
        $this->itemOptions = $value;
293
294 1
        return $this;
295
    }
296
297
    /**
298
     * {@see labelTemplate}
299
     *
300
     * @param string $value
301
     *
302
     * @return $this
303
     */
304 2
    public function labelTemplate(string $value): self
305
    {
306 2
        $this->labelTemplate = $value;
307
308 2
        return $this;
309
    }
310
311
    /**
312
     * {@see lastItemCssClass}
313
     *
314
     * @param string $value
315
     *
316
     * @return $this
317
     */
318
    public function lastItemCssClass(string $value): self
319
    {
320
        $this->lastItemCssClass = $value;
321
322
        return $this;
323
    }
324
325
    /**
326
     * {@see linkTemplate}
327
     *
328
     * @param string $value
329
     *
330
     * @return $this
331
     */
332 2
    public function linkTemplate(string $value): self
333
    {
334 2
        $this->linkTemplate = $value;
335
336 2
        return $this;
337
    }
338
339
    /**
340
     * {@see options}
341
     *
342
     * @param array $value
343
     *
344
     * @return $this
345
     */
346 1
    public function options(array $value): self
347
    {
348 1
        $this->options = $value;
349
350 1
        return $this;
351
    }
352
353
    /**
354
     * Recursively renders the menu items (without the container tag).
355
     *
356
     * @param array $items the menu items to be rendered recursively
357
     *
358
     * @return string the rendering result
359
     */
360 7
    protected function renderItems(array $items): string
361
    {
362 7
        $n = count($items);
363 7
        $lines = [];
364
365 7
        foreach ($items as $i => $item) {
366 7
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
367 7
            $tag = ArrayHelper::remove($options, 'tag', 'li');
368 7
            $class = [];
369
370 7
            if ($item['active']) {
371 3
                $class[] = $this->activeCssClass;
372
            }
373
374 7
            if ($i === 0 && $this->firstItemCssClass !== null) {
375
                $class[] = $this->firstItemCssClass;
376
            }
377
378 7
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
379
                $class[] = $this->lastItemCssClass;
380
            }
381
382 7
            Html::addCssClass($options, $class);
383
384 7
            $menu = $this->renderItem($item);
385
386 7
            if (!empty($item['items'])) {
387
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
388
                $menu .= strtr($submenuTemplate, [
389
                    '{items}' => $this->renderItems($item['items']),
390
                ]);
391
            }
392
393 7
            $lines[] = Html::tag($tag, $menu, $options);
394
        }
395
396 7
        return implode("\n", $lines);
397
    }
398
399
    /**
400
     * Renders the content of a menu item.
401
     * Note that the container and the sub-menus are not rendered here.
402
     *
403
     * @param array $item the menu item to be rendered. Please refer to {@see items} to see what data might be in the
404
     *              item.
405
     *
406
     * @return string the rendering result
407
     */
408 7
    protected function renderItem(array $item): string
409
    {
410 7
        if (isset($item['url'])) {
411 7
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
412
413 7
            return strtr($template, [
414 7
                '{url}'   => Html::encode($item['url']),
415 7
                '{label}' => $item['label'],
416
            ]);
417
        }
418
419 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
420
421 2
        return strtr($template, [
422 2
            '{label}' => $item['label'],
423
        ]);
424
    }
425
426
    /**
427
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
428
     *
429
     * @param array $items  the items to be normalized.
430
     * @param bool|null $active whether there is an active child menu item.
431
     *
432
     * @return array the normalized menu items
433
     */
434 7
    protected function normalizeItems(array $items, ?bool &$active): array
435
    {
436 7
        foreach ($items as $i => $item) {
437 7
            if (isset($item['visible']) && !$item['visible']) {
438
                unset($items[$i]);
439
                continue;
440
            }
441
442 7
            if (!isset($item['label'])) {
443
                $item['label'] = '';
444
            }
445
446 7
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
447 7
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
448 7
            $hasActiveChild = false;
449
450 7
            if (isset($item['items'])) {
451
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
452
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
453
                    unset($items[$i]['items']);
454
                    if (!isset($item['url'])) {
455
                        unset($items[$i]);
456
                        continue;
457
                    }
458
                }
459
            }
460
461 7
            if (!isset($item['active'])) {
462 6
                if (($this->activateParents && $hasActiveChild) || ($this->activateItems && $this->isItemActive($item))) {
463
                    $active = $items[$i]['active'] = true;
464
                } else {
465 6
                    $items[$i]['active'] = false;
466
                }
467 3
            } elseif ($item['active'] instanceof Closure) {
468 1
                $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
469 3
            } elseif ($item['active']) {
470 3
                $active = true;
471
            }
472
        }
473
474 7
        return array_values($items);
475
    }
476
477
    /**
478
     * Checks whether a menu item is active.
479
     *
480
     * This is done by checking match that specified in the `url` option of the menu item.
481
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
482
     *
483
     * @param array $item the menu item to be checked
484
     *
485
     * @param bool $active
486
     * @return bool whether the menu item is active
487
     */
488 7
    protected function isItemActive(array $item, bool $active = false): bool
489
    {
490 7
        if ($this->activateItems && $this->currentPath !== '/' && isset($item['url']) && $item['url'] === $this->currentPath) {
491
            $active = true;
492
        }
493
494 7
        return $active;
495
    }
496
497 7
    public function __toString()
498
    {
499 7
        return $this->run();
500
    }
501
}
502