Passed
Push — master ( 61d1a6...f87603 )
by Alexander
04:12 queued 02:11
created

Nav::enclosedByStartMenu()   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 InvalidArgumentException;
8
use ReflectionException;
9
use Yiisoft\Html\Html;
10
use Yiisoft\Html\Tag\A;
11
use Yiisoft\Html\Tag\CustomTag;
12
use Yiisoft\Html\Tag\Div;
13
use Yiisoft\Html\Tag\Span;
14
use Yiisoft\Widget\Widget;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Yii\Bulma\Widget. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
15
16
use function implode;
17
use function is_array;
18
19
/**
20
 * Nav renders a nav HTML component.
21
 *
22
 * @link https://bulma.io/documentation/components/navbar/#basic-navbar
23
 */
24
final class Nav extends Widget
25
{
26
    private bool $activateItems = true;
27
    private bool $activateParents = false;
28
    private array $attributes = [];
29
    private string $autoIdPrefix = 'w';
30
    private string $currentPath = '';
31
    private bool $enclosedByStartMenu = false;
32
    private bool $enclosedByEndMenu = false;
33
    private array $items = [];
34
    private string $hasDropdownCssClass = 'has-dropdown';
35
    private string $isHoverableCssClass = 'is-hoverable';
36
    private string $navBarDropdownCssClass = 'navbar-dropdown';
37
    private string $navBarEndCssClass = 'navbar-end';
38
    private string $navBarItemCssClass = 'navbar-item';
39
    private string $navBarLinkCssClass = 'navbar-link';
40
    private string $navBarMenuCssClass = 'navbar-menu';
41
    private string $navBarStartCssClass = 'navbar-start';
42
43
    /**
44
     * Returns a new instance with the specified attributes.
45
     *
46
     * @param array $value The HTML attributes for the widget container nav tag.
47
     *
48
     * @return self
49
     *
50
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
51
     */
52 1
    public function attributes(array $value): self
53
    {
54 1
        $new = clone $this;
55 1
        $new->attributes = $value;
56 1
        return $new;
57
    }
58
59
    /**
60
     * Returns a new instance with the specified prefix to the automatically generated widget IDs.
61
     *
62
     * @param string $value The prefix to the automatically generated widget IDs.
63
     *
64
     * @return self
65
     */
66 1
    public function autoIdPrefix(string $value): self
67
    {
68 1
        $new = clone $this;
69 1
        $new->autoIdPrefix = $value;
70 1
        return $new;
71
    }
72
73
    /**
74
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
75
     *
76
     * @return $this
77
     */
78 2
    public function activateParents(): self
79
    {
80 2
        $new = clone $this;
81 2
        $new->activateParents = true;
82 2
        return $new;
83
    }
84
85
    /**
86
     * Allows you to assign the current path of the url from request controller.
87
     *
88
     * @param string $value
89
     *
90
     * @return self
91
     */
92 3
    public function currentPath(string $value): self
93
    {
94 3
        $new = clone $this;
95 3
        $new->currentPath = $value;
96 3
        return $new;
97
    }
98
99
    /**
100
     * @return self
101
     *
102
     * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end
103
     */
104 2
    public function enclosedByEndMenu(): self
105
    {
106 2
        $new = clone $this;
107 2
        $new->enclosedByEndMenu = true;
108 2
        return $new;
109
    }
110
111
    /**
112
     * @return self
113
     *
114
     * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end
115
     */
116 2
    public function enclosedByStartMenu(): self
117
    {
118 2
        $new = clone $this;
119 2
        $new->enclosedByStartMenu = true;
120 2
        return $new;
121
    }
122
123
    /**
124
     * List of items in the nav widget. Each array element represents a single  menu item which can be either a string
125
     * or an array with the following structure:
126
     *
127
     * - label: string, required, the nav item label.
128
     * - url: optional, the item's URL. Defaults to "#".
129
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
130
     * - linkOptions: array, optional, the HTML attributes of the item's link.
131
     * - options: array, optional, the HTML attributes of the item container (LI).
132
     * - active: bool, optional, whether the item should be on active state or not.
133
     * - dropdownAttributes: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
134
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
135
     *   representing the dropdown menu.
136
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
137
     *   only this item.
138
     *
139
     * If a menu item is a string, it will be rendered directly without HTML encoding.
140
     *
141
     * @param array $value
142
     *
143
     * @return self
144
     */
145 17
    public function items(array $value): self
146
    {
147 17
        $new = clone $this;
148 17
        $new->items = $value;
149 17
        return $new;
150
    }
151
152
    /**
153
     * Disable activate items according to whether their currentPath.
154
     *
155
     * @return $this
156
     *
157
     * {@see isItemActive}
158
     */
159 3
    public function withoutActivateItems(): self
160
    {
161 3
        $new = clone $this;
162 3
        $new->activateItems = false;
163 3
        return $new;
164
    }
165
166
    /**
167
     * @throws ReflectionException
168
     */
169 16
    protected function run(): string
170
    {
171 16
        return $this->renderNav();
172
    }
173
174
    /**
175
     * Renders the given items as a dropdown.
176
     *
177
     * This method is called to create sub-menus.
178
     *
179
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
180
     *
181
     * @throws ReflectionException
182
     *
183
     * @return string the rendering result.
184
     *
185
     * @link https://bulma.io/documentation/components/navbar/#dropdown-menu
186
     */
187 8
    private function renderDropdown(array $items): string
188
    {
189 8
        return Dropdown::widget()
190 8
            ->dividerCssClass('navbar-divider')
0 ignored issues
show
Bug introduced by
The method dividerCssClass() 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. ( Ignorable by Annotation )

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

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