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

Menu::activateItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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