Test Failed
Pull Request — master (#35)
by Wilmer
02:09
created

Carousel::renderItem()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 22
c 1
b 0
f 0
nc 17
nop 2
dl 0
loc 37
ccs 0
cts 6
cp 0
crap 56
rs 8.6346
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use JsonException;
8
use RuntimeException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function count;
13
use function implode;
14
use function is_string;
15
16
/**
17
 * Carousel renders a carousel bootstrap javascript component.
18
 *
19
 * For example:
20
 *
21
 * ```php
22
 * echo Carousel::widget()
23
 *     ->withItems([
24
 *         // the item contains only the image
25
 *         '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-01.jpg"/>',
26
 *         // equivalent to the above
27
 *         ['content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-02.jpg"/>'],
28
 *         // the item contains both the image and the caption
29
 *         [
30
 *             'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-03.jpg"/>',
31
 *             'caption' => '<h4>This is title</h4><p>This is the caption text</p>',
32
 *             'captionOptions' => ['class' => ['d-none', 'd-md-block']],
33
 *             'options' => [...],
34
 *         ],
35
 *     ]);
36
 * ```
37
 */
38
final class Carousel extends Widget
39
{
40
    private array $controls = [
41
        '<span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous</span>',
42
        '<span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next</span>',
43
    ];
44
    private bool $encodeTags = false;
45
    private bool $showIndicators = true;
46
    private array $items = [];
47
    private bool $crossfade = false;
48
    private array $options = ['data-bs-ride' => 'carousel'];
49 2
50
    public function run(): string
51 2
    {
52 2
        if (!isset($this->options['id'])) {
53
            $this->options['id'] = "{$this->getId()}-carousel";
54
        }
55
56 2
        /** @psalm-suppress InvalidArgument */
57
        Html::addCssClass($this->options, ['widget' => 'carousel', 'slide']);
58 2
59 1
        if ($this->crossfade) {
60
            Html::addCssClass($this->options, 'carousel-fade');
61
        }
62 2
63
        $this->registerPlugin('carousel', $this->options);
64 2
65 2
        if ($this->encodeTags === false) {
66 2
            $this->options = array_merge($this->options, ['encode' => false]);
67
        }
68
69
        return Html::div(
70
            $this->renderIndicators() . $this->renderItems() . $this->renderControls(),
71
            $this->options
72
        );
73 2
    }
74
75 2
    /**
76
     * The labels for the previous and the next control buttons.
77
     *
78
     * If null, it means the previous and the next control buttons should not be displayed.
79 2
     *
80
     * @param array|null $value
81 2
     *
82 2
     * @return $this
83 2
     */
84
    public function withControls(array $value): self
85 2
    {
86
        $new = clone $this;
87 2
        $new->controls = $value;
88
89
        return $new;
90 2
    }
91
92
    /**
93
     * Animate slides with a fade transition instead of a slide. Defaults to `false`.
94
     *
95
     * @param bool $value
96 2
     *
97
     * @return $this
98 2
     */
99
    public function withCrossfade(bool $value): self
100 2
    {
101 2
        $new = clone $this;
102
        $new->crossfade = $value;
103
104 2
        return $new;
105
    }
106
107
    /**
108
     * List of slides in the carousel. Each array element represents a single slide with the following structure:
109
     *
110
     * ```php
111
     * [
112
     *     // required, slide content (HTML), such as an image tag
113
     *     'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-01.jpg"/>',
114
     *     // optional, the caption (HTML) of the slide
115
     *     'caption' => '<h4>This is title</h4><p>This is the caption text</p>',
116
     *     // optional the HTML attributes of the slide container
117 2
     *     'options' => [],
118
     * ]
119 2
     * ```
120
     *
121
     * @param array $value
122
     *
123 2
     * @return $this
124 2
     */
125 2
    public function withItems(array $value): self
126
    {
127 2
        $new = clone $this;
128 2
        $new->items = $value;
129 2
130
        return $new;
131 2
    }
132
133
    /**
134 2
     * The HTML attributes for the container tag. The following special options are recognized.
135
     *
136
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
137
     *
138
     * @param array $value
139 2
     *
140
     * @return $this
141 2
     */
142 2
    public function withOptions(array $value): self
143
    {
144
        $new = clone $this;
145 2
        $new->options = $value;
146
147
        return $new;
148
    }
149
150
    /**
151
     * Whether carousel indicators (<ol> tag with anchors to items) should be displayed or not.
152
     *
153
     * @param bool $value
154
     *
155 2
     * @return $this
156
     */
157 2
    public function withoutShowIndicators(bool $value = false): self
158 2
    {
159 2
        $new = clone $this;
160
        $new->showIndicators = $value;
161
162 2
        return $new;
163 2
    }
164 2
165
    /**
166
     * Allows you to enable or disable the encoding tags html.
167
     *
168
     * @param bool $value
169
     *
170
     * @return self
171
     */
172
    public function withEncodeTags(bool $value = true): self
173
    {
174
        $new = clone $this;
175
        $new->encodeTags = $value;
176
177
        return $new;
178
    }
179
180
    /**
181
     * Renders carousel indicators.
182
     */
183
    private function renderIndicators(): string
184
    {
185
        if ($this->showIndicators === false) {
186
            return '';
187
        }
188
189
        $indicators = [];
190
191
        for ($i = 0, $count = count($this->items); $i < $count; $i++) {
192
            $options = ['data-bs-target' => '#' . $this->options['id'], 'data-bs-slide-to' => $i];
193
            if ($i === 0) {
194
                /** @psalm-suppress InvalidArgument */
195
                Html::addCssClass($options, 'active');
196
            }
197
            $indicators[] = Html::tag('li', '', $options);
198
        }
199
200
        $indicatorOptions = ['class' => ['carousel-indicators']];
201
202 1
        if ($this->encodeTags === false) {
203
            $indicatorOptions = array_merge($indicatorOptions, ['encode' => false]);
204 1
        }
205
206 1
        return Html::tag('ol', implode("\n", $indicators), $indicatorOptions);
207
    }
208
209
    /**
210
     * Renders carousel items as specified on {@see items}.
211
     */
212
    private function renderItems(): string
213
    {
214
        $items = [];
215
216
        foreach ($this->items as $i => $iValue) {
217
            $items[] = $this->renderItem($iValue, $i);
218
        }
219
220
        $itemOptions = ['class' => 'carousel-inner'];
221
222
        if ($this->encodeTags === false) {
223
            $itemOptions = array_merge($itemOptions, ['encode' => false]);
224
        }
225
226
        return Html::div(implode("\n", $items), $itemOptions);
227 2
    }
228
229 2
    /**
230
     * Renders a single carousel item
231 2
     *
232
     * @param array|string $item a single item from {@see items}
233
     * @param int $index the item index as the first item should be set to `active`
234
     *
235
     * @throws JsonException|RuntimeException if the item is invalid.
236
     *
237
     * @return string the rendering result.
238
     */
239
    private function renderItem($item, int $index): string
240
    {
241
        if (is_string($item)) {
242
            $content = $item;
243
            $caption = null;
244
            $options = [];
245
        } elseif (isset($item['content'])) {
246
            $content = $item['content'];
247
            $caption = ArrayHelper::getValue($item, 'caption');
248
249
            if ($caption !== null) {
250
                $captionOptions = ArrayHelper::remove($item, 'captionOptions', []);
251
                Html::addCssClass($captionOptions, ['widget' => 'carousel-caption']);
252
253
                if ($this->encodeTags === false) {
254
                    $captionOptions = array_merge($captionOptions, ['encode' => false]);
255
                }
256
257
                $caption = Html::div($caption, $captionOptions);
258
            }
259
260
            $options = ArrayHelper::getValue($item, 'options', []);
261
        } else {
262
            throw new RuntimeException('The "content" option is required.');
263
        }
264
265
        Html::addCssClass($options, ['widget' => 'carousel-item']);
266
267
        if ($this->encodeTags === false) {
268
            $options = array_merge($options, ['encode' => false]);
269
        }
270
271
        if ($index === 0) {
272
            Html::addCssClass($options, 'active');
273
        }
274
275
        return Html::div($content . "\n" . $caption, $options);
276
    }
277
278
    /**
279
     * Renders previous and next control buttons.
280
     *
281
     * @throws JsonException|RuntimeException if {@see controls} is invalid.
282
     *
283
     * @return string
284
     */
285
    private function renderControls(): string
286
    {
287
        $controlsOptions0 = [
288
            'class' => 'carousel-control-prev',
289
            'data-bs-slide' => 'prev',
290
            'role' => 'button',
291
        ];
292
293
        $controlsOptions1 = [
294
            'class' => 'carousel-control-next',
295
            'data-bs-slide' => 'next',
296
            'role' => 'button',
297
        ];
298
299
        if ($this->encodeTags === false) {
300
            $controlsOptions0 = array_merge($controlsOptions0, ['encode' => false]);
301
            $controlsOptions1 = array_merge($controlsOptions1, ['encode' => false]);
302
        }
303
304
        if (isset($this->controls[0], $this->controls[1])) {
305
            return Html::a($this->controls[0], '#' . $this->options['id'], $controlsOptions0) . "\n" .
306
                Html::a($this->controls[1], '#' . $this->options['id'], $controlsOptions1);
307
        }
308
309
        if ($this->controls === []) {
310
            return '';
311
        }
312
313
        throw new RuntimeException(
314
            'The "controls" property must be either null or an array of two elements.'
315
        );
316
    }
317
}
318