Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 39 additions & 33 deletions src/backend/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, HtmlAnchorElement, HtmlButtonElement, HtmlElement, HtmlInputElement};
use web_sys::{Element, HtmlButtonElement, HtmlElement, HtmlInputElement};

#[derive(Clone, Debug)]
pub enum FileKind<'a> {
Expand All @@ -17,7 +17,7 @@ pub enum FileKind<'a> {
enum HtmlIoElement<'a> {
Input(HtmlInputElement),
Output {
element: HtmlAnchorElement,
element: HtmlButtonElement,
name: String,
data: &'a [u8],
},
Expand Down Expand Up @@ -87,11 +87,11 @@ impl<'a> WasmDialog<'a> {
HtmlIoElement::Input(input)
}
FileKind::Out(dialog, data) => {
let output_el = document.create_element("a").unwrap();
let output: HtmlAnchorElement = wasm_bindgen::JsCast::dyn_into(output_el).unwrap();
let output_el = document.create_element("button").unwrap();
let output: HtmlButtonElement = wasm_bindgen::JsCast::dyn_into(output_el).unwrap();

output.set_id("rfd-output");
output.set_inner_text("click here to download your file");
output.set_id("rfd-button");
output.set_inner_text("Download");

card.append_child(&output).unwrap();
HtmlIoElement::Output {
Expand Down Expand Up @@ -190,8 +190,9 @@ impl<'a> WasmDialog<'a> {
} => {
js_sys::Promise::new(&mut |res, rej| {
// Moved to keep closure as FnMut
let output = element.clone();
let download_button = element.clone();
let file_name = name.clone();
let file_data: Vec<_> = data.iter().cloned().collect();

let resolve_promise = Closure::wrap(Box::new(move || {
res.call1(&JsValue::undefined(), &JsValue::from(true))
Expand All @@ -204,38 +205,43 @@ impl<'a> WasmDialog<'a> {
}) as Box<dyn FnMut()>);

// Resolve the promise once the user clicks the download link or the button.
output.set_onclick(Some(resolve_promise.as_ref().unchecked_ref()));
ok_button.set_onclick(Some(resolve_promise.as_ref().unchecked_ref()));
cancel_button.set_onclick(Some(reject_promise.as_ref().unchecked_ref()));

resolve_promise.forget();
reject_promise.forget();

let set_download_link = move |in_array: &[u8], name: &str| {
// See <https://stackoverflow.com/questions/69556755/web-sysurlcreate-object-url-with-blobblob-not-formatting-binary-data-co>
let array = js_sys::Array::new();
let uint8arr = js_sys::Uint8Array::new(
// Safety: No wasm allocations happen between creating the view and consuming it in the array.push
&unsafe { js_sys::Uint8Array::view(&in_array) }.into(),
);
array.push(&uint8arr.buffer());

let blob_property = web_sys::BlobPropertyBag::new();
blob_property.set_type("application/octet-stream");

let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(
&array,
&blob_property,
)
.unwrap();
let download_url =
web_sys::Url::create_object_url_with_blob(&blob).unwrap();

output.set_href(&download_url);
output.set_download(&name);
};

set_download_link(&*data, &file_name);
let download_file = Closure::wrap(Box::new(move || {
let _ = js_sys::eval(include_str!("wasm/FileSaver.js"));
if let Ok(f) = js_sys::eval("saveAs") {
// See <https://stackoverflow.com/questions/69556755/web-sysurlcreate-object-url-with-blobblob-not-formatting-binary-data-co>
let array = js_sys::Array::new();
let uint8arr = js_sys::Uint8Array::new(
// Safety: No wasm allocations happen between creating the view and consuming it in the array.push
&unsafe { js_sys::Uint8Array::view(&*file_data) }.into(),
);
array.push(&uint8arr.buffer());

let blob_property = web_sys::BlobPropertyBag::new();
blob_property.set_type("application/octet-stream");

let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(
&array,
&blob_property,
)
.unwrap();

if let Some(f) = wasm_bindgen::JsCast::dyn_ref::<js_sys::Function>(&f) {
f.call2(
&js_sys::global(),
&blob,
&wasm_bindgen::JsValue::from_str(&file_name),
);
}
}
}) as Box<dyn FnMut()>);
download_button.set_onclick(Some(download_file.as_ref().unchecked_ref()));
download_file.forget();

body.append_child(&overlay).ok();
})
Expand Down
172 changes: 172 additions & 0 deletions src/backend/wasm/FileSaver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* FileSaver.js
* A saveAs() FileSaver implementation.
*
* By Eli Grey, http://eligrey.com
*
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
* source : http://purl.eligrey.com/github/FileSaver.js
*/

// The one and only way of getting global scope in all environments
// https://stackoverflow.com/q/3277182/1008999
var _global = typeof window === 'object' && window.window === window
? window : typeof self === 'object' && self.self === self
? self : typeof global === 'object' && global.global === global
? global
: this

function bom (blob, opts) {
if (typeof opts === 'undefined') opts = { autoBom: false }
else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object')
opts = { autoBom: !opts }
}

// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
}
return blob
}

function download (url, name, opts) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = function () {
saveAs(xhr.response, name, opts)
}
xhr.onerror = function () {
console.error('could not download file')
}
xhr.send()
}

function corsEnabled (url) {
var xhr = new XMLHttpRequest()
// use sync to avoid popup blocker
xhr.open('HEAD', url, false)
try {
xhr.send()
} catch (e) {}
return xhr.status >= 200 && xhr.status <= 299
}

// `a.click()` doesn't work for all browsers (#465)
function click (node) {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}

// Detect WebView inside a native macOS app by ruling out all browsers
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent)

var saveAs = _global.saveAs || (
// probably in some web worker
(typeof window !== 'object' || window !== _global)
? function saveAs () { /* noop */ }

// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
: ('download' in HTMLAnchorElement.prototype && !isMacOSWebView)
? function saveAs (blob, name, opts) {
var URL = _global.URL || _global.webkitURL
// Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561)
var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a')
name = name || blob.name || 'download'

a.download = name
a.rel = 'noopener' // tabnabbing

// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'

if (typeof blob === 'string') {
// Support regular links
a.href = blob
if (a.origin !== location.origin) {
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, a.target = '_blank')
} else {
click(a)
}
} else {
// Support blobs
a.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
setTimeout(function () { click(a) }, 0)
}
}

// Use msSaveOrOpenBlob as a second approach
: 'msSaveOrOpenBlob' in navigator
? function saveAs (blob, name, opts) {
name = name || blob.name || 'download'

if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts)
} else {
var a = document.createElement('a')
a.href = blob
a.target = '_blank'
setTimeout(function () { click(a) })
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name)
}
}

// Fallback to using FileReader and a popup
: function saveAs (blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank')
if (popup) {
popup.document.title =
popup.document.body.innerText = 'downloading...'
}

if (typeof blob === 'string') return download(blob, name, opts)

var force = blob.type === 'application/octet-stream'
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)

if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader()
reader.onloadend = function () {
var url = reader.result
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
if (popup) popup.location.href = url
else location = url
popup = null // reverse-tabnabbing #460
}
reader.readAsDataURL(blob)
} else {
var URL = _global.URL || _global.webkitURL
var url = URL.createObjectURL(blob)
if (popup) popup.location = url
else location.href = url
popup = null // reverse-tabnabbing #460
setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
}
}
)

_global.saveAs = saveAs.saveAs = saveAs

if (typeof module !== 'undefined') {
module.exports = saveAs;
}
Loading