Passed
Push — master ( 566566...d0f53d )
by Alexander
02:18
created

Carousel::renderControls()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

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