Passed
Pull Request — master (#127)
by
unknown
02:34
created

Modal::centered()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
crap 2.0185
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
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 AbstractToggleWidget
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 $closeButton = ['class' => 'btn-close'];
60
    private array $options = [];
61
    private bool $encodeTags = false;
62
    private bool $fade = true;
63
    private bool $staticBackdrop = false;
64
    private bool $scrollable = false;
65
    private bool $centered = false;
66
    private ?string $fullscreen = null;
67
    protected string|Stringable $toggleLabel = 'Show';
68
69 20
    public function getId(?string $suffix = '-modal'): ?string
70
    {
71 20
        return $this->options['id'] ?? parent::getId($suffix);
72
    }
73
74 20
    public function bsToggle(): string
75
    {
76 20
        return 'modal';
77
    }
78
79 4
    public function getTitleId(): string
80
    {
81 4
        return $this->titleOptions['id'] ?? $this->getId() . '-label';
82
    }
83
84 19
    public function begin(): string
85
    {
86 19
        parent::begin();
87
88 19
        $options = $this->prepareOptions();
89 19
        $dialogOptions = $this->prepareDialogOptions();
90 19
        $contentOptions = $this->contentOptions;
91 19
        $contentTag = ArrayHelper::remove($contentOptions, 'tag', 'div');
92 19
        $dialogTag = ArrayHelper::remove($dialogOptions, 'tag', 'div');
93
94 19
        Html::addCssClass($contentOptions, ['modal-content']);
95
96 19
        return
97 19
            ($this->renderToggle ? $this->renderToggle() : '') .
98 19
            Html::openTag('div', $options) .
99 19
            Html::openTag($dialogTag, $dialogOptions) .
100 19
            Html::openTag($contentTag, $contentOptions) .
101 19
            $this->renderHeader() .
102 19
            $this->renderBodyBegin();
103
    }
104
105 19
    public function render(): string
106
    {
107 19
        return
108 19
            $this->renderBodyEnd() .
109 19
            $this->renderFooter() .
110 19
            Html::closeTag($this->contentOptions['tag'] ?? 'div') . // modal-content
111 19
            Html::closeTag($this->dialogOptions['tag'] ?? 'div') . // modal-dialog
112 19
            Html::closeTag('div');
113
    }
114
115
    /**
116
     * Prepare options for modal layer
117
     */
118 19
    private function prepareOptions(): array
119
    {
120 19
        $options = array_merge([
121 19
            'role' => 'dialog',
122 19
            'tabindex' => -1,
123 19
            '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 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
    public function dialogOptions(array $options): self
179
    {
180
        $new = clone $this;
181
        $new->dialogOptions = $options;
182
183
        return $new;
184
    }
185
186
    /**
187
     * Set options for content layer
188
     */
189 1
    public function contentOptions(array $options): self
190
    {
191 1
        $new = clone $this;
192 1
        $new->contentOptions = $options;
193
194 1
        return $new;
195
    }
196
197
    /**
198
     * Body options.
199
     *
200
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
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
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
257
     */
258 2
    public function footerOptions(array $value): self
259
    {
260 2
        $new = clone $this;
261 2
        $new->footerOptions = $value;
262
263 2
        return $new;
264
    }
265
266
    /**
267
     * Additional header options.
268
     *
269
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
270
     */
271 2
    public function headerOptions(array $value): self
272
    {
273 2
        $new = clone $this;
274 2
        $new->headerOptions = $value;
275
276 2
        return $new;
277
    }
278
279
    /**
280
     * @param array $value the HTML attributes for the widget container tag. The following special options are
281
     * recognized.
282
     *
283
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
284
     */
285 1
    public function options(array $value): self
286
    {
287 1
        $new = clone $this;
288 1
        $new->options = $value;
289
290 1
        return $new;
291
    }
292
293
    /**
294
     * The title content in the modal window.
295
     */
296 4
    public function title(?string $value): self
297
    {
298 4
        $new = clone $this;
299 4
        $new->title = $value;
300
301 4
        return $new;
302
    }
303
304
    /**
305
     * Additional title options.
306
     *
307
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
308
     */
309 2
    public function titleOptions(array $value): self
310
    {
311 2
        $new = clone $this;
312 2
        $new->titleOptions = $value;
313
314 2
        return $new;
315
    }
316
317
    /**
318
     * The modal size. Can be {@see SIZE_LARGE} or {@see SIZE_SMALL}, or null for default.
319
     *
320
     * @link https://getbootstrap.com/docs/5.1/components/modal/#optional-sizes
321
     */
322 1
    public function size(?string $value): self
323
    {
324 1
        $new = clone $this;
325 1
        $new->size = $value;
326
327 1
        return $new;
328
    }
329
330
    /**
331
     * Enable/disable static backdrop
332
     *
333
     * @link https://getbootstrap.com/docs/5.1/components/modal/#static-backdrop
334
     */
335 1
    public function staticBackdrop(bool $value = true): self
336
    {
337 1
        if ($value === $this->staticBackdrop) {
338
            return $this;
339
        }
340
341 1
        $new = clone $this;
342 1
        $new->staticBackdrop = $value;
343
344 1
        return $new;
345
    }
346
347
    /**
348
     * Enable/Disable scrolling long content
349
     *
350
     * @link https://getbootstrap.com/docs/5.1/components/modal/#scrolling-long-content
351
     */
352 1
    public function scrollable(bool $scrollable = true): self
353
    {
354 1
        if ($scrollable === $this->scrollable) {
355
            return $this;
356
        }
357
358 1
        $new = clone $this;
359 1
        $new->scrollable = $scrollable;
360
361 1
        return $new;
362
    }
363
364
    /**
365
     * Enable/Disable vertically centered
366
     *
367
     * @link https://getbootstrap.com/docs/5.1/components/modal/#vertically-centered
368
     */
369 1
    public function centered(bool $centered = true): self
370
    {
371 1
        if ($centered === $this->centered) {
372
            return $this;
373
        }
374
375 1
        $new = clone $this;
376 1
        $new->centered = $centered;
377
378 1
        return $new;
379
    }
380
381
    /**
382
     * Set/remove fade animation
383
     *
384
     * @link https://getbootstrap.com/docs/5.1/components/modal/#remove-animation
385
     */
386 1
    public function fade(bool $fade = true): self
387
    {
388 1
        $new = clone $this;
389 1
        $new->fade = $fade;
390
391 1
        return $new;
392
    }
393
394
    /**
395
     * Enable/disable fullscreen mode
396
     *
397
     * @link https://getbootstrap.com/docs/5.1/components/modal/#fullscreen-modal
398
     */
399 1
    public function fullscreen(?string $fullscreen): self
400
    {
401 1
        $new = clone $this;
402 1
        $new->fullscreen = $fullscreen;
403
404 1
        return $new;
405
    }
406
407
    /**
408
     * Renders the header HTML markup of the modal.
409
     *
410
     * @throws JsonException
411
     *
412
     * @return string the rendering result
413
     */
414 19
    private function renderHeader(): string
415
    {
416 19
        $title = (string) $this->renderTitle();
417 19
        $button = (string) $this->renderCloseButton();
418
419 19
        if ($button === '' && $title === '') {
420 1
            return '';
421
        }
422
423 18
        $options = $this->headerOptions;
424 18
        $tag = ArrayHelper::remove($options, 'tag', 'div');
425 18
        $content = $title . $button;
426
427 18
        Html::addCssClass($options, ['headerOptions' => 'modal-header']);
428
429 18
        return Html::tag($tag, $content, $options)
430 18
            ->encode(false)
431 18
            ->render();
432
    }
433
434
    /**
435
     * Render title HTML markup
436
     */
437 19
    private function renderTitle(): ?string
438
    {
439 19
        if ($this->title === null) {
440 15
            return '';
441
        }
442
443 4
        $options = $this->titleOptions;
444 4
        $options['id'] = $this->getTitleId();
445 4
        $tag = ArrayHelper::remove($options, 'tag', 'h5');
446 4
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
447
448 4
        Html::addCssClass($options, ['modal-title']);
449
450 4
        return Html::tag($tag, $this->title, $options)
451 4
            ->encode($encode)
452 4
            ->render();
453
    }
454
455
    /**
456
     * Renders the opening tag of the modal body.
457
     *
458
     * @throws JsonException
459
     *
460
     * @return string the rendering result
461
     */
462 19
    private function renderBodyBegin(): string
463
    {
464 19
        $options = $this->bodyOptions;
465 19
        $tag = ArrayHelper::remove($options, 'tag', 'div');
466
467 19
        Html::addCssClass($options, ['widget' => 'modal-body']);
468
469 19
        return Html::openTag($tag, $options);
470
    }
471
472
    /**
473
     * Renders the closing tag of the modal body.
474
     *
475
     * @return string the rendering result
476
     */
477 19
    private function renderBodyEnd(): string
478
    {
479 19
        $tag = ArrayHelper::getValue($this->bodyOptions, 'tag', 'div');
480
481 19
        return Html::closeTag($tag);
482
    }
483
484
    /**
485
     * Renders the HTML markup for the footer of the modal.
486
     *
487
     * @throws JsonException
488
     *
489
     * @return string the rendering result
490
     */
491 19
    private function renderFooter(): string
492
    {
493 19
        if ($this->footer === null) {
494 16
            return '';
495
        }
496
497 3
        $options = $this->footerOptions;
498 3
        $tag = ArrayHelper::remove($options, 'tag', 'div');
499 3
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
500 3
        Html::addCssClass($options, ['widget' => 'modal-footer']);
501
502 3
        return Html::tag($tag, $this->footer, $options)
503 3
            ->encode($encode)
504 3
            ->render();
505
    }
506
507
    /**
508
     * Renders the close button.
509
     *
510
     * @throws JsonException
511
     *
512
     * @return string|null the rendering result
513
     *
514
     * @link https://getbootstrap.com/docs/5.1/components/close-button/
515
     */
516 19
    private function renderCloseButton(): ?string
517
    {
518 19
        if ($this->closeButton === null) {
519 1
            return null;
520
        }
521
522 18
        $options = array_merge([
523 18
            'data-bs-dismiss' => $this->bsToggle(),
524 18
            'aria-label' => 'Close',
525 18
        ], $this->closeButton);
526 18
        $tag = ArrayHelper::remove($options, 'tag', 'button');
527 18
        $label = ArrayHelper::remove($options, 'label', '');
528 18
        $encode = ArrayHelper::remove($options, 'encode', !empty($label));
529
530 18
        if ($tag === 'button' && !isset($options['type'])) {
531 18
            $options['type'] = 'button';
532
        }
533
534 18
        return Html::tag($tag, $label, $options)
535 18
            ->encode($encode)
536 18
            ->render();
537
    }
538
}
539