Passed
Pull Request — master (#90)
by
unknown
13:09
created

Carousel   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 89
dl 0
loc 241
ccs 83
cts 83
cp 1
rs 10
c 0
b 0
f 0
wmc 22

10 Methods

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