yiisoft /
yii-bulma
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace Yiisoft\Yii\Bulma; |
||
| 6 | |||
| 7 | use InvalidArgumentException; |
||
| 8 | use Yiisoft\Definitions\Exception\CircularReferenceException; |
||
| 9 | use Yiisoft\Definitions\Exception\InvalidConfigException; |
||
| 10 | use Yiisoft\Definitions\Exception\NotInstantiableException; |
||
| 11 | use Yiisoft\Factory\NotFoundException; |
||
| 12 | use Yiisoft\Html\Html; |
||
| 13 | use Yiisoft\Html\Tag\A; |
||
| 14 | use Yiisoft\Html\Tag\CustomTag; |
||
| 15 | use Yiisoft\Html\Tag\Div; |
||
| 16 | use Yiisoft\Html\Tag\Span; |
||
| 17 | use Yiisoft\Widget\Widget; |
||
| 18 | |||
| 19 | use function implode; |
||
| 20 | use function is_array; |
||
| 21 | use function is_string; |
||
| 22 | |||
| 23 | /** |
||
| 24 | * Nav renders a nav HTML component. |
||
| 25 | * |
||
| 26 | * @link https://bulma.io/documentation/components/navbar/#basic-navbar |
||
| 27 | */ |
||
| 28 | final class Nav extends Widget |
||
| 29 | { |
||
| 30 | private bool $activateItems = true; |
||
| 31 | private bool $activateParents = false; |
||
| 32 | private array $attributes = []; |
||
| 33 | private string $currentPath = ''; |
||
| 34 | private string $dropdownCssClass = 'navbar-dropdown'; |
||
| 35 | private string $endCssClass = 'navbar-end'; |
||
| 36 | private bool $enclosedByStartMenu = false; |
||
| 37 | private bool $enclosedByEndMenu = false; |
||
| 38 | private string $hasDropdownCssClass = 'has-dropdown'; |
||
| 39 | private string $isHoverableCssClass = 'is-hoverable'; |
||
| 40 | private string $itemCssClass = 'navbar-item'; |
||
| 41 | private array $items = []; |
||
| 42 | private string $linkCssClass = 'navbar-link'; |
||
| 43 | private string $menuCssClass = 'navbar-menu'; |
||
| 44 | private string $startCssClass = 'navbar-start'; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * Returns a new instance with the specified HTML attributes for widget. |
||
| 48 | * |
||
| 49 | * @param array $values Attribute values indexed by attribute names. |
||
| 50 | * |
||
| 51 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} For details on how attributes are being rendered. |
||
| 52 | */ |
||
| 53 | 1 | public function attributes(array $values): self |
|
| 54 | { |
||
| 55 | 1 | $new = clone $this; |
|
| 56 | 1 | $new->attributes = $values; |
|
| 57 | 1 | return $new; |
|
| 58 | } |
||
| 59 | |||
| 60 | /** |
||
| 61 | * Returns a new instance with the specified whether to activate parent menu items when one of the corresponding |
||
| 62 | * child menu items is active. |
||
| 63 | */ |
||
| 64 | 2 | public function activateParents(): self |
|
| 65 | { |
||
| 66 | 2 | $new = clone $this; |
|
| 67 | 2 | $new->activateParents = true; |
|
| 68 | 2 | return $new; |
|
| 69 | } |
||
| 70 | |||
| 71 | /** |
||
| 72 | * Returns a new instance with the specified allows you to assign the current path of the url from request |
||
| 73 | * controller. |
||
| 74 | * |
||
| 75 | * @param string $value The current path. |
||
| 76 | */ |
||
| 77 | 3 | public function currentPath(string $value): self |
|
| 78 | { |
||
| 79 | 3 | $new = clone $this; |
|
| 80 | 3 | $new->currentPath = $value; |
|
| 81 | 3 | return $new; |
|
| 82 | } |
||
| 83 | |||
| 84 | /** |
||
| 85 | * Returns a new instance with the specified align the menu items to the right. |
||
| 86 | * |
||
| 87 | * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end |
||
| 88 | */ |
||
| 89 | 2 | public function enclosedByEndMenu(): self |
|
| 90 | { |
||
| 91 | 2 | $new = clone $this; |
|
| 92 | 2 | $new->enclosedByEndMenu = true; |
|
| 93 | 2 | return $new; |
|
| 94 | } |
||
| 95 | |||
| 96 | /** |
||
| 97 | * Returns a new instance with the specified align the menu items to left. |
||
| 98 | * |
||
| 99 | * @link https://bulma.io/documentation/components/navbar/#navbar-start-and-navbar-end |
||
| 100 | */ |
||
| 101 | 2 | public function enclosedByStartMenu(): self |
|
| 102 | { |
||
| 103 | 2 | $new = clone $this; |
|
| 104 | 2 | $new->enclosedByStartMenu = true; |
|
| 105 | 2 | return $new; |
|
| 106 | } |
||
| 107 | |||
| 108 | /** |
||
| 109 | * Returns a new instance with the specified items. |
||
| 110 | * |
||
| 111 | * Each array element represents a single menu item which can be either a string |
||
| 112 | * or an array with the following structure: |
||
| 113 | * |
||
| 114 | * - label: string, required, the nav item label. |
||
| 115 | * - url: optional, the item's URL. Defaults to "#". |
||
| 116 | * - urlAttributes: optional, the attributes to be rendered in the item's URL. |
||
| 117 | * - visible: bool, optional, whether this menu item is visible. Defaults to true. |
||
| 118 | * - linkAttributes: array, optional, the HTML attributes of the item's link. |
||
| 119 | * - active: bool, optional, whether the item should be on active state or not. |
||
| 120 | * - disable: bool, optional, whether the item should be disabled. |
||
| 121 | * - dropdownAttributes: array, optional, the HTML options that will be passed to the {@see Dropdown} widget. |
||
| 122 | * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string |
||
| 123 | * representing the dropdown menu. |
||
| 124 | * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for |
||
| 125 | * only this item. |
||
| 126 | * - iconAttributes: array, optional, the HTML attributes of the item's icon. |
||
| 127 | * - iconCssClass: string, optional, the icon CSS class. |
||
| 128 | * - iconText: string, optional, the icon text. |
||
| 129 | * |
||
| 130 | * If a menu item is a string, it will be rendered directly without HTML encoding. |
||
| 131 | * |
||
| 132 | * @param array $value The menu items. |
||
| 133 | */ |
||
| 134 | 17 | public function items(array $value): self |
|
| 135 | { |
||
| 136 | 17 | $new = clone $this; |
|
| 137 | 17 | $new->items = $value; |
|
| 138 | 17 | return $new; |
|
| 139 | } |
||
| 140 | |||
| 141 | /** |
||
| 142 | * Returns a new instance with the specified disable activate items according to whether their currentPath. |
||
| 143 | * |
||
| 144 | * {@see isItemActive} |
||
| 145 | */ |
||
| 146 | 3 | public function withoutActivateItems(): self |
|
| 147 | { |
||
| 148 | 3 | $new = clone $this; |
|
| 149 | 3 | $new->activateItems = false; |
|
| 150 | 3 | return $new; |
|
| 151 | } |
||
| 152 | |||
| 153 | /** |
||
| 154 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
| 155 | */ |
||
| 156 | 16 | public function render(): string |
|
| 157 | { |
||
| 158 | 16 | return $this->renderNav(); |
|
| 159 | } |
||
| 160 | |||
| 161 | /** |
||
| 162 | * Renders the given items as a dropdown. |
||
| 163 | * |
||
| 164 | * This method is called to create sub-menus. |
||
| 165 | * |
||
| 166 | * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure. |
||
| 167 | * |
||
| 168 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
| 169 | * |
||
| 170 | * @return string the rendering result. |
||
| 171 | * |
||
| 172 | * @link https://bulma.io/documentation/components/navbar/#dropdown-menu |
||
| 173 | */ |
||
| 174 | 8 | private function renderDropdown(array $items): string |
|
| 175 | { |
||
| 176 | 8 | return Dropdown::widget() |
|
| 177 | 8 | ->cssClass('navbar-dropdown') |
|
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 178 | 8 | ->dividerCssClass('navbar-divider') |
|
| 179 | 8 | ->enclosedByContainer() |
|
| 180 | 8 | ->itemCssClass('navbar-item') |
|
| 181 | 8 | ->items($items) |
|
| 182 | 8 | ->render() . PHP_EOL; |
|
| 183 | } |
||
| 184 | |||
| 185 | /** |
||
| 186 | * Check to see if a child item is active optionally activating the parent. |
||
| 187 | * |
||
| 188 | * @param array $items {@see items} |
||
| 189 | * @param bool $active Should the parent be active too. |
||
| 190 | * |
||
| 191 | * {@see items} |
||
| 192 | */ |
||
| 193 | 8 | private function isChildActive(array $items, bool &$active = false): array |
|
| 194 | { |
||
| 195 | /** |
||
| 196 | * @psalm-var array< |
||
| 197 | * string, |
||
| 198 | * array{ |
||
| 199 | * active?: bool, |
||
| 200 | * disable?: bool, |
||
| 201 | * encode?: bool, |
||
| 202 | * icon?: string, |
||
| 203 | * iconAttributes?: array, |
||
| 204 | * items?: array, |
||
| 205 | * label: string, |
||
| 206 | * url: string, |
||
| 207 | * visible?: bool |
||
| 208 | * }|string> $items |
||
| 209 | */ |
||
| 210 | 8 | foreach ($items as $i => $child) { |
|
| 211 | 8 | $url = $child['url'] ?? '#'; |
|
| 212 | 8 | $active = $child['active'] ?? false; |
|
| 213 | |||
| 214 | 8 | if ($active === false && is_array($child)) { |
|
| 215 | 8 | $child['active'] = $this->isItemActive($url, $this->currentPath, $this->activateItems); |
|
| 216 | } |
||
| 217 | |||
| 218 | 8 | if ($this->activateParents) { |
|
| 219 | 1 | $active = true; |
|
| 220 | } |
||
| 221 | |||
| 222 | 8 | $childItems = $child['items'] ?? []; |
|
| 223 | |||
| 224 | 8 | if ($childItems !== [] && is_array($items[$i])) { |
|
| 225 | 1 | $items[$i]['items'] = $this->isChildActive($childItems); |
|
| 226 | |||
| 227 | 1 | if ($active) { |
|
| 228 | 1 | $items[$i]['attributes'] = ['active' => true]; |
|
| 229 | 1 | $active = true; |
|
| 230 | } |
||
| 231 | } |
||
| 232 | } |
||
| 233 | |||
| 234 | 8 | return $items; |
|
| 235 | } |
||
| 236 | |||
| 237 | /** |
||
| 238 | * Checks whether a menu item is active. |
||
| 239 | * |
||
| 240 | * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When |
||
| 241 | * the `url` option of a menu item is specified in terms of an array, its first element is treated as the |
||
| 242 | * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath |
||
| 243 | * and parameters match {@see currentPath}, respectively, will a menu item be considered active. |
||
| 244 | * |
||
| 245 | * @param string $url The menu item's URL. |
||
| 246 | * @param string $currentPath The currentPath. |
||
| 247 | * @param bool $activateItems Whether to activate the parent menu items when the currentPath matches. |
||
| 248 | * |
||
| 249 | * @return bool whether the menu item is active |
||
| 250 | */ |
||
| 251 | 15 | private function isItemActive(string $url, string $currentPath, bool $activateItems): bool |
|
| 252 | { |
||
| 253 | 15 | return ($currentPath !== '/') && ($url === $currentPath) && $activateItems; |
|
| 254 | } |
||
| 255 | |||
| 256 | 15 | private function renderLabelItem( |
|
| 257 | string $label, |
||
| 258 | string $iconText, |
||
| 259 | string $iconCssClass, |
||
| 260 | array $iconAttributes = [] |
||
| 261 | ): string { |
||
| 262 | 15 | $html = ''; |
|
| 263 | |||
| 264 | 15 | if ($iconText !== '' || $iconCssClass !== '') { |
|
| 265 | 1 | $html = Span::tag() |
|
| 266 | 1 | ->addAttributes($iconAttributes) |
|
| 267 | 1 | ->content(CustomTag::name('i') |
|
| 268 | 1 | ->addClass($iconCssClass) |
|
| 269 | 1 | ->content($iconText) |
|
| 270 | 1 | ->encode(false) |
|
| 271 | 1 | ->render()) |
|
| 272 | 1 | ->encode(false) |
|
| 273 | 1 | ->render(); |
|
| 274 | } |
||
| 275 | |||
| 276 | 15 | if ($label !== '') { |
|
| 277 | 15 | $html .= $label; |
|
| 278 | } |
||
| 279 | |||
| 280 | 15 | return $html; |
|
| 281 | } |
||
| 282 | |||
| 283 | /** |
||
| 284 | * Renders a widget's item. |
||
| 285 | * |
||
| 286 | * @param array $item the item to render. |
||
| 287 | * |
||
| 288 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
| 289 | * |
||
| 290 | * @return string the rendering result. |
||
| 291 | */ |
||
| 292 | 16 | private function renderItem(array $item): string |
|
| 293 | { |
||
| 294 | 16 | $html = ''; |
|
| 295 | |||
| 296 | 16 | if (!isset($item['label'])) { |
|
| 297 | 1 | throw new InvalidArgumentException('The "label" option is required.'); |
|
| 298 | } |
||
| 299 | |||
| 300 | /** @var string */ |
||
| 301 | 15 | $itemLabel = $item['label'] ?? ''; |
|
| 302 | |||
| 303 | 15 | if (isset($item['encode']) && $item['encode'] === true) { |
|
| 304 | 1 | $itemLabel = Html::encode($itemLabel); |
|
| 305 | } |
||
| 306 | |||
| 307 | /** @var array */ |
||
| 308 | 15 | $items = $item['items'] ?? []; |
|
| 309 | |||
| 310 | /** @var string */ |
||
| 311 | 15 | $url = $item['url'] ?? '#'; |
|
| 312 | |||
| 313 | /** @var array */ |
||
| 314 | 15 | $urlAttributes = $item['urlAttributes'] ?? []; |
|
| 315 | |||
| 316 | /** @var array */ |
||
| 317 | 15 | $dropdownAttributes = $item['dropdownAttributes'] ?? []; |
|
| 318 | |||
| 319 | /** @var string */ |
||
| 320 | 15 | $iconText = $item['iconText'] ?? ''; |
|
| 321 | |||
| 322 | /** @var string */ |
||
| 323 | 15 | $iconCssClass = $item['iconCssClass'] ?? ''; |
|
| 324 | |||
| 325 | /** @var array */ |
||
| 326 | 15 | $iconAttributes = $item['iconAttributes'] ?? []; |
|
| 327 | |||
| 328 | /** @var bool */ |
||
| 329 | 15 | $active = $item['active'] ?? $this->isItemActive($url, $this->currentPath, $this->activateItems); |
|
| 330 | |||
| 331 | /** @var bool */ |
||
| 332 | 15 | $disabled = $item['disabled'] ?? false; |
|
| 333 | |||
| 334 | 15 | $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes); |
|
| 335 | |||
| 336 | 15 | if ($disabled) { |
|
| 337 | 1 | Html::addCssStyle($urlAttributes, 'opacity:.65; pointer-events:none;'); |
|
| 338 | } |
||
| 339 | |||
| 340 | 15 | if ($this->activateItems && $active) { |
|
| 341 | 1 | Html::addCssClass($urlAttributes, ['active' => 'is-active']); |
|
| 342 | } |
||
| 343 | |||
| 344 | 15 | if ($items !== []) { |
|
| 345 | 8 | $attributes = $this->attributes; |
|
| 346 | 8 | Html::addCssClass( |
|
| 347 | 8 | $attributes, |
|
| 348 | 8 | [$this->itemCssClass, $this->hasDropdownCssClass, $this->isHoverableCssClass] |
|
| 349 | 8 | ); |
|
| 350 | 8 | Html::addCssClass($urlAttributes, $this->linkCssClass); |
|
| 351 | 8 | Html::addCssClass($dropdownAttributes, $this->dropdownCssClass); |
|
| 352 | |||
| 353 | 8 | $items = $this->isChildActive($items, $active); |
|
| 354 | 8 | $dropdown = PHP_EOL . $this->renderDropdown($items); |
|
| 355 | 8 | $a = A::tag() |
|
| 356 | 8 | ->attributes($urlAttributes) |
|
| 357 | 8 | ->content($itemLabel) |
|
| 358 | 8 | ->encode(false) |
|
| 359 | 8 | ->url($url) |
|
| 360 | 8 | ->render(); |
|
| 361 | 8 | $div = Div::tag() |
|
| 362 | 8 | ->attributes($dropdownAttributes) |
|
| 363 | 8 | ->content($dropdown) |
|
| 364 | 8 | ->encode(false) |
|
| 365 | 8 | ->render(); |
|
| 366 | 8 | $html = Div::tag() |
|
| 367 | 8 | ->attributes($attributes) |
|
| 368 | 8 | ->content(PHP_EOL . $a . PHP_EOL . $div . PHP_EOL) |
|
| 369 | 8 | ->encode(false) |
|
| 370 | 8 | ->render(); |
|
| 371 | } |
||
| 372 | |||
| 373 | 15 | if ($html === '') { |
|
| 374 | 11 | Html::addCssClass($urlAttributes, 'navbar-item'); |
|
| 375 | 11 | $html = A::tag() |
|
| 376 | 11 | ->attributes($urlAttributes) |
|
| 377 | 11 | ->content($itemLabel) |
|
| 378 | 11 | ->url($url) |
|
| 379 | 11 | ->encode(false) |
|
| 380 | 11 | ->render(); |
|
| 381 | } |
||
| 382 | |||
| 383 | 15 | return $html; |
|
| 384 | } |
||
| 385 | |||
| 386 | /** |
||
| 387 | * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException |
||
| 388 | */ |
||
| 389 | 16 | private function renderNav(): string |
|
| 390 | { |
||
| 391 | 16 | $items = []; |
|
| 392 | |||
| 393 | /** @var array|string $item */ |
||
| 394 | 16 | foreach ($this->items as $item) { |
|
| 395 | 16 | $visible = !isset($item['visible']) || $item['visible']; |
|
| 396 | |||
| 397 | 16 | if ($visible) { |
|
| 398 | 16 | $items[] = is_string($item) ? $item : $this->renderItem($item); |
|
| 399 | } |
||
| 400 | } |
||
| 401 | |||
| 402 | 15 | $links = PHP_EOL . implode(PHP_EOL, $items) . PHP_EOL; |
|
| 403 | |||
| 404 | 15 | if ($this->enclosedByStartMenu) { |
|
| 405 | 1 | $links = PHP_EOL . Div::tag() |
|
| 406 | 1 | ->class($this->startCssClass) |
|
| 407 | 1 | ->content($links) |
|
| 408 | 1 | ->encode(false) |
|
| 409 | 1 | ->render() . |
|
| 410 | 1 | PHP_EOL; |
|
| 411 | } |
||
| 412 | |||
| 413 | 15 | if ($this->enclosedByEndMenu) { |
|
| 414 | 1 | $links = PHP_EOL . Div::tag() |
|
| 415 | 1 | ->class($this->endCssClass) |
|
| 416 | 1 | ->content($links) |
|
| 417 | 1 | ->encode(false) |
|
| 418 | 1 | ->render() . |
|
| 419 | 1 | PHP_EOL; |
|
| 420 | } |
||
| 421 | |||
| 422 | 15 | return $this->items !== [] |
|
| 423 | 15 | ? Div::tag() |
|
| 424 | 15 | ->class($this->menuCssClass) |
|
| 425 | 15 | ->content($links) |
|
| 426 | 15 | ->encode(false) |
|
| 427 | 15 | ->render() |
|
| 428 | 15 | : ''; |
|
| 429 | } |
||
| 430 | } |
||
| 431 |