Reputation: 2008
I am working with a standard file input for uploads, and I am looking for a way to attach a function to an event when the user clicks/hits enter on the "cancel" button (or escapes out from) the choose file dialog.
I can't find any events that work across all browsers and platforms consistently.
I've read the answers to this question: Capturing cancel event on input type=file but they don't work, as the change event doesn't fire in most browsers on canceling out of the choose file dialog.
I'm looking for a pure js solution, but open to jquery solutions as well.
Anyone solve this problem successfully?
Upvotes: 71
Views: 142954
Reputation: 11
To detect cancellation can be a bit messy depending on what you are trying to do. If all you need is to prevent your code from in example clearing a stored list of files if the user accidentally opens the file dialogue again and closes it, you can listen to the 'change'
event and check if event.target.files.length
is larger than 0, or in other words that files has been provided, and then do what you want to do within that if statement.
If you want to do something specific on the user cancelling the file dialogue, you need to listen to both the 'change'
and 'cancel'
events. The reason for this is a quirk in how these events work. If no file was previously selected, then the 'change'
event will not trigger on closing the dialogue, instead the 'cancel'
event triggers. However, if there were files previously selected and the dialogue is closed, the 'cancel'
event doesn't trigger and now the 'change'
event takes a turn. If you open and close the dialogue one more time, the 'cancel'
event will resume triggering. Oh, and it will fire if the same exact files are selected again, but the 'change'
event won't. Because of course.
You can play around with the code snippet below by opening the dialogue, then closing it right away, only the 'cancel'
event will trigger. Open it again and select one or more files to trigger the 'change'
event, then open the dialogue again and close it and it will trigger once more. Open and close it again, and the 'cancel'
event triggers. Open it and select one or more files to trigger the 'change'
event, then open it and select the same exact files again to trigger the 'cancel'
event.
Hopefully that can help give an understanding of how the two events work in relation to one another.
const fileSelector = document.getElementById('file-selector');
fileSelector.addEventListener('change', (event) => {
/* Only do something as long as files has been selected. */
if (event.target.files.length > 0) {
/*
Do what you want to do if files are supplied
*/
let sOR = (event.target.files.length > 1) ? "s" : "";
console.log("File" + sOR + " selected.");
} else {
/*
Do what you want to do if no files are supplied or
just remove the else statement if you have no code to run
*/
console.log("No files selected, so user has selected to Cancel.");
}
});
/*
The 'cancel' event fires if the file dialogue previously didn't have files provided
and the user closed it, or if the user provides the same exact file(s)
*/
fileSelector.addEventListener('cancel', (event) => {
console.log("Same files were selected or user has selected to Cancel.");
});
<input type="file" id="file-selector" multiple>
Upvotes: 0
Reputation: 18398
As of 2021 the 'cancel' event was introduced for file inputs.
The cancel event is fired by and elements. The event is fired when the user cancels the currently open dialog by closing it with the Esc key. It is also fired by the file input when the user cancels the file picker dialog via the Esc key or the cancel button and when the user re-selects the same files that were previously selected.
You can use it like this:
inputElement.addEventListener('cancel', () => {
// logic here
});
or
<input type="file" oncancel="//logic here" />
React devs beware, the onCancel event isn't released yet, but is currently being implemented. In the meantime you can do something like this:
const yourInputRef = useRef(null);
useEffect(() => {
yourInputRef.current?.addEventListener('cancel', () => {
// logic here
});
}, [...]);
Compatibility as of today (Jan 2024) is great. It pretty much works across the board in all modern browsers it seems. I'd recommend not using fallbacks unless the cancel is absolutely critical and not just a visual bug.
Upvotes: 4
Reputation: 923
Note change event will casted on both upload new file or input cancel event so better to detect cancel within the change event
$("input[type='file']").on('change', changeCB);
function changeCB(ev){
if (this.files && this.files.length > 0) {
// upload new file
alert('upload');
} else {
// cancel upload no files received when event change casted
alert('cancel');
}
}
as you can see in change event if there are no files that's means the user
cancel the upload, so input value will be cleared and be empty so this case this.files.length
will be equal to 0
Upvotes: 0
Reputation: 399
In WebKit by this issue https://github.com/whatwg/html/pull/6735 a cancel event is implemented by https://github.com/WebKit/WebKit/pull/4986
The HTMLDialogElement also implements the cancel event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement
Here is a sample code with change and cancel event listening: https://stackblitz.com/edit/js-lzmne4
Upvotes: 0
Reputation: 21
You can cancel an event as follows:
input.addEventListener('cancel', e => { ... })
Upvotes: 1
Reputation: 136618
There is now a cancel
event being fired in such a case, which is supported by all 3 main browser engines since about a year (2 for Chrome and Firefox).
document.querySelector("input").addEventListener("cancel", (evt) => {
console.log("You closed the file picker dialog without selecting a file.");
});
<input type="file">
Unfortunately, we can't use the oncancel
global handler to feature detect this event, since that could point at the homonym event fired on <dialog>
elements and thus be a false positive. There is thus no easy detection path to my knowledge.
Upvotes: 26
Reputation: 20648
There is an alternative if you can use File System Access API's showOpenFilePicker
method.
At the time of writing this answer, the API is fully specs but only Chromium browsers (Chrome and Edge) support this but hopefully it will be more avaiable soon.
(Note that the below snippet doesn't work in sandboxed or cross-origin iframe so StackOverflow, CodePen and jsfiddler cannot run it, you have to have a HTTPS environment to test it)
document.querySelector("button").addEventListener("click", async () => {
try {
const file = await globalThis.showOpenFilePicker({
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
},
}
]
});
console.log("You picked: ", file[0].name);
} catch (e) {
if (e.code === e.ABORT_ERR) {
alert("You cancelled.")
} else {
throw e;
}
}
});
<button>Pick a file</button>
Upvotes: 0
Reputation: 5718
// wait function to delay
const wait = (ms) => new Promise((res) => setTimeout(res, ms));
class FilePickerServiceK {
getFileInput() {
if (this.ref)
return this.ref;
const input = document.createElement('input');
input.type = 'file';
this.ref = input;
return this.ref;
}
async pick(opt = {}) {
const input = this.getFileInput();
input.multiple = opt.multiple;
const onWindowFocusP = new Promise((res) => window.addEventListener('focus', res, {once: true}));
input.click();
await onWindowFocusP;
await wait(100);
const files = Array.from(input.files ?? []);
input.value = '';
return files;
}
}
const FilePickerService = new FilePickerServiceK();
// for demo
const button = document.createElement('button');
button.innerHTML = 'Pick File';
document.body.appendChild(button);
const div = document.createElement('div');
document.body.appendChild(div);
const handle = async () => {
const [file] = await FilePickerService.pick();
div.innerHTML = file ? file.name : 'cancelled';
};
button.addEventListener('click', handle);
Upvotes: 0
Reputation: 1262
In react , on Change event of input field , the event has no files on cancel event, we can proceed with this assumption. Cancel event will be captured.
handleChange = (event) => {
console.log(event);
console.log(event.target.files[0]);
this.setState({
tableDataResult: false,
});
if(event.target.files[0]){
this.setState({
csvfile: event.target.files[0],
});
}else{
//Cancel event called
console.log("false");
this.setState({
csvfile: oldValue,
});
}
};
<input
style={{
width: "450px",
marginLeft: "15px",
marginTop: "5px",
}}
className="csv-input"
type="file"
ref={(input) => {
this.filesInput = input;
}}
name="file"
placeholder={null}
onChange={this.handleChange}
/>
Upvotes: 0
Reputation: 1441
I was able to get this to work in Chrome and Safari with inspiration from this answer, however it did not appear to work in Firefox the same way -- the focus
event was never triggered when the dialog closed. Looks like Firefox makes use of the cancel
event (more info here). I successfully tested this in the latest versions of Chrome, Safari, and Firefox:
function selectFiles(
options?: {
/** Allow the selection of multiple files */
multiple?: boolean,
/** Restrict the selection to certain types of files (ex: `.txt`) */
accept?: Array<string>
}
): Promise<{
/** The list of selected files (empty if none selected) */
files: Array<File>,
/** The event that prompted the dialog to close */
event: Event
}> {
return new Promise((resolve) => {
const fileSelector: HTMLInputElement = document.createElement('input');
fileSelector.type = 'file';
fileSelector.style.display = 'none';
fileSelector.multiple = !!options?.multiple;
if (Array.isArray(options?.accept)) {
fileSelector.accept = options.accept.join(',');
}
let currTimeout;
const resolveWithFiles = (event?: Event) => {
clearTimeout(currTimeout);
currTimeout = setTimeout(() => {
// cleanup
window.removeEventListener('focus', resolveWithFiles);
fileSelector.remove();
// resolve with file array and the event associated with
// what prompted the dialog to close
resolve({ files: Array.from(fileSelector.files || []), event });
}, 300);
};
// EVENTS
// "cancel" event in Chrome and Safari
window.addEventListener('focus', resolveWithFiles);
// "cancel" event in Firefox
fileSelector.addEventListener('cancel', resolveWithFiles);
// files selected
fileSelector.addEventListener('change', resolveWithFiles);
// INITIATE
// open the selection window
document.body.append(fileSelector);
fileSelector.click();
});
}
Upvotes: 0
Reputation: 11
This code does not listen if the cancel button is clicked but it worked for me. It checks if the input has files attached onto it every time its value changes. From there I can pretty much do anything what I need to do.
$("#imageUpload").change(function() {
if (this.files && this.files[0]) {
console.log("Has file selected");
} else {
console.log("No file selected");
}
});
<input type="file" id="imageUpload" />
<script src="https://code.jquery.com/jquery-3.5.1.js" integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
Upvotes: 1
Reputation: 1828
This solves my problem though, although not tested on other browsers except Google Chrome:
$("#fileUpload").on("change", function (event) {
if (!$(this)[0].files[0]) {
event.preventDefault();
return;
};
// Else do something here.
});
$(this)[0]
is to convert from jQuery object to plain JavaScript object. When you click the Cancel button, the array length for files
is zero. You can either use $(this)[0].files.length < 1
or check if $(this)[0].files[0]
is undefined
.
Upvotes: 0
Reputation: 962
I needed to style my file upload differently whether the user was browsing the file explorer (active/inactive). Only click
and blur
events. Just consider that when the user clicks the Cancel button, a click outside the file upload (button
in my case) is required.
Here is my solution for Angular, but I guess anyone can grab the idea and adapt it with your favorite framework/library/Vanilla JS.
<!-- HTML - Angular -->
<input hidden type="file" #fileInput (change)="onFileUpload($event.target.files)">
<button (click)="initBrowsingFlags(); fileInput.click()" (blur)="onBlur()" [class.browsing]="browsing">Upload</button>
// Typescript - Angular
/** Whether the user is browsing the file explorer. */
browsing = false;
/**
* If a 2nd `blur` event is emitted while {@link browsing} is still true, it means that the user
* clicked the Cancel button on the file explorer.
* */
alreadyOneBlurEventWhileBrowsing = false;
onFileUpload(files: FileList) {
// ...
this.resetBrowsingFlags();
}
onBlur() {
if (!this.browsing) return;
if (this.onCancelClickWhileBrowsing) {
this.resetBrowsingFlags();
} else {
this.onCancelClickWhileBrowsing = true;
}
}
initBrowsingFlags() {
this.browsing = true;
this.alreadyOneBlurEventWhileBrowsing= false;
}
resetBrowsingFlags() {
this.browsing = false;
this.alreadyOneBlurEventWhileBrowsing= false;
}
Upvotes: 0
Reputation: 105
In your "inputElement.onclick" you should set a flag (in my case I set window.inputFileTrueClosed = true) so you can detect when the window gets the focus after pressing the button "Cancel" for that type of event. The following detect if the window gets the focus again: it means that "Cancel" button could have been pressed:
var isChrome = !!window.chrome;
window.addEventListener('focus', function (e) {
if(window.inputFileTrueClosed != false){
if(isChrome == true){
setTimeout(
function()
{
if(window.inputFileTrueClosed != false){
//if it is Chrome we have to wait because file.change(function(){... comes after "the window gets the focus"
window.inputFileTrueClosed = false;
}
}, 1000);
}
else
{
// if it is NOT Chrome (ex.Safari) do something when the "cancel" button is pressed.
window.inputFileTrueClosed = false;
}
}
})
Obviously the window gets the focus for many other events BUT with the flag you can select the one you need to detect.
Upvotes: 3
Reputation: 11
I was wrestling with a similar issue after implementing an automatic submit of an image upload form using jQuery. If the user cancelled the dialogue it sent a blank. All that was needed was to detect this empty value in the same script:
$('#upload-portrait-input').on('change', function(){
if ( $(this).val() != '' )
{
$('#portraitForm').submit();
}
else { // do something when the user clicks cancel
}
});
Upvotes: 0
Reputation: 476
I have a perfect solution.
The focus event will be executed before the change event.
So I need to use setTimeout to make the focus method execute later than the change method.
const createUpload = () => {
let lock = false
return new Promise((resolve, reject) => {
// create input file
const el = document.createElement('input')
el.id = +new Date()
el.style.display = 'none'
el.setAttribute('type', 'file')
document.body.appendChild(el)
el.addEventListener('change', () => {
lock = true
const file = el.files[0]
resolve(file)
// remove dom
document.body.removeChild(document.getElementById(el.id))
}, { once: true })
// file blur
window.addEventListener('focus', () => {
setTimeout(() => {
if (!lock && document.getElementById(el.id)) {
reject(new Error('onblur'))
// remove dom
document.body.removeChild(document.getElementById(el.id))
}
}, 300)
}, { once: true })
// open file select box
el.click()
})
}
try {
const file = createUpload()
console.log(file)
} catch(e) {
console.log(e.message) // onblur
}
Upvotes: 16
Reputation: 21
When we select the file following events are called -
Scenario 1 : When the select file is clicked and then cancel is clicked
Focus
event value=""
Click
event value=""
Blur
event value=""
Focus
event value=""
Blur
event value="" (when the user clicks somewhere out)
Scenario 2 : When the file is selected -
Focus
event value=""
Click
event value=""
Blur
event value=""
Focus
event value=""
Change
event value="filevalue"
Blur
event value="filevalue"
Focus
event value="filevalue"
Blur
event value="filevalue" (when the user clicks somewhere out)
We see here, when the Blur event (last event) is called after focus event with no value of file means that the Cancel button is clicked.
My scenario was to change the Submit button text to "Please wait" while the file is loading currentEvent variable is used to hold the current event value either click or focus if the currentEvent = focus and file value is null means Cancel button is clicked.
Javascript
var currentEvent = "focus";
function onFileBrowse() {
var vtbFile = $('#uploadFile')[0].files[0];
if (vtbFile != undefined) {
var extension = vtbFile.name.split('.').pop().toLowerCase();
var valError = "";
if (extension === 'xlsx' || extension === 'xlsb' || extension === 'csv') {
if (vtbFile.size === 0)
valError = "File '" + vtbFile.name + "' is empty";
}
else
valError = "Extension '" + extension + "' is not supported.";
if (valError !== "") {
alert("There was an issue validating the file. " + valError, 20000);
}
}
//hide Indicator
var buttonUpload = document.getElementById('btnUploadTB');
buttonUpload.innerText = "Submit";
};
function fileClick() {
//show Indicator
var buttonUpload = document.getElementById('btnUploadTB');
buttonUpload.innerText = "Please wait..";
document.getElementById('uploadFile').value = null;
currentEvent = "click";
}
function fileFocus() {
currentEvent = "focus";
}
function fileBlur() {
if (!document.getElementById('uploadFile').value && currentEvent == "focus") {
console.log('blur' + 'change to submit');
//hide Indicator
var buttonUpload = document.getElementById('btnUploadTB');
buttonUpload.innerText = "Submit";
}
}
HTML
<input class="k-button k-upload-button" type="file" id="uploadFile" name="uploadFile"
accept=".csv,.xlsx,.xlsb" onChange='onFileBrowse()' onclick="fileClick()" onfocus="fileFocus()" onblur="fileBlur()" />
<button id="btnUploadTB" type="button" class="btn btn-default" onclick="uploadTBFile()" style="width:28%;margin-left: 3px;">Submit</button>
Upvotes: 2
Reputation: 19
I had the problem where I clicked the cancel button on the input type="file" element and wanted the function to do nothing. if something was selected and I clicked the open button then I wanted my function to do something. The example only shows the method, I stripped out what I do with it after it opens. I put in the alerts just so you could see there isn't a filename coming back from the dialog when cancel is clicked. Here is a method I use, it is simple but it works.
function openFileOnClick(){
document.getElementById("fileSelector").value = "";
document.getElementById("fileSelector").files.length = 0;
document.getElementById("fileSelector").click();
if(document.getElementById("fileSelector").files.length >= 1){
alert(document.getElementById("fileSelector").value);
//Do something
}
else{
alert(document.getElementById("fileSelector").value);
//Cancel button has been called.
}
}
<html>
<head>
</head>
<body>
<input type="file" id="fileSelector" name="fileSelector" value="" style="display:none;" />
<input type="button" value="Open File" name="openFile" onclick="openFileOnClick();" />
</body>
</html>
Upvotes: 0
Reputation: 30883
A bit of research indicates that there is no way to detect when Cancel is selected in the File Selection dialog window. You can use onchange
or onblur
to check if files have been selected or if something has been added to the input value
.
This could look like: https://jsfiddle.net/Twisty/j18td9cs/
HTML
<form>
Select File:
<input type="file" name="test1" id="testFile" />
<button type="reset" id="pseudoCancel">
Cancel
</button>
</form>
JavaScript
var inputElement = document.getElementById("testFile");
var cancelButton = document.getElementById("pseudoCancel");
var numFiles = 0;
inputElement.onclick = function(event) {
var target = event.target || event.srcElement;
console.log(target, "clicked.");
console.log(event);
if (target.value.length == 0) {
console.log("Suspect Cancel was hit, no files selected.");
cancelButton.onclick();
} else {
console.log("File selected: ", target.value);
numFiles = target.files.length;
}
}
inputElement.onchange = function(event) {
var target = event.target || event.srcElement;
console.log(target, "changed.");
console.log(event);
if (target.value.length == 0) {
console.log("Suspect Cancel was hit, no files selected.");
if (numFiles == target.files.length) {
cancelButton.onclick();
}
} else {
console.log("File selected: ", target.value);
numFiles = target.files.length;
}
}
inputElement.onblur = function(event) {
var target = event.target || event.srcElement;
console.log(target, "changed.");
console.log(event);
if (target.value.length == 0) {
console.log("Suspect Cancel was hit, no files selected.");
if (numFiles == target.files.length) {
cancelButton.onclick();
}
} else {
console.log("File selected: ", target.value);
numFiles = target.files.length;
}
}
cancelButton.onclick = function(event) {
console.log("Pseudo Cancel button clicked.");
}
I suggest making your own cancel or reset button that resets the form or clears the value from the input.
Upvotes: 38