Passed
Push — master ( e80b4e...741461 )
by Alexander
02:19
created

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

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