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

331
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item, $linkOptions);
Loading history...
332
333 18
            if (!empty($item['items'])) {
334 9
                $subMenuTemplate = ArrayHelper::getValue($item, 'subMenuTemplate', $this->subMenuTemplate);
335 9
                $menu .= strtr($subMenuTemplate, [
336 9
                    '{items}' => $this->renderItems($item['items'])
337
                ]);
338
            }
339
340 18
            if (isset($item['label']) && !isset($item['url'])) {
341 11
                if (!empty($menu)) {
342 11
                    $lines[] = $menu;
343
                } else {
344 11
                    $lines[] = $item['label'];
345
                }
346
            } else {
347 18
                $lines[] = Html::tag($tag, $menu, $options);
348
            }
349
        }
350
351 18
        return implode("\n", $lines);
352
    }
353
354 18
    private function renderItem(array $item, array $linkOptions): string
355
    {
356 18
        if (isset($item['url'])) {
357 18
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
358
359 18
            $htmlIcon = '';
360
361 18
            if (isset($item['icon'])) {
362 2
                $htmlIcon = $this->renderIcon($item['icon'], $item['iconOptions']);
363
            }
364
365 18
            if (Html::renderTagAttributes($linkOptions) !== '') {
366 10
                $url = '"' . Html::encode($item['url']) . '"' . Html::renderTagAttributes($linkOptions);
367
            } else {
368 17
                $url = '"' . Html::encode($item['url']) . '"';
369
            }
370 18
            return strtr($template, [
371 18
                '{url}'   => $url,
372 18
                '{label}' => $item['label'],
373 18
                '{icon}' => $htmlIcon
374
            ]);
375
        }
376
377 11
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
378
379 11
        return strtr($template, [
380 11
            '{label}' => Html::tag('p', $item['label'], ['class' => 'menu-label']) . "\n",
381
        ]);
382
    }
383
384 19
    private function normalizeItems(array $items, ?bool &$active): array
385
    {
386 19
        foreach ($items as $i => $item) {
387 18
            if (isset($item['visible']) && !$item['visible']) {
388 1
                unset($items[$i]);
389 1
                continue;
390
            }
391
392 18
            if (!isset($item['label'])) {
393 1
                $item['label'] = '';
394
            }
395
396 18
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
397 18
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
398 18
            $hasActiveChild = false;
399
400 18
            if (isset($item['items'])) {
401 9
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
402 9
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
403 1
                    unset($items[$i]['items']);
404 1
                    if (!isset($item['url'])) {
405 1
                        unset($items[$i]);
406 1
                        continue;
407
                    }
408
                }
409
            }
410
411 18
            if (!isset($item['active'])) {
412 17
                if (($this->activateParents && $hasActiveChild) || ($this->activateItems && $this->isItemActive($item))) {
413 7
                    $active = $items[$i]['active'] = true;
414
                } else {
415 17
                    $items[$i]['active'] = false;
416
                }
417 3
            } elseif ($item['active'] instanceof Closure) {
418 1
                $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
419 3
            } elseif ($item['active']) {
420 2
                $active = true;
421
            }
422
        }
423
424 19
        return array_values($items);
425
    }
426
427 18
    private function isItemActive(array $item): bool
428
    {
429 18
        return isset($item['url']) && $item['url'] === $this->currentPath && $this->activateItems;
430
    }
431
432 2
    private function renderIcon(string $icon, array $iconOptions): string
433
    {
434 2
        $html = '';
435
436 2
        if ($icon !== '') {
437 2
            $html = Html::beginTag('span', $iconOptions) .
438 2
                Html::tag('i', '', ['class' => $icon]) .
439 2
                Html::endTag('span');
440
        }
441
442 2
        return $html;
443
    }
444
445 18
    private function buildOptions(): void
446
    {
447 18
        $this->options = $this->addOptions($this->options, 'menu');
448 18
        $this->itemsOptions = $this->addOptions($this->itemsOptions, 'menu-list');
449 18
    }
450
451 18
    private function buildMenu(): string
452
    {
453 18
        $tag = ArrayHelper::remove($this->options, 'tag', 'ul');
454 18
        $html = Html::beginTag('aside', $this->options) . "\n";
455
456 18
        if ($this->brand !== '') {
457 1
            $html .= $this->brand . "\n";
458
        }
459
460 18
        $html .= Html::beginTag($tag, $this->itemsOptions) . "\n";
461 18
        $html .=  $this->renderItems($this->items) . "\n";
462 18
        $html .= Html::endTag($tag) . "\n";
463 18
        $html .= Html::endTag('aside');
464
465 18
        return $html;
466
    }
467
}
468