1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Yiisoft\Yii\Bootstrap5; |
6
|
|
|
|
7
|
|
|
use JsonException; |
8
|
|
|
use Stringable; |
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
|
|
|
* <?= 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 |
78
|
|
|
* ->getUri() |
79
|
|
|
* ->getPath(); |
80
|
|
|
* $output = $this->render('index', ['currentPath' => $currentPath]); |
81
|
|
|
* $response |
82
|
|
|
* ->getBody() |
83
|
|
|
* ->write($output); |
84
|
|
|
* |
85
|
|
|
* return $response; |
86
|
|
|
* } |
87
|
|
|
* ``` |
88
|
|
|
* |
89
|
|
|
* Controller.php |
90
|
|
|
* |
91
|
|
|
* ```php |
92
|
|
|
* private function renderContent($content, array $parameters = []): string |
93
|
|
|
* { |
94
|
|
|
* $user = $this->user->getIdentity(); |
95
|
|
|
* $layout = $this->findLayoutFile($this->layout); |
96
|
|
|
* |
97
|
|
|
* if ($layout !== null) { |
98
|
|
|
* return $this->view->renderFile( |
99
|
|
|
* $layout, |
100
|
|
|
* [ |
101
|
|
|
* 'aliases' => $this->aliases, |
102
|
|
|
* 'content' => $content, |
103
|
|
|
* 'user' => $user, |
104
|
|
|
* 'params' => $this->params, |
105
|
|
|
* 'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath'] |
106
|
|
|
* ], |
107
|
|
|
* $this |
108
|
|
|
* ); |
109
|
|
|
* } |
110
|
|
|
* |
111
|
|
|
* return $content; |
112
|
|
|
* } |
113
|
|
|
* ``` |
114
|
|
|
*/ |
115
|
|
|
final class NavBar extends Widget |
116
|
|
|
{ |
117
|
|
|
public const EXPAND_SM = 'navbar-expand-sm'; |
118
|
|
|
public const EXPAND_MD = 'navbar-expand-md'; |
119
|
|
|
public const EXPAND_LG = 'navbar-expand-lg'; |
120
|
|
|
public const EXPAND_XL = 'navbar-expand-xl'; |
121
|
|
|
public const EXPAND_XXL = 'navbar-expand-xxl'; |
122
|
|
|
|
123
|
|
|
private array $collapseOptions = []; |
124
|
|
|
private ?string $brandText = null; |
125
|
|
|
private ?string $brandImage = null; |
126
|
|
|
private array $brandImageAttributes = []; |
127
|
|
|
private ?string $brandUrl = '/'; |
128
|
|
|
private array $brandOptions = []; |
129
|
|
|
private string $screenReaderToggleText = 'Toggle navigation'; |
130
|
|
|
private string $togglerContent = '<span class="navbar-toggler-icon"></span>'; |
131
|
|
|
private array $togglerOptions = []; |
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 $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
|
1 |
|
$options['data-bs-theme'] = $this->theme; |
162
|
|
|
|
163
|
1 |
|
if ($this->theme === self::THEME_DARK) { |
164
|
1 |
|
$classNames['theme'] = 'navbar-dark'; |
165
|
1 |
|
} elseif ($this->theme === self::THEME_LIGHT) { |
166
|
1 |
|
$classNames['theme'] = 'navbar-light'; |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
|
170
|
20 |
|
Html::addCssClass($options, $classNames); |
171
|
|
|
|
172
|
20 |
|
if (!isset($this->innerContainerOptions['class'])) { |
173
|
18 |
|
Html::addCssClass($this->innerContainerOptions, ['innerContainerOptions' => 'container']); |
174
|
|
|
} |
175
|
|
|
|
176
|
20 |
|
$htmlStart = Html::openTag($navTag, $options); |
177
|
|
|
|
178
|
20 |
|
if ($this->renderInnerContainer) { |
179
|
19 |
|
$htmlStart .= Html::openTag('div', $this->innerContainerOptions); |
180
|
|
|
} |
181
|
|
|
|
182
|
20 |
|
$htmlStart .= $this->renderBrand(); |
183
|
|
|
|
184
|
20 |
|
if ($offcanvas) { |
185
|
2 |
|
$offcanvasId = $this->offcanvas ? $this->offcanvas->getId() : null; |
186
|
2 |
|
$htmlStart .= $this->renderToggleButton($offcanvasId); |
187
|
2 |
|
$htmlStart .= $offcanvas; |
188
|
18 |
|
} elseif ($this->expandSize) { |
189
|
16 |
|
$collapseOptions = $this->collapseOptions; |
190
|
16 |
|
$collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div'); |
191
|
|
|
|
192
|
16 |
|
if (!isset($collapseOptions['id'])) { |
193
|
16 |
|
$collapseOptions['id'] = $options['id'] . '-collapse'; |
194
|
|
|
} |
195
|
|
|
|
196
|
16 |
|
Html::addCssClass($collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']); |
197
|
|
|
|
198
|
16 |
|
$htmlStart .= $this->renderToggleButton($collapseOptions['id']); |
199
|
16 |
|
$htmlStart .= Html::openTag($collapseTag, $collapseOptions); |
200
|
2 |
|
} elseif ($this->togglerOptions) { |
|
|
|
|
201
|
1 |
|
$htmlStart .= $this->renderToggleButton(null); |
202
|
|
|
} |
203
|
|
|
|
204
|
20 |
|
return $htmlStart; |
205
|
|
|
} |
206
|
|
|
|
207
|
20 |
|
public function render(): string |
208
|
|
|
{ |
209
|
20 |
|
$htmlRun = ''; |
210
|
|
|
|
211
|
20 |
|
if ($this->offcanvas) { |
212
|
2 |
|
$htmlRun = $this->offcanvas::end(); |
213
|
18 |
|
} elseif ($this->expandSize) { |
214
|
16 |
|
$tag = ArrayHelper::getValue($this->collapseOptions, 'tag', 'div'); |
215
|
16 |
|
$htmlRun = Html::closeTag($tag); |
216
|
|
|
} |
217
|
|
|
|
218
|
20 |
|
if ($this->renderInnerContainer) { |
219
|
19 |
|
$htmlRun .= Html::closeTag('div'); |
220
|
|
|
} |
221
|
|
|
|
222
|
20 |
|
$tag = ArrayHelper::getValue($this->options, 'tag', 'nav'); |
223
|
|
|
|
224
|
20 |
|
$htmlRun .= Html::closeTag($tag); |
225
|
|
|
|
226
|
20 |
|
return $htmlRun; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Set size before then content will be expanded |
231
|
|
|
*/ |
232
|
5 |
|
public function expandSize(?string $size): self |
233
|
|
|
{ |
234
|
5 |
|
$new = clone $this; |
235
|
5 |
|
$new->expandSize = $size; |
236
|
|
|
|
237
|
5 |
|
return $new; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* The HTML attributes for the container tag. The following special options are recognized. |
242
|
|
|
* |
243
|
|
|
* {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
244
|
|
|
*/ |
245
|
1 |
|
public function collapseOptions(array $value): self |
246
|
|
|
{ |
247
|
1 |
|
$new = clone $this; |
248
|
1 |
|
$new->collapseOptions = $value; |
249
|
|
|
|
250
|
1 |
|
return $new; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Set/remove Offcanvas::widget instead of collapse |
255
|
|
|
*/ |
256
|
2 |
|
public function offcanvas(?Offcanvas $offcanvas): self |
257
|
|
|
{ |
258
|
2 |
|
$new = clone $this; |
259
|
2 |
|
$new->offcanvas = $offcanvas; |
260
|
|
|
|
261
|
2 |
|
return $new; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* The text of the brand or empty if it's not used. Note that this is not HTML-encoded. |
266
|
|
|
* |
267
|
|
|
* @link https://getbootstrap.com/docs/5.0/components/navbar/#text |
268
|
|
|
*/ |
269
|
7 |
|
public function brandText(?string $value): self |
270
|
|
|
{ |
271
|
7 |
|
$new = clone $this; |
272
|
7 |
|
$new->brandText = $value; |
273
|
|
|
|
274
|
7 |
|
return $new; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Src of the brand image or empty if it's not used. Note that this param will override `$this->brandText` param. |
279
|
|
|
* |
280
|
|
|
* @link https://getbootstrap.com/docs/5.0/components/navbar/#image |
281
|
|
|
*/ |
282
|
3 |
|
public function brandImage(?string $value): self |
283
|
|
|
{ |
284
|
3 |
|
$new = clone $this; |
285
|
3 |
|
$new->brandImage = $value; |
286
|
|
|
|
287
|
3 |
|
return $new; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Set attributes for brandImage |
292
|
|
|
* |
293
|
|
|
* {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
294
|
|
|
*/ |
295
|
1 |
|
public function brandImageAttributes(array $attributes): self |
296
|
|
|
{ |
297
|
1 |
|
$new = clone $this; |
298
|
1 |
|
$new->brandImageAttributes = $attributes; |
299
|
|
|
|
300
|
1 |
|
return $new; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* The URL for the brand's hyperlink tag and will be used for the "href" attribute of the brand link. Default value |
305
|
|
|
* is "/". You may set it to empty string if you want no link at all. |
306
|
|
|
* |
307
|
|
|
* @link https://getbootstrap.com/docs/5.0/components/navbar/#text |
308
|
|
|
*/ |
309
|
6 |
|
public function brandUrl(?string $value): self |
310
|
|
|
{ |
311
|
6 |
|
$new = clone $this; |
312
|
6 |
|
$new->brandUrl = $value; |
313
|
|
|
|
314
|
6 |
|
return $new; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* The HTML attributes of the brand link. |
319
|
|
|
* |
320
|
|
|
* {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
321
|
|
|
*/ |
322
|
1 |
|
public function brandOptions(array $value): self |
323
|
|
|
{ |
324
|
1 |
|
$new = clone $this; |
325
|
1 |
|
$new->brandOptions = $value; |
326
|
|
|
|
327
|
1 |
|
return $new; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Text to show for screen readers for the button to toggle the navbar. |
332
|
|
|
*/ |
333
|
1 |
|
public function screenReaderToggleText(string $value): self |
334
|
|
|
{ |
335
|
1 |
|
$new = clone $this; |
336
|
1 |
|
$new->screenReaderToggleText = $value; |
337
|
|
|
|
338
|
1 |
|
return $new; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* The toggle button content. Defaults to bootstrap 4 default `<span class="navbar-toggler-icon"></span>`. |
343
|
|
|
*/ |
344
|
1 |
|
public function togglerContent(string $value): self |
345
|
|
|
{ |
346
|
1 |
|
$new = clone $this; |
347
|
1 |
|
$new->togglerContent = $value; |
348
|
|
|
|
349
|
1 |
|
return $new; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* The HTML attributes of the navbar toggler button. |
354
|
|
|
* |
355
|
|
|
* {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
356
|
|
|
*/ |
357
|
2 |
|
public function togglerOptions(array $value): self |
358
|
|
|
{ |
359
|
2 |
|
$new = clone $this; |
360
|
2 |
|
$new->togglerOptions = $value; |
361
|
|
|
|
362
|
2 |
|
return $new; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* This for a 100% width navbar. |
367
|
|
|
*/ |
368
|
1 |
|
public function withoutRenderInnerContainer(): self |
369
|
|
|
{ |
370
|
1 |
|
$new = clone $this; |
371
|
1 |
|
$new->renderInnerContainer = false; |
372
|
|
|
|
373
|
1 |
|
return $new; |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* The HTML attributes of the inner container. |
378
|
|
|
* |
379
|
|
|
* {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
380
|
|
|
*/ |
381
|
2 |
|
public function innerContainerOptions(array $value): self |
382
|
|
|
{ |
383
|
2 |
|
$new = clone $this; |
384
|
2 |
|
$new->innerContainerOptions = $value; |
385
|
|
|
|
386
|
2 |
|
return $new; |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* The HTML attributes for the widget container tag. The following special options are recognized. |
391
|
|
|
* |
392
|
|
|
* {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
393
|
|
|
*/ |
394
|
3 |
|
public function options(array $value): self |
395
|
|
|
{ |
396
|
3 |
|
$new = clone $this; |
397
|
3 |
|
$new->options = $value; |
398
|
|
|
|
399
|
3 |
|
return $new; |
400
|
|
|
} |
401
|
|
|
|
402
|
20 |
|
private function renderBrand(): string |
403
|
|
|
{ |
404
|
20 |
|
if (empty($this->brandImage) && empty($this->brandText)) { |
405
|
11 |
|
return ''; |
406
|
|
|
} |
407
|
|
|
|
408
|
9 |
|
$content = ''; |
409
|
9 |
|
$options = $this->brandOptions; |
410
|
9 |
|
$encode = ArrayHelper::remove($options, 'encode', $this->encodeTags); |
411
|
|
|
|
412
|
9 |
|
Html::addCssClass($options, ['widget' => 'navbar-brand']); |
413
|
|
|
|
414
|
9 |
|
if (!empty($this->brandImage)) { |
415
|
3 |
|
$encode = false; |
416
|
3 |
|
$content = Html::img($this->brandImage)->addAttributes($this->brandImageAttributes); |
417
|
|
|
} |
418
|
|
|
|
419
|
9 |
|
if (!empty($this->brandText)) { |
420
|
7 |
|
$content .= $this->brandText; |
421
|
|
|
} |
422
|
|
|
/** @var string|Stringable $content */ |
423
|
9 |
|
if (empty($this->brandUrl)) { |
424
|
1 |
|
$brand = Html::span($content, $options); |
425
|
|
|
} else { |
426
|
8 |
|
$brand = Html::a($content, $this->brandUrl, $options); |
427
|
|
|
} |
428
|
|
|
|
429
|
9 |
|
return $brand |
430
|
9 |
|
->encode($encode) |
431
|
9 |
|
->render(); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* Renders collapsible toggle button. |
436
|
|
|
* |
437
|
|
|
* @param string|null $targetId - ID of target element for current button |
438
|
|
|
* |
439
|
|
|
* @throws JsonException |
440
|
|
|
* |
441
|
|
|
* @return string the rendering toggle button. |
442
|
|
|
* |
443
|
|
|
* @link https://getbootstrap.com/docs/5.0/components/navbar/#toggler |
444
|
|
|
*/ |
445
|
19 |
|
private function renderToggleButton(?string $targetId): string |
446
|
|
|
{ |
447
|
19 |
|
$options = $this->togglerOptions; |
448
|
19 |
|
$encode = ArrayHelper::remove($options, 'encode', $this->encodeTags); |
449
|
19 |
|
Html::addCssClass($options, ['widget' => 'navbar-toggler']); |
450
|
|
|
|
451
|
19 |
|
$defauts = [ |
452
|
19 |
|
'type' => 'button', |
453
|
19 |
|
'data' => [ |
454
|
19 |
|
'bs-toggle' => $this->offcanvas ? 'offcanvas' : 'collapse', |
455
|
19 |
|
], |
456
|
19 |
|
'aria' => [ |
457
|
19 |
|
'controls' => $targetId, |
458
|
19 |
|
'expanded' => 'false', |
459
|
19 |
|
'label' => $this->screenReaderToggleText, |
460
|
19 |
|
], |
461
|
19 |
|
]; |
462
|
|
|
|
463
|
19 |
|
if ($targetId) { |
464
|
18 |
|
$defauts['data']['bs-target'] = '#' . $targetId; |
465
|
|
|
} |
466
|
|
|
|
467
|
19 |
|
return Html::button( |
468
|
19 |
|
$this->togglerContent, |
469
|
19 |
|
ArrayHelper::merge($defauts, $options) |
470
|
19 |
|
) |
471
|
19 |
|
->encode($encode) |
472
|
19 |
|
->render(); |
473
|
|
|
} |
474
|
|
|
} |
475
|
|
|
|
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.