Test Failed
Pull Request — master (#127)
by
unknown
13:20
created

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