vertigo_forms/
drop_image_file.rs1use 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#[component]
10pub fn DropImageFile(
11 original_link: Computed<Option<Rc<String>>>,
12 item: Value<Option<DropFileItem>>,
13 params: DropImageFileParams,
14 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
31 let item_for_select = item.clone();
33 let callback_for_select = params.callback.clone();
34 let make_on_change_file = Rc::new(move || {
35 let item = item_for_select.clone();
36 let callback = callback_for_select.clone();
37 move |event: DropFileEvent| {
38 for file in event.items {
39 if let Some(cb) = callback.as_deref() {
40 cb(Some(file));
41 } else {
42 item.set(Some(file));
43 }
44 }
45 }
46 });
47
48 let image_view = view_deps.render_value(move |(original, item, base64_date)| {
49 let hidden_input_css = css! {"display: none;"};
50 let btn_css = css! {"
51 text-align: center;
52 display: inline-block;
53 cursor: pointer;
54 padding: 4px 12px;
55 border: 1px solid currentColor;
56 border-radius: 4px;
57 font-size: 0.85em;
58 margin-top: 6px;
59 margin-bottom: 6px;
60 "};
61 let select_label = params.select_label.clone();
62 let accept = params.accept.clone();
63 let flex_column = css! {"display: flex; flex-direction: column; align-items: center;"};
64
65 match item {
66 Some(item) => {
67 let message = format_line(&item);
68 let restore = bind!(item_clone, callback, |_| {
69 if let Some(callback) = &callback {
70 callback(None);
71 } else {
72 item_clone.set(None);
73 }
74 });
75 let restore_text = if original.is_some() {
76 ¶ms.revert_label
77 } else {
78 ¶ms.cancel_label
79 };
80 let select_button = if select_label.is_empty() {
81 None
82 } else {
83 Some(dom! {
84 <label css={&btn_css}>
85 <input
86 css={hidden_input_css}
87 type="file"
88 accept={accept}
89 on_change_file={make_on_change_file()}
90 />
91 {select_label}
92 </label>
93 })
94 };
95 dom! {
96 <div css={flex_column}>
97 <button css={btn_css}on_click={restore}>{restore_text}</button>
98 {..select_button}
99 <img css={¶ms.img_css} src={base64_date} />
100 { message }
101 </div>
102 }
103 }
104 None => match original {
105 Some(original) => {
106 if select_label.is_empty() {
107 dom! { <div><img css={¶ms.img_css} src={original} /></div> }
108 } else {
109 dom! {
110 <div css={flex_column}>
111 <img css={¶ms.img_css} src={original} />
112 <label css={btn_css}>
113 <input
114 css={hidden_input_css}
115 type="file"
116 accept={accept}
117 on_change_file={make_on_change_file()}
118 />
119 {select_label}
120 </label>
121 </div>
122 }
123 }
124 }
125 None => {
126 if select_label.is_empty() {
127 dom! { <div>{¶ms.no_image_text}</div> }
128 } else {
129 dom! {
130 <div css={flex_column}>
131 {¶ms.no_image_text}
132 <label css={btn_css}>
133 <input
134 css={hidden_input_css}
135 type="file"
136 accept={accept}
137 on_change_file={make_on_change_file()}
138 />
139 {select_label}
140 </label>
141 </div>
142 }
143 }
144 }
145 },
146 }
147 });
148
149 let item = item.clone();
150 let callback = params.callback.clone();
151 let on_dropfile = move |event: DropFileEvent| {
152 for file in event.items.into_iter() {
153 if let Some(callback) = callback.as_deref() {
154 callback(Some(file));
155 } else {
156 item.set(Some(file));
157 }
158 }
159 };
160
161 let dropzone_css = ¶ms.dropzone_css + ¶ms.dropzone_add_css;
162
163 dom! {
164 <div css={dropzone_css} on_dropfile={on_dropfile} {..zone}>
165 { image_view }
166 </div>
167 }
168}
169
170#[derive(Clone)]
171pub struct DropImageFileParams {
172 pub callback: Option<Rc<dyn Fn(Option<DropFileItem>)>>,
174 pub revert_label: String,
175 pub cancel_label: String,
176 pub no_image_text: String,
177 pub dropzone_css: Css,
178 pub dropzone_add_css: Css,
179 pub img_css: Css,
180 pub select_label: String,
182 pub accept: String,
184}
185
186impl Default for DropImageFileParams {
187 fn default() -> Self {
188 Self {
189 callback: None,
190 revert_label: "Revert".to_string(),
191 cancel_label: "Cancel".to_string(),
192 no_image_text: "No image".to_string(),
193 dropzone_css: css! {"
194 width: 400px;
195 height: 400px;
196
197 display: flex;
198 align-items: center;
199 justify-content: center;
200
201 padding: 10px;
202 "},
203 dropzone_add_css: css! {""},
204 img_css: css! {"
205 max-width: 100%;
206 max-height: 320px;
207 object-fit: contain;
208 "},
209 select_label: "Select file...".to_string(),
210 accept: "image/*".to_string(),
211 }
212 }
213}
214
215fn format_line(item: &DropFileItem) -> String {
216 let file_name = &item.name;
217 let size = item.data.len();
218 format!("{file_name} ({size})")
219}
220
221pub fn name_to_mime(name: &str) -> &'static str {
222 use std::{ffi::OsStr, path::Path};
223
224 let extension = Path::new(name)
225 .extension()
226 .and_then(OsStr::to_str)
227 .unwrap_or_default();
228
229 match extension {
230 "jpg" | "jpeg" | "jpe" => "image/jpeg",
231 "png" => "image/png",
232 "svg" => "image/svg+xml",
233 "gif" => "image/gif",
234 "bmp" => "image/bmp",
235 "ico" => "image/ico",
236 _ => "application/octet-stream",
237 }
238}
239
240pub fn image_as_uri(item: &DropFileItem) -> String {
241 let mime = name_to_mime(&item.name);
242 let data = BASE_64.encode(&*item.data);
243 format!("data:{mime};base64,{data}")
244}