yiisoft /
yii-bootstrap5
| 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 | use function array_merge; |
||
| 13 | |||
| 14 | /** |
||
| 15 | * Modal renders a modal window that can be toggled by clicking on a button. |
||
| 16 | * |
||
| 17 | * The following example will show the content enclosed between the {@see begin()} and {@see end()} calls within the |
||
| 18 | * modal window: |
||
| 19 | * |
||
| 20 | * ```php |
||
| 21 | * Modal::widget() |
||
| 22 | * ->title('Hello world') |
||
| 23 | * ->withToggleOptions(['label' => 'click me']) |
||
| 24 | * ->begin(); |
||
| 25 | * |
||
| 26 | * echo 'Say hello...'; |
||
| 27 | * |
||
| 28 | * echo Modal::end(); |
||
| 29 | * ``` |
||
| 30 | */ |
||
| 31 | final class Modal extends AbstractToggleWidget |
||
| 32 | { |
||
| 33 | use CloseButtonTrait; |
||
| 34 | |||
| 35 | /** |
||
| 36 | * Size classes |
||
| 37 | */ |
||
| 38 | public const SIZE_SMALL = 'modal-sm'; |
||
| 39 | public const SIZE_DEFAULT = null; |
||
| 40 | public const SIZE_LARGE = 'modal-lg'; |
||
| 41 | public const SIZE_EXTRA_LARGE = 'modal-xl'; |
||
| 42 | |||
| 43 | /** |
||
| 44 | * Fullsceen classes |
||
| 45 | */ |
||
| 46 | public const FULLSCREEN_ALWAYS = 'modal-fullscreen'; |
||
| 47 | public const FULLSCREEN_BELOW_SM = 'modal-fullscreen-sm-down'; |
||
| 48 | public const FULLSCREEN_BELOW_MD = 'modal-fullscreen-md-down'; |
||
| 49 | public const FULLSCREEN_BELOW_LG = 'modal-fullscreen-lg-down'; |
||
| 50 | public const FULLSCREEN_BELOW_XL = 'modal-fullscreen-xl-down'; |
||
| 51 | public const FULLSCREEN_BELOW_XXL = 'modal-fullscreen-xxl-down'; |
||
| 52 | |||
| 53 | private string|Stringable|null $title = null; |
||
| 54 | private array $titleOptions = []; |
||
| 55 | private array $headerOptions = []; |
||
| 56 | private array $dialogOptions = []; |
||
| 57 | private array $contentOptions = []; |
||
| 58 | private array $bodyOptions = []; |
||
| 59 | private ?string $footer = null; |
||
| 60 | private array $footerOptions = []; |
||
| 61 | private ?string $size = self::SIZE_DEFAULT; |
||
| 62 | private array $options = []; |
||
| 63 | private bool $encodeTags = false; |
||
| 64 | private bool $fade = true; |
||
| 65 | private bool $staticBackdrop = false; |
||
| 66 | private bool $scrollable = false; |
||
| 67 | private bool $centered = false; |
||
| 68 | private ?string $fullscreen = null; |
||
| 69 | protected string|Stringable $toggleLabel = 'Show'; |
||
| 70 | |||
| 71 | 28 | public function getId(?string $suffix = '-modal'): ?string |
|
| 72 | { |
||
| 73 | 28 | return $this->options['id'] ?? parent::getId($suffix); |
|
|
0 ignored issues
–
show
|
|||
| 74 | } |
||
| 75 | |||
| 76 | 28 | public function toggleComponent(): string |
|
| 77 | { |
||
| 78 | 28 | return 'modal'; |
|
| 79 | } |
||
| 80 | |||
| 81 | 4 | public function getTitleId(): string |
|
| 82 | { |
||
| 83 | 4 | return $this->titleOptions['id'] ?? $this->getId() . '-label'; |
|
| 84 | } |
||
| 85 | |||
| 86 | 27 | public function begin(): string |
|
| 87 | { |
||
| 88 | 27 | parent::begin(); |
|
| 89 | |||
| 90 | 27 | $options = $this->prepareOptions(); |
|
| 91 | 27 | $dialogOptions = $this->prepareDialogOptions(); |
|
| 92 | 27 | $contentOptions = $this->contentOptions; |
|
| 93 | 27 | $contentTag = ArrayHelper::remove($contentOptions, 'tag', 'div'); |
|
| 94 | 27 | $dialogTag = ArrayHelper::remove($dialogOptions, 'tag', 'div'); |
|
| 95 | |||
| 96 | 27 | Html::addCssClass($contentOptions, ['modal-content']); |
|
| 97 | |||
| 98 | 27 | return |
|
| 99 | 27 | ($this->renderToggle ? $this->renderToggle() : '') . |
|
| 100 | 27 | Html::openTag('div', $options) . |
|
| 101 | 27 | Html::openTag($dialogTag, $dialogOptions) . |
|
| 102 | 27 | Html::openTag($contentTag, $contentOptions) . |
|
| 103 | 27 | $this->renderHeader() . |
|
| 104 | 27 | $this->renderBodyBegin(); |
|
| 105 | } |
||
| 106 | |||
| 107 | 27 | public function render(): string |
|
| 108 | { |
||
| 109 | 27 | return |
|
| 110 | 27 | $this->renderBodyEnd() . |
|
| 111 | 27 | $this->renderFooter() . |
|
| 112 | 27 | Html::closeTag($this->contentOptions['tag'] ?? 'div') . // modal-content |
|
| 113 | 27 | Html::closeTag($this->dialogOptions['tag'] ?? 'div') . // modal-dialog |
|
| 114 | 27 | Html::closeTag('div'); |
|
| 115 | } |
||
| 116 | |||
| 117 | /** |
||
| 118 | * Prepare options for modal layer |
||
| 119 | */ |
||
| 120 | 27 | private function prepareOptions(): array |
|
| 121 | { |
||
| 122 | 27 | $options = array_merge([ |
|
| 123 | 27 | 'role' => 'dialog', |
|
| 124 | 27 | 'tabindex' => -1, |
|
| 125 | 27 | 'aria-hidden' => 'true', |
|
| 126 | 27 | ], $this->options); |
|
| 127 | |||
| 128 | 27 | $options['id'] = $this->getId(); |
|
| 129 | |||
| 130 | /** @psalm-suppress InvalidArgument */ |
||
| 131 | 27 | Html::addCssClass($options, ['widget' => 'modal']); |
|
| 132 | |||
| 133 | 27 | if ($this->fade) { |
|
| 134 | 26 | Html::addCssClass($options, ['animation' => 'fade']); |
|
| 135 | } |
||
| 136 | |||
| 137 | 27 | if (!isset($options['aria-label'], $options['aria-labelledby']) && !empty($this->title)) { |
|
| 138 | 3 | $options['aria-labelledby'] = $this->getTitleId(); |
|
| 139 | } |
||
| 140 | |||
| 141 | 27 | if ($this->staticBackdrop) { |
|
| 142 | 1 | $options['data-bs-backdrop'] = 'static'; |
|
| 143 | } |
||
| 144 | |||
| 145 | 27 | return $options; |
|
| 146 | } |
||
| 147 | |||
| 148 | /** |
||
| 149 | * Prepare options for dialog layer |
||
| 150 | */ |
||
| 151 | 27 | private function prepareDialogOptions(): array |
|
| 152 | { |
||
| 153 | 27 | $options = $this->dialogOptions; |
|
| 154 | 27 | $classNames = ['modal-dialog']; |
|
| 155 | |||
| 156 | 27 | if ($this->size) { |
|
| 157 | 3 | $classNames[] = $this->size; |
|
| 158 | } |
||
| 159 | |||
| 160 | 27 | if ($this->fullscreen) { |
|
| 161 | 6 | $classNames[] = $this->fullscreen; |
|
| 162 | } |
||
| 163 | |||
| 164 | 27 | if ($this->scrollable) { |
|
| 165 | 1 | $classNames[] = 'modal-dialog-scrollable'; |
|
| 166 | } |
||
| 167 | |||
| 168 | 27 | if ($this->centered) { |
|
| 169 | 2 | $classNames[] = 'modal-dialog-centered'; |
|
| 170 | } |
||
| 171 | |||
| 172 | 27 | Html::addCssClass($options, $classNames); |
|
| 173 | |||
| 174 | 27 | return $options; |
|
| 175 | } |
||
| 176 | |||
| 177 | /** |
||
| 178 | * Dialog layer options |
||
| 179 | */ |
||
| 180 | 1 | public function dialogOptions(array $options): self |
|
| 181 | { |
||
| 182 | 1 | $new = clone $this; |
|
| 183 | 1 | $new->dialogOptions = $options; |
|
| 184 | |||
| 185 | 1 | return $new; |
|
| 186 | } |
||
| 187 | |||
| 188 | /** |
||
| 189 | * Set options for content layer |
||
| 190 | */ |
||
| 191 | 1 | public function contentOptions(array $options): self |
|
| 192 | { |
||
| 193 | 1 | $new = clone $this; |
|
| 194 | 1 | $new->contentOptions = $options; |
|
| 195 | |||
| 196 | 1 | return $new; |
|
| 197 | } |
||
| 198 | |||
| 199 | /** |
||
| 200 | * Body options. |
||
| 201 | * |
||
| 202 | * {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
||
| 203 | */ |
||
| 204 | 2 | public function bodyOptions(array $value): self |
|
| 205 | { |
||
| 206 | 2 | $new = clone $this; |
|
| 207 | 2 | $new->bodyOptions = $value; |
|
| 208 | |||
| 209 | 2 | return $new; |
|
| 210 | } |
||
| 211 | |||
| 212 | /** |
||
| 213 | * The footer content in the modal window. |
||
| 214 | */ |
||
| 215 | 3 | public function footer(?string $value): self |
|
| 216 | { |
||
| 217 | 3 | $new = clone $this; |
|
| 218 | 3 | $new->footer = $value; |
|
| 219 | |||
| 220 | 3 | return $new; |
|
| 221 | } |
||
| 222 | |||
| 223 | /** |
||
| 224 | * Additional footer options. |
||
| 225 | * |
||
| 226 | * {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
||
| 227 | */ |
||
| 228 | 2 | public function footerOptions(array $value): self |
|
| 229 | { |
||
| 230 | 2 | $new = clone $this; |
|
| 231 | 2 | $new->footerOptions = $value; |
|
| 232 | |||
| 233 | 2 | return $new; |
|
| 234 | } |
||
| 235 | |||
| 236 | /** |
||
| 237 | * Additional header options. |
||
| 238 | * |
||
| 239 | * {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
||
| 240 | */ |
||
| 241 | 2 | public function headerOptions(array $value): self |
|
| 242 | { |
||
| 243 | 2 | $new = clone $this; |
|
| 244 | 2 | $new->headerOptions = $value; |
|
| 245 | |||
| 246 | 2 | return $new; |
|
| 247 | } |
||
| 248 | |||
| 249 | /** |
||
| 250 | * @param array $value the HTML attributes for the widget container tag. The following special options are |
||
| 251 | * recognized. |
||
| 252 | * |
||
| 253 | * {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
||
| 254 | */ |
||
| 255 | 1 | public function options(array $value): self |
|
| 256 | { |
||
| 257 | 1 | $new = clone $this; |
|
| 258 | 1 | $new->options = $value; |
|
| 259 | |||
| 260 | 1 | return $new; |
|
| 261 | } |
||
| 262 | |||
| 263 | /** |
||
| 264 | * The title content in the modal window. |
||
| 265 | */ |
||
| 266 | 4 | public function title(?string $value): self |
|
| 267 | { |
||
| 268 | 4 | $new = clone $this; |
|
| 269 | 4 | $new->title = $value; |
|
| 270 | |||
| 271 | 4 | return $new; |
|
| 272 | } |
||
| 273 | |||
| 274 | /** |
||
| 275 | * Additional title options. |
||
| 276 | * |
||
| 277 | * {@see Html::renderTagAttributes()} for details on how attributes are being rendered. |
||
| 278 | */ |
||
| 279 | 2 | public function titleOptions(array $value): self |
|
| 280 | { |
||
| 281 | 2 | $new = clone $this; |
|
| 282 | 2 | $new->titleOptions = $value; |
|
| 283 | |||
| 284 | 2 | return $new; |
|
| 285 | } |
||
| 286 | |||
| 287 | /** |
||
| 288 | * The modal size. Can be {@see SIZE_LARGE} or {@see SIZE_SMALL}, or null for default. |
||
| 289 | * |
||
| 290 | * @link https://getbootstrap.com/docs/5.1/components/modal/#optional-sizes |
||
| 291 | */ |
||
| 292 | 3 | public function size(?string $value): self |
|
| 293 | { |
||
| 294 | 3 | $new = clone $this; |
|
| 295 | 3 | $new->size = $value; |
|
| 296 | |||
| 297 | 3 | return $new; |
|
| 298 | } |
||
| 299 | |||
| 300 | /** |
||
| 301 | * Enable/disable static backdrop |
||
| 302 | * |
||
| 303 | * @link https://getbootstrap.com/docs/5.1/components/modal/#static-backdrop |
||
| 304 | */ |
||
| 305 | 1 | public function staticBackdrop(bool $value = true): self |
|
| 306 | { |
||
| 307 | 1 | if ($value === $this->staticBackdrop) { |
|
| 308 | return $this; |
||
| 309 | } |
||
| 310 | |||
| 311 | 1 | $new = clone $this; |
|
| 312 | 1 | $new->staticBackdrop = $value; |
|
| 313 | |||
| 314 | 1 | return $new; |
|
| 315 | } |
||
| 316 | |||
| 317 | /** |
||
| 318 | * Enable/Disable scrolling long content |
||
| 319 | * |
||
| 320 | * @link https://getbootstrap.com/docs/5.1/components/modal/#scrolling-long-content |
||
| 321 | */ |
||
| 322 | 1 | public function scrollable(bool $scrollable = true): self |
|
| 323 | { |
||
| 324 | 1 | if ($scrollable === $this->scrollable) { |
|
| 325 | return $this; |
||
| 326 | } |
||
| 327 | |||
| 328 | 1 | $new = clone $this; |
|
| 329 | 1 | $new->scrollable = $scrollable; |
|
| 330 | |||
| 331 | 1 | return $new; |
|
| 332 | } |
||
| 333 | |||
| 334 | /** |
||
| 335 | * Enable/Disable vertically centered |
||
| 336 | * |
||
| 337 | * @link https://getbootstrap.com/docs/5.1/components/modal/#vertically-centered |
||
| 338 | */ |
||
| 339 | 2 | public function centered(bool $centered = true): self |
|
| 340 | { |
||
| 341 | 2 | if ($centered === $this->centered) { |
|
| 342 | return $this; |
||
| 343 | } |
||
| 344 | |||
| 345 | 2 | $new = clone $this; |
|
| 346 | 2 | $new->centered = $centered; |
|
| 347 | |||
| 348 | 2 | return $new; |
|
| 349 | } |
||
| 350 | |||
| 351 | /** |
||
| 352 | * Set/remove fade animation |
||
| 353 | * |
||
| 354 | * @link https://getbootstrap.com/docs/5.1/components/modal/#remove-animation |
||
| 355 | */ |
||
| 356 | 1 | public function fade(bool $fade = true): self |
|
| 357 | { |
||
| 358 | 1 | $new = clone $this; |
|
| 359 | 1 | $new->fade = $fade; |
|
| 360 | |||
| 361 | 1 | return $new; |
|
| 362 | } |
||
| 363 | |||
| 364 | /** |
||
| 365 | * Enable/disable fullscreen mode |
||
| 366 | * |
||
| 367 | * @link https://getbootstrap.com/docs/5.1/components/modal/#fullscreen-modal |
||
| 368 | */ |
||
| 369 | 6 | public function fullscreen(?string $fullscreen): self |
|
| 370 | { |
||
| 371 | 6 | $new = clone $this; |
|
| 372 | 6 | $new->fullscreen = $fullscreen; |
|
| 373 | |||
| 374 | 6 | return $new; |
|
| 375 | } |
||
| 376 | |||
| 377 | /** |
||
| 378 | * Renders the header HTML markup of the modal. |
||
| 379 | * |
||
| 380 | * @throws JsonException |
||
| 381 | * |
||
| 382 | * @return string the rendering result |
||
| 383 | */ |
||
| 384 | 27 | private function renderHeader(): string |
|
| 385 | { |
||
| 386 | 27 | $title = (string) $this->renderTitle(); |
|
| 387 | 27 | $button = (string) $this->renderCloseButton(true); |
|
| 388 | |||
| 389 | 27 | if ($button === '' && $title === '') { |
|
| 390 | 1 | return ''; |
|
| 391 | } |
||
| 392 | |||
| 393 | 26 | $options = $this->headerOptions; |
|
| 394 | 26 | $tag = ArrayHelper::remove($options, 'tag', 'div'); |
|
| 395 | 26 | $content = $title . $button; |
|
| 396 | |||
| 397 | 26 | Html::addCssClass($options, ['headerOptions' => 'modal-header']); |
|
| 398 | |||
| 399 | 26 | return Html::tag($tag, $content, $options) |
|
| 400 | 26 | ->encode(false) |
|
| 401 | 26 | ->render(); |
|
| 402 | } |
||
| 403 | |||
| 404 | /** |
||
| 405 | * Render title HTML markup |
||
| 406 | */ |
||
| 407 | 27 | private function renderTitle(): ?string |
|
| 408 | { |
||
| 409 | 27 | if ($this->title === null) { |
|
| 410 | 23 | return ''; |
|
| 411 | } |
||
| 412 | |||
| 413 | 4 | $options = $this->titleOptions; |
|
| 414 | 4 | $options['id'] = $this->getTitleId(); |
|
| 415 | 4 | $tag = ArrayHelper::remove($options, 'tag', 'h5'); |
|
| 416 | 4 | $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags); |
|
| 417 | |||
| 418 | 4 | Html::addCssClass($options, ['modal-title']); |
|
| 419 | |||
| 420 | 4 | return Html::tag($tag, $this->title, $options) |
|
| 421 | 4 | ->encode($encode) |
|
| 422 | 4 | ->render(); |
|
| 423 | } |
||
| 424 | |||
| 425 | /** |
||
| 426 | * Renders the opening tag of the modal body. |
||
| 427 | * |
||
| 428 | * @throws JsonException |
||
| 429 | * |
||
| 430 | * @return string the rendering result |
||
| 431 | */ |
||
| 432 | 27 | private function renderBodyBegin(): string |
|
| 433 | { |
||
| 434 | 27 | $options = $this->bodyOptions; |
|
| 435 | 27 | $tag = ArrayHelper::remove($options, 'tag', 'div'); |
|
| 436 | |||
| 437 | 27 | Html::addCssClass($options, ['widget' => 'modal-body']); |
|
| 438 | |||
| 439 | 27 | return Html::openTag($tag, $options); |
|
| 440 | } |
||
| 441 | |||
| 442 | /** |
||
| 443 | * Renders the closing tag of the modal body. |
||
| 444 | * |
||
| 445 | * @return string the rendering result |
||
| 446 | */ |
||
| 447 | 27 | private function renderBodyEnd(): string |
|
| 448 | { |
||
| 449 | 27 | $tag = ArrayHelper::getValue($this->bodyOptions, 'tag', 'div'); |
|
| 450 | |||
| 451 | 27 | return Html::closeTag($tag); |
|
| 452 | } |
||
| 453 | |||
| 454 | /** |
||
| 455 | * Renders the HTML markup for the footer of the modal. |
||
| 456 | * |
||
| 457 | * @throws JsonException |
||
| 458 | * |
||
| 459 | * @return string the rendering result |
||
| 460 | */ |
||
| 461 | 27 | private function renderFooter(): string |
|
| 462 | { |
||
| 463 | 27 | if ($this->footer === null) { |
|
| 464 | 24 | return ''; |
|
| 465 | } |
||
| 466 | |||
| 467 | 3 | $options = $this->footerOptions; |
|
| 468 | 3 | $tag = ArrayHelper::remove($options, 'tag', 'div'); |
|
| 469 | 3 | $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags); |
|
| 470 | 3 | Html::addCssClass($options, ['widget' => 'modal-footer']); |
|
| 471 | |||
| 472 | 3 | return Html::tag($tag, $this->footer, $options) |
|
| 473 | 3 | ->encode($encode) |
|
| 474 | 3 | ->render(); |
|
| 475 | } |
||
| 476 | } |
||
| 477 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.