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.