jinya_ui/widgets/form/
file_upload.rs

1use yew::prelude::*;
2use yew::services::reader::File;
3use yew::{Callback, Component, ComponentLink, Html};
4
5pub fn get_css<'a>() -> &'a str {
6    // language=CSS
7    "
8.jinya-file-upload__color-container--default {
9    --state-color: var(--primary-color);
10}
11
12.jinya-file-upload__color-container--negative {
13    --state-color: var(--negative-color);
14}
15
16.jinya-file-upload__color-container--positive {
17    --state-color: var(--positive-color);
18}
19
20.jinya-file-upload__color-container--disabled {
21    --state-color: var(--disabled-border-color);
22}
23
24.jinya-file-upload__color-container--drag-over .jinya-file-upload__label,
25.jinya-file-upload__color-container--drag-over .jinya-file-upload__file-info,
26.jinya-file-upload__color-container--drag-over .jinya-file-upload__container {
27    background-color: var(--input-background-color);
28}
29
30.jinya-file-upload__color-container {
31    display: flex;
32    position: relative;
33    width: 100%;
34    flex: 0 0 100%;
35    flex-flow: row wrap;
36}
37
38.jinya-file-upload__container {
39    display: flex;
40    border: 2px solid var(--state-color);
41    border-radius: 5px;
42    padding: 0.5rem 0.75rem 0.25rem;
43    position: relative;
44    margin-top: 0.75rem;
45    width: 100%;
46    box-sizing: border-box;
47}
48
49.jinya-file-upload__input {
50    display: none;
51}
52
53.jinya-file-upload__file-info {
54    font-size: var(--font-size-16);
55    color: var(--state-color);
56    background: var(--white);
57    border: none;
58    padding: 0;
59    width: 100%;
60}
61
62.jinya-file-upload__file-info:disabled {
63    cursor: not-allowed;
64}
65
66.jinya-file-upload__label {
67    display: block;
68    font-size: var(--font-size-12);
69    color: var(--state-color);
70    position: absolute;
71    top: -0.75rem;
72    background: var(--white);
73    padding-left: 0.25rem;
74    padding-right: 0.25rem;
75    box-sizing: border-box;
76    left: 0.5rem;
77    z-index: 0;
78}
79
80.jinya-file-upload__validation-message {
81    display: block;
82    font-size: var(--font-size-12);
83    color: var(--state-color);
84}
85
86.jinya-file-upload__button {
87    margin-left: auto;
88    font-size: var(--font-size-24);
89    border: 2px solid var(--state-color);
90    height: calc(100% - 2rem);
91    top: 0.75rem;
92    position: absolute;
93    right: 0;
94    padding: 0.5rem;
95    transition: color 0.3s, background 0.3s;
96    background: var(--white);
97    border-bottom-right-radius: 5px;
98    border-top-right-radius: 5px;
99    cursor: pointer;
100}
101
102.jinya-file-upload__button:hover {
103    color: var(--white);
104    background: var(--state-color);
105}
106"
107}
108
109#[derive(Clone, PartialEq)]
110pub enum FileUploadState {
111    Default,
112    Negative,
113    Positive,
114}
115
116pub struct FileUpload {
117    link: ComponentLink<Self>,
118    label: String,
119    state: FileUploadState,
120    validation_message: String,
121    placeholder: String,
122    filename: String,
123    disabled: bool,
124    on_select: Callback<Vec<File>>,
125    is_drag_over: bool,
126    multiple: bool,
127}
128
129#[derive(Clone, PartialEq, Properties)]
130pub struct FileUploadProps {
131    pub label: String,
132    #[prop_or(FileUploadState::Default)]
133    pub state: FileUploadState,
134    #[prop_or("".to_string())]
135    pub validation_message: String,
136    #[prop_or("".to_string())]
137    pub placeholder: String,
138    #[prop_or(false)]
139    pub disabled: bool,
140    #[prop_or(false)]
141    pub multiple: bool,
142    pub on_select: Callback<Vec<File>>,
143    #[prop_or("".to_string())]
144    pub filename: String,
145}
146
147pub enum Msg {
148    Files(Vec<File>),
149    Drop(DragEvent),
150    DragOver(DragEvent),
151    DragExit,
152}
153
154impl Default for FileUploadState {
155    fn default() -> Self {
156        FileUploadState::Default
157    }
158}
159
160impl FileUpload {
161    fn get_input_container_class(&self) -> String {
162        let class = if self.disabled {
163            "jinya-file-upload__color-container jinya-file-upload__color-container--disabled"
164                .to_string()
165        } else {
166            match self.state {
167                FileUploadState::Default => "jinya-file-upload__color-container jinya-file-upload__color-container--default",
168                FileUploadState::Negative => "jinya-file-upload__color-container jinya-file-upload__color-container--negative",
169                FileUploadState::Positive => "jinya-file-upload__color-container jinya-file-upload__color-container--positive",
170            }.to_string()
171        };
172
173        if self.is_drag_over {
174            format!("{} jinya-file-upload__color-container--drag-over", class)
175        } else {
176            class
177        }
178    }
179}
180
181impl Component for FileUpload {
182    type Message = Msg;
183    type Properties = FileUploadProps;
184
185    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
186        FileUpload {
187            link,
188            label: props.label,
189            state: props.state,
190            validation_message: props.validation_message,
191            placeholder: props.placeholder,
192            disabled: props.disabled,
193            on_select: props.on_select,
194            filename: props.filename,
195            is_drag_over: false,
196            multiple: props.multiple,
197        }
198    }
199
200    fn update(&mut self, msg: Self::Message) -> bool {
201        match msg {
202            Msg::Files(value) => {
203                self.on_select.emit(value.clone());
204                if !value.is_empty() {
205                    self.filename = if self.multiple {
206                        value
207                            .iter()
208                            .map(|file| file.name())
209                            .collect::<Vec<String>>()
210                            .join(", ")
211                    } else {
212                        value.first().unwrap().name()
213                    }
214                }
215            }
216            Msg::Drop(event) => {
217                self.is_drag_over = false;
218                event.prevent_default();
219                event.stop_propagation();
220                let data_transfer = event.data_transfer().unwrap();
221                let files: Vec<File> = js_sys::try_iter(&data_transfer.files().unwrap())
222                    .unwrap()
223                    .unwrap()
224                    .map(|v| File::from(v.unwrap()))
225                    .collect();
226                if !files.is_empty() {
227                    self.on_select.emit(files.clone());
228                    self.filename = if self.multiple {
229                        files
230                            .iter()
231                            .map(|file| file.name())
232                            .collect::<Vec<String>>()
233                            .join(", ")
234                    } else {
235                        files.first().unwrap().name()
236                    }
237                }
238            }
239            Msg::DragOver(event) => {
240                self.is_drag_over = true;
241                event.prevent_default();
242                event.stop_propagation();
243                let data_transfer = event.data_transfer().unwrap();
244                data_transfer.set_drop_effect("copy");
245            }
246            Msg::DragExit => {
247                self.is_drag_over = false;
248            }
249        }
250
251        true
252    }
253
254    fn change(&mut self, _props: Self::Properties) -> bool {
255        self.label = _props.label;
256        self.state = _props.state;
257        self.validation_message = _props.validation_message;
258        self.placeholder = _props.placeholder;
259        self.disabled = _props.disabled;
260
261        true
262    }
263
264    fn view(&self) -> Html {
265        let id = super::super::super::id_generator::generate_id();
266        html! {
267            <div
268                class=self.get_input_container_class()
269                ondrop=self.link.callback(move |event| {
270                    Msg::Drop(event)
271                })
272                ondragover=self.link.callback(move |event: DragEvent| {
273                    Msg::DragOver(event)
274                })
275                ondragexit=self.link.callback(|_| {
276                    Msg::DragExit
277                })
278            >
279                <div class="jinya-file-upload__color-container">
280                    <div class="jinya-file-upload__container">
281                        <label for=id class="jinya-file-upload__label">{&self.label}</label>
282                        <input
283                            id=id
284                            type="file"
285                            disabled=self.disabled
286                            placeholder=self.placeholder
287                            class="jinya-file-upload__input"
288                            multiple=self.multiple
289                            onchange=self.link.callback(move |value| {
290                                let mut result = vec![];
291                                if let ChangeData::Files(files) = value {
292                                    let files = js_sys::try_iter(&files)
293                                        .unwrap()
294                                        .unwrap()
295                                        .into_iter()
296                                        .map(|v| File::from(v.unwrap()));
297                                    result.extend(files);
298                                }
299                                Msg::Files(result)
300                            })
301                        />
302                        <label for=id class="jinya-file-upload__file-info">
303                            {if self.filename.is_empty() {
304                                &self.placeholder
305                            } else {
306                                &self.filename
307                            }}
308                        </label>
309                    </div>
310                    <label for=id class="jinya-file-upload__button">
311                        <span class="mdi mdi-upload"></span>
312                    </label>
313                </div>
314                <span class="jinya-file-upload__validation-message">{&self.validation_message}</span>
315            </div>
316        }
317    }
318}