1use ironhtml::typed::Element;
7use ironhtml_elements::{Button, Div, H1, H5};
8
9extern crate alloc;
10use alloc::format;
11use alloc::string::ToString;
12
13#[derive(Clone, Copy, Default)]
15pub enum ModalSize {
16 Small,
17 #[default]
18 Default,
19 Large,
20 ExtraLarge,
21 Fullscreen,
22}
23
24impl ModalSize {
25 const fn class(self) -> &'static str {
26 match self {
27 Self::Small => "modal-sm",
28 Self::Default => "",
29 Self::Large => "modal-lg",
30 Self::ExtraLarge => "modal-xl",
31 Self::Fullscreen => "modal-fullscreen",
32 }
33 }
34}
35
36#[must_use]
48pub fn modal_button(target_id: &str, color: crate::Color, text: &str) -> Element<Button> {
49 let class = format!("btn btn-{}", color.as_str());
50 Element::<Button>::new()
51 .attr("type", "button")
52 .class(&class)
53 .attr("data-bs-toggle", "modal")
54 .attr("data-bs-target", format!("#{target_id}"))
55 .text(text)
56}
57
58#[must_use]
71pub fn modal<F>(id: &str, size: ModalSize, title: &str, body_fn: F) -> Element<Div>
72where
73 F: FnOnce(Element<Div>) -> Element<Div>,
74{
75 let dialog_class = if size.class().is_empty() {
76 "modal-dialog".to_string()
77 } else {
78 format!("modal-dialog {}", size.class())
79 };
80
81 Element::<Div>::new()
82 .class("modal fade")
83 .attr("id", id)
84 .attr("tabindex", "-1")
85 .attr("aria-labelledby", format!("{id}Label"))
86 .attr("aria-hidden", "true")
87 .child::<Div, _>(|d| {
88 d.class(&dialog_class).child::<Div, _>(|content| {
89 content
90 .class("modal-content")
91 .child::<Div, _>(|header| {
93 header
94 .class("modal-header")
95 .child::<H1, _>(|h| {
96 h.class("modal-title fs-5")
97 .attr("id", format!("{id}Label"))
98 .text(title)
99 })
100 .child::<Button, _>(|b| {
101 b.attr("type", "button")
102 .class("btn-close")
103 .attr("data-bs-dismiss", "modal")
104 .attr("aria-label", "Close")
105 })
106 })
107 .child::<Div, _>(|body| body_fn(body.class("modal-body")))
109 })
110 })
111}
112
113#[must_use]
115pub fn modal_with_footer<F>(
116 id: &str,
117 size: ModalSize,
118 title: &str,
119 body_fn: F,
120 primary_btn_text: &str,
121) -> Element<Div>
122where
123 F: FnOnce(Element<Div>) -> Element<Div>,
124{
125 let dialog_class = if size.class().is_empty() {
126 "modal-dialog".to_string()
127 } else {
128 format!("modal-dialog {}", size.class())
129 };
130
131 Element::<Div>::new()
132 .class("modal fade")
133 .attr("id", id)
134 .attr("tabindex", "-1")
135 .attr("aria-labelledby", format!("{id}Label"))
136 .attr("aria-hidden", "true")
137 .child::<Div, _>(|d| {
138 d.class(&dialog_class).child::<Div, _>(|content| {
139 content
140 .class("modal-content")
141 .child::<Div, _>(|header| {
143 header
144 .class("modal-header")
145 .child::<H5, _>(|h| {
146 h.class("modal-title")
147 .attr("id", format!("{id}Label"))
148 .text(title)
149 })
150 .child::<Button, _>(|b| {
151 b.attr("type", "button")
152 .class("btn-close")
153 .attr("data-bs-dismiss", "modal")
154 .attr("aria-label", "Close")
155 })
156 })
157 .child::<Div, _>(|body| body_fn(body.class("modal-body")))
159 .child::<Div, _>(|footer| {
161 footer
162 .class("modal-footer")
163 .child::<Button, _>(|b| {
164 b.attr("type", "button")
165 .class("btn btn-secondary")
166 .attr("data-bs-dismiss", "modal")
167 .text("Close")
168 })
169 .child::<Button, _>(|b| {
170 b.attr("type", "button")
171 .class("btn btn-primary")
172 .text(primary_btn_text)
173 })
174 })
175 })
176 })
177}
178
179#[must_use]
181pub fn modal_scrollable<F>(id: &str, title: &str, body_fn: F) -> Element<Div>
182where
183 F: FnOnce(Element<Div>) -> Element<Div>,
184{
185 Element::<Div>::new()
186 .class("modal fade")
187 .attr("id", id)
188 .attr("tabindex", "-1")
189 .attr("aria-labelledby", format!("{id}Label"))
190 .attr("aria-hidden", "true")
191 .child::<Div, _>(|d| {
192 d.class("modal-dialog modal-dialog-scrollable")
193 .child::<Div, _>(|content| {
194 content
195 .class("modal-content")
196 .child::<Div, _>(|header| {
197 header
198 .class("modal-header")
199 .child::<H5, _>(|h| {
200 h.class("modal-title")
201 .attr("id", format!("{id}Label"))
202 .text(title)
203 })
204 .child::<Button, _>(|b| {
205 b.attr("type", "button")
206 .class("btn-close")
207 .attr("data-bs-dismiss", "modal")
208 .attr("aria-label", "Close")
209 })
210 })
211 .child::<Div, _>(|body| body_fn(body.class("modal-body")))
212 })
213 })
214}
215
216#[must_use]
218pub fn modal_centered<F>(id: &str, title: &str, body_fn: F) -> Element<Div>
219where
220 F: FnOnce(Element<Div>) -> Element<Div>,
221{
222 Element::<Div>::new()
223 .class("modal fade")
224 .attr("id", id)
225 .attr("tabindex", "-1")
226 .attr("aria-labelledby", format!("{id}Label"))
227 .attr("aria-hidden", "true")
228 .child::<Div, _>(|d| {
229 d.class("modal-dialog modal-dialog-centered")
230 .child::<Div, _>(|content| {
231 content
232 .class("modal-content")
233 .child::<Div, _>(|header| {
234 header
235 .class("modal-header")
236 .child::<H5, _>(|h| {
237 h.class("modal-title")
238 .attr("id", format!("{id}Label"))
239 .text(title)
240 })
241 .child::<Button, _>(|b| {
242 b.attr("type", "button")
243 .class("btn-close")
244 .attr("data-bs-dismiss", "modal")
245 .attr("aria-label", "Close")
246 })
247 })
248 .child::<Div, _>(|body| body_fn(body.class("modal-body")))
249 })
250 })
251}
252
253#[must_use]
255pub fn modal_static<F>(id: &str, title: &str, body_fn: F) -> Element<Div>
256where
257 F: FnOnce(Element<Div>) -> Element<Div>,
258{
259 Element::<Div>::new()
260 .class("modal fade")
261 .attr("id", id)
262 .attr("data-bs-backdrop", "static")
263 .attr("data-bs-keyboard", "false")
264 .attr("tabindex", "-1")
265 .attr("aria-labelledby", format!("{id}Label"))
266 .attr("aria-hidden", "true")
267 .child::<Div, _>(|d| {
268 d.class("modal-dialog").child::<Div, _>(|content| {
269 content
270 .class("modal-content")
271 .child::<Div, _>(|header| {
272 header
273 .class("modal-header")
274 .child::<H5, _>(|h| {
275 h.class("modal-title")
276 .attr("id", format!("{id}Label"))
277 .text(title)
278 })
279 .child::<Button, _>(|b| {
280 b.attr("type", "button")
281 .class("btn-close")
282 .attr("data-bs-dismiss", "modal")
283 .attr("aria-label", "Close")
284 })
285 })
286 .child::<Div, _>(|body| body_fn(body.class("modal-body")))
287 })
288 })
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_modal() {
297 let m = modal("test", ModalSize::Default, "Title", |body| {
298 body.text("Content")
299 });
300 let html = m.render();
301 assert!(html.contains("modal fade"));
302 assert!(html.contains("modal-dialog"));
303 assert!(html.contains("modal-content"));
304 assert!(html.contains("modal-header"));
305 assert!(html.contains("modal-body"));
306 }
307
308 #[test]
309 fn test_modal_sizes() {
310 let small = modal("sm", ModalSize::Small, "Small", |b| b);
311 assert!(small.render().contains("modal-sm"));
312
313 let large = modal("lg", ModalSize::Large, "Large", |b| b);
314 assert!(large.render().contains("modal-lg"));
315
316 let xl = modal("xl", ModalSize::ExtraLarge, "XL", |b| b);
317 assert!(xl.render().contains("modal-xl"));
318 }
319
320 #[test]
321 fn test_modal_button() {
322 let btn = modal_button("myModal", crate::Color::Primary, "Open");
323 let html = btn.render();
324 assert!(html.contains(r#"data-bs-toggle="modal""#));
325 assert!(html.contains(r##"data-bs-target="#myModal""##));
326 }
327}