1use ironhtml::typed::Element;
7use ironhtml_elements::{Button, Div, Img, Span};
8
9extern crate alloc;
10use alloc::format;
11use alloc::string::String;
12use alloc::string::ToString;
13
14pub struct CarouselItem {
16 pub image_src: String,
17 pub image_alt: String,
18 pub caption_title: Option<String>,
19 pub caption_text: Option<String>,
20 pub active: bool,
21}
22
23impl CarouselItem {
24 #[must_use]
26 pub fn new(src: impl Into<String>, alt: impl Into<String>) -> Self {
27 Self {
28 image_src: src.into(),
29 image_alt: alt.into(),
30 caption_title: None,
31 caption_text: None,
32 active: false,
33 }
34 }
35
36 #[must_use]
38 pub const fn active(mut self) -> Self {
39 self.active = true;
40 self
41 }
42
43 #[must_use]
45 pub fn caption(mut self, title: impl Into<String>, text: impl Into<String>) -> Self {
46 self.caption_title = Some(title.into());
47 self.caption_text = Some(text.into());
48 self
49 }
50}
51
52#[must_use]
69pub fn carousel(id: &str, items: &[CarouselItem]) -> Element<Div> {
70 let target = format!("#{id}");
71
72 Element::<Div>::new()
73 .attr("id", id)
74 .class("carousel slide")
75 .child::<Div, _>(|inner| {
76 items
77 .iter()
78 .fold(inner.class("carousel-inner"), |inner, item| {
79 inner.child::<Div, _>(|_| carousel_item(item))
80 })
81 })
82 .child::<Button, _>(|b| {
83 b.class("carousel-control-prev")
84 .attr("type", "button")
85 .attr("data-bs-target", &target)
86 .attr("data-bs-slide", "prev")
87 .child::<Span, _>(|s| {
88 s.class("carousel-control-prev-icon")
89 .attr("aria-hidden", "true")
90 })
91 .child::<Span, _>(|s| s.class("visually-hidden").text("Previous"))
92 })
93 .child::<Button, _>(|b| {
94 b.class("carousel-control-next")
95 .attr("type", "button")
96 .attr("data-bs-target", &target)
97 .attr("data-bs-slide", "next")
98 .child::<Span, _>(|s| {
99 s.class("carousel-control-next-icon")
100 .attr("aria-hidden", "true")
101 })
102 .child::<Span, _>(|s| s.class("visually-hidden").text("Next"))
103 })
104}
105
106#[must_use]
108pub fn carousel_with_indicators(id: &str, items: &[CarouselItem]) -> Element<Div> {
109 let target = format!("#{id}");
110
111 Element::<Div>::new()
112 .attr("id", id)
113 .class("carousel slide")
114 .child::<Div, _>(|indicators| {
116 items.iter().enumerate().fold(
117 indicators.class("carousel-indicators"),
118 |indicators, (i, item)| {
119 indicators.child::<Button, _>(|b| {
120 let mut btn = b
121 .attr("type", "button")
122 .attr("data-bs-target", &target)
123 .attr("data-bs-slide-to", i.to_string())
124 .attr("aria-label", format!("Slide {}", i + 1));
125 if item.active {
126 btn = btn.class("active").attr("aria-current", "true");
127 }
128 btn
129 })
130 },
131 )
132 })
133 .child::<Div, _>(|inner| {
135 items
136 .iter()
137 .fold(inner.class("carousel-inner"), |inner, item| {
138 inner.child::<Div, _>(|_| carousel_item(item))
139 })
140 })
141 .child::<Button, _>(|b| {
143 b.class("carousel-control-prev")
144 .attr("type", "button")
145 .attr("data-bs-target", &target)
146 .attr("data-bs-slide", "prev")
147 .child::<Span, _>(|s| {
148 s.class("carousel-control-prev-icon")
149 .attr("aria-hidden", "true")
150 })
151 .child::<Span, _>(|s| s.class("visually-hidden").text("Previous"))
152 })
153 .child::<Button, _>(|b| {
154 b.class("carousel-control-next")
155 .attr("type", "button")
156 .attr("data-bs-target", &target)
157 .attr("data-bs-slide", "next")
158 .child::<Span, _>(|s| {
159 s.class("carousel-control-next-icon")
160 .attr("aria-hidden", "true")
161 })
162 .child::<Span, _>(|s| s.class("visually-hidden").text("Next"))
163 })
164}
165
166#[must_use]
168pub fn carousel_autoplay(id: &str, items: &[CarouselItem]) -> Element<Div> {
169 carousel_with_indicators(id, items).attr("data-bs-ride", "carousel")
170}
171
172fn carousel_item(item: &CarouselItem) -> Element<Div> {
174 use ironhtml_elements::{H5, P};
175
176 let class = if item.active {
177 "carousel-item active"
178 } else {
179 "carousel-item"
180 };
181
182 let mut elem = Element::<Div>::new().class(class).child::<Img, _>(|i| {
183 i.class("d-block w-100")
184 .attr("src", &item.image_src)
185 .attr("alt", &item.image_alt)
186 });
187
188 if item.caption_title.is_some() || item.caption_text.is_some() {
189 elem = elem.child::<Div, _>(|d| {
190 let mut caption = d.class("carousel-caption d-none d-md-block");
191 if let Some(ref title) = item.caption_title {
192 caption = caption.child::<H5, _>(|h| h.text(title));
193 }
194 if let Some(ref text) = item.caption_text {
195 caption = caption.child::<P, _>(|p| p.text(text));
196 }
197 caption
198 });
199 }
200
201 elem
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use alloc::vec;
208
209 #[test]
210 fn test_carousel() {
211 let items = vec![
212 CarouselItem::new("/1.jpg", "First").active(),
213 CarouselItem::new("/2.jpg", "Second"),
214 ];
215 let c = carousel("test", &items);
216 let html = c.render();
217 assert!(html.contains("carousel"));
218 assert!(html.contains("carousel-item active"));
219 assert!(html.contains("carousel-control-prev"));
220 }
221
222 #[test]
223 fn test_carousel_with_captions() {
224 let items = vec![CarouselItem::new("/1.jpg", "First")
225 .active()
226 .caption("Title", "Description")];
227 let c = carousel("test", &items);
228 let html = c.render();
229 assert!(html.contains("carousel-caption"));
230 assert!(html.contains("Title"));
231 }
232}