1
+ mod file_dialog;
2
+
3
+ use crate :: {
4
+ file_dialog:: FileDialog , file_handle:: WasmFileHandleKind , FileHandle , MessageDialogResult ,
5
+ } ;
1
6
use wasm_bindgen:: prelude:: * ;
2
7
use wasm_bindgen:: JsCast ;
3
- use web_sys:: Element ;
8
+ use web_sys:: { Element , HtmlAnchorElement , HtmlButtonElement , HtmlElement , HtmlInputElement } ;
4
9
5
- use web_sys:: { HtmlButtonElement , HtmlElement , HtmlInputElement } ;
10
+ #[ derive( Clone , Debug ) ]
11
+ pub enum FileKind < ' a > {
12
+ In ( FileDialog ) ,
13
+ Out ( FileDialog , & ' a [ u8 ] ) ,
14
+ }
6
15
7
- use crate :: file_dialog:: FileDialog ;
8
- use crate :: { FileHandle , MessageDialogResult } ;
16
+ #[ derive( Clone , Debug ) ]
17
+ enum HtmlIoElement < ' a > {
18
+ Input ( HtmlInputElement ) ,
19
+ Output {
20
+ element : HtmlAnchorElement ,
21
+ name : String ,
22
+ data : & ' a [ u8 ] ,
23
+ } ,
24
+ }
9
25
10
- pub struct WasmDialog {
26
+ pub struct WasmDialog < ' a > {
11
27
overlay : Element ,
12
28
card : Element ,
13
29
title : Option < HtmlElement > ,
14
- input : HtmlInputElement ,
30
+ io : HtmlIoElement < ' a > ,
15
31
button : HtmlButtonElement ,
16
32
17
33
style : Element ,
18
34
}
19
35
20
- impl WasmDialog {
21
- pub fn new ( opt : & FileDialog ) -> Self {
36
+ impl < ' a > WasmDialog < ' a > {
37
+ pub fn new ( opt : & FileKind < ' a > ) -> Self {
22
38
let window = web_sys:: window ( ) . expect ( "Window not found" ) ;
23
39
let document = window. document ( ) . expect ( "Document not found" ) ;
24
40
@@ -33,9 +49,13 @@ impl WasmDialog {
33
49
card
34
50
} ;
35
51
36
- let title = opt. title . as_ref ( ) . map ( |title| {
37
- let title_el: web_sys:: HtmlElement =
38
- document. create_element ( "div" ) . unwrap ( ) . dyn_into ( ) . unwrap ( ) ;
52
+ let title = match opt {
53
+ FileKind :: In ( dialog) => & dialog. title ,
54
+ FileKind :: Out ( dialog, _) => & dialog. title ,
55
+ }
56
+ . as_ref ( )
57
+ . map ( |title| {
58
+ let title_el: HtmlElement = document. create_element ( "div" ) . unwrap ( ) . dyn_into ( ) . unwrap ( ) ;
39
59
40
60
title_el. set_id ( "rfd-title" ) ;
41
61
title_el. set_inner_html ( title) ;
@@ -44,25 +64,41 @@ impl WasmDialog {
44
64
title_el
45
65
} ) ;
46
66
47
- let input = {
48
- let input_el = document. create_element ( "input" ) . unwrap ( ) ;
49
- let input: HtmlInputElement = wasm_bindgen:: JsCast :: dyn_into ( input_el) . unwrap ( ) ;
67
+ let io = match opt {
68
+ FileKind :: In ( dialog) => {
69
+ let input_el = document. create_element ( "input" ) . unwrap ( ) ;
70
+ let input: HtmlInputElement = wasm_bindgen:: JsCast :: dyn_into ( input_el) . unwrap ( ) ;
50
71
51
- input. set_id ( "rfd-input" ) ;
52
- input. set_type ( "file" ) ;
72
+ input. set_id ( "rfd-input" ) ;
73
+ input. set_type ( "file" ) ;
53
74
54
- let mut accept: Vec < String > = Vec :: new ( ) ;
75
+ let mut accept: Vec < String > = Vec :: new ( ) ;
55
76
56
- for filter in opt . filters . iter ( ) {
57
- accept. append ( & mut filter. extensions . to_vec ( ) ) ;
58
- }
77
+ for filter in dialog . filters . iter ( ) {
78
+ accept. append ( & mut filter. extensions . to_vec ( ) ) ;
79
+ }
59
80
60
- accept. iter_mut ( ) . for_each ( |ext| ext. insert_str ( 0 , "." ) ) ;
81
+ accept. iter_mut ( ) . for_each ( |ext| ext. insert_str ( 0 , "." ) ) ;
61
82
62
- input. set_accept ( & accept. join ( "," ) ) ;
83
+ input. set_accept ( & accept. join ( "," ) ) ;
63
84
64
- card. append_child ( & input) . unwrap ( ) ;
65
- input
85
+ card. append_child ( & input) . unwrap ( ) ;
86
+ HtmlIoElement :: Input ( input)
87
+ }
88
+ FileKind :: Out ( dialog, data) => {
89
+ let output_el = document. create_element ( "a" ) . unwrap ( ) ;
90
+ let output: HtmlAnchorElement = wasm_bindgen:: JsCast :: dyn_into ( output_el) . unwrap ( ) ;
91
+
92
+ output. set_id ( "rfd-output" ) ;
93
+ output. set_inner_text ( "click here to download your file" ) ;
94
+
95
+ card. append_child ( & output) . unwrap ( ) ;
96
+ HtmlIoElement :: Output {
97
+ element : output,
98
+ name : dialog. file_name . clone ( ) . unwrap_or_default ( ) ,
99
+ data,
100
+ }
101
+ }
66
102
} ;
67
103
68
104
let button = {
@@ -85,7 +121,7 @@ impl WasmDialog {
85
121
card,
86
122
title,
87
123
button,
88
- input ,
124
+ io ,
89
125
90
126
style,
91
127
}
@@ -94,26 +130,78 @@ impl WasmDialog {
94
130
async fn show ( & self ) {
95
131
let window = web_sys:: window ( ) . expect ( "Window not found" ) ;
96
132
let document = window. document ( ) . expect ( "Document not found" ) ;
97
- let body = document. body ( ) . expect ( "document should have a body" ) ;
133
+ let body = document. body ( ) . expect ( "Document should have a body" ) ;
98
134
99
135
let overlay = self . overlay . clone ( ) ;
100
136
let button = self . button . clone ( ) ;
101
137
102
- let promise = js_sys:: Promise :: new ( & mut move |res, _rej| {
103
- let closure = Closure :: wrap ( Box :: new ( move || {
104
- res. call0 ( & JsValue :: undefined ( ) ) . unwrap ( ) ;
105
- } ) as Box < dyn FnMut ( ) > ) ;
138
+ let promise = match & self . io {
139
+ HtmlIoElement :: Input ( _) => js_sys:: Promise :: new ( & mut move |res, _rej| {
140
+ let resolve_promise = Closure :: wrap ( Box :: new ( move || {
141
+ res. call0 ( & JsValue :: undefined ( ) ) . unwrap ( ) ;
142
+ } ) as Box < dyn FnMut ( ) > ) ;
143
+
144
+ button. set_onclick ( Some ( resolve_promise. as_ref ( ) . unchecked_ref ( ) ) ) ;
145
+ resolve_promise. forget ( ) ;
146
+ body. append_child ( & overlay) . ok ( ) ;
147
+ } ) ,
148
+ HtmlIoElement :: Output {
149
+ element,
150
+ name,
151
+ data,
152
+ } => {
153
+ js_sys:: Promise :: new ( & mut |res, _rej| {
154
+ // Moved to keep closure as FnMut
155
+ let output = element. clone ( ) ;
156
+ let file_name = name. clone ( ) ;
157
+
158
+ let resolve_promise = Closure :: wrap ( Box :: new ( move || {
159
+ res. call1 ( & JsValue :: undefined ( ) , & JsValue :: from ( true ) )
160
+ . unwrap ( ) ;
161
+ } ) as Box < dyn FnMut ( ) > ) ;
162
+
163
+ // Resolve the promise once the user clicks the download link or the button.
164
+ output. set_onclick ( Some ( resolve_promise. as_ref ( ) . unchecked_ref ( ) ) ) ;
165
+ button. set_onclick ( Some ( resolve_promise. as_ref ( ) . unchecked_ref ( ) ) ) ;
166
+ resolve_promise. forget ( ) ;
167
+
168
+ let set_download_link = move |in_array : & [ u8 ] , name : & str | {
169
+ // See <https://stackoverflow.com/questions/69556755/web-sysurlcreate-object-url-with-blobblob-not-formatting-binary-data-co>
170
+ let array = js_sys:: Array :: new ( ) ;
171
+ let uint8arr = js_sys:: Uint8Array :: new (
172
+ // Safety: No wasm allocations happen between creating the view and consuming it in the array.push
173
+ & unsafe { js_sys:: Uint8Array :: view ( & in_array) } . into ( ) ,
174
+ ) ;
175
+ array. push ( & uint8arr. buffer ( ) ) ;
176
+ let blob = web_sys:: Blob :: new_with_u8_array_sequence_and_options (
177
+ & array,
178
+ web_sys:: BlobPropertyBag :: new ( ) . type_ ( "application/octet-stream" ) ,
179
+ )
180
+ . unwrap ( ) ;
181
+ let download_url =
182
+ web_sys:: Url :: create_object_url_with_blob ( & blob) . unwrap ( ) ;
183
+
184
+ output. set_href ( & download_url) ;
185
+ output. set_download ( & name) ;
186
+ } ;
187
+
188
+ set_download_link ( & * data, & file_name) ;
189
+
190
+ body. append_child ( & overlay) . ok ( ) ;
191
+ } )
192
+ }
193
+ } ;
106
194
107
- button. set_onclick ( Some ( closure. as_ref ( ) . unchecked_ref ( ) ) ) ;
108
- closure. forget ( ) ;
109
- body. append_child ( & overlay) . ok ( ) ;
110
- } ) ;
111
195
let future = wasm_bindgen_futures:: JsFuture :: from ( promise) ;
112
196
future. await . unwrap ( ) ;
113
197
}
114
198
115
199
fn get_results ( & self ) -> Option < Vec < FileHandle > > {
116
- if let Some ( files) = self . input . files ( ) {
200
+ let input = match & self . io {
201
+ HtmlIoElement :: Input ( input) => input,
202
+ _ => panic ! ( "Internal Error: Results only exist for input dialog" ) ,
203
+ } ;
204
+ if let Some ( files) = input. files ( ) {
117
205
let len = files. length ( ) ;
118
206
if len > 0 {
119
207
let mut file_handles = Vec :: new ( ) ;
@@ -136,26 +224,41 @@ impl WasmDialog {
136
224
}
137
225
138
226
async fn pick_files ( self ) -> Option < Vec < FileHandle > > {
139
- self . input . set_multiple ( true ) ;
227
+ if let HtmlIoElement :: Input ( input) = & self . io {
228
+ input. set_multiple ( true ) ;
229
+ } else {
230
+ panic ! ( "Internal error: Pick files only on input wasm dialog" )
231
+ }
140
232
141
233
self . show ( ) . await ;
142
234
143
235
self . get_results ( )
144
236
}
145
237
146
238
async fn pick_file ( self ) -> Option < FileHandle > {
147
- self . input . set_multiple ( false ) ;
239
+ if let HtmlIoElement :: Input ( input) = & self . io {
240
+ input. set_multiple ( false ) ;
241
+ } else {
242
+ panic ! ( "Internal error: Pick file only on input wasm dialog" )
243
+ }
148
244
149
245
self . show ( ) . await ;
150
246
151
247
self . get_result ( )
152
248
}
249
+
250
+ fn io_element ( & self ) -> Element {
251
+ match self . io . clone ( ) {
252
+ HtmlIoElement :: Input ( element) => element. unchecked_into ( ) ,
253
+ HtmlIoElement :: Output { element, .. } => element. unchecked_into ( ) ,
254
+ }
255
+ }
153
256
}
154
257
155
- impl Drop for WasmDialog {
258
+ impl < ' a > Drop for WasmDialog < ' a > {
156
259
fn drop ( & mut self ) {
157
260
self . button . remove ( ) ;
158
- self . input . remove ( ) ;
261
+ self . io_element ( ) . remove ( ) ;
159
262
self . title . as_ref ( ) . map ( |elem| elem. remove ( ) ) ;
160
263
self . card . remove ( ) ;
161
264
@@ -168,11 +271,11 @@ use super::{AsyncFilePickerDialogImpl, DialogFutureType};
168
271
169
272
impl AsyncFilePickerDialogImpl for FileDialog {
170
273
fn pick_file_async ( self ) -> DialogFutureType < Option < FileHandle > > {
171
- let dialog = WasmDialog :: new ( & self ) ;
274
+ let dialog = WasmDialog :: new ( & FileKind :: In ( self ) ) ;
172
275
Box :: pin ( dialog. pick_file ( ) )
173
276
}
174
277
fn pick_files_async ( self ) -> DialogFutureType < Option < Vec < FileHandle > > > {
175
- let dialog = WasmDialog :: new ( & self ) ;
278
+ let dialog = WasmDialog :: new ( & FileKind :: In ( self ) ) ;
176
279
Box :: pin ( dialog. pick_files ( ) )
177
280
}
178
281
}
@@ -209,11 +312,21 @@ impl MessageDialogImpl for MessageDialog {
209
312
}
210
313
}
211
314
212
- use crate :: backend:: AsyncMessageDialogImpl ;
213
-
214
- impl AsyncMessageDialogImpl for MessageDialog {
315
+ impl crate :: backend:: AsyncMessageDialogImpl for MessageDialog {
215
316
fn show_async ( self ) -> DialogFutureType < MessageDialogResult > {
216
317
let val = MessageDialogImpl :: show ( self ) ;
217
318
Box :: pin ( std:: future:: ready ( val) )
218
319
}
219
320
}
321
+
322
+ impl FileHandle {
323
+ pub async fn write ( & self , data : & [ u8 ] ) -> std:: io:: Result < ( ) > {
324
+ let dialog = match & self . 0 {
325
+ WasmFileHandleKind :: Writable ( dialog) => dialog,
326
+ _ => panic ! ( "This File Handle doesn't support writing. Use `save_file` to get a writeable FileHandle in Wasm" ) ,
327
+ } ;
328
+ let dialog = WasmDialog :: new ( & FileKind :: Out ( dialog. clone ( ) , data) ) ;
329
+ dialog. show ( ) . await ;
330
+ Ok ( ( ) )
331
+ }
332
+ }
0 commit comments