ironhtml_bootstrap/
carousel.rs

1//! Bootstrap carousel components.
2//!
3//! Provides type-safe Bootstrap carousels matching the
4//! [Bootstrap carousel documentation](https://getbootstrap.com/docs/5.3/components/carousel/).
5
6use 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
14/// A carousel slide item.
15pub 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    /// Create a new carousel item.
25    #[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    /// Mark this slide as active.
37    #[must_use]
38    pub const fn active(mut self) -> Self {
39        self.active = true;
40        self
41    }
42
43    /// Add a caption to this slide.
44    #[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/// Create a Bootstrap carousel.
53///
54/// ## Example
55///
56/// ```rust
57/// use ironhtml_bootstrap::carousel::{carousel, CarouselItem};
58///
59/// let items = vec![
60///     CarouselItem::new("/img/1.jpg", "First slide").active(),
61///     CarouselItem::new("/img/2.jpg", "Second slide"),
62///     CarouselItem::new("/img/3.jpg", "Third slide"),
63/// ];
64///
65/// let c = carousel("myCarousel", &items);
66/// assert!(c.render().contains("carousel"));
67/// ```
68#[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/// Create a carousel with indicators.
107#[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        // Indicators
115        .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        // Slides
134        .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        // Controls
142        .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/// Create a carousel that autoplays.
167#[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
172/// Create a single carousel item.
173fn 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}