Skip to main content

vertigo_forms/
drop_image_file.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD as BASE_64};
2use std::rc::Rc;
3use vertigo::{
4    AttrGroup, Computed, Css, DropFileEvent, DropFileItem, Value, bind, component, computed_tuple,
5    css, dom,
6};
7
8/// Box that allows to accept image files on it, connected to `Value<Option<DropFileItem>>`.
9#[component]
10pub fn DropImageFile(
11    original_link: Computed<Option<Rc<String>>>,
12    item: Value<Option<DropFileItem>>,
13    params: DropImageFileParams,
14    /// Any additional attributes for the dropzone
15    zone: AttrGroup,
16) {
17    let base64_data = item.to_computed().map(|item| match item {
18        Some(item) => image_as_uri(&item),
19        None => "".to_string(),
20    });
21
22    let view_deps = computed_tuple!(
23        a => original_link,
24        b => item,
25        c => base64_data
26    );
27    let item_clone = item.clone();
28    let params = params.clone();
29    let callback = params.callback.clone();
30    let image_view = view_deps.render_value(move |(original, item, base64_date)| match item {
31        Some(item) => {
32            let message = format_line(&item);
33            let image_css = css! {"
34                display: flex;
35                flex-flow: column;
36            "};
37            let restore = bind!(item_clone, callback, |_| {
38                if let Some(callback) = &callback {
39                    callback(None);
40                } else {
41                    item_clone.set(None);
42                }
43            });
44            let restore_text = if original.is_some() {
45                &params.revert_label
46            } else {
47                &params.cancel_label
48            };
49            dom! {
50                <div css={image_css}>
51                    <button on_click={restore}>{restore_text}</button>
52                    <img css={&params.img_css} src={base64_date} />
53                    { message }
54                </div>
55            }
56        }
57        None => match original {
58            Some(original) => {
59                dom! { <div><img css={&params.img_css} src={original} /></div> }
60            }
61            None => dom! { <div>{&params.no_image_text}</div> },
62        },
63    });
64
65    let item = item.clone();
66    let callback = params.callback.clone();
67    let on_dropfile = move |event: DropFileEvent| {
68        for file in event.items.into_iter() {
69            if let Some(callback) = callback.as_deref() {
70                callback(Some(file));
71            } else {
72                item.set(Some(file));
73            }
74        }
75    };
76
77    let dropzone_css = &params.dropzone_css + &params.dropzone_add_css;
78
79    dom! {
80        <div css={dropzone_css} on_dropfile={on_dropfile} {..zone}>
81            { image_view }
82        </div>
83    }
84}
85
86#[derive(Clone)]
87pub struct DropImageFileParams {
88    /// Custom callback when new image dropped, leave empty to automatically set/unset `item`
89    pub callback: Option<Rc<dyn Fn(Option<DropFileItem>)>>,
90    pub revert_label: String,
91    pub cancel_label: String,
92    pub no_image_text: String,
93    pub dropzone_css: Css,
94    pub dropzone_add_css: Css,
95    pub img_css: Css,
96}
97
98impl Default for DropImageFileParams {
99    fn default() -> Self {
100        Self {
101            callback: None,
102            revert_label: "Revert".to_string(),
103            cancel_label: "Cancel".to_string(),
104            no_image_text: "No image".to_string(),
105            dropzone_css: css! {"
106                width: 400px;
107                height: 400px;
108
109                display: flex;
110                align-items: center;
111                justify-content: center;
112
113                padding: 10px;
114            "},
115            dropzone_add_css: css!(""),
116            img_css: css!(""),
117        }
118    }
119}
120
121fn format_line(item: &DropFileItem) -> String {
122    let file_name = &item.name;
123    let size = item.data.len();
124    format!("{file_name} ({size})")
125}
126
127pub fn name_to_mime(name: &str) -> &'static str {
128    use std::{ffi::OsStr, path::Path};
129
130    let extension = Path::new(name)
131        .extension()
132        .and_then(OsStr::to_str)
133        .unwrap_or_default();
134
135    match extension {
136        "jpg" | "jpeg" | "jpe" => "image/jpeg",
137        "png" => "image/png",
138        "svg" => "image/svg+xml",
139        "gif" => "image/gif",
140        "bmp" => "image/bmp",
141        "ico" => "image/ico",
142        _ => "application/octet-stream",
143    }
144}
145
146pub fn image_as_uri(item: &DropFileItem) -> String {
147    let mime = name_to_mime(&item.name);
148    let data = BASE_64.encode(&*item.data);
149    format!("data:{mime};base64,{data}")
150}