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