Completed
Pull Request — master (#52)
by Wilmer
01:37
created

Menu::isItemActive()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4.25

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 10
c 0
b 0
f 0
cc 4
nc 2
nop 2
crap 4.25
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
class Menu extends Widget
31
{
32
    /**
33
     * @var array list of menu items. Each menu item should be an array of the following structure:
34
     *
35
     * - label: string, optional, specifies the menu item label. When {@see encodeLabels} is true, the label will be
36
     *   HTML-encoded. If the label is not specified, an empty string will be used.
37
     * - encode: boolean, optional, whether this item`s label should be HTML-encoded. This param will override global
38
     *   {@see encodeLabels} param.
39
     * - url: string or array, optional, specifies the URL of the menu item. When this is set, the actual menu item
40
     *   content will be generated using {@see linkTemplate}; otherwise, {@see labelTemplate} will be used.
41
     * - visible: boolean, optional, whether this menu item is visible. Defaults to true.
42
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
43
     * - active: boolean or Closure, optional, whether this menu item is in active state (currently selected). When
44
     *   using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $Widget)`. Closure
45
     *   must return `true` if item should be marked as `active`, otherwise - `false`. If a menu item is active, its CSS
46
     *   class will be appended with {@see activeCssClass}. If this option is not set, the menu item will be set active
47
     *   automatically when the current request is triggered by `url`. For more details, please refer to
48
     *   {@see isItemActive()}.
49
     * - template: string, optional, the template used to render the content of this menu item. The token `{url}` will
50
     *   be replaced by the URL associated with this menu item, and the token `{label}` will be replaced by the label
51
     *   of the menu item. If this option is not set, {@see linkTemplate} or {@see labelTemplate} will be used instead.
52
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus. The token `{items}` will
53
     *   be replaced with the rendered sub-menu items. If this option is not set, [[submenuTemplate]] will be used
54
     *   instead.
55
     * - options: array, optional, the HTML attributes for the menu container tag.
56
     */
57
    private $items = [];
58
59
    /**
60
     * @var array list of HTML attributes shared by all menu [[items]]. If any individual menu item specifies its
61
     *            `options`, it will be merged with this property before being used to generate the HTML attributes for
62
     *            the menu item tag. The following special options are recognized:
63
     *
64
     * - tag: string, defaults to "li", the tag name of the item container tags. Set to false to disable container tag.
65
     *   See also {@see \Yiisoft\Html\Html::tag()}
66
     *
67
     * {@see \Yiisoft\Html\Html::renderTagAttributes() for details on how attributes are being rendered}
68
     */
69
    private $itemOptions = [];
70
71
    /**
72
     * @var string the template used to render the body of a menu which is a link. In this template, the token `{url}`
73
     *             will be replaced with the corresponding link URL; while `{label}` will be replaced with the link
74
     *             text. This property will be overridden by the `template` option set in individual menu items via
75
     *             {@see items}.
76
     */
77
    private $linkTemplate = '<a href="{url}">{label}</a>';
78
79
    /**
80
     * @var string the template used to render the body of a menu which is NOT a link.
81
     *             In this template, the token `{label}` will be replaced with the label of the menu item.
82
     *             This property will be overridden by the `template` option set in individual menu items via
83
     *             {@see items}.
84
     */
85
    private $labelTemplate = '{label}';
86
87
    /**
88
     * @var string the template used to render a list of sub-menus.
89
     *             In this template, the token `{items}` will be replaced with the rendered sub-menu items.
90
     */
91
    private $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
92
93
    /**
94
     * @var bool whether the labels for menu items should be HTML-encoded.
95
     */
96
    private $encodeLabels = true;
97
98
    /**
99
     * @var string the CSS class to be appended to the active menu item.
100
     */
101
    private $activeCssClass = 'active';
102
103
    /**
104
     * @var bool whether to automatically activate items according to whether their route setting matches the currently
105
     *           requested route.
106
     *
107
     * {@see isItemActive()}
108
     */
109
    private $activateItems = true;
110
111
    /**
112
     * @var bool whether to activate parent menu items when one of the corresponding child menu items is active. The
113
     *           activated parent menu items will also have its CSS classes appended with {@see activeCssClass}.
114
     */
115
    private $activateParents = false;
116
117
    /**
118
     * @var bool whether to hide empty menu items. An empty menu item is one whose `url` option is not set and which has
119
     *           no visible child menu items.
120
     */
121
    private $hideEmptyItems = true;
122
123
    /**
124
     * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
125
     *
126
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
127
     *   See also {@see \Yiisoft\Html\Html::tag()}.
128
     *
129
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
130
     */
131
    private $options = [];
132
133
    /**
134
     * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. Defaults to
135
     *             null, meaning no such CSS class will be assigned.
136
     */
137
    private $firstItemCssClass;
138
139
    /**
140
     * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. Defaults to
141
     *             null, meaning no such CSS class will be assigned.
142
     */
143
    private $lastItemCssClass;
144
145
    /**
146
     * Renders the menu.
147
     *
148
     * @return string the result of Widget execution to be outputted.
149
     */
150 6
    public function show(): string
151
    {
152 6
        $items = $this->normalizeItems($this->items, $hasActiveChild);
153
154 6
        if (empty($items)) {
155
            return '';
156
        }
157
158 6
        $options = $this->options;
159 6
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
160
161 6
        return Html::tag($tag, $this->renderItems($items), $options);
162
    }
163
164
    /**
165
     * {@see activateItems}
166
     *
167
     * @param boolean $value
168
     *
169
     * @return Widget
170
     */
171
    public function activateItems(bool $value): Widget
172
    {
173
        $this->activateItems = $value;
174
175
        return $this;
176
    }
177
178
    /**
179
     * {@see activateParents}
180
     *
181
     * @param boolean $value
182
     *
183
     * @return Widget
184
     */
185
    public function activateParents(bool $value): Widget
186
    {
187
        $this->activateParents = $value;
188
189
        return $this;
190
    }
191
192
    /**
193
     * {@see activeCssClass}
194
     *
195
     * @param string $value
196
     *
197
     * @return Widget
198
     */
199 2
    public function activeCssClass(string $value): Widget
200
    {
201 2
        $this->activeCssClass = $value;
202
203 2
        return $this;
204
    }
205
206
    /**
207
     * {@see encodeLabels}
208
     *
209
     * @param boolean $value
210
     *
211
     * @return Widget
212
     */
213 4
    public function encodeLabels(bool $value): Widget
214
    {
215 4
        $this->encodeLabels = $value;
216
217 4
        return $this;
218
    }
219
220
    /**
221
     * {@see firstItemCssClass}
222
     *
223
     * @param string $value
224
     *
225
     * @return Widget
226
     */
227
    public function firstItemCssClass(string $value): Widget
228
    {
229
        $this->firstItemCssClass = $value;
230
231
        return $this;
232
    }
233
234
    /**
235
     * {@see hideEmptyItems}
236
     *
237
     * @param boolean $value
238
     *
239
     * @return Widget
240
     */
241
    public function hideEmptyItems(bool $value): Widget
242
    {
243
        $this->hideEmptyItems = $value;
244
245
        return $this;
246
    }
247
248
    /**
249
     * {@see items}
250
     *
251
     * @param array $value
252
     *
253
     * @return Widget
254
     */
255 6
    public function items(array $value): Widget
256
    {
257 6
        $this->items = $value;
258
259 6
        return $this;
260
    }
261
262
    /**
263
     * {@see itemOptions}
264
     *
265
     * @param array $value
266
     *
267
     * @return Widget
268
     */
269 1
    public function itemOptions(array $value): Widget
270
    {
271 1
        $this->itemOptions = $value;
272
273 1
        return $this;
274
    }
275
276
    /**
277
     * {@see labelTemplate}
278
     *
279
     * @param string $value
280
     *
281
     * @return Widget
282
     */
283 2
    public function labelTemplate(string $value): Widget
284
    {
285 2
        $this->labelTemplate = $value;
286
287 2
        return $this;
288
    }
289
290
    /**
291
     * {@see lastItemCssClass}
292
     *
293
     * @param string $value
294
     *
295
     * @return Widget
296
     */
297
    public function lastItemCssClass(string $value): Widget
298
    {
299
        $this->lastItemCssClass = $value;
300
301
        return $this;
302
    }
303
304
    /**
305
     * {@see linkTemplate}
306
     *
307
     * @param string $value
308
     *
309
     * @return Widget
310
     */
311 2
    public function linkTemplate(string $value): Widget
312
    {
313 2
        $this->linkTemplate = $value;
314
315 2
        return $this;
316
    }
317
318
    /**
319
     * {@see options}
320
     *
321
     * @param array $value
322
     *
323
     * @return Widget
324
     */
325 1
    public function options(array $value): Widget
326
    {
327 1
        $this->options = $value;
328
329 1
        return $this;
330
    }
331
332
    /**
333
     * Recursively renders the menu items (without the container tag).
334
     *
335
     * @param array $items the menu items to be rendered recursively
336
     *
337
     * @return string the rendering result
338
     */
339 6
    protected function renderItems(array $items): string
340
    {
341 6
        $n = count($items);
342 6
        $lines = [];
343 6
        foreach ($items as $i => $item) {
344 6
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
345 6
            $tag = ArrayHelper::remove($options, 'tag', 'li');
346 6
            $class = [];
347 6
            if ($item['active']) {
348 3
                $class[] = $this->activeCssClass;
349
            }
350 6
            if ($i === 0 && $this->firstItemCssClass !== null) {
351
                $class[] = $this->firstItemCssClass;
352
            }
353 6
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
354
                $class[] = $this->lastItemCssClass;
355
            }
356 6
            Html::addCssClass($options, $class);
357
358 6
            $menu = $this->renderItem($item);
359 6
            if (!empty($item['items'])) {
360
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
361
                $menu .= strtr($submenuTemplate, [
362
                    '{items}' => $this->renderItems($item['items']),
363
                ]);
364
            }
365 6
            $lines[] = Html::tag($tag, $menu, $options);
366
        }
367
368 6
        return implode("\n", $lines);
369
    }
370
371
    /**
372
     * Renders the content of a menu item.
373
     * Note that the container and the sub-menus are not rendered here.
374
     *
375
     * @param array $item the menu item to be rendered. Please refer to {@see items} to see what data might be in the
376
     *              item.
377
     *
378
     * @return string the rendering result
379
     */
380 6
    protected function renderItem(array $item): string
381
    {
382 6
        if (isset($item['url'])) {
383 6
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
384
385 6
            return strtr($template, [
386 6
                '{url}'   => Html::encode($item['url']),
387 6
                '{label}' => $item['label'],
388
            ]);
389
        }
390
391 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
392
393 2
        return strtr($template, [
394 2
            '{label}' => $item['label'],
395
        ]);
396
    }
397
398
    /**
399
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
400
     *
401
     * @param array $items  the items to be normalized.
402
     * @param bool|null $active whether there is an active child menu item.
403
     *
404
     * @return array the normalized menu items
405
     */
406 6
    protected function normalizeItems(array $items, ?bool &$active): array
407
    {
408 6
        foreach ($items as $i => $item) {
409 6
            if (isset($item['visible']) && !$item['visible']) {
410
                unset($items[$i]);
411
                continue;
412
            }
413 6
            if (!isset($item['label'])) {
414
                $item['label'] = '';
415
            }
416 6
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
417 6
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
418 6
            $hasActiveChild = false;
419 6
            if (isset($item['items'])) {
420
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
421
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
422
                    unset($items[$i]['items']);
423
                    if (!isset($item['url'])) {
424
                        unset($items[$i]);
425
                        continue;
426
                    }
427
                }
428
            }
429 6
            if (!isset($item['active'])) {
430 5
                if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->activateParents ...is->isItemActive($item), Probably Intended Meaning: $this->activateParents &...s->isItemActive($item))
Loading history...
431
                    $active = $items[$i]['active'] = true;
432
                } else {
433 5
                    $items[$i]['active'] = false;
434
                }
435 3
            } elseif ($item['active'] instanceof Closure) {
436 1
                $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
437 3
            } elseif ($item['active']) {
438 3
                $active = true;
439
            }
440
        }
441
442 6
        return array_values($items);
443
    }
444
445
    /**
446
     * Checks whether a menu item is active.
447
     *
448
     * This is done by checking match that specified in the `url` option of the menu item.
449
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
450
     *
451
     * @param array $item the menu item to be checked
452
     *
453
     * @return bool whether the menu item is active
454
     */
455 6
    protected function isItemActive(array $item, bool $active = false): bool
456
    {
457 6
        if (($this->activateItems) && ($_SERVER['REQUEST_URI'] !== '/') && ($item['url'] === $_SERVER['REQUEST_URI'])) {
458
            $active = true;
459
        }
460
461 6
        return $active;
462
    }
463
464 6
    public function __toString()
465
    {
466 6
        return $this->show();
467
    }
468
}
469