Passed
Pull Request — master (#127)
by Sergei
02:52
created

Modal   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 442
Duplicated Lines 0 %

Test Coverage

Coverage 98.16%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 163
c 1
b 0
f 0
dl 0
loc 442
ccs 160
cts 163
cp 0.9816
rs 8.96
wmc 43

27 Methods

Rating   Name   Duplication   Size   Complexity  
A options() 0 6 1
A title() 0 6 1
A prepareDialogOptions() 0 24 5
A renderTitle() 0 16 2
A contentOptions() 0 6 1
A render() 0 8 1
A renderHeader() 0 18 3
A size() 0 6 1
A renderBodyEnd() 0 5 1
A getTitleId() 0 3 1
A headerOptions() 0 6 1
A renderBodyBegin() 0 8 1
A renderFooter() 0 14 2
A scrollable() 0 10 2
A staticBackdrop() 0 10 2
A bsToggle() 0 3 1
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 getId() 0 3 1
A footerOptions() 0 6 1
A begin() 0 19 2
A bodyOptions() 0 6 1
A dialogOptions() 0 6 1
A prepareOptions() 0 26 5

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