Test Failed
Pull Request — master (#41)
by Sergei
06:46
created

Carousel::controls()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

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