Passed
Push — master ( 6d0312...290831 )
by Alexander
04:13 queued 01:47
created

Menu::deactivateItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

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