Passed
Pull Request — master (#78)
by
unknown
05:23 queued 02:14
created

Modal::run()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 1
rs 10
c 1
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
88 19
        Html::addCssClass($contentOptions, ['modal-content']);
89
90
        return
91 19
            $this->renderToggleButton() .
92 19
            Html::openTag('div', $options) .
93 19
            Html::openTag('div', $dialogOptions) .
94 19
            Html::openTag($contentTag, $contentOptions) .
95 19
            $this->renderHeader() .
96 19
            $this->renderBodyBegin();
97
    }
98
99 19
    protected function run(): string
100
    {
101 19
        $contentTag = ArrayHelper::getValue($this->contentOptions, 'tag', 'div');
102
103
        return
104 19
            $this->renderBodyEnd() .
105 19
            $this->renderFooter() .
106 19
            Html::closeTag($contentTag) . // modal-content
107 19
            Html::closeTag('div') . // modal-dialog
108 19
            Html::closeTag('div');
109
    }
110
111
    /**
112
     * Prepare options for modal layer
113
     *
114
     * @return array
115
     */
116 19
    private function prepareOptions(): array
117
    {
118 19
        $options = array_merge([
119 19
            'role' => 'dialog',
120
            'tabindex' => -1,
121
            '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
     * @return array
148
     */
149 19
    private function prepareDialogOptions(): array
150
    {
151 19
        $options = $this->dialogOptions;
152 19
        $classNames = ['modal-dialog'];
153
154 19
        if ($this->size) {
155 1
            $classNames[] = $this->size;
156
        }
157
158 19
        if ($this->fullscreen) {
159 1
            $classNames[] = $this->fullscreen;
160
        }
161
162 19
        if ($this->scrollable) {
163 1
            $classNames[] = 'modal-dialog-scrollable';
164
        }
165
166 19
        if ($this->centered) {
167 1
            $classNames[] = 'modal-dialog-centered';
168
        }
169
170 19
        Html::addCssClass($options, $classNames);
171
172 19
        return $options;
173
    }
174
175
    /**
176
     * Dialog layer options
177
     *
178
     * @param array $options
179
     *
180
     * @return self
181
     */
182
    public function dialogOptions(array $options): self
183
    {
184
        $new = clone $this;
185
        $new->dialogOptions = $options;
186
187
        return $new;
188
    }
189
190
    /**
191
     * Set options for content layer
192
     *
193
     * @param array $options
194
     *
195
     * @return self
196
     */
197 1
    public function contentOptions(array $options): self
198
    {
199 1
        $new = clone $this;
200 1
        $new->contentOptions = $options;
201
202 1
        return $new;
203
    }
204
205
    /**
206
     * Body options.
207
     *
208
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
209
     *
210
     * @param array $value
211
     *
212
     * @return self
213
     */
214 2
    public function bodyOptions(array $value): self
215
    {
216 2
        $new = clone $this;
217 2
        $new->bodyOptions = $value;
218
219 2
        return $new;
220
    }
221
222
    /**
223
     * The options for rendering the close button tag.
224
     *
225
     * The close button is displayed in the header of the modal window. Clicking on the button will hide the modal
226
     * window. If {@see closeButtonEnabled} is false, no close button will be rendered.
227
     *
228
     * The following special options are supported:
229
     *
230
     * - tag: string, the tag name of the button. Defaults to 'button'.
231
     * - label: string, the label of the button. Defaults to '&times;'.
232
     *
233
     * The rest of the options will be rendered as the HTML attributes of the button tag. Please refer to the
234
     * [Modal plugin help](http://getbootstrap.com/javascript/#modals) for the supported HTML attributes.
235
     *
236
     * @param array $value
237
     *
238
     * @return self
239
     */
240 2
    public function closeButton(?array $value): self
241
    {
242 2
        $new = clone $this;
243 2
        $new->closeButton = $value;
244
245 2
        return $new;
246
    }
247
248
    /**
249
     * Disable close button.
250
     *
251
     * @return self
252
     */
253 1
    public function withoutCloseButton(): self
254
    {
255 1
        return $this->closeButton(null);
256
    }
257
258
    /**
259
     * The footer content in the modal window.
260
     *
261
     * @param string $value
262
     *
263
     * @return self
264
     */
265 3
    public function footer(?string $value): self
266
    {
267 3
        $new = clone $this;
268 3
        $new->footer = $value;
269
270 3
        return $new;
271
    }
272
273
    /**
274
     * Additional footer options.
275
     *
276
     * @param array $value
277
     *
278
     * @return self
279
     *
280
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
281
     */
282 2
    public function footerOptions(array $value): self
283
    {
284 2
        $new = clone $this;
285 2
        $new->footerOptions = $value;
286
287 2
        return $new;
288
    }
289
290
    /**
291
     * Additional header options.
292
     *
293
     * @param array $value
294
     *
295
     * @return self
296
     *
297
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
298
     */
299 2
    public function headerOptions(array $value): self
300
    {
301 2
        $new = clone $this;
302 2
        $new->headerOptions = $value;
303
304 2
        return $new;
305
    }
306
307
    /**
308
     * @param array $value the HTML attributes for the widget container tag. The following special options are
309
     * recognized.
310
     *
311
     * @return self
312
     *
313
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
314
     */
315 1
    public function options(array $value): self
316
    {
317 1
        $new = clone $this;
318 1
        $new->options = $value;
319
320 1
        return $new;
321
    }
322
323
    /**
324
     * The title content in the modal window.
325
     *
326
     * @param string $value
327
     *
328
     * @return self
329
     */
330 4
    public function title(?string $value): self
331
    {
332 4
        $new = clone $this;
333 4
        $new->title = $value;
334
335 4
        return $new;
336
    }
337
338
    /**
339
     * Additional title options.
340
     *
341
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
342
     *
343
     * @param array $value
344
     *
345
     * @return self
346
     */
347 2
    public function titleOptions(array $value): self
348
    {
349 2
        $new = clone $this;
350 2
        $new->titleOptions = $value;
351
352 2
        return $new;
353
    }
354
355
    /**
356
     * The options for rendering the toggle button tag.
357
     *
358
     * The toggle button is used to toggle the visibility of the modal window. If {@see toggleButtonEnabled} is false,
359
     * no toggle button will be rendered.
360
     *
361
     * The following special options are supported:
362
     *
363
     * - tag: string, the tag name of the button. Defaults to 'button'.
364
     * - label: string, the label of the button. Defaults to 'Show'.
365
     *
366
     * The rest of the options will be rendered as the HTML attributes of the button tag. Please refer to the
367
     * [Modal plugin help](http://getbootstrap.com/javascript/#modals) for the supported HTML attributes.
368
     *
369
     * @param array $value
370
     *
371
     * @return self
372
     */
373 2
    public function toggleButton(?array $value): self
374
    {
375 2
        $new = clone $this;
376 2
        $new->toggleButton = $value;
377
378 2
        return $new;
379
    }
380
381
    /**
382
     * Disable toggle button.
383
     *
384
     * @return self
385
     */
386 1
    public function withoutToggleButton(): self
387
    {
388 1
        return $this->toggleButton(null);
389
    }
390
391
    /**
392
     * The modal size. Can be {@see SIZE_LARGE} or {@see SIZE_SMALL}, or null for default.
393
     *
394
     * @param string $value
395
     *
396
     * @return self
397
     *
398
     * @link https://getbootstrap.com/docs/5.1/components/modal/#optional-sizes
399
     */
400 1
    public function size(?string $value): self
401
    {
402 1
        $new = clone $this;
403 1
        $new->size = $value;
404
405 1
        return $new;
406
    }
407
408
    /**
409
     * Enable/disable static backdrop
410
     *
411
     * @param bool $value
412
     *
413
     * @return self
414
     *
415
     * @link https://getbootstrap.com/docs/5.1/components/modal/#static-backdrop
416
     */
417 1
    public function staticBackdrop(bool $value = true): self
418
    {
419 1
        if ($value === $this->staticBackdrop) {
420
            return $this;
421
        }
422
423 1
        $new = clone $this;
424 1
        $new->staticBackdrop = $value;
425
426 1
        return $new;
427
    }
428
429
    /**
430
     * Enable/Disable scrolling long content
431
     *
432
     * @param bool $scrollable
433
     *
434
     * @return self
435
     *
436
     * @link https://getbootstrap.com/docs/5.1/components/modal/#scrolling-long-content
437
     */
438 1
    public function scrollable(bool $scrollable = true): self
439
    {
440 1
        if ($scrollable === $this->scrollable) {
441
            return $this;
442
        }
443
444 1
        $new = clone $this;
445 1
        $new->scrollable = $scrollable;
446
447 1
        return $new;
448
    }
449
450
    /**
451
     * Enable/Disable vertically centered
452
     *
453
     * @param bool $centered
454
     *
455
     * @return self
456
     *
457
     * @link https://getbootstrap.com/docs/5.1/components/modal/#vertically-centered
458
     */
459 1
    public function centered(bool $centered = true): self
460
    {
461 1
        if ($centered === $this->centered) {
462
            return $this;
463
        }
464
465 1
        $new = clone $this;
466 1
        $new->centered = $centered;
467
468 1
        return $new;
469
    }
470
471
    /**
472
     * Set/remove fade animation
473
     *
474
     * @param bool $fade
475
     *
476
     * @return self
477
     *
478
     * @link https://getbootstrap.com/docs/5.1/components/modal/#remove-animation
479
     */
480 1
    public function fade(bool $fade = true): self
481
    {
482 1
        $new = clone $this;
483 1
        $new->fade = $fade;
484
485 1
        return $new;
486
    }
487
488
    /**
489
     * Enable/disable fullscreen mode
490
     *
491
     * @param string|null $fullscreen
492
     *
493
     * @return self
494
     *
495
     * @link https://getbootstrap.com/docs/5.1/components/modal/#fullscreen-modal
496
     */
497 1
    public function fullscreen(?string $fullscreen): self
498
    {
499 1
        $new = clone $this;
500 1
        $new->fullscreen = $fullscreen;
501
502 1
        return $new;
503
    }
504
505
    /**
506
     * Renders the header HTML markup of the modal.
507
     *
508
     * @throws JsonException
509
     *
510
     * @return string the rendering result
511
     */
512 19
    private function renderHeader(): string
513
    {
514 19
        $title = (string) $this->renderTitle();
515 19
        $button = (string) $this->renderCloseButton();
516
517 19
        if ($button === '' && $title === '') {
518 1
            return '';
519
        }
520
521 18
        $options = $this->headerOptions;
522 18
        $tag = ArrayHelper::remove($options, 'tag', 'div');
523 18
        $content = $title . $button;
524
525 18
        Html::addCssClass($options, ['headerOptions' => 'modal-header']);
526
527 18
        return Html::tag($tag, $content, $options)->encode(false)->render();
528
    }
529
530
    /**
531
     * Render title HTML markup
532
     *
533
     * @return string|null
534
     */
535 19
    private function renderTitle(): ?string
536
    {
537 19
        if ($this->title === null) {
538 15
            return '';
539
        }
540
541 4
        $options = $this->titleOptions;
542 4
        $options['id'] = $this->getTitleId();
543 4
        $tag = ArrayHelper::remove($options, 'tag', 'h5');
544 4
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
545
546 4
        Html::addCssClass($options, ['modal-title']);
547
548 4
        return Html::tag($tag, $this->title, $options)->encode($encode)->render();
549
    }
550
551
    /**
552
     * Renders the opening tag of the modal body.
553
     *
554
     * @throws JsonException
555
     *
556
     * @return string the rendering result
557
     */
558 19
    private function renderBodyBegin(): string
559
    {
560 19
        $options = $this->bodyOptions;
561 19
        $tag = ArrayHelper::remove($options, 'tag', 'div');
562
563 19
        Html::addCssClass($options, ['widget' => 'modal-body']);
564
565 19
        return Html::openTag($tag, $options);
566
    }
567
568
    /**
569
     * Renders the closing tag of the modal body.
570
     *
571
     * @return string the rendering result
572
     */
573 19
    private function renderBodyEnd(): string
574
    {
575 19
        $tag = ArrayHelper::getValue($this->bodyOptions, 'tag', 'div');
576
577 19
        return Html::closeTag($tag);
578
    }
579
580
    /**
581
     * Renders the HTML markup for the footer of the modal.
582
     *
583
     * @throws JsonException
584
     *
585
     * @return string the rendering result
586
     */
587 19
    private function renderFooter(): string
588
    {
589 19
        if ($this->footer === null) {
590 16
            return '';
591
        }
592
593 3
        $options = $this->footerOptions;
594 3
        $tag = ArrayHelper::remove($options, 'tag', 'div');
595 3
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
596 3
        Html::addCssClass($options, ['widget' => 'modal-footer']);
597
598 3
        return Html::tag($tag, $this->footer, $options)->encode($encode)->render();
599
    }
600
601
    /**
602
     * Renders the toggle button.
603
     *
604
     * @param array $options additional options for current button
605
     *
606
     * @throws JsonException
607
     *
608
     * @return string|null the rendering result
609
     */
610 20
    public function renderToggleButton(array $options = []): ?string
611
    {
612 20
        if ($this->toggleButton === null && count($options) === 0) {
613 1
            return null;
614
        }
615
616 19
        $options = array_merge(
617
            [
618 19
                'data-bs-toggle' => 'modal',
619
            ],
620 19
            $this->toggleButton,
0 ignored issues
show
Bug introduced by
It seems like $this->toggleButton can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

620
            /** @scrutinizer ignore-type */ $this->toggleButton,
Loading history...
621
            $options
622
        );
623
624 19
        $tag = ArrayHelper::remove($options, 'tag', 'button');
625 19
        $label = ArrayHelper::remove($options, 'label', 'Show');
626 19
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
627
628 19
        if ($tag === 'button' && !isset($options['type'])) {
629 19
            $options['type'] = 'button';
630
        }
631
632 19
        if (!isset($options['data-bs-target'])) {
633 19
            $target = (string) $this->getId();
634
635 19
            if ($tag === 'a') {
636 1
                if (!isset($options['href'])) {
637 1
                    $options['href'] = '#' . $target;
638
                } else {
639 1
                    $options['data-bs-target'] = '#' . $target;
640
                }
641
            } else {
642 19
                $options['data-bs-target'] = '#' . $target;
643
            }
644
        }
645
646 19
        return Html::tag($tag, $label, $options)->encode($encode)->render();
647
    }
648
649
    /**
650
     * Renders the close button.
651
     *
652
     * @throws JsonException
653
     *
654
     * @return string|null the rendering result
655
     *
656
     * @link https://getbootstrap.com/docs/5.1/components/close-button/
657
     */
658 19
    private function renderCloseButton(): ?string
659
    {
660 19
        if ($this->closeButton === null) {
661 1
            return null;
662
        }
663
664 18
        $options = array_merge([
665 18
            'data-bs-dismiss' => 'modal',
666
            'aria-label' => 'Close',
667 18
        ], $this->closeButton);
668 18
        $tag = ArrayHelper::remove($options, 'tag', 'button');
669 18
        $label = ArrayHelper::remove($options, 'label', '');
670 18
        $encode = ArrayHelper::remove($options, 'encode', !empty($label));
671
672 18
        if ($tag === 'button' && !isset($options['type'])) {
673 18
            $options['type'] = 'button';
674
        }
675
676 18
        return Html::tag($tag, $label, $options)->encode($encode)->render();
677
    }
678
}
679