Passed
Pull Request — master (#78)
by
unknown
02:15
created

NavBar::togglerContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use Stringable;
8
use JsonException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
/**
13
 * NavBar renders a navbar HTML component.
14
 *
15
 * Any content enclosed between the {@see begin()} and {@see end()} calls of NavBar is treated as the content of the
16
 * navbar. You may use widgets such as {@see Nav} or {@see \Yiisoft\Widget\Menu} to build up such content. For example,
17
 *
18
 * ```php
19
 *    if ($user->getId() !== null) {
20
 *        $menuItems = [
21
 *            [
22
 *                'label' => 'About',
23
 *                'url' => '/about',
24
 *            ],
25
 *            [
26
 *                'label' => 'Contact',
27
 *                'url' => '/contact',
28
 *            ],
29
 *            [
30
 *                'label' => 'Logout' . ' ' . '(' . $user->getUsername() . ')',
31
 *                'url' => '/logout'
32
 *            ],
33
 *        ];
34
 *    } else {
35
 *        $menuItems = [
36
 *            [
37
 *                'label' => 'About',
38
 *                'url' => '/about',
39
 *            ],
40
 *            [
41
 *                'label' => 'Contact',
42
 *                'url' => '/contact',
43
 *            ],
44
 *            [
45
 *                'label' => 'Login',
46
 *                'url' => '/login',
47
 *            ],
48
 *        ];
49
 *    }
50
 *
51
 *    <?php NavBar::widget()
52
 *        ->brandText('My Application Basic')
53
 *        ->brandUrl('/')
54
 *        ->options([
55
 *            'class' => 'navbar navbar-dark bg-dark navbar-expand-lg text-white',
56
 *        ])
57
 *        ->begin();
58
 *
59
 *        echo Nav::widget()
60
 *            ->currentPath($currentPath)
61
 *            ->items($menuItems)
62
 *            ->options([
63
 *                'class' => 'navbar-nav float-right ml-auto'
64
 *            ]);
65
 *
66
 *    echo NavBar::end(); ?>
67
 * ```
68
 * Note: $currentPath it must be injected from each controller to the main controller.
69
 *
70
 * SiteController.php
71
 *
72
 * ```php
73
 *
74
 *    public function index(ServerRequestInterface $request): ResponseInterface
75
 *    {
76
 *        $response = $this->responseFactory->createResponse();
77
 *        $currentPath = $request->getUri()->getPath();
78
 *        $output = $this->render('index', ['currentPath' => $currentPath]);
79
 *        $response->getBody()->write($output);
80
 *
81
 *        return $response;
82
 *    }
83
 * ```
84
 *
85
 * Controller.php
86
 *
87
 * ```php
88
 *    private function renderContent($content, array $parameters = []): string
89
 *    {
90
 *        $user = $this->user->getIdentity();
91
 *        $layout = $this->findLayoutFile($this->layout);
92
 *
93
 *        if ($layout !== null) {
94
 *            return $this->view->renderFile(
95
 *                $layout,
96
 *                    [
97
 *                        'aliases' => $this->aliases,
98
 *                        'content' => $content,
99
 *                        'user' => $user,
100
 *                        'params' => $this->params,
101
 *                        'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
102
 *                    ],
103
 *                $this
104
 *            );
105
 *        }
106
 *
107
 *        return $content;
108
 *    }
109
 * ```
110
 */
111
final class NavBar extends Widget
112
{
113
    public const EXPAND_SM = 'navbar-expand-sm';
114
    public const EXPAND_MD = 'navbar-expand-md';
115
    public const EXPAND_LG = 'navbar-expand-lg';
116
    public const EXPAND_XL = 'navbar-expand-xl';
117
    public const EXPAND_XXL = 'navbar-expand-xxl';
118
119
    public const THEME_LIGHT = 'navbar-light';
120
    public const THEME_DARK = 'navbar-dark';
121
122
    private array $collapseOptions = [];
123
    private ?string $brandText = null;
124
    private ?string $brandImage = null;
125
    private array $brandImageAttributes = [];
126
    private ?string $brandUrl = '/';
127
    private array $brandOptions = [];
128
    private string $screenReaderToggleText = 'Toggle navigation';
129
    private string $togglerContent = '<span class="navbar-toggler-icon"></span>';
130
    private array $togglerOptions = [];
131
    private bool $renderInnerContainer = true;
132
    private array $innerContainerOptions = [];
133
    private array $options = [];
134
    private bool $encodeTags = false;
135
    private ?string $expandSize = self::EXPAND_LG;
136
    private ?string $theme = self::THEME_LIGHT;
137
    private ?Offcanvas $offcanvas = null;
138
139 20
    public function getId(?string $suffix = '-navbar'): ?string
140
    {
141 20
        return $this->options['id'] ?? parent::getId($suffix);
142
    }
143
144 20
    public function begin(): string
145
    {
146
        /** Run Offcanvas::begin before NavBar parent::begin for right stack order */
147 20
        $offcanvas = $this->offcanvas ? $this->offcanvas->begin() : null;
148
149 20
        parent::begin();
150
151 20
        $options = $this->options;
152 20
        $options['id'] = $this->getId();
153 20
        $navTag = ArrayHelper::remove($options, 'tag', 'nav');
154 20
        $classNames = ['widget' => 'navbar'];
155
156 20
        if ($this->expandSize) {
157 17
            $classNames['size'] = $this->expandSize;
158
        }
159
160 20
        if ($this->theme) {
161 19
            $classNames['theme'] = $this->theme;
162
        }
163
164 20
        Html::addCssClass($options, $classNames);
165
166 20
        if (!isset($this->innerContainerOptions['class'])) {
167 18
            Html::addCssClass($this->innerContainerOptions, ['innerContainerOptions' => 'container']);
168
        }
169
170 20
        $htmlStart = Html::openTag($navTag, $options);
171
172 20
        if ($this->renderInnerContainer) {
173 19
            $htmlStart .= Html::openTag('div', $this->innerContainerOptions);
174
        }
175
176 20
        $htmlStart .= $this->renderBrand();
177
178 20
        if ($offcanvas) {
179 2
            $htmlStart .= $this->renderToggleButton($this->offcanvas->getId());
0 ignored issues
show
Bug introduced by
The method getId() does not exist on null. ( Ignorable by Annotation )

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

179
            $htmlStart .= $this->renderToggleButton($this->offcanvas->/** @scrutinizer ignore-call */ getId());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
180 2
            $htmlStart .= $offcanvas;
181 18
        } elseif ($this->expandSize) {
182 16
            $collapseOptions = $this->collapseOptions;
183 16
            $collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div');
184
185 16
            if (!isset($collapseOptions['id'])) {
186 16
                $collapseOptions['id'] = $options['id'] . '-collapse';
187
            }
188
189 16
            Html::addCssClass($collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']);
190
191 16
            $htmlStart .= $this->renderToggleButton($collapseOptions['id']);
192 16
            $htmlStart .= Html::openTag($collapseTag, $collapseOptions);
193 2
        } elseif ($this->togglerOptions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->togglerOptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
194 1
            $htmlStart .= $this->renderToggleButton(null);
195
        }
196
197 20
        return $htmlStart;
198
    }
199
200 20
    protected function run(): string
201
    {
202 20
        $htmlRun = '';
203
204 20
        if ($this->offcanvas) {
205 2
            $htmlRun = $this->offcanvas::end();
206 18
        } elseif ($this->expandSize) {
207 16
            $tag = ArrayHelper::getValue($this->collapseOptions, 'tag', 'div');
208 16
            $htmlRun = Html::closeTag($tag);
209
        }
210
211 20
        if ($this->renderInnerContainer) {
212 19
            $htmlRun .= Html::closeTag('div');
213
        }
214
215 20
        $tag = ArrayHelper::getValue($this->options, 'tag', 'nav');
216
217 20
        $htmlRun .= Html::closeTag($tag);
218
219 20
        return $htmlRun;
220
    }
221
222
    /**
223
     * Set size before then content will be expanded
224
     *
225
     * @param string|null $size
226
     *
227
     * @return self
228
     */
229 5
    public function expandSize(?string $size): self
230
    {
231 5
        $new = clone $this;
232 5
        $new->expandSize = $size;
233
234 5
        return $new;
235
    }
236
237
    /**
238
     * Set color theme for NavBar
239
     *
240
     * @param string|null $theme
241
     *
242
     * @return self
243
     */
244 2
    public function theme(?string $theme): self
245
    {
246 2
        $new = clone $this;
247 2
        $new->theme = $theme;
248
249 2
        return $new;
250
    }
251
252
    /**
253
     * The HTML attributes for the container tag. The following special options are recognized.
254
     *
255
     * @param array $value
256
     *
257
     * @return self
258
     *
259
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
260
     */
261 1
    public function collapseOptions(array $value): self
262
    {
263 1
        $new = clone $this;
264 1
        $new->collapseOptions = $value;
265
266 1
        return $new;
267
    }
268
269
    /**
270
     * Set/remove Offcanvas::widget instead of collapse
271
     *
272
     * @param Offcanvas|null $offcanvas
273
     *
274
     * @return self
275
     */
276 2
    public function offcanvas(?Offcanvas $offcanvas): self
277
    {
278 2
        $new = clone $this;
279 2
        $new->offcanvas = $offcanvas;
280
281 2
        return $new;
282
    }
283
284
    /**
285
     * The text of the brand or empty if it's not used. Note that this is not HTML-encoded.
286
     *
287
     * @param string|null $value
288
     *
289
     * @return self
290
     *
291
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#text
292
     */
293 7
    public function brandText(?string $value): self
294
    {
295 7
        $new = clone $this;
296 7
        $new->brandText = $value;
297
298 7
        return $new;
299
    }
300
301
    /**
302
     * Src of the brand image or empty if it's not used. Note that this param will override `$this->brandText` param.
303
     *
304
     * @param string|null $value
305
     *
306
     * @return self
307
     *
308
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#image
309
     */
310 3
    public function brandImage(?string $value): self
311
    {
312 3
        $new = clone $this;
313 3
        $new->brandImage = $value;
314
315 3
        return $new;
316
    }
317
318
    /**
319
     * Set attributes for brandImage
320
     *
321
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
322
     *
323
     * @param array $attributes
324
     *
325
     * @return self
326
     */
327 1
    public function brandImageAttributes(array $attributes): self
328
    {
329 1
        $new = clone $this;
330 1
        $new->brandImageAttributes = $attributes;
331
332 1
        return $new;
333
    }
334
335
    /**
336
     * The URL for the brand's hyperlink tag and will be used for the "href" attribute of the brand link. Default value
337
     * is "/". You may set it to empty string if you want no link at all.
338
     *
339
     * @param string|null $value
340
     *
341
     * @return self
342
     *
343
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#text
344
     */
345 6
    public function brandUrl(?string $value): self
346
    {
347 6
        $new = clone $this;
348 6
        $new->brandUrl = $value;
349
350 6
        return $new;
351
    }
352
353
    /**
354
     * The HTML attributes of the brand link.
355
     *
356
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
357
     *
358
     * @param array $value
359
     *
360
     * @return self
361
     */
362 1
    public function brandOptions(array $value): self
363
    {
364 1
        $new = clone $this;
365 1
        $new->brandOptions = $value;
366
367 1
        return $new;
368
    }
369
370
    /**
371
     * Text to show for screen readers for the button to toggle the navbar.
372
     *
373
     * @param string $value
374
     *
375
     * @return self
376
     */
377 1
    public function screenReaderToggleText(string $value): self
378
    {
379 1
        $new = clone $this;
380 1
        $new->screenReaderToggleText = $value;
381
382 1
        return $new;
383
    }
384
385
    /**
386
     * The toggle button content. Defaults to bootstrap 4 default `<span class="navbar-toggler-icon"></span>`.
387
     *
388
     * @param string $value
389
     *
390
     * @return self
391
     */
392 1
    public function togglerContent(string $value): self
393
    {
394 1
        $new = clone $this;
395 1
        $new->togglerContent = $value;
396
397 1
        return $new;
398
    }
399
400
    /**
401
     * The HTML attributes of the navbar toggler button.
402
     *
403
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
404
     *
405
     * @param array $value
406
     *
407
     * @return self
408
     */
409 2
    public function togglerOptions(array $value): self
410
    {
411 2
        $new = clone $this;
412 2
        $new->togglerOptions = $value;
413
414 2
        return $new;
415
    }
416
417
    /**
418
     * This for a 100% width navbar.
419
     *
420
     * @return self
421
     */
422 1
    public function withoutRenderInnerContainer(): self
423
    {
424 1
        $new = clone $this;
425 1
        $new->renderInnerContainer = false;
426
427 1
        return $new;
428
    }
429
430
    /**
431
     * The HTML attributes of the inner container.
432
     *
433
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
434
     *
435
     * @param array $value
436
     *
437
     * @return self
438
     */
439 2
    public function innerContainerOptions(array $value): self
440
    {
441 2
        $new = clone $this;
442 2
        $new->innerContainerOptions = $value;
443
444 2
        return $new;
445
    }
446
447
    /**
448
     * The HTML attributes for the widget container tag. The following special options are recognized.
449
     *
450
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
451
     *
452
     * @param array $value
453
     *
454
     * @return self
455
     */
456 3
    public function options(array $value): self
457
    {
458 3
        $new = clone $this;
459 3
        $new->options = $value;
460
461 3
        return $new;
462
    }
463
464 20
    private function renderBrand(): string
465
    {
466 20
        if (empty($this->brandImage) && empty($this->brandText)) {
467 11
            return '';
468
        }
469
470 9
        $content = '';
471 9
        $options = $this->brandOptions;
472 9
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
473
474 9
        Html::addCssClass($options, ['widget' => 'navbar-brand']);
475
476 9
        if (!empty($this->brandImage)) {
477 3
            $encode = false;
478 3
            $content = Html::img($this->brandImage)->attributes($this->brandImageAttributes);
479
        }
480
481 9
        if (!empty($this->brandText)) {
482 7
            $content .= $this->brandText;
483
        }
484
        /** @var string|Stringable $content */
485 9
        if (empty($this->brandUrl)) {
486 1
            $brand = Html::span($content, $options);
487
        } else {
488 8
            $brand = Html::a($content, $this->brandUrl, $options);
489
        }
490
491 9
        return $brand->encode($encode)->render();
492
    }
493
494
    /**
495
     * Renders collapsible toggle button.
496
     *
497
     * @param string|null $targetId - ID of target element for current button
498
     *
499
     * @throws JsonException
500
     *
501
     * @return string the rendering toggle button.
502
     *
503
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#toggler
504
     */
505 19
    private function renderToggleButton(?string $targetId): string
506
    {
507 19
        $options = $this->togglerOptions;
508 19
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
509 19
        Html::addCssClass($options, ['widget' => 'navbar-toggler']);
510
511 19
        $defauts = [
512 19
            'type' => 'button',
513
            'data' => [
514 19
                'bs-toggle' => $this->offcanvas ? 'offcanvas' : 'collapse',
515
            ],
516
            'aria' => [
517 19
                'controls' => $targetId,
518 19
                'expanded' => 'false',
519 19
                'label' => $this->screenReaderToggleText,
520
            ],
521
        ];
522
523 19
        if ($targetId) {
524 18
            $defauts['data']['bs-target'] = '#' . $targetId;
525
        }
526
527 19
        return Html::button(
528 19
            $this->togglerContent,
529 19
            ArrayHelper::merge($defauts, $options)
530
        )->encode($encode)->render();
531
    }
532
}
533