Passed
Push — master ( 5f6e57...34e66c )
by Sergei
05:58 queued 03:17
created

Modal::dialogOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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