Completed
Push — master ( 92c51b...1c2d7c )
by Wilmer
02:09 queued 02:09
created

Modal   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 649
Duplicated Lines 0 %

Test Coverage

Coverage 96.39%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 200
dl 0
loc 649
ccs 187
cts 194
cp 0.9639
rs 5.04
c 1
b 0
f 0
wmc 57

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getTitleId() 0 3 1
A getId() 0 3 1
A toggleButton() 0 6 1
A options() 0 6 1
A title() 0 6 1
A prepareDialogOptions() 0 24 5
A withoutCloseButton() 0 3 1
A renderTitle() 0 14 2
A withoutToggleButton() 0 3 1
A contentOptions() 0 6 1
A closeButton() 0 6 1
A size() 0 6 1
A renderHeader() 0 16 3
A renderBodyEnd() 0 5 1
A headerOptions() 0 6 1
A renderBodyBegin() 0 8 1
A renderFooter() 0 12 2
A scrollable() 0 10 2
A staticBackdrop() 0 10 2
A fullscreen() 0 6 1
A centered() 0 10 2
A footer() 0 6 1
A titleOptions() 0 6 1
A fade() 0 6 1
A footerOptions() 0 6 1
A renderCloseButton() 0 19 4
A bodyOptions() 0 6 1
A dialogOptions() 0 6 1
A prepareOptions() 0 26 5
B renderToggleButton() 0 37 8
A begin() 0 19 1
A run() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like Modal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Modal, and based on these observations, apply Extract Interface, too.

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

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