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

NavBar::withoutRenderInnerContainer()   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 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 6
ccs 4
cts 4
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\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
    private array $collapseOptions = [];
120
    private ?string $brandText = null;
121
    private ?string $brandImage = null;
122
    private array $brandImageAttributes = [];
123
    private ?string $brandUrl = '/';
124
    private array $brandOptions = [];
125
    private string $screenReaderToggleText = 'Toggle navigation';
126
    private string $togglerContent = '<span class="navbar-toggler-icon"></span>';
127
    private array $togglerOptions = [];
128
    private bool $renderInnerContainer = true;
129
    private array $innerContainerOptions = [];
130
    private array $options = [];
131
    private bool $encodeTags = false;
132
    private ?string $expandSize = self::EXPAND_LG;
133
    private ?Offcanvas $offcanvas = null;
134
135 19
    public function getId(?string $suffix = '-navbar'): ?string
136
    {
137 19
        return $this->options['id'] ?? parent::getId($suffix);
138
    }
139
140 19
    public function begin(): string
141
    {
142
        /** Run Offcanvas::begin before NavBar parent::begin for right stack order */
143 19
        $offcanvas = $this->offcanvas ? $this->offcanvas->begin() : null;
144
145 19
        parent::begin();
146
147 19
        $options = $this->options;
148 19
        $options['id'] = $this->getId();
149 19
        $navTag = ArrayHelper::remove($options, 'tag', 'nav');
150 19
        $classNames = ['widget' => 'navbar'];
151
152 19
        if ($this->expandSize) {
153 16
            $classNames['size'] = $this->expandSize;
154
        }
155
156 19
        if (empty($options['class'])) {
157 18
            $classNames = array_merge($classNames, ['navbar-light', 'bg-light']);
158
        }
159
160 19
        Html::addCssClass($options, $classNames);
161
162 19
        if (!isset($this->innerContainerOptions['class'])) {
163 17
            Html::addCssClass($this->innerContainerOptions, ['innerContainerOptions' => 'container']);
164
        }
165
166 19
        $htmlStart = Html::openTag($navTag, $options);
167
168 19
        if ($this->renderInnerContainer) {
169 18
            $htmlStart .= Html::openTag('div', $this->innerContainerOptions);
170
        }
171
172 19
        $htmlStart .= $this->renderBrand();
173
174 19
        if ($offcanvas) {
175 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

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