Test Failed
Pull Request — master (#127)
by
unknown
13:20
created

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

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

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