Passed
Pull Request — master (#79)
by Wilmer
02:24
created

Nav::render()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bulma;
6
7
use InvalidArgumentException;
8
use Yiisoft\Definitions\Exception\CircularReferenceException;
9
use Yiisoft\Definitions\Exception\InvalidConfigException;
10
use Yiisoft\Definitions\Exception\NotInstantiableException;
11
use Yiisoft\Factory\NotFoundException;
12
use Yiisoft\Html\Html;
13
use Yiisoft\Html\Tag\A;
14
use Yiisoft\Html\Tag\CustomTag;
15
use Yiisoft\Html\Tag\Div;
16
use Yiisoft\Html\Tag\Span;
17
use Yiisoft\Widget\Widget;
18
19
use function implode;
20
use function is_array;
21
use function is_string;
22
23
/**
24
 * Nav renders a nav HTML component.
25
 *
26
 * @link https://bulma.io/documentation/components/navbar/#basic-navbar
27
 */
28
final class Nav extends Widget
29
{
30
    private bool $activateItems = true;
31
    private bool $activateParents = false;
32
    private array $attributes = [];
33
    private string $currentPath = '';
34
    private string $dropdownCssClass = 'navbar-dropdown';
35
    private string $endCssClass = 'navbar-end';
36
    private bool $enclosedByStartMenu = false;
37
    private bool $enclosedByEndMenu = false;
38
    private string $hasDropdownCssClass = 'has-dropdown';
39
    private string $isHoverableCssClass = 'is-hoverable';
40
    private string $itemCssClass = 'navbar-item';
41
    private array $items = [];
42
    private string $linkCssClass = 'navbar-link';
43
    private string $menuCssClass = 'navbar-menu';
44
    private string $startCssClass = 'navbar-start';
45
46
    /**
47
     * Returns a new instance with the specified HTML attributes for widget.
48
     *
49
     * @param array $values Attribute values indexed by attribute names.
50
     *
51
     * @return self
52
     *
53
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} For details on how attributes are being rendered.
54
     */
55 1
    public function attributes(array $values): self
56
    {
57 1
        $new = clone $this;
58 1
        $new->attributes = $values;
59 1
        return $new;
60
    }
61
62
    /**
63
     * Returns a new instance with the specified whether to activate parent menu items when one of the corresponding
64
     * child menu items is active.
65
     *
66
     * @return self
67
     */
68 2
    public function activateParents(): self
69
    {
70 2
        $new = clone $this;
71 2
        $new->activateParents = true;
72 2
        return $new;
73
    }
74
75
    /**
76
     * Returns a new instance with the specified allows you to assign the current path of the url from request
77
     * controller.
78
     *
79
     * @param string $value The current path.
80
     *
81
     * @return self
82
     */
83 3
    public function currentPath(string $value): self
84
    {
85 3
        $new = clone $this;
86 3
        $new->currentPath = $value;
87 3
        return $new;
88
    }
89
90
    /**
91
     * Returns a new instance with the specified align the menu items to the right.
92
     *
93
     * @return self
94
     *
95
     * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end
96
     */
97 2
    public function enclosedByEndMenu(): self
98
    {
99 2
        $new = clone $this;
100 2
        $new->enclosedByEndMenu = true;
101 2
        return $new;
102
    }
103
104
    /**
105
     * Returns a new instance with the specified align the menu items to left.
106
     *
107
     * @return self
108
     *
109
     * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end
110
     */
111 2
    public function enclosedByStartMenu(): self
112
    {
113 2
        $new = clone $this;
114 2
        $new->enclosedByStartMenu = true;
115 2
        return $new;
116
    }
117
118
    /**
119
     * Returns a new instance with the specified items.
120
     *
121
     * Each array element represents a single  menu item which can be either a string
122
     * or an array with the following structure:
123
     *
124
     * - label: string, required, the nav item label.
125
     * - url: optional, the item's URL. Defaults to "#".
126
     * - urlAttributes: optional, the attributes to be rendered in the item's URL.
127
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
128
     * - linkAttributes: array, optional, the HTML attributes of the item's link.
129
     * - active: bool, optional, whether the item should be on active state or not.
130
     * - disable: bool, optional, whether the item should be disabled.
131
     * - dropdownAttributes: array, optional, the HTML options that will be passed to the {@see Dropdown} widget.
132
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
133
     *   representing the dropdown menu.
134
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
135
     *   only this item.
136
     * - iconAttributes: array, optional, the HTML attributes of the item's icon.
137
     * - iconCssClass: string, optional, the icon CSS class.
138
     * - iconText: string, optional, the icon text.
139
     *
140
     * If a menu item is a string, it will be rendered directly without HTML encoding.
141
     *
142
     * @param array $value The menu items.
143
     *
144
     * @return self
145
     */
146 17
    public function items(array $value): self
147
    {
148 17
        $new = clone $this;
149 17
        $new->items = $value;
150 17
        return $new;
151
    }
152
153
    /**
154
     * Returns a new instance with the specified disable activate items according to whether their currentPath.
155
     *
156
     * @return self
157
     *
158
     * {@see isItemActive}
159
     */
160 3
    public function withoutActivateItems(): self
161
    {
162 3
        $new = clone $this;
163 3
        $new->activateItems = false;
164 3
        return $new;
165
    }
166
167
    /**
168
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
169
     */
170 16
    public function render(): string
171
    {
172 16
        return $this->renderNav();
173
    }
174
175
    /**
176
     * Renders the given items as a dropdown.
177
     *
178
     * This method is called to create sub-menus.
179
     *
180
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
181
     *
182
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
183
     *
184
     * @return string the rendering result.
185
     *
186
     * @link https://bulma.io/documentation/components/navbar/#dropdown-menu
187
     */
188 8
    private function renderDropdown(array $items): string
189
    {
190 8
        return Dropdown::widget()
191 8
            ->cssClass('navbar-dropdown')
0 ignored issues
show
Bug introduced by
The method cssClass() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bulma\Dropdown or Yiisoft\Yii\Bulma\NavBar or Yiisoft\Yii\Bulma\Panel. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

191
            ->/** @scrutinizer ignore-call */ cssClass('navbar-dropdown')
Loading history...
192 8
            ->dividerCssClass('navbar-divider')
193 8
            ->enclosedByContainer()
194 8
            ->itemCssClass('navbar-item')
195 8
            ->items($items)
196 8
            ->render() . PHP_EOL;
197
    }
198
199
    /**
200
     * Check to see if a child item is active optionally activating the parent.
201
     *
202
     * @param array $items {@see items}
203
     * @param bool $active Should the parent be active too.
204
     *
205
     * @return array
206
     *
207
     * {@see items}
208
     */
209 8
    private function isChildActive(array $items, bool &$active = false): array
210
    {
211
        /**
212
         * @psalm-var array<
213
         *  string,
214
         *  array{
215
         *    active?: bool,
216
         *    disable?: bool,
217
         *    encode?: bool,
218
         *    icon?: string,
219
         *    iconAttributes?: array,
220
         *    items?: array,
221
         *    label: string,
222
         *    url: string,
223
         *    visible?: bool
224
         * }|string> $items
225
         */
226 8
        foreach ($items as $i => $child) {
227 8
            $url = $child['url'] ?? '#';
228 8
            $active = $child['active'] ?? false;
229
230 8
            if ($active === false && is_array($child)) {
231 8
                $child['active'] = $this->isItemActive($url, $this->currentPath, $this->activateItems);
232
            }
233
234 8
            if ($this->activateParents) {
235 1
                $active = true;
236
            }
237
238 8
            $childItems = $child['items'] ?? [];
239
240 8
            if ($childItems !== [] && is_array($items[$i])) {
241 1
                $items[$i]['items'] = $this->isChildActive($childItems);
242
243 1
                if ($active) {
244 1
                    $items[$i]['attributes'] = ['active' => true];
245 1
                    $active = true;
246
                }
247
            }
248
        }
249
250 8
        return $items;
251
    }
252
253
    /**
254
     * Checks whether a menu item is active.
255
     *
256
     * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
257
     * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
258
     * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
259
     * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
260
     *
261
     * @param string $url The menu item's URL.
262
     * @param string $currentPath The currentPath.
263
     * @param bool $activateItems Whether to activate the parent menu items when the currentPath matches.
264
     *
265
     * @return bool whether the menu item is active
266
     */
267 15
    private function isItemActive(string $url, string $currentPath, bool $activateItems): bool
268
    {
269 15
        return ($currentPath !== '/') && ($url === $currentPath) && $activateItems;
270
    }
271
272 15
    private function renderLabelItem(
273
        string $label,
274
        string $iconText,
275
        string $iconCssClass,
276
        array $iconAttributes = []
277
    ): string {
278 15
        $html = '';
279
280 15
        if ($iconText !== '' || $iconCssClass !== '') {
281 1
            $html = Span::tag()
282 1
                ->addAttributes($iconAttributes)
283 1
                ->content(CustomTag::name('i')
284 1
                    ->addClass($iconCssClass)
285 1
                    ->content($iconText)
286 1
                    ->encode(false)
287 1
                    ->render())
288 1
                ->encode(false)
289 1
                ->render();
290
        }
291
292 15
        if ($label !== '') {
293 15
            $html .= $label;
294
        }
295
296 15
        return $html;
297
    }
298
299
    /**
300
     * Renders a widget's item.
301
     *
302
     * @param array $item the item to render.
303
     *
304
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
305
     *
306
     * @return string the rendering result.
307
     */
308 16
    private function renderItem(array $item): string
309
    {
310 16
        $html = '';
311
312 16
        if (!isset($item['label'])) {
313 1
            throw new InvalidArgumentException('The "label" option is required.');
314
        }
315
316
        /** @var string */
317 15
        $itemLabel = $item['label'] ?? '';
318
319 15
        if (isset($item['encode']) && $item['encode'] === true) {
320 1
            $itemLabel = Html::encode($itemLabel);
321
        }
322
323
        /** @var array */
324 15
        $items = $item['items'] ?? [];
325
326
        /** @var string */
327 15
        $url = $item['url'] ?? '#';
328
329
        /** @var array */
330 15
        $urlAttributes = $item['urlAttributes'] ?? [];
331
332
        /** @var array */
333 15
        $dropdownAttributes = $item['dropdownAttributes'] ?? [];
334
335
        /** @var string */
336 15
        $iconText = $item['iconText'] ?? '';
337
338
        /** @var string */
339 15
        $iconCssClass = $item['iconCssClass'] ?? '';
340
341
        /** @var array */
342 15
        $iconAttributes = $item['iconAttributes'] ?? [];
343
344
        /** @var bool */
345 15
        $active = $item['active'] ?? $this->isItemActive($url, $this->currentPath, $this->activateItems);
346
347
        /** @var bool */
348 15
        $disabled = $item['disabled'] ?? false;
349
350 15
        $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes);
351
352 15
        if ($disabled) {
353 1
            Html::addCssStyle($urlAttributes, 'opacity:.65; pointer-events:none;');
354
        }
355
356 15
        if ($this->activateItems && $active) {
357 1
            Html::addCssClass($urlAttributes, ['active' => 'is-active']);
358
        }
359
360 15
        if ($items !== []) {
361 8
            $attributes = $this->attributes;
362 8
            Html::addCssClass(
363 8
                $attributes,
364 8
                [$this->itemCssClass, $this->hasDropdownCssClass, $this->isHoverableCssClass]
365 8
            );
366 8
            Html::addCssClass($urlAttributes, $this->linkCssClass);
367 8
            Html::addCssClass($dropdownAttributes, $this->dropdownCssClass);
368
369 8
            $items = $this->isChildActive($items, $active);
370 8
            $dropdown = PHP_EOL . $this->renderDropdown($items);
371 8
            $a = A::tag()
372 8
                ->attributes($urlAttributes)
373 8
                ->content($itemLabel)
374 8
                ->encode(false)
375 8
                ->url($url)
376 8
                ->render();
377 8
            $div = Div::tag()
378 8
                ->attributes($dropdownAttributes)
379 8
                ->content($dropdown)
380 8
                ->encode(false)
381 8
                ->render();
382 8
            $html = Div::tag()
383 8
                ->attributes($attributes)
384 8
                ->content(PHP_EOL . $a . PHP_EOL . $div . PHP_EOL)
385 8
                ->encode(false)
386 8
                ->render();
387
        }
388
389 15
        if ($html === '') {
390 11
            Html::addCssClass($urlAttributes, 'navbar-item');
391 11
            $html = A::tag()
392 11
                ->attributes($urlAttributes)
393 11
                ->content($itemLabel)
394 11
                ->url($url)
395 11
                ->encode(false)
396 11
                ->render();
397
        }
398
399 15
        return $html;
400
    }
401
402
    /**
403
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
404
     */
405 16
    private function renderNav(): string
406
    {
407 16
        $items = [];
408
409
        /** @var array|string $item */
410 16
        foreach ($this->items as $item) {
411 16
            $visible = !isset($item['visible']) || $item['visible'];
412
413 16
            if ($visible) {
414 16
                $items[] = is_string($item) ? $item : $this->renderItem($item);
415
            }
416
        }
417
418 15
        $links = PHP_EOL . implode(PHP_EOL, $items) . PHP_EOL;
419
420 15
        if ($this->enclosedByStartMenu) {
421 1
            $links = PHP_EOL . Div::tag()
422 1
                ->class($this->startCssClass)
423 1
                ->content($links)
424 1
                ->encode(false)
425 1
                ->render() .
426 1
                PHP_EOL;
427
        }
428
429 15
        if ($this->enclosedByEndMenu) {
430 1
            $links = PHP_EOL . Div::tag()
431 1
                ->class($this->endCssClass)
432 1
                ->content($links)
433 1
                ->encode(false)
434 1
                ->render() .
435 1
                PHP_EOL;
436
        }
437
438 15
        return $this->items !== []
439 15
            ? Div::tag()
440 15
                ->class($this->menuCssClass)
441 15
                ->content($links)
442 15
                ->encode(false)
443 15
                ->render()
444 15
            : '';
445
    }
446
}
447