Issues (1)

src/Nav.php (1 issue)

Labels
Severity
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
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} For details on how attributes are being rendered.
52
     */
53 1
    public function attributes(array $values): self
54
    {
55 1
        $new = clone $this;
56 1
        $new->attributes = $values;
57 1
        return $new;
58
    }
59
60
    /**
61
     * Returns a new instance with the specified whether to activate parent menu items when one of the corresponding
62
     * child menu items is active.
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 allows you to assign the current path of the url from request
73
     * controller.
74
     *
75
     * @param string $value The current path.
76
     */
77 3
    public function currentPath(string $value): self
78
    {
79 3
        $new = clone $this;
80 3
        $new->currentPath = $value;
81 3
        return $new;
82
    }
83
84
    /**
85
     * Returns a new instance with the specified align the menu items to the right.
86
     *
87
     * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end
88
     */
89 2
    public function enclosedByEndMenu(): self
90
    {
91 2
        $new = clone $this;
92 2
        $new->enclosedByEndMenu = true;
93 2
        return $new;
94
    }
95
96
    /**
97
     * Returns a new instance with the specified align the menu items to left.
98
     *
99
     * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end
100
     */
101 2
    public function enclosedByStartMenu(): self
102
    {
103 2
        $new = clone $this;
104 2
        $new->enclosedByStartMenu = true;
105 2
        return $new;
106
    }
107
108
    /**
109
     * Returns a new instance with the specified items.
110
     *
111
     * Each array element represents a single  menu item which can be either a string
112
     * or an array with the following structure:
113
     *
114
     * - label: string, required, the nav item label.
115
     * - url: optional, the item's URL. Defaults to "#".
116
     * - urlAttributes: optional, the attributes to be rendered in the item's URL.
117
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
118
     * - linkAttributes: array, optional, the HTML attributes of the item's link.
119
     * - active: bool, optional, whether the item should be on active state or not.
120
     * - disable: bool, optional, whether the item should be disabled.
121
     * - dropdownAttributes: array, optional, the HTML options that will be passed to the {@see Dropdown} widget.
122
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
123
     *   representing the dropdown menu.
124
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
125
     *   only this item.
126
     * - iconAttributes: array, optional, the HTML attributes of the item's icon.
127
     * - iconCssClass: string, optional, the icon CSS class.
128
     * - iconText: string, optional, the icon text.
129
     *
130
     * If a menu item is a string, it will be rendered directly without HTML encoding.
131
     *
132
     * @param array $value The menu items.
133
     */
134 17
    public function items(array $value): self
135
    {
136 17
        $new = clone $this;
137 17
        $new->items = $value;
138 17
        return $new;
139
    }
140
141
    /**
142
     * Returns a new instance with the specified disable activate items according to whether their currentPath.
143
     *
144
     * {@see isItemActive}
145
     */
146 3
    public function withoutActivateItems(): self
147
    {
148 3
        $new = clone $this;
149 3
        $new->activateItems = false;
150 3
        return $new;
151
    }
152
153
    /**
154
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
155
     */
156 16
    public function render(): string
157
    {
158 16
        return $this->renderNav();
159
    }
160
161
    /**
162
     * Renders the given items as a dropdown.
163
     *
164
     * This method is called to create sub-menus.
165
     *
166
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
167
     *
168
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
169
     *
170
     * @return string the rendering result.
171
     *
172
     * @link https://bulma.io/documentation/components/navbar/#dropdown-menu
173
     */
174 8
    private function renderDropdown(array $items): string
175
    {
176 8
        return Dropdown::widget()
177 8
            ->cssClass('navbar-dropdown')
0 ignored issues
show
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

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