Passed
Push — master ( ceaf36...0d8993 )
by Alexander
04:20 queued 02:01
created

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

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