Skip to content

Commit

Permalink
Merge pull request #4 from AnatoliyProjects/main
Browse files Browse the repository at this point in the history
Safari implementation and cancel handling
  • Loading branch information
slowriot authored Nov 8, 2024
2 parents d0d597f + 3f27938 commit d21dec7
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ emscripten_browser_file::download(
For files containing binary data, you will usually want to use the MIME type `application/octet-stream`.
### Upload
From the user's point of view, the `upload` function acts as if the user is uploading a file to a remote website. In this case, the file is loaded into a buffer in memory (referred to by a `std::string_view`) that is accessible to a C++ callback function you define.
From the user's point of view, the `upload` function acts as if the user is uploading a file to a remote website. In this case, the file is loaded into a buffer in memory (referred to by a `std::string_view`) that is accessible to a C++ callback function you define. If the user cancels the file dialog without uploading a file, the callback function will be called with the empty `std::string_view`.
#### Example
Expand Down
40 changes: 38 additions & 2 deletions emscripten_browser_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define EMSCRIPTEN_UPLOAD_FILE_H_INCLUDED

#include <string>
#include <string_view>
#include <emscripten.h>

#define _EM_JS_INLINE(ret, c_name, js_name, params, code) \
Expand Down Expand Up @@ -46,12 +47,36 @@ EM_JS_INLINE(void, upload, (char const *accept_types, upload_handler callback, v
file_reader.mime_type = e.target.files[0].type;
file_reader.readAsArrayBuffer(e.target.files[0]);
};

var file_selector = document.createElement('input');
file_selector.setAttribute('type', 'file');
file_selector.setAttribute('onchange', 'globalThis["open_file"](event)');
/// The 'cancel' event is fired when the user cancels the currently open dialog.
/// In this case, the upload handler will get the empty string_view.
/// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/cancel_event
file_selector.addEventListener('cancel', () => {
Module["ccall"]('upload_file_return', 'number', ['string', 'string', 'number', 'number', 'number', 'number'], ["", "", 0, 0, callback, callback_data]);
});
file_selector.setAttribute('accept', UTF8ToString(accept_types));
file_selector.click();
/// file_selector.click() approach doesn't work in Safari (tested with native desktop v. 17.5 and iPhone/iPad simulators).
/// It seems that the Safari browser limits programmatical clicking in our case.
/// As a workaround, we create <dialog> where the user manually clicks on <input>.
var is_safari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (is_safari) {
var dialog = document.createElement('dialog');
dialog.setAttribute('id', 'EmJsFileDialog');
var desc = document.createElement('p');
desc.innerText = 'Please choose a file. Allowed extension(s): ' + UTF8ToString(accept_types);
dialog.appendChild(desc);
/// We should recreate <dialog> every call; it is the most natural way to reset input.value.
/// Otherwise, if the user re-selects the same file, it triggers the 'cancel' event instead of 'onchange'.
file_selector.setAttribute('onclick', 'var dg = document.getElementById("EmJsFileDialog"); dg.close(); dg.remove()');
dialog.appendChild(file_selector);
document.body.append(dialog);
dialog.showModal();
} else {
/// Not a Safari browser, so file_selector.click() is ok.
file_selector.click();
}
});
#pragma GCC diagnostic pop

Expand Down Expand Up @@ -84,6 +109,17 @@ EMSCRIPTEN_KEEPALIVE inline int upload_file_return(char const *filename, char co

EMSCRIPTEN_KEEPALIVE inline int upload_file_return(char const *filename, char const *mime_type, char *buffer, size_t buffer_size, upload_handler callback, void *callback_data) {
/// Load a file - this function is called from javascript when the file upload is activated

/// The file was not uploaded.
/// We must process this case separately because std::string_view(nullptr, 0) results in UB.
/// <The behavior is undefined if [s, s + count) is not a valid range
/// (even though the constructor may not access any of the elements of this range)>
/// https://en.cppreference.com/w/cpp/string/basic_string_view/basic_string_view
if (! buffer || buffer_size == 0) {
callback(filename, mime_type, std::string_view(), callback_data);
return 1;
}
/// Ok
callback(filename, mime_type, {buffer, buffer_size}, callback_data);
return 1;
}
Expand Down

0 comments on commit d21dec7

Please sign in to comment.