Passed
Push — master ( 2301b1...386ec7 )
by Alexander
14:22 queued 11:36
created

Modal::render()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 11
ccs 9
cts 9
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use JsonException;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Html\Html;
10
11
use function array_merge;
12
13
/**
14
 * Modal renders a modal window that can be toggled by clicking on a button.
15
 *
16
 * The following example will show the content enclosed between the {@see begin()} and {@see end()} calls within the
17
 * modal window:
18
 *
19
 * ```php
20
 * Modal::widget()
21
 *     ->title('Hello world')
22
 *     ->toggleButton(['label' => 'click me'])
23
 *     ->begin();
24
 *
25
 * echo 'Say hello...';
26
 *
27
 * echo Modal::end();
28
 * ```
29
 */
30
final class Modal extends Widget
31
{
32
    /**
33
     * Size classes
34
     */
35
    public const SIZE_SMALL = 'modal-sm';
36
    public const SIZE_DEFAULT = null;
37
    public const SIZE_LARGE = 'modal-lg';
38
    public const SIZE_EXTRA_LARGE = 'modal-xl';
39
40
    /**
41
     * Fullsceen classes
42
     */
43
    public const FULLSCREEN_ALWAYS = 'modal-fullscreen';
44
    public const FULLSCREEN_BELOW_SM = 'modal-fullscreen-sm-down';
45
    public const FULLSCREEN_BELOW_MD = 'modal-fullscreen-md-down';
46
    public const FULLSCREEN_BELOW_LG = 'modal-fullscreen-lg-down';
47
    public const FULLSCREEN_BELOW_XL = 'modal-fullscreen-xl-down';
48
    public const FULLSCREEN_BELOW_XXL = 'modal-fullscreen-xxl-down';
49
50
    private ?string $title = null;
51
    private array $titleOptions = [];
52
    private array $headerOptions = [];
53
    private array $dialogOptions = [];
54
    private array $contentOptions = [];
55
    private array $bodyOptions = [];
56
    private ?string $footer = null;
57
    private array $footerOptions = [];
58
    private ?string $size = self::SIZE_DEFAULT;
59
    private ?array $closeButton = ['class' => 'btn-close'];
60
    private ?array $toggleButton = [];
61
    private array $options = [];
62
    private bool $encodeTags = false;
63
    private bool $fade = true;
64
    private bool $staticBackdrop = false;
65
    private bool $scrollable = false;
66
    private bool $centered = false;
67
    private ?string $fullscreen = null;
68
69 20
    public function getId(?string $suffix = '-modal'): ?string
70
    {
71 20
        return $this->options['id'] ?? parent::getId($suffix);
72
    }
73
74 4
    public function getTitleId(): string
75
    {
76 4
        return $this->titleOptions['id'] ?? $this->getId() . '-label';
77
    }
78
79 19
    public function begin(): string
80
    {
81 19
        parent::begin();
82
83 19
        $options = $this->prepareOptions();
84 19
        $dialogOptions = $this->prepareDialogOptions();
85 19
        $contentOptions = $this->contentOptions;
86 19
        $contentTag = ArrayHelper::remove($contentOptions, 'tag', 'div');
87 19
        $dialogTag = ArrayHelper::remove($dialogOptions, 'tag', 'div');
88
89 19
        Html::addCssClass($contentOptions, ['modal-content']);
90
91 19
        return
92 19
            $this->renderToggleButton() .
93 19
            Html::openTag('div', $options) .
94 19
            Html::openTag($dialogTag, $dialogOptions) .
95 19
            Html::openTag($contentTag, $contentOptions) .
96 19
            $this->renderHeader() .
97 19
            $this->renderBodyBegin();
98
    }
99
100 19
    public function render(): string
101
    {
102 19
        $contentTag = ArrayHelper::getValue($this->contentOptions, 'tag', 'div');
103 19
        $dialogTag = ArrayHelper::getValue($this->dialogOptions, 'tag', 'div');
104
105 19
        return
106 19
            $this->renderBodyEnd() .
107 19
            $this->renderFooter() .
108 19
            Html::closeTag($contentTag) . // modal-content
109 19
            Html::closeTag($dialogTag) . // modal-dialog
110 19
            Html::closeTag('div');
111
    }
112
113
    /**
114
     * Prepare options for modal layer
115
     */
116 19
    private function prepareOptions(): array
117
    {
118 19
        $options = array_merge([
119 19
            'role' => 'dialog',
120 19
            'tabindex' => -1,
121 19
            'aria-hidden' => 'true',
122 19
        ], $this->options);
123
124 19
        $options['id'] = $this->getId();
125
126
        /** @psalm-suppress InvalidArgument */
127 19
        Html::addCssClass($options, ['widget' => 'modal']);
128
129 19
        if ($this->fade) {
130 18
            Html::addCssClass($options, ['animation' => 'fade']);
131
        }
132
133 19
        if (!isset($options['aria-label'], $options['aria-labelledby']) && !empty($this->title)) {
134 3
            $options['aria-labelledby'] = $this->getTitleId();
135
        }
136
137 19
        if ($this->staticBackdrop) {
138 1
            $options['data-bs-backdrop'] = 'static';
139
        }
140
141 19
        return $options;
142
    }
143
144
    /**
145
     * Prepare options for dialog layer
146
     */
147 19
    private function prepareDialogOptions(): array
148
    {
149 19
        $options = $this->dialogOptions;
150 19
        $classNames = ['modal-dialog'];
151
152 19
        if ($this->size) {
153 1
            $classNames[] = $this->size;
154
        }
155
156 19
        if ($this->fullscreen) {
157 1
            $classNames[] = $this->fullscreen;
158
        }
159
160 19
        if ($this->scrollable) {
161 1
            $classNames[] = 'modal-dialog-scrollable';
162
        }
163
164 19
        if ($this->centered) {
165 1
            $classNames[] = 'modal-dialog-centered';
166
        }
167
168 19
        Html::addCssClass($options, $classNames);
169
170 19
        return $options;
171
    }
172
173
    /**
174
     * Dialog layer options
175
     */
176
    public function dialogOptions(array $options): self
177
    {
178
        $new = clone $this;
179
        $new->dialogOptions = $options;
180
181
        return $new;
182
    }
183
184
    /**
185
     * Set options for content layer
186
     */
187 1
    public function contentOptions(array $options): self
188
    {
189 1
        $new = clone $this;
190 1
        $new->contentOptions = $options;
191
192 1
        return $new;
193
    }
194
195
    /**
196
     * Body options.
197
     *
198
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
199
     *
200
     * @param array $value
201
     */
202 2
    public function bodyOptions(array $value): self
203
    {
204 2
        $new = clone $this;
205 2
        $new->bodyOptions = $value;
206
207 2
        return $new;
208
    }
209
210
    /**
211
     * The options for rendering the close button tag.
212
     *
213
     * The close button is displayed in the header of the modal window. Clicking on the button will hide the modal
214
     * window. If {@see closeButtonEnabled} is false, no close button will be rendered.
215
     *
216
     * The following special options are supported:
217
     *
218
     * - tag: string, the tag name of the button. Defaults to 'button'.
219
     * - label: string, the label of the button. Defaults to '&times;'.
220
     *
221
     * The rest of the options will be rendered as the HTML attributes of the button tag. Please refer to the
222
     * [Modal plugin help](http://getbootstrap.com/javascript/#modals) for the supported HTML attributes.
223
     *
224
     * @param array|null $value
225
     */
226 2
    public function closeButton(?array $value): self
227
    {
228 2
        $new = clone $this;
229 2
        $new->closeButton = $value;
230
231 2
        return $new;
232
    }
233
234
    /**
235
     * Disable close button.
236
     */
237 1
    public function withoutCloseButton(): self
238
    {
239 1
        return $this->closeButton(null);
240
    }
241
242
    /**
243
     * The footer content in the modal window.
244
     */
245 3
    public function footer(?string $value): self
246
    {
247 3
        $new = clone $this;
248 3
        $new->footer = $value;
249
250 3
        return $new;
251
    }
252
253
    /**
254
     * Additional footer options.
255
     *
256
     * @param array $value
257
     *
258
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
259
     */
260 2
    public function footerOptions(array $value): self
261
    {
262 2
        $new = clone $this;
263 2
        $new->footerOptions = $value;
264
265 2
        return $new;
266
    }
267
268
    /**
269
     * Additional header options.
270
     *
271
     * @param array $value
272
     *
273
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
274
     */
275 2
    public function headerOptions(array $value): self
276
    {
277 2
        $new = clone $this;
278 2
        $new->headerOptions = $value;
279
280 2
        return $new;
281
    }
282
283
    /**
284
     * @param array $value the HTML attributes for the widget container tag. The following special options are
285
     * recognized.
286
     *
287
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
288
     */
289 1
    public function options(array $value): self
290
    {
291 1
        $new = clone $this;
292 1
        $new->options = $value;
293
294 1
        return $new;
295
    }
296
297
    /**
298
     * The title content in the modal window.
299
     */
300 4
    public function title(?string $value): self
301
    {
302 4
        $new = clone $this;
303 4
        $new->title = $value;
304
305 4
        return $new;
306
    }
307
308
    /**
309
     * Additional title options.
310
     *
311
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
312
     *
313
     * @param array $value
314
     */
315 2
    public function titleOptions(array $value): self
316
    {
317 2
        $new = clone $this;
318 2
        $new->titleOptions = $value;
319
320 2
        return $new;
321
    }
322
323
    /**
324
     * The options for rendering the toggle button tag.
325
     *
326
     * The toggle button is used to toggle the visibility of the modal window. If {@see toggleButtonEnabled} is false,
327
     * no toggle button will be rendered.
328
     *
329
     * The following special options are supported:
330
     *
331
     * - tag: string, the tag name of the button. Defaults to 'button'.
332
     * - label: string, the label of the button. Defaults to 'Show'.
333
     *
334
     * The rest of the options will be rendered as the HTML attributes of the button tag. Please refer to the
335
     * [Modal plugin help](http://getbootstrap.com/javascript/#modals) for the supported HTML attributes.
336
     *
337
     * @param array|null $value
338
     */
339 2
    public function toggleButton(?array $value): self
340
    {
341 2
        $new = clone $this;
342 2
        $new->toggleButton = $value;
343
344 2
        return $new;
345
    }
346
347
    /**
348
     * Disable toggle button.
349
     */
350 1
    public function withoutToggleButton(): self
351
    {
352 1
        return $this->toggleButton(null);
353
    }
354
355
    /**
356
     * The modal size. Can be {@see SIZE_LARGE} or {@see SIZE_SMALL}, or null for default.
357
     *
358
     * @link https://getbootstrap.com/docs/5.1/components/modal/#optional-sizes
359
     */
360 1
    public function size(?string $value): self
361
    {
362 1
        $new = clone $this;
363 1
        $new->size = $value;
364
365 1
        return $new;
366
    }
367
368
    /**
369
     * Enable/disable static backdrop
370
     *
371
     * @link https://getbootstrap.com/docs/5.1/components/modal/#static-backdrop
372
     */
373 1
    public function staticBackdrop(bool $value = true): self
374
    {
375 1
        if ($value === $this->staticBackdrop) {
376
            return $this;
377
        }
378
379 1
        $new = clone $this;
380 1
        $new->staticBackdrop = $value;
381
382 1
        return $new;
383
    }
384
385
    /**
386
     * Enable/Disable scrolling long content
387
     *
388
     * @link https://getbootstrap.com/docs/5.1/components/modal/#scrolling-long-content
389
     */
390 1
    public function scrollable(bool $scrollable = true): self
391
    {
392 1
        if ($scrollable === $this->scrollable) {
393
            return $this;
394
        }
395
396 1
        $new = clone $this;
397 1
        $new->scrollable = $scrollable;
398
399 1
        return $new;
400
    }
401
402
    /**
403
     * Enable/Disable vertically centered
404
     *
405
     * @link https://getbootstrap.com/docs/5.1/components/modal/#vertically-centered
406
     */
407 1
    public function centered(bool $centered = true): self
408
    {
409 1
        if ($centered === $this->centered) {
410
            return $this;
411
        }
412
413 1
        $new = clone $this;
414 1
        $new->centered = $centered;
415
416 1
        return $new;
417
    }
418
419
    /**
420
     * Set/remove fade animation
421
     *
422
     * @link https://getbootstrap.com/docs/5.1/components/modal/#remove-animation
423
     */
424 1
    public function fade(bool $fade = true): self
425
    {
426 1
        $new = clone $this;
427 1
        $new->fade = $fade;
428
429 1
        return $new;
430
    }
431
432
    /**
433
     * Enable/disable fullscreen mode
434
     *
435
     * @link https://getbootstrap.com/docs/5.1/components/modal/#fullscreen-modal
436
     */
437 1
    public function fullscreen(?string $fullscreen): self
438
    {
439 1
        $new = clone $this;
440 1
        $new->fullscreen = $fullscreen;
441
442 1
        return $new;
443
    }
444
445
    /**
446
     * Renders the header HTML markup of the modal.
447
     *
448
     * @throws JsonException
449
     *
450
     * @return string the rendering result
451
     */
452 19
    private function renderHeader(): string
453
    {
454 19
        $title = (string) $this->renderTitle();
455 19
        $button = (string) $this->renderCloseButton();
456
457 19
        if ($button === '' && $title === '') {
458 1
            return '';
459
        }
460
461 18
        $options = $this->headerOptions;
462 18
        $tag = ArrayHelper::remove($options, 'tag', 'div');
463 18
        $content = $title . $button;
464
465 18
        Html::addCssClass($options, ['headerOptions' => 'modal-header']);
466
467 18
        return Html::tag($tag, $content, $options)
468 18
            ->encode(false)
469 18
            ->render();
470
    }
471
472
    /**
473
     * Render title HTML markup
474
     */
475 19
    private function renderTitle(): ?string
476
    {
477 19
        if ($this->title === null) {
478 15
            return '';
479
        }
480
481 4
        $options = $this->titleOptions;
482 4
        $options['id'] = $this->getTitleId();
483 4
        $tag = ArrayHelper::remove($options, 'tag', 'h5');
484 4
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
485
486 4
        Html::addCssClass($options, ['modal-title']);
487
488 4
        return Html::tag($tag, $this->title, $options)
489 4
            ->encode($encode)
490 4
            ->render();
491
    }
492
493
    /**
494
     * Renders the opening tag of the modal body.
495
     *
496
     * @throws JsonException
497
     *
498
     * @return string the rendering result
499
     */
500 19
    private function renderBodyBegin(): string
501
    {
502 19
        $options = $this->bodyOptions;
503 19
        $tag = ArrayHelper::remove($options, 'tag', 'div');
504
505 19
        Html::addCssClass($options, ['widget' => 'modal-body']);
506
507 19
        return Html::openTag($tag, $options);
508
    }
509
510
    /**
511
     * Renders the closing tag of the modal body.
512
     *
513
     * @return string the rendering result
514
     */
515 19
    private function renderBodyEnd(): string
516
    {
517 19
        $tag = ArrayHelper::getValue($this->bodyOptions, 'tag', 'div');
518
519 19
        return Html::closeTag($tag);
520
    }
521
522
    /**
523
     * Renders the HTML markup for the footer of the modal.
524
     *
525
     * @throws JsonException
526
     *
527
     * @return string the rendering result
528
     */
529 19
    private function renderFooter(): string
530
    {
531 19
        if ($this->footer === null) {
532 16
            return '';
533
        }
534
535 3
        $options = $this->footerOptions;
536 3
        $tag = ArrayHelper::remove($options, 'tag', 'div');
537 3
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
538 3
        Html::addCssClass($options, ['widget' => 'modal-footer']);
539
540 3
        return Html::tag($tag, $this->footer, $options)
541 3
            ->encode($encode)
542 3
            ->render();
543
    }
544
545
    /**
546
     * Renders the toggle button.
547
     *
548
     * @param array $options additional options for current button
549
     *
550
     * @throws JsonException
551
     *
552
     * @return string|null the rendering result
553
     */
554 20
    public function renderToggleButton(array $options = []): ?string
555
    {
556 20
        if ($this->toggleButton === null && count($options) === 0) {
557 1
            return null;
558
        }
559
560 19
        $options = array_merge(
561 19
            [
562 19
                'data-bs-toggle' => 'modal',
563 19
            ],
564 19
            $this->toggleButton ?? [],
565 19
            $options
566 19
        );
567
568 19
        $tag = ArrayHelper::remove($options, 'tag', 'button');
569 19
        $label = ArrayHelper::remove($options, 'label', 'Show');
570 19
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
571
572 19
        if ($tag === 'button' && !isset($options['type'])) {
573 19
            $options['type'] = 'button';
574
        }
575
576 19
        if (!isset($options['data-bs-target'])) {
577 19
            $target = (string) $this->getId();
578
579 19
            if ($tag === 'a' && !isset($options['href'])) {
580 1
                $options['href'] = '#' . $target;
581
            } else {
582 19
                $options['data-bs-target'] = '#' . $target;
583
            }
584
        }
585
586 19
        return Html::tag($tag, $label, $options)
587 19
            ->encode($encode)
588 19
            ->render();
589
    }
590
591
    /**
592
     * Renders the close button.
593
     *
594
     * @throws JsonException
595
     *
596
     * @return string|null the rendering result
597
     *
598
     * @link https://getbootstrap.com/docs/5.1/components/close-button/
599
     */
600 19
    private function renderCloseButton(): ?string
601
    {
602 19
        if ($this->closeButton === null) {
603 1
            return null;
604
        }
605
606 18
        $options = array_merge([
607 18
            'data-bs-dismiss' => 'modal',
608 18
            'aria-label' => 'Close',
609 18
        ], $this->closeButton);
610 18
        $tag = ArrayHelper::remove($options, 'tag', 'button');
611 18
        $label = ArrayHelper::remove($options, 'label', '');
612 18
        $encode = ArrayHelper::remove($options, 'encode', !empty($label));
613
614 18
        if ($tag === 'button' && !isset($options['type'])) {
615 18
            $options['type'] = 'button';
616
        }
617
618 18
        return Html::tag($tag, $label, $options)
619 18
            ->encode($encode)
620 18
            ->render();
621
    }
622
}
623