ironhtml_bootstrap/
placeholder.rs

1//! Bootstrap placeholder components.
2//!
3//! Provides type-safe Bootstrap placeholders matching the
4//! [Bootstrap placeholders documentation](https://getbootstrap.com/docs/5.3/components/placeholders/).
5
6use ironhtml::typed::Element;
7use ironhtml_elements::{Div, Span, A, P};
8
9extern crate alloc;
10use alloc::format;
11
12/// Placeholder width options.
13#[derive(Clone, Copy)]
14pub enum PlaceholderWidth {
15    Col1,
16    Col2,
17    Col3,
18    Col4,
19    Col5,
20    Col6,
21    Col7,
22    Col8,
23    Col9,
24    Col10,
25    Col11,
26    Col12,
27}
28
29impl PlaceholderWidth {
30    const fn class(self) -> &'static str {
31        match self {
32            Self::Col1 => "col-1",
33            Self::Col2 => "col-2",
34            Self::Col3 => "col-3",
35            Self::Col4 => "col-4",
36            Self::Col5 => "col-5",
37            Self::Col6 => "col-6",
38            Self::Col7 => "col-7",
39            Self::Col8 => "col-8",
40            Self::Col9 => "col-9",
41            Self::Col10 => "col-10",
42            Self::Col11 => "col-11",
43            Self::Col12 => "col-12",
44        }
45    }
46}
47
48/// Placeholder size options.
49#[derive(Clone, Copy, Default)]
50pub enum PlaceholderSize {
51    ExtraSmall,
52    Small,
53    #[default]
54    Default,
55    Large,
56}
57
58impl PlaceholderSize {
59    const fn class(self) -> &'static str {
60        match self {
61            Self::ExtraSmall => "placeholder-xs",
62            Self::Small => "placeholder-sm",
63            Self::Default => "",
64            Self::Large => "placeholder-lg",
65        }
66    }
67}
68
69/// Create a placeholder span.
70///
71/// ## Example
72///
73/// ```rust
74/// use ironhtml_bootstrap::placeholder::{placeholder, PlaceholderWidth};
75///
76/// let p = placeholder(PlaceholderWidth::Col6);
77/// assert!(p.render().contains("placeholder"));
78/// ```
79#[must_use]
80pub fn placeholder(width: PlaceholderWidth) -> Element<Span> {
81    let class = format!("placeholder {}", width.class());
82    Element::<Span>::new().class(&class)
83}
84
85/// Create a sized placeholder.
86#[must_use]
87pub fn placeholder_sized(width: PlaceholderWidth, size: PlaceholderSize) -> Element<Span> {
88    let size_class = size.class();
89    let class = if size_class.is_empty() {
90        format!("placeholder {}", width.class())
91    } else {
92        format!("placeholder {} {size_class}", width.class())
93    };
94    Element::<Span>::new().class(&class)
95}
96
97/// Create a colored placeholder.
98#[must_use]
99pub fn placeholder_colored(width: PlaceholderWidth, color: crate::Color) -> Element<Span> {
100    let class = format!("placeholder {} bg-{}", width.class(), color.as_str());
101    Element::<Span>::new().class(&class)
102}
103
104/// Create a placeholder paragraph (loading text simulation).
105///
106/// ## Example
107///
108/// ```rust
109/// use ironhtml_bootstrap::placeholder::placeholder_paragraph;
110///
111/// let p = placeholder_paragraph();
112/// assert!(p.render().contains("placeholder-glow"));
113/// ```
114#[must_use]
115pub fn placeholder_paragraph() -> Element<P> {
116    Element::<P>::new()
117        .class("placeholder-glow")
118        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col7))
119        .text(" ")
120        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col4))
121        .text(" ")
122        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col4))
123        .text(" ")
124        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col6))
125        .text(" ")
126        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col8))
127}
128
129/// Create a placeholder with glow animation.
130#[must_use]
131pub fn placeholder_glow<F>(f: F) -> Element<Div>
132where
133    F: FnOnce(Element<Div>) -> Element<Div>,
134{
135    f(Element::<Div>::new().class("placeholder-glow"))
136}
137
138/// Create a placeholder with wave animation.
139#[must_use]
140pub fn placeholder_wave<F>(f: F) -> Element<Div>
141where
142    F: FnOnce(Element<Div>) -> Element<Div>,
143{
144    f(Element::<Div>::new().class("placeholder-wave"))
145}
146
147/// Create a placeholder button.
148#[must_use]
149pub fn placeholder_button(color: crate::Color, width: PlaceholderWidth) -> Element<A> {
150    let class = format!(
151        "btn btn-{} disabled placeholder {}",
152        color.as_str(),
153        width.class()
154    );
155    Element::<A>::new()
156        .class(&class)
157        .attr("aria-disabled", "true")
158}
159
160/// Create a loading card placeholder (matches Bootstrap docs example).
161#[must_use]
162pub fn placeholder_card() -> Element<Div> {
163    use ironhtml_elements::{Img, H5};
164
165    Element::<Div>::new()
166        .class("card")
167        .attr("aria-hidden", "true")
168        .child::<Img, _>(|i| {
169            i.attr("src", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 286 180'%3E%3Crect fill='%23868e96' width='286' height='180'/%3E%3C/svg%3E")
170                .class("card-img-top")
171                .attr("alt", "")
172        })
173        .child::<Div, _>(|body| {
174            body.class("card-body")
175                .child::<H5, _>(|h| {
176                    h.class("card-title placeholder-glow")
177                        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col6))
178                })
179                .child::<P, _>(|p| {
180                    p.class("card-text placeholder-glow")
181                        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col7))
182                        .text(" ")
183                        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col4))
184                        .text(" ")
185                        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col4))
186                        .text(" ")
187                        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col6))
188                        .text(" ")
189                        .child::<Span, _>(|_| placeholder(PlaceholderWidth::Col8))
190                })
191                .child::<A, _>(|_| placeholder_button(crate::Color::Primary, PlaceholderWidth::Col6))
192        })
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_placeholder() {
201        let p = placeholder(PlaceholderWidth::Col6);
202        let html = p.render();
203        assert!(html.contains("placeholder"));
204        assert!(html.contains("col-6"));
205    }
206
207    #[test]
208    fn test_placeholder_sized() {
209        let p = placeholder_sized(PlaceholderWidth::Col4, PlaceholderSize::Large);
210        let html = p.render();
211        assert!(html.contains("placeholder-lg"));
212    }
213
214    #[test]
215    fn test_placeholder_colored() {
216        let p = placeholder_colored(PlaceholderWidth::Col3, crate::Color::Primary);
217        let html = p.render();
218        assert!(html.contains("bg-primary"));
219    }
220
221    #[test]
222    fn test_placeholder_paragraph() {
223        let p = placeholder_paragraph();
224        let html = p.render();
225        assert!(html.contains("placeholder-glow"));
226        assert!(html.contains("placeholder"));
227    }
228
229    #[test]
230    fn test_placeholder_card() {
231        let card = placeholder_card();
232        let html = card.render();
233        assert!(html.contains("card"));
234        assert!(html.contains("placeholder-glow"));
235    }
236}