Passed
Pull Request — master (#127)
by
unknown
02:46
created

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