Modal::render()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 8
ccs 7
cts 7
cp 1
crap 1
rs 10
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
Unused Code introduced by
The call to Yiisoft\Yii\Bootstrap5\CloseButtonTrait::getId() has too many arguments starting with $suffix. ( Ignorable by Annotation )

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

73
        return $this->options['id'] ?? parent::/** @scrutinizer ignore-call */ getId($suffix);

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.

Loading history...
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